mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-12 13:24:25 +00:00
Merge branch 'signature-app/parse-pdf' into 'signature-app-master'
Add PDF signature zone parsing functionality See merge request Chill-Projet/chill-bundles!703
This commit is contained in:
commit
b65e2c62c4
@ -32,6 +32,7 @@
|
|||||||
"phpoffice/phpspreadsheet": "^1.16",
|
"phpoffice/phpspreadsheet": "^1.16",
|
||||||
"ramsey/uuid-doctrine": "^1.7",
|
"ramsey/uuid-doctrine": "^1.7",
|
||||||
"sensio/framework-extra-bundle": "^5.5",
|
"sensio/framework-extra-bundle": "^5.5",
|
||||||
|
"smalot/pdfparser": "^2.10",
|
||||||
"spomky-labs/base64url": "^2.0",
|
"spomky-labs/base64url": "^2.0",
|
||||||
"symfony/asset": "^5.4",
|
"symfony/asset": "^5.4",
|
||||||
"symfony/browser-kit": "^5.4",
|
"symfony/browser-kit": "^5.4",
|
||||||
@ -97,7 +98,6 @@
|
|||||||
"symfony/debug-bundle": "^5.4",
|
"symfony/debug-bundle": "^5.4",
|
||||||
"symfony/dotenv": "^5.4",
|
"symfony/dotenv": "^5.4",
|
||||||
"symfony/maker-bundle": "^1.20",
|
"symfony/maker-bundle": "^1.20",
|
||||||
"symfony/phpunit-bridge": "^5.4",
|
|
||||||
"symfony/runtime": "^5.4",
|
"symfony/runtime": "^5.4",
|
||||||
"symfony/stopwatch": "^5.4",
|
"symfony/stopwatch": "^5.4",
|
||||||
"symfony/var-dumper": "^5.4"
|
"symfony/var-dumper": "^5.4"
|
||||||
|
@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase
|
|||||||
$this->assertInstanceOf(
|
$this->assertInstanceOf(
|
||||||
ActivityType::class,
|
ActivityType::class,
|
||||||
$form->getData()['type'],
|
$form->getData()['type'],
|
||||||
'The data is an instance of Chill\\ActivityBundle\\Entity\\ActivityType'
|
'The data is an instance of Chill\ActivityBundle\Entity\ActivityType'
|
||||||
);
|
);
|
||||||
$this->assertEquals($type->getId(), $form->getData()['type']->getId());
|
$this->assertEquals($type->getId(), $form->getData()['type']->getId());
|
||||||
|
|
||||||
|
@ -37,12 +37,12 @@ class RemoteEventConverter
|
|||||||
* valid when the remote string contains also a timezone, like in
|
* valid when the remote string contains also a timezone, like in
|
||||||
* lastModifiedDate.
|
* lastModifiedDate.
|
||||||
*/
|
*/
|
||||||
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P';
|
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as above, but sometimes the date is expressed with only 6 milliseconds.
|
* Same as above, but sometimes the date is expressed with only 6 milliseconds.
|
||||||
*/
|
*/
|
||||||
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP';
|
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP';
|
||||||
|
|
||||||
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
|
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
|
||||||
|
|
||||||
|
28
src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php
Normal file
28
src/Bundle/ChillDocStoreBundle/Service/Signature/PDFPage.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature;
|
||||||
|
|
||||||
|
final readonly class PDFPage
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $index,
|
||||||
|
public float $width,
|
||||||
|
public float $height,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function equals(self $page): bool
|
||||||
|
{
|
||||||
|
return $page->index === $this->index
|
||||||
|
&& round($page->width, 2) === round($this->width, 2)
|
||||||
|
&& round($page->height, 2) === round($this->height, 2);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature;
|
||||||
|
|
||||||
|
final readonly class PDFSignatureZone
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public float $x,
|
||||||
|
public float $y,
|
||||||
|
public float $height,
|
||||||
|
public float $width,
|
||||||
|
public PDFPage $PDFPage,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function equals(self $other): bool
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$this->x == $other->x
|
||||||
|
&& $this->y == $other->y
|
||||||
|
&& $this->height == $other->height
|
||||||
|
&& $this->width == $other->width
|
||||||
|
&& $this->PDFPage->equals($other->PDFPage);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature;
|
||||||
|
|
||||||
|
use Smalot\PdfParser\Parser;
|
||||||
|
|
||||||
|
class PDFSignatureZoneParser
|
||||||
|
{
|
||||||
|
public const ZONE_SIGNATURE_START = 'signature_zone';
|
||||||
|
|
||||||
|
private Parser $parser;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public float $defaultHeight = 180.0,
|
||||||
|
public float $defaultWidth = 180.0,
|
||||||
|
) {
|
||||||
|
$this->parser = new Parser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<PDFSignatureZone>
|
||||||
|
*/
|
||||||
|
public function findSignatureZones(string $fileContent): array
|
||||||
|
{
|
||||||
|
$pdf = $this->parser->parseContent($fileContent);
|
||||||
|
$zones = [];
|
||||||
|
|
||||||
|
$defaults = $pdf->getObjectsByType('Pages');
|
||||||
|
$defaultPage = reset($defaults);
|
||||||
|
$defaultPageDetails = $defaultPage->getDetails();
|
||||||
|
|
||||||
|
foreach ($pdf->getPages() as $index => $page) {
|
||||||
|
$details = $page->getDetails();
|
||||||
|
$pdfPage = new PDFPage(
|
||||||
|
$index,
|
||||||
|
(float) ($details['MediaBox'][2] ?? $defaultPageDetails['MediaBox'][2]),
|
||||||
|
(float) ($details['MediaBox'][3] ?? $defaultPageDetails['MediaBox'][3]),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($page->getDataTm() as $dataTm) {
|
||||||
|
if (str_starts_with($dataTm[1], self::ZONE_SIGNATURE_START)) {
|
||||||
|
$zones[] = new PDFSignatureZone((float) $dataTm[0][4], (float) $dataTm[0][5], $this->defaultHeight, $this->defaultWidth, $pdfPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zones;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tests\Service\Signature;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PDFSignatureZoneParserTest extends TestCase
|
||||||
|
{
|
||||||
|
private static PDFSignatureZoneParser $parser;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::$parser = new PDFSignatureZoneParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideFiles
|
||||||
|
*
|
||||||
|
* @param list<PDFSignatureZone> $expected
|
||||||
|
*/
|
||||||
|
public function testFindSignatureZones(string $filePath, array $expected): void
|
||||||
|
{
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
|
||||||
|
if (false === $content) {
|
||||||
|
throw new \LogicException("Unable to read file {$filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$actual = self::$parser->findSignatureZones($content);
|
||||||
|
|
||||||
|
self::assertEquals(count($expected), count($actual));
|
||||||
|
|
||||||
|
foreach ($actual as $index => $signatureZone) {
|
||||||
|
self::assertObjectEquals($expected[$index], $signatureZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideFiles(): iterable
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
__DIR__.'/data/signature_2_signature_page_1.pdf',
|
||||||
|
[
|
||||||
|
new PDFSignatureZone(
|
||||||
|
127.7,
|
||||||
|
95.289,
|
||||||
|
180.0,
|
||||||
|
180.0,
|
||||||
|
$page = new PDFPage(0, 595.30393, 841.8897)
|
||||||
|
),
|
||||||
|
new PDFSignatureZone(
|
||||||
|
269.5,
|
||||||
|
95.289,
|
||||||
|
180.0,
|
||||||
|
180.0,
|
||||||
|
$page,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
@ -43,7 +43,7 @@ class ShortMessageCompilerPass implements CompilerPassInterface
|
|||||||
$defaultTransporter = new Reference(NullShortMessageSender::class);
|
$defaultTransporter = new Reference(NullShortMessageSender::class);
|
||||||
} elseif ('ovh' === $dsn['scheme']) {
|
} elseif ('ovh' === $dsn['scheme']) {
|
||||||
if (!class_exists('\\'.\Ovh\Api::class)) {
|
if (!class_exists('\\'.\Ovh\Api::class)) {
|
||||||
throw new RuntimeException('Class \\Ovh\\Api not found');
|
throw new RuntimeException('Class \Ovh\Api not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (['user', 'host', 'pass'] as $component) {
|
foreach (['user', 'host', 'pass'] as $component) {
|
||||||
|
@ -190,7 +190,7 @@ class ExportManager
|
|||||||
// throw an error if the export require other modifier, which is
|
// throw an error if the export require other modifier, which is
|
||||||
// not allowed when the export return a `NativeQuery`
|
// not allowed when the export return a `NativeQuery`
|
||||||
if (\count($export->supportsModifiers()) > 0) {
|
if (\count($export->supportsModifiers()) > 0) {
|
||||||
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`');
|
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
|
||||||
}
|
}
|
||||||
} elseif ($query instanceof QueryBuilder) {
|
} elseif ($query instanceof QueryBuilder) {
|
||||||
// handle filters
|
// handle filters
|
||||||
@ -203,7 +203,7 @@ class ExportManager
|
|||||||
'dql' => $query->getDQL(),
|
'dql' => $query->getDQL(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.');
|
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);
|
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);
|
||||||
|
@ -14,9 +14,9 @@ namespace Chill\MainBundle\Search\Utils;
|
|||||||
class ExtractDateFromPattern
|
class ExtractDateFromPattern
|
||||||
{
|
{
|
||||||
private const DATE_PATTERN = [
|
private const DATE_PATTERN = [
|
||||||
['([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))', 'Y-m-d'], // 1981-05-12
|
['([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))', 'Y-m-d'], // 1981-05-12
|
||||||
['((0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/([12]\\d{3}))', 'd/m/Y'], // 15/12/1980
|
['((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))', 'd/m/Y'], // 15/12/1980
|
||||||
['((0[1-9]|[12]\\d|3[01])-(0[1-9]|1[0-2])-([12]\\d{3}))', 'd-m-Y'], // 15/12/1980
|
['((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))', 'd-m-Y'], // 15/12/1980
|
||||||
];
|
];
|
||||||
|
|
||||||
public function extractDates(string $subject): SearchExtractionResult
|
public function extractDates(string $subject): SearchExtractionResult
|
||||||
|
@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
|||||||
|
|
||||||
class ExtractPhonenumberFromPattern
|
class ExtractPhonenumberFromPattern
|
||||||
{
|
{
|
||||||
private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})';
|
private const PATTERN = '([\+]{0,1}[0-9\ ]{5,})';
|
||||||
|
|
||||||
private readonly string $defaultCarrierCode;
|
private readonly string $defaultCarrierCode;
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase
|
|||||||
$this->assertResponseRedirects();
|
$this->assertResponseRedirects();
|
||||||
$location = $this->client->getResponse()->headers->get('Location');
|
$location = $this->client->getResponse()->headers->get('Location');
|
||||||
|
|
||||||
$this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location));
|
$this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +93,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase
|
|||||||
$location = $this->client->getResponse()->headers->get('Location');
|
$location = $this->client->getResponse()->headers->get('Location');
|
||||||
$matches = [];
|
$matches = [];
|
||||||
|
|
||||||
$this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location, $matches));
|
$this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location, $matches));
|
||||||
$id = $matches[1];
|
$id = $matches[1];
|
||||||
|
|
||||||
$period = self::getContainer()->get(EntityManagerInterface::class)
|
$period = self::getContainer()->get(EntityManagerInterface::class)
|
||||||
|
@ -41,7 +41,7 @@ final class ChillReportExtensionTest extends KernelTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$reportFounded) {
|
if (!$reportFounded) {
|
||||||
throw new \Exception('Class Chill\\ReportBundle\\Entity\\Report not found in chill_custom_fields.customizables_entities', 1);
|
throw new \Exception('Class Chill\ReportBundle\Entity\Report not found in chill_custom_fields.customizables_entities', 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,10 @@ return static function (ContainerConfigurator $container) {
|
|||||||
->autoconfigure();
|
->autoconfigure();
|
||||||
|
|
||||||
$services
|
$services
|
||||||
->load('Chill\\WopiBundle\\Service\\', __DIR__.'/../../Service');
|
->load('Chill\WopiBundle\Service\\', __DIR__.'/../../Service');
|
||||||
|
|
||||||
$services
|
$services
|
||||||
->load('Chill\\WopiBundle\\Controller\\', __DIR__.'/../../Controller')
|
->load('Chill\WopiBundle\Controller\\', __DIR__.'/../../Controller')
|
||||||
->tag('controller.service_arguments');
|
->tag('controller.service_arguments');
|
||||||
|
|
||||||
$services
|
$services
|
||||||
|
Loading…
x
Reference in New Issue
Block a user