diff --git a/config/builder_pdf.php b/config/builder_pdf.php index c838458b..00f9904f 100644 --- a/config/builder_pdf.php +++ b/config/builder_pdf.php @@ -1,5 +1,6 @@ call('setLogger', [service('logger')->nullOnInvalid()]) ->tag('sensiolabs_gotenberg.pdf_builder') ; + + $services->set('.sensiolabs_gotenberg.pdf_builder.convert', ConvertPdfBuilder::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/ConvertPdfBuilder.php b/src/Builder/Pdf/ConvertPdfBuilder.php new file mode 100644 index 00000000..883bae21 --- /dev/null +++ b/src/Builder/Pdf/ConvertPdfBuilder.php @@ -0,0 +1,90 @@ + $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; + } + + public function getMultipartFormData(): array + { + if (!\array_key_exists('pdfa', $this->formFields) && !\array_key_exists('pdfua', $this->formFields)) { + throw new MissingRequiredFieldException('At least "pdfa" or "pdfua" must be provided.'); + } + + 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), + default => throw new InvalidBuilderConfiguration(sprintf('Invalid option "%s": no method does not exist in class "%s" to configured it.', $configurationName, static::class)), + }; + } +} diff --git a/src/Debug/TraceableGotenbergPdf.php b/src/Debug/TraceableGotenbergPdf.php index 86eb91f1..d70dd42b 100644 --- a/src/Debug/TraceableGotenbergPdf.php +++ b/src/Debug/TraceableGotenbergPdf.php @@ -2,6 +2,7 @@ namespace Sensiolabs\GotenbergBundle\Debug; +use Sensiolabs\GotenbergBundle\Builder\Pdf\ConvertPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\HtmlPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\LibreOfficePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\MarkdownPdfBuilder; @@ -121,6 +122,23 @@ public function merge(): PdfBuilderInterface return $traceableBuilder; } + /** + * @return ConvertPdfBuilder|TraceablePdfBuilder + */ + public function convert(): PdfBuilderInterface + { + /** @var ConvertPdfBuilder|TraceablePdfBuilder $traceableBuilder */ + $traceableBuilder = $this->inner->convert(); + + if (!$traceableBuilder instanceof TraceablePdfBuilder) { + return $traceableBuilder; + } + + $this->builders[] = ['convert', $traceableBuilder]; + + return $traceableBuilder; + } + /** * @return list */ diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 52904844..d7185582 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -49,6 +49,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->append($this->addPdfMarkdownNode()) ->append($this->addPdfOfficeNode()) ->append($this->addPdfMergeNode()) + ->append($this->addPdfConvertNode()) ->end() ->arrayNode('screenshot') ->addDefaultsIfNotSet() @@ -325,18 +326,11 @@ private function addChromiumPdfOptionsNode(ArrayNodeDefinition $parent): void ->info('Do not wait for Chromium network to be idle. - default false. https://gotenberg.dev/docs/routes#performance-mode-chromium') ->defaultNull() ->end() - ->enumNode('pdf_format') - ->info('Convert the resulting PDF into the given PDF/A format - default None. https://gotenberg.dev/docs/routes#pdfa-chromium') - ->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. https://gotenberg.dev/docs/routes#console-exceptions') - ->defaultNull() - ->end() ->append($this->addPdfMetadata()) ->end() ; + + $this->addPdfFormat($parent); } private function addChromiumScreenshotOptionsNode(ArrayNodeDefinition $parent): void @@ -471,7 +465,7 @@ private function addPdfOfficeNode(): NodeDefinition { $treeBuilder = new TreeBuilder('office'); - return $treeBuilder->getRootNode() + $treeBuilder->getRootNode() ->addDefaultsIfNotSet() ->children() ->booleanNode('landscape') @@ -500,18 +494,21 @@ private function addPdfOfficeNode(): NodeDefinition ->info('Merge alphanumerically the resulting PDFs. - default false. https://gotenberg.dev/docs/routes#merge-libreoffice') ->defaultNull() ->end() - ->enumNode('pdf_format') - ->info('Convert the resulting PDF into the given PDF/A format - default None. https://gotenberg.dev/docs/routes#pdfa-chromium') - ->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. https://gotenberg.dev/docs/routes#console-exceptions') - ->defaultNull() - ->end() ->append($this->addPdfMetadata()) ->end() ; + + $this->addPdfFormat($treeBuilder->getRootNode()); + + return $treeBuilder->getRootNode(); + } + + private function addPdfConvertNode(): NodeDefinition + { + $treeBuilder = new TreeBuilder('convert'); + $this->addPdfFormat($treeBuilder->getRootNode()); + + return $treeBuilder->getRootNode(); } private function addPdfMergeNode(): NodeDefinition diff --git a/src/DependencyInjection/SensiolabsGotenbergExtension.php b/src/DependencyInjection/SensiolabsGotenbergExtension.php index 953edafb..4ec5a280 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, merge: 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, convert: array}, screenshot: array{html: array, url: array, markdown: array}}} $config */ $config = $this->processConfiguration($configuration, $configs); $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); @@ -35,6 +35,7 @@ public function load(array $configs, ContainerBuilder $container): void 'markdown' => $this->cleanUserOptions($config['default_options']['pdf']['markdown']), 'office' => $this->cleanUserOptions($config['default_options']['pdf']['office']), 'merge' => $this->cleanUserOptions($config['default_options']['pdf']['merge']), + 'convert' => $this->cleanUserOptions($config['default_options']['pdf']['convert']), ]) ; } @@ -77,6 +78,9 @@ public function load(array $configs, ContainerBuilder $container): void $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.merge'); $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['merge'])]); + $definition = $container->getDefinition('.sensiolabs_gotenberg.pdf_builder.convert'); + $definition->addMethodCall('setConfigurations', [$this->cleanUserOptions($config['default_options']['pdf']['convert'])]); + $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 b2f392e9..69781cdf 100644 --- a/src/GotenbergPdf.php +++ b/src/GotenbergPdf.php @@ -22,7 +22,7 @@ public function get(string $builder): PdfBuilderInterface } /** - * @param 'html'|'url'|'markdown'|'office'|'merge' $key + * @param 'html'|'url'|'markdown'|'office'|'merge'|'convert' $key * * @return ( * $key is 'html' ? HtmlPdfBuilder : @@ -30,6 +30,7 @@ public function get(string $builder): PdfBuilderInterface * $key is 'markdown' ? MarkdownPdfBuilder : * $key is 'office' ? LibreOfficePdfBuilder : * $key is 'merge' ? MergePdfBuilder : + * $key is 'convert' ? ConvertPdfBuilder : * PdfBuilderInterface * ) */ @@ -62,4 +63,9 @@ public function merge(): PdfBuilderInterface { return $this->getInternal('merge'); } + + public function convert(): PdfBuilderInterface + { + return $this->getInternal('convert'); + } } diff --git a/src/GotenbergPdfInterface.php b/src/GotenbergPdfInterface.php index 7285d3b6..e695c17d 100644 --- a/src/GotenbergPdfInterface.php +++ b/src/GotenbergPdfInterface.php @@ -2,6 +2,7 @@ namespace Sensiolabs\GotenbergBundle; +use Sensiolabs\GotenbergBundle\Builder\Pdf\ConvertPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\HtmlPdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\LibreOfficePdfBuilder; use Sensiolabs\GotenbergBundle\Builder\Pdf\MarkdownPdfBuilder; @@ -44,4 +45,9 @@ public function markdown(): PdfBuilderInterface; * @return MergePdfBuilder */ public function merge(): PdfBuilderInterface; + + /** + * @return ConvertPdfBuilder + */ + public function convert(): PdfBuilderInterface; } diff --git a/tests/Builder/Pdf/ConvertPdfBuilderTest.php b/tests/Builder/Pdf/ConvertPdfBuilderTest.php new file mode 100644 index 00000000..5e970b4e --- /dev/null +++ b/tests/Builder/Pdf/ConvertPdfBuilderTest.php @@ -0,0 +1,93 @@ +gotenbergClient + ->expects($this->once()) + ->method('call') + ->with( + $this->equalTo('/forms/pdfengines/convert'), + $this->anything(), + $this->anything(), + ) + ; + + $this->getConvertPdfBuilder() + ->files(self::PDF_DOCUMENTS_DIR.'/document.pdf') + ->pdfUniversalAccess() + ->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', + ]]; + } + + /** + * @param array $expected + */ + #[DataProvider('configurationIsCorrectlySetProvider')] + public function testConfigurationIsCorrectlySet(string $key, mixed $value, array $expected): void + { + $builder = $this->getConvertPdfBuilder(); + $builder->setConfigurations([ + $key => $value, + ]); + $builder->files(self::PDF_DOCUMENTS_DIR.'/document.pdf'); + + self::assertEquals($expected, $builder->getMultipartFormData()[0]); + } + + public function testRequiredFormat(): void + { + $builder = $this->getConvertPdfBuilder(); + $builder + ->files(self::PDF_DOCUMENTS_DIR.'/document.pdf') + ; + + $this->expectException(MissingRequiredFieldException::class); + $this->expectExceptionMessage('At least "pdfa" or "pdfua" must be provided.'); + + $builder->getMultipartFormData(); + } + + public function testRequiredPdfFile(): void + { + $builder = $this->getConvertPdfBuilder(); + $builder->pdfUniversalAccess(); + + $this->expectException(MissingRequiredFieldException::class); + $this->expectExceptionMessage('At least one PDF file is required'); + + $builder->getMultipartFormData(); + } + + private function getConvertPdfBuilder(): ConvertPdfBuilder + { + return new ConvertPdfBuilder($this->gotenbergClient, self::$assetBaseDirFormatter); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index cfe12fa0..1b73b4fc 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -75,6 +75,7 @@ public function testWithExtraHeadersConfiguration(): void * 'markdown': array, * 'office': array, * 'merge': array, + * 'convert': array, * } * } * } @@ -175,6 +176,10 @@ private static function getBundleDefaultConfig(): array 'pdf_format' => null, 'pdf_universal_access' => null, ], + 'convert' => [ + 'pdf_format' => null, + 'pdf_universal_access' => null, + ], ], 'screenshot' => [ 'html' => [ diff --git a/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php b/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php index 5959ccb1..ccaacbaf 100644 --- a/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php +++ b/tests/DependencyInjection/SensiolabsGotenbergExtensionTest.php @@ -122,6 +122,10 @@ public function testGotenbergConfiguredWithValidConfig(): void 'pdf_format' => 'PDF/A-3b', 'pdf_universal_access' => true, ], + 'convert' => [ + 'pdf_format' => 'PDF/A-2b', + 'pdf_universal_access' => true, + ], ], 'screenshot' => [ 'html' => [ @@ -347,6 +351,9 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void 'Author' => 'SensioLabs MERGE', ], ], + 'convert' => [ + 'pdf_format' => 'PDF/A-2b', + ], ], ], ]], $containerBuilder); @@ -381,6 +388,9 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void 'Author' => 'SensioLabs MERGE', ], ], + 'convert' => [ + 'pdf_format' => 'PDF/A-2b', + ], ], $dataCollectorOptions); } @@ -394,6 +404,7 @@ public function testDataCollectorIsProperlyConfiguredIfEnabled(): void * 'markdown': array, * 'office': array, * 'merge': array, + * 'convert': array, * }, * 'screenshot': array{ * 'html': array, @@ -498,6 +509,10 @@ private static function getValidConfig(): array 'pdf_format' => PdfFormat::Pdf3b->value, 'pdf_universal_access' => true, ], + 'convert' => [ + 'pdf_format' => PdfFormat::Pdf2b->value, + 'pdf_universal_access' => true, + ], ], 'screenshot' => [ 'html' => [ diff --git a/tests/Fixtures/assets/pdf/document.pdf b/tests/Fixtures/assets/pdf/document.pdf new file mode 100644 index 00000000..2bc47bac Binary files /dev/null and b/tests/Fixtures/assets/pdf/document.pdf differ