diff --git a/config/builder_pdf.php b/config/builder_pdf.php index b80f9a42..c838458b 100644 --- a/config/builder_pdf.php +++ b/config/builder_pdf.php @@ -3,6 +3,7 @@ use Sensiolabs\GotenbergBundle\Builder\Pdf\HtmlPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\LibreOfficePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\MarkdownPdfBuilder; +use Sensiolabs\GotenbergBundle\Builder\Pdf\MergePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\UrlPdfBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; @@ -58,4 +59,14 @@ ->call('setLogger', [service('logger')->nullOnInvalid()]) ->tag('sensiolabs_gotenberg.pdf_builder') ; + + $services->set('.sensiolabs_gotenberg.pdf_builder.merge', MergePdfBuilder::class) + ->share(false) + ->args([ + service('sensiolabs_gotenberg.client'), + service('sensiolabs_gotenberg.asset.base_dir_formatter'), + ]) + ->call('setLogger', [service('logger')->nullOnInvalid()]) + ->tag('sensiolabs_gotenberg.pdf_builder') + ; }; diff --git a/src/Builder/Pdf/MergePdfBuilder.php b/src/Builder/Pdf/MergePdfBuilder.php new file mode 100644 index 00000000..070c93d4 --- /dev/null +++ b/src/Builder/Pdf/MergePdfBuilder.php @@ -0,0 +1,113 @@ + $configurations + */ + public function setConfigurations(array $configurations): self + { + foreach ($configurations as $property => $value) { + $this->addConfiguration($property, $value); + } + + return $this; + } + + /** + * Convert the resulting PDF into the given PDF/A format. + */ + public function pdfFormat(PdfFormat $format): self + { + $this->formFields['pdfa'] = $format->value; + + return $this; + } + + /** + * Enable PDF for Universal Access for optimal accessibility. + */ + public function pdfUniversalAccess(bool $bool = true): self + { + $this->formFields['pdfua'] = $bool; + + return $this; + } + + public function files(string ...$paths): self + { + $this->formFields['files'] = []; + + foreach ($paths as $path) { + $this->assertFileExtension($path, ['pdf']); + + $dataPart = new DataPart(new DataPartFile($this->asset->resolve($path))); + + $this->formFields['files'][$path] = $dataPart; + } + + return $this; + } + + /** + * Resets the metadata. + * + * @see https://gotenberg.dev/docs/routes#metadata-chromium + * @see https://exiftool.org/TagNames/XMP.html#pdf + * + * @param array $metadata + */ + public function metadata(array $metadata): static + { + $this->formFields['metadata'] = $metadata; + + return $this; + } + + /** + * The metadata to write. + */ + public function addMetadata(string $key, string $value): static + { + $this->formFields['metadata'] ??= []; + $this->formFields['metadata'][$key] = $value; + + return $this; + } + + public function getMultipartFormData(): array + { + if ([] === ($this->formFields['files'] ?? [])) { + throw new MissingRequiredFieldException('At least one PDF file is required'); + } + + return parent::getMultipartFormData(); + } + + protected function getEndpoint(): string + { + return self::ENDPOINT; + } + + private function addConfiguration(string $configurationName, mixed $value): void + { + match ($configurationName) { + 'pdf_format' => $this->pdfFormat(PdfFormat::from($value)), + 'pdf_universal_access' => $this->pdfUniversalAccess($value), + 'metadata' => $this->metadata($value), + default => throw new InvalidBuilderConfiguration(sprintf('Invalid option "%s": no method does not exist in class "%s" to configured it.', $configurationName, self::class)), + }; + } +} diff --git a/src/Debug/TraceableGotenbergPdf.php b/src/Debug/TraceableGotenbergPdf.php index c26a31fc..86eb91f1 100644 --- a/src/Debug/TraceableGotenbergPdf.php +++ b/src/Debug/TraceableGotenbergPdf.php @@ -5,6 +5,7 @@ use Sensiolabs\GotenbergBundle\Builder\Pdf\HtmlPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\LibreOfficePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\MarkdownPdfBuilder; +use Sensiolabs\GotenbergBundle\Builder\Pdf\MergePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\PdfBuilderInterface; use Sensiolabs\GotenbergBundle\Builder\Pdf\UrlPdfBuilder; use Sensiolabs\GotenbergBundle\Debug\Builder\TraceablePdfBuilder; @@ -103,6 +104,23 @@ public function markdown(): PdfBuilderInterface return $traceableBuilder; } + /** + * @return MergePdfBuilder|TraceablePdfBuilder + */ + public function merge(): PdfBuilderInterface + { + /** @var MergePdfBuilder|TraceablePdfBuilder $traceableBuilder */ + $traceableBuilder = $this->inner->merge(); + + if (!$traceableBuilder instanceof TraceablePdfBuilder) { + return $traceableBuilder; + } + + $this->builders[] = ['merge', $traceableBuilder]; + + return $traceableBuilder; + } + /** * @return list */ diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index fc9baa6e..d23d9865 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -48,6 +48,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->append($this->addPdfUrlNode()) ->append($this->addPdfMarkdownNode()) ->append($this->addPdfOfficeNode()) + ->append($this->addPdfMergeNode()) ->end() ->arrayNode('screenshot') ->addDefaultsIfNotSet() @@ -487,6 +488,35 @@ private function addPdfOfficeNode(): NodeDefinition ; } + private function addPdfMergeNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('merge'); + $this->addPdfFormat($treeBuilder->getRootNode()); + $treeBuilder->getRootNode() + ->append($this->addPdfMetadata()) + ->end(); + + return $treeBuilder->getRootNode(); + } + + private function addPdfFormat(ArrayNodeDefinition $parent): void + { + $parent + ->addDefaultsIfNotSet() + ->children() + ->enumNode('pdf_format') + ->info('Convert PDF into the given PDF/A format - default None.') + ->values(array_map(static fn (PdfFormat $case): string => $case->value, PdfFormat::cases())) + ->defaultNull() + ->end() + ->booleanNode('pdf_universal_access') + ->info('Enable PDF for Universal Access for optimal accessibility - default false.') + ->defaultNull() + ->end() + ->end() + ; + } + private function addPdfMetadata(): NodeDefinition { $treeBuilder = new TreeBuilder('metadata'); diff --git a/src/DependencyInjection/SensiolabsGotenbergExtension.php b/src/DependencyInjection/SensiolabsGotenbergExtension.php index 1b5c3dd0..953edafb 100644 --- a/src/DependencyInjection/SensiolabsGotenbergExtension.php +++ b/src/DependencyInjection/SensiolabsGotenbergExtension.php @@ -18,7 +18,7 @@ public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); - /** @var array{base_uri: string, http_client: string|null, request_context?: array{base_uri?: string}, assets_directory: string, default_options: array{pdf: array{html: array, url: array, markdown: array, office: array}, screenshot: array{html: array, url: array, markdown: array}}} $config */ + /** @var array{base_uri: string, http_client: string|null, request_context?: array{base_uri?: string}, assets_directory: string, default_options: array{pdf: array{html: array, url: array, markdown: array, office: array, merge: array}, screenshot: array{html: array, url: array, markdown: array}}} $config */ $config = $this->processConfiguration($configuration, $configs); $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); @@ -34,6 +34,7 @@ public function load(array $configs, ContainerBuilder $container): void 'url' => $this->cleanUserOptions($config['default_options']['pdf']['url']), 'markdown' => $this->cleanUserOptions($config['default_options']['pdf']['markdown']), 'office' => $this->cleanUserOptions($config['default_options']['pdf']['office']), + 'merge' => $this->cleanUserOptions($config['default_options']['pdf']['merge']), ]) ; } @@ -73,6 +74,9 @@ public function load(array $configs, ContainerBuilder $container): void $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.office'); $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['office'])]); + $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.merge'); + $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['merge'])]); + $definition = $container->getDefinition('.sensiolabs_gotenberg.screenshot_builder.html'); $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['screenshot']['html'])]); diff --git a/src/GotenbergPdf.php b/src/GotenbergPdf.php index fd5f829b..4d9b1d31 100644 --- a/src/GotenbergPdf.php +++ b/src/GotenbergPdf.php @@ -22,12 +22,13 @@ public function get(string $builder): PdfBuilderInterface } /** - * @param 'html'|'url'|'markdown'|'office' $key + * @param 'html'|'url'|'markdown'|'office'|'merge' $key * * @return ($key is 'html' ? HtmlPdfBuilder : * $key is 'url' ? UrlPdfBuilder : * $key is 'markdown' ? MarkdownPdfBuilder : * $key is 'office' ? LibreOfficePdfBuilder : + * $key is 'merge' ? MergePdfBuilder : * PdfBuilderInterface) * ) */ @@ -55,4 +56,9 @@ public function markdown(): PdfBuilderInterface { return $this->getInternal('markdown'); } + + public function merge(): PdfBuilderInterface + { + return $this->getInternal('merge'); + } } diff --git a/src/GotenbergPdfInterface.php b/src/GotenbergPdfInterface.php index b7916f23..7285d3b6 100644 --- a/src/GotenbergPdfInterface.php +++ b/src/GotenbergPdfInterface.php @@ -5,6 +5,7 @@ use Sensiolabs\GotenbergBundle\Builder\Pdf\HtmlPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\LibreOfficePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\MarkdownPdfBuilder; +use Sensiolabs\GotenbergBundle\Builder\Pdf\MergePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\PdfBuilderInterface; use Sensiolabs\GotenbergBundle\Builder\Pdf\UrlPdfBuilder; @@ -38,4 +39,9 @@ public function office(): PdfBuilderInterface; * @return MarkdownPdfBuilder */ public function markdown(): PdfBuilderInterface; + + /** + * @return MergePdfBuilder + */ + public function merge(): PdfBuilderInterface; } diff --git a/tests/Builder/Pdf/MergePdfBuilderTest.php b/tests/Builder/Pdf/MergePdfBuilderTest.php new file mode 100644 index 00000000..f373208a --- /dev/null +++ b/tests/Builder/Pdf/MergePdfBuilderTest.php @@ -0,0 +1,87 @@ +gotenbergClient + ->expects($this->once()) + ->method('call') + ->with( + $this->equalTo('/forms/pdfengines/merge'), + $this->anything(), + $this->anything(), + ) + ; + + $this->getMergePdfBuilder() + ->files( + self::PDF_DOCUMENTS_DIR.'/simple_pdf.pdf', + self::PDF_DOCUMENTS_DIR.'/simple_pdf_1.pdf', + ) + ->generate() + ; + } + + public static function configurationIsCorrectlySetProvider(): \Generator + { + yield 'pdf_format' => ['pdf_format', 'PDF/A-1b', [ + 'pdfa' => 'PDF/A-1b', + ]]; + yield 'pdf_universal_access' => ['pdf_universal_access', false, [ + 'pdfua' => 'false', + ]]; + yield 'metadata' => ['metadata', ['Author' => 'SensioLabs'], [ + 'metadata' => '{"Author":"SensioLabs"}', + ]]; + } + + /** + * @param array $expected + */ + #[DataProvider('configurationIsCorrectlySetProvider')] + public function testConfigurationIsCorrectlySet(string $key, mixed $value, array $expected): void + { + $builder = $this->getMergePdfBuilder(); + $builder->setConfigurations([ + $key => $value, + ]); + $builder->files( + self::PDF_DOCUMENTS_DIR.'/simple_pdf.pdf', + self::PDF_DOCUMENTS_DIR.'/simple_pdf_1.pdf', + ); + + self::assertEquals($expected, $builder->getMultipartFormData()[0]); + } + + public function testRequiredFormData(): void + { + $builder = $this->getMergePdfBuilder(); + + $this->expectException(MissingRequiredFieldException::class); + $this->expectExceptionMessage('At least one PDF file is required'); + + $builder->getMultipartFormData(); + } + + private function getMergePdfBuilder(): MergePdfBuilder + { + return new MergePdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 55555d33..cfe12fa0 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -74,6 +74,7 @@ public function testWithExtraHeadersConfiguration(): void * 'url': array, * 'markdown': array, * 'office': array, + * 'merge': array, * } * } * } @@ -170,6 +171,10 @@ private static function getBundleDefaultConfig(): array 'pdf_format' => null, 'pdf_universal_access' => null, ], + 'merge' => [ + 'pdf_format' => null, + 'pdf_universal_access' => null, + ], ], 'screenshot' => [ 'html' => [ diff --git a/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php b/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php index ec5b5bf8..5959ccb1 100644 --- a/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php +++ b/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php @@ -118,6 +118,10 @@ public function testGotenbergConfiguredWithValidConfig(): void 'pdf_format' => 'PDF/A-1b', 'pdf_universal_access' => true, ], + 'merge' => [ + 'pdf_format' => 'PDF/A-3b', + 'pdf_universal_access' => true, + ], ], 'screenshot' => [ 'html' => [ @@ -338,6 +342,11 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void 'Author' => 'SensioLabs OFFICE', ], ], + 'merge' => [ + 'metadata' => [ + 'Author' => 'SensioLabs MERGE', + ], + ], ], ], ]], $containerBuilder); @@ -367,6 +376,11 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void 'Author' => 'SensioLabs OFFICE', ], ], + 'merge' => [ + 'metadata' => [ + 'Author' => 'SensioLabs MERGE', + ], + ], ], $dataCollectorOptions); } @@ -379,6 +393,7 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void * 'url': array, * 'markdown': array, * 'office': array, + * 'merge': array, * }, * 'screenshot': array{ * 'html': array, @@ -479,6 +494,10 @@ private static function getValidConfig(): array 'pdf_format' => PdfFormat::Pdf1b->value, 'pdf_universal_access' => true, ], + 'merge' => [ + 'pdf_format' => PdfFormat::Pdf3b->value, + 'pdf_universal_access' => true, + ], ], 'screenshot' => [ 'html' => [ diff --git a/tests/Fixtures/pdf/simple_pdf_1.pdf b/tests/Fixtures/pdf/simple_pdf_1.pdf new file mode 100644 index 00000000..3de77499 Binary files /dev/null and b/tests/Fixtures/pdf/simple_pdf_1.pdf differ