mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +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:
		| @@ -32,6 +32,7 @@ | ||||
|         "phpoffice/phpspreadsheet": "^1.16", | ||||
|         "ramsey/uuid-doctrine": "^1.7", | ||||
|         "sensio/framework-extra-bundle": "^5.5", | ||||
|         "smalot/pdfparser": "^2.10", | ||||
|         "spomky-labs/base64url": "^2.0", | ||||
|         "symfony/asset": "^5.4", | ||||
|         "symfony/browser-kit": "^5.4", | ||||
| @@ -97,7 +98,6 @@ | ||||
|         "symfony/debug-bundle": "^5.4", | ||||
|         "symfony/dotenv": "^5.4", | ||||
|         "symfony/maker-bundle": "^1.20", | ||||
|         "symfony/phpunit-bridge": "^5.4", | ||||
|         "symfony/runtime": "^5.4", | ||||
|         "symfony/stopwatch": "^5.4", | ||||
|         "symfony/var-dumper": "^5.4" | ||||
|   | ||||
| @@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase | ||||
|         $this->assertInstanceOf( | ||||
|             ActivityType::class, | ||||
|             $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()); | ||||
|  | ||||
|   | ||||
| @@ -37,12 +37,12 @@ class RemoteEventConverter | ||||
|      * valid when the remote string contains also a timezone, like in | ||||
|      * 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. | ||||
|      */ | ||||
|     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'; | ||||
|  | ||||
|   | ||||
							
								
								
									
										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); | ||||
|         } elseif ('ovh' === $dsn['scheme']) { | ||||
|             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) { | ||||
|   | ||||
| @@ -190,7 +190,7 @@ class ExportManager | ||||
|             // throw an error if the export require other modifier, which is | ||||
|             // not allowed when the export return a `NativeQuery` | ||||
|             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) { | ||||
|             // handle filters | ||||
| @@ -203,7 +203,7 @@ class ExportManager | ||||
|                 'dql' => $query->getDQL(), | ||||
|             ]); | ||||
|         } 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]); | ||||
|   | ||||
| @@ -14,9 +14,9 @@ namespace Chill\MainBundle\Search\Utils; | ||||
| class ExtractDateFromPattern | ||||
| { | ||||
|     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 | ||||
|         ['((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 | ||||
|         ['([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 | ||||
|     ]; | ||||
|  | ||||
|     public function extractDates(string $subject): SearchExtractionResult | ||||
|   | ||||
| @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; | ||||
|  | ||||
| class ExtractPhonenumberFromPattern | ||||
| { | ||||
|     private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})'; | ||||
|     private const PATTERN = '([\+]{0,1}[0-9\ ]{5,})'; | ||||
|  | ||||
|     private readonly string $defaultCarrierCode; | ||||
|  | ||||
|   | ||||
| @@ -74,7 +74,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase | ||||
|         $this->assertResponseRedirects(); | ||||
|         $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'); | ||||
|         $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]; | ||||
|  | ||||
|         $period = self::getContainer()->get(EntityManagerInterface::class) | ||||
|   | ||||
| @@ -41,7 +41,7 @@ final class ChillReportExtensionTest extends KernelTestCase | ||||
|         } | ||||
|  | ||||
|         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(); | ||||
|  | ||||
|     $services | ||||
|         ->load('Chill\\WopiBundle\\Service\\', __DIR__.'/../../Service'); | ||||
|         ->load('Chill\WopiBundle\Service\\', __DIR__.'/../../Service'); | ||||
|  | ||||
|     $services | ||||
|         ->load('Chill\\WopiBundle\\Controller\\', __DIR__.'/../../Controller') | ||||
|         ->load('Chill\WopiBundle\Controller\\', __DIR__.'/../../Controller') | ||||
|         ->tag('controller.service_arguments'); | ||||
|  | ||||
|     $services | ||||
|   | ||||
		Reference in New Issue
	
	Block a user