From 42795e84c40aa0b71872fab3120c3bee281a86bd Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 28 Nov 2024 11:11:37 +0100 Subject: [PATCH] new cli `alchemy:rendition-factory:config` to generate doc or validate configuration file. mandatory documentation for all modules. refacto. --- .../Resources/config/services.yaml | 50 +-- .../src/Command/ConfigCommand.php | 117 ++---- .../src/Config/YamlLoader.php | 5 +- .../src/DTO/BuildConfig/FamilyBuildConfig.php | 7 + .../src/DTO/BuildConfig/Transformation.php | 16 + .../Video/FFMpeg => }/Format/AacFormat.php | 4 +- .../FFMpeg => }/Format/AnimatedGifFormat.php | 2 +- .../FFMpeg => }/Format/AnimatedPngFormat.php | 2 +- .../FFMpeg => }/Format/AnimatedWebpFormat.php | 2 +- .../Video/FFMpeg => }/Format/Audio/Aac.php | 2 +- .../FFMpeg => }/Format/FormatInterface.php | 2 +- .../Video/FFMpeg => }/Format/JpegFormat.php | 2 +- .../Video/FFMpeg => }/Format/MkvFormat.php | 2 +- .../Video/FFMpeg => }/Format/Mp3Format.php | 2 +- .../Video/FFMpeg => }/Format/Mpeg4Format.php | 2 +- .../Video/FFMpeg => }/Format/MpegFormat.php | 2 +- .../FFMpeg => }/Format/QuicktimeFormat.php | 2 +- .../Video/FFMpeg => }/Format/WavFormat.php | 2 +- .../Video/FFMpeg => }/Format/WebmFormat.php | 2 +- .../src/RenditionCreator.php | 4 +- .../DocumentToPdfTransformerModule.php | 24 ++ .../Document/PdfToImageTransformerModule.php | 24 ++ .../src/Transformer/Documentation.php | 69 ++++ .../Imagine/ImagineTransformerModule.php | 24 ++ .../TransformerModuleInterface.php | 4 +- .../Video/AbstractVideoTransformer.php | 91 ----- .../Video/FFMpegTransformerModule.php | 353 +++++++++--------- .../Transformer/Video/ModuleCommonArgs.php | 2 +- .../Video/VideoSummaryTransformerModule.php | 70 +++- .../VideoToAnimationTransformerModule.php | 78 ++-- .../Video/VideoToFrameTransformerModule.php | 55 ++- .../src/Transformer/VoidTransformerModule.php | 30 +- 32 files changed, 604 insertions(+), 449 deletions(-) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/AacFormat.php (82%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/AnimatedGifFormat.php (87%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/AnimatedPngFormat.php (87%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/AnimatedWebpFormat.php (87%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/Audio/Aac.php (82%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/FormatInterface.php (84%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/JpegFormat.php (87%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/MkvFormat.php (91%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/Mp3Format.php (91%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/Mpeg4Format.php (91%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/MpegFormat.php (91%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/QuicktimeFormat.php (91%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/WavFormat.php (91%) rename lib/php/rendition-factory/src/{Transformer/Video/FFMpeg => }/Format/WebmFormat.php (91%) create mode 100644 lib/php/rendition-factory/src/Transformer/Documentation.php delete mode 100644 lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php diff --git a/lib/php/rendition-factory-bundle/Resources/config/services.yaml b/lib/php/rendition-factory-bundle/Resources/config/services.yaml index a0d27ad61..1e2d8def2 100644 --- a/lib/php/rendition-factory-bundle/Resources/config/services.yaml +++ b/lib/php/rendition-factory-bundle/Resources/config/services.yaml @@ -45,54 +45,54 @@ services: tags: - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } - # FFMpeg "formats" - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\JpegFormat: + # Output "formats" + Alchemy\RenditionFactory\Format\JpegFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MkvFormat: + Alchemy\RenditionFactory\Format\MkvFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\Mpeg4Format: + Alchemy\RenditionFactory\Format\Mpeg4Format: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MpegFormat: + Alchemy\RenditionFactory\Format\MpegFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\QuicktimeFormat: + Alchemy\RenditionFactory\Format\QuicktimeFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\WebmFormat: + Alchemy\RenditionFactory\Format\WebmFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedGifFormat: + Alchemy\RenditionFactory\Format\AnimatedGifFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedPngFormat: + Alchemy\RenditionFactory\Format\AnimatedPngFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedWebpFormat: + Alchemy\RenditionFactory\Format\AnimatedWebpFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\WavFormat: + Alchemy\RenditionFactory\Format\WavFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AacFormat: + Alchemy\RenditionFactory\Format\AacFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\Mp3Format: + Alchemy\RenditionFactory\Format\Mp3Format: tags: - - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } Imagine\Imagick\Imagine: ~ diff --git a/lib/php/rendition-factory/src/Command/ConfigCommand.php b/lib/php/rendition-factory/src/Command/ConfigCommand.php index b6023beab..7ccad1a40 100644 --- a/lib/php/rendition-factory/src/Command/ConfigCommand.php +++ b/lib/php/rendition-factory/src/Command/ConfigCommand.php @@ -6,16 +6,15 @@ use Alchemy\RenditionFactory\Config\YamlLoader; use Alchemy\RenditionFactory\DTO\FamilyEnum; -use Alchemy\RenditionFactory\Transformer\DocumentationTree; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -35,41 +34,43 @@ protected function configure(): void { parent::configure(); - $this->addArgument('build-config', InputArgument::OPTIONAL, 'A build config YAML file to validate') - ->addOption('module', 'm', InputOption::VALUE_REQUIRED, 'Display optiond for a specific module') - ->setHelp('Display the options for a module, or validate a build-config YAML file.') + $this->addArgument('config', InputArgument::OPTIONAL, 'A build config YAML file to validate') + ->setHelp('Display rendition modules documentation, or validate a config file.') ; } protected function execute(InputInterface $input, OutputInterface $output): int { - if ($transformerName = $input->getOption('module')) { - /** @var TransformerModuleInterface $transformer */ - $transformer = $this->transformers->get($transformerName); - $output->writeln($this->getTransformerDocumentation($transformerName, $transformer)); - } - - if ($buildConfigPath = $input->getArgument('build-config')) { - $buildConfig = $this->yamlLoader->load($buildConfigPath); + if (null !== ($configPath = $input->getArgument('config'))) { + $config = $this->yamlLoader->load($configPath); foreach (FamilyEnum::cases() as $family) { - $familyConfig = $buildConfig->getFamily($family); + $familyConfig = $config->getFamily($family); if (null === $familyConfig) { continue; } - - $output->writeln(sprintf('# Family `%s`:', $family->name)); foreach ($familyConfig->getTransformations() as $transformation) { $transformerName = $transformation->getModule(); - // $output->writeln(sprintf(' - %s', $transformerName)); /** @var TransformerModuleInterface $transformer */ $transformer = $this->transformers->get($transformerName); - $output->writeln($this->getTransformerDocumentation($transformerName, $transformer)); - $this->checkTransformerConfiguration($transformerName, $transformer, $transformation->getOptions()); + try { + $this->checkTransformerConfiguration($transformerName, $transformer, $transformation->asArray()); + } catch (\Throwable $e) { + $msg = sprintf("Error in module \"%s\"\n%s", $transformerName, $e->getMessage()); + throw new InvalidConfigurationException($msg); + } } } + $output->writeln('Configuration is valid.'); + } else { + foreach ($this->transformers->getProvidedServices() as $transformerName => $transformerFqcn) { + + /** @var TransformerModuleInterface $transformer */ + $transformer = $this->transformers->get($transformerName); + $output->writeln($this->getTransformerDocumentation($transformerName, $transformer)); + } } return 0; @@ -77,11 +78,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getTransformerDocumentation(string $transformerName, TransformerModuleInterface $transformer): string { - $docToText = function(DocumentationTree $documentation, int $depth=0) use (&$docToText): string { + $docToText = function (Documentation $documentation, int $depth = 0) use (&$docToText): string { $text = ''; - if($t = $documentation->getHeader()) { - $text .= $t . "\n"; + if ($t = $documentation->getHeader()) { + $text .= $t."\n"; } $treeBuilder = $documentation->getTreeBuilder(); @@ -89,76 +90,34 @@ private function getTransformerDocumentation(string $transformerName, Transforme $dumper = new YamlReferenceDumper(); $t = $dumper->dumpNode($node); - $t = preg_replace("#^root:(\n( {4})?|\s+\[])#", "-\n", (string) $t); - $t = str_replace("\n\n", "\n", $t); - $t = str_replace("\n", "\n ", $t); + $t = preg_replace("#^root:($|(\s+)\[]$)#m", "-\n", (string) $t); + $t = preg_replace("#\n+#", "\n", $t); + $t = trim($t); - $text .= "```yaml\n" . $t . "\n```\n"; + $text .= "```yaml\n".$t."\n```\n"; - foreach ($documentation->getChildren() as $child) { - $text .= $docToText($child, $depth+1); + if ($t = $documentation->getFooter()) { + $text .= $t."\n"; } - if($t = $documentation->getFooter()) { - $text .= $t . "\n"; + foreach ($documentation->getChildren() as $child) { + $text .= $docToText($child, $depth + 1); } return $text; }; + $documentation = $transformer->getDocumentation(); - $doc = "\n\n## $transformerName transformer module\n"; - if (method_exists($transformer, 'getDocumentation')) { - $documentation = $transformer->getDocumentation(); - $doc .= $docToText($documentation); - } - - return $doc; + return "## `$transformerName` transformer module\n".$docToText($documentation); } private function checkTransformerConfiguration(string $transformerName, TransformerModuleInterface $transformer, array $options): void { - if (method_exists($transformer, 'getDocumentation')) { - $documentation = $transformer->getDocumentation(); - $treeBuilder = $documentation->getTreeBuilder(); - - $processor = new Processor(); - $processor->process($treeBuilder->buildTree(), ['root' => $options]); - } - } - - private function no_getTransformerDocumentation(string $fqcn, string $transformerName, TransformerModuleInterface $transformer): string - { - $doc = "\n\n## $transformerName\n"; - - $reflectionClass = new \ReflectionClass($fqcn); - if ($reflectionClass->hasMethod('getDocumentationHeader') && $reflectionClass->getMethod('getDocumentationHeader')->class == $fqcn) { - $doc .= $transformer->getDocumentationHeader()."\n"; - } - - if (method_exists($transformer, 'buildConfiguration')) { - $treeBuilder = new TreeBuilder('root'); - $transformer->buildConfiguration($treeBuilder->getRootNode()->children()); - - $node = $treeBuilder->buildTree(); - $dumper = new YamlReferenceDumper(); - - $t = $dumper->dumpNode($node); - $t = preg_replace("#^root:(\n( {4})?|\s+\[])#", "-\n", (string) $t); - $t = str_replace("\n\n", "\n", $t); - $t = str_replace("\n", "\n ", $t); - // $t = preg_replace("#^root:(\n( {4})?|\s+\[])#", '', (string)$t); - // $t = preg_replace("#\n {4}#", "\n", $t); - // $t = preg_replace("#\n\n#", "\n", $t); - // $t = trim(preg_replace("#^\n+#", '', $t)); - - $doc .= "```yaml\n".$t."```\n"; - } - - if ($reflectionClass->hasMethod('getDocumentationFooter') && $reflectionClass->getMethod('getDocumentationFooter')->class == $fqcn) { - $doc .= $transformer->getDocumentationFooter()."\n"; - } + $documentation = $transformer->getDocumentation(); + $treeBuilder = $documentation->getTreeBuilder(); - return $doc; + $processor = new Processor(); + $processor->process($treeBuilder->buildTree(), ['root' => $options]); } } diff --git a/lib/php/rendition-factory/src/Config/YamlLoader.php b/lib/php/rendition-factory/src/Config/YamlLoader.php index 372df8806..bed285f97 100644 --- a/lib/php/rendition-factory/src/Config/YamlLoader.php +++ b/lib/php/rendition-factory/src/Config/YamlLoader.php @@ -60,9 +60,7 @@ private function parseFamilyConfig(array $data): FamilyBuildConfig $transformations = []; foreach ($data['transformations'] as $transformation) { - if ($transformation['enabled'] ?? true) { - $transformations[] = $this->parseTransformation($transformation); - } + $transformations[] = $this->parseTransformation($transformation); } return new FamilyBuildConfig($transformations, $data['normalization'] ?? []); @@ -72,6 +70,7 @@ private function parseTransformation(array $transformation): Transformation { return new Transformation( $transformation['module'], + $transformation['enabled'] ?? true, $transformation['options'] ?? [], $transformation['description'] ?? null ); diff --git a/lib/php/rendition-factory/src/DTO/BuildConfig/FamilyBuildConfig.php b/lib/php/rendition-factory/src/DTO/BuildConfig/FamilyBuildConfig.php index 9d4293d0b..68829b05a 100644 --- a/lib/php/rendition-factory/src/DTO/BuildConfig/FamilyBuildConfig.php +++ b/lib/php/rendition-factory/src/DTO/BuildConfig/FamilyBuildConfig.php @@ -17,6 +17,13 @@ public function getTransformations(): array return $this->transformations; } + public function getEnabledTransformations(): array + { + return array_filter($this->transformations, function (Transformation $transformation) { + return $transformation->isEnabled(); + }); + } + public function getNormalization(): array { return $this->normalization; diff --git a/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php b/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php index e4c8b30ce..202656432 100644 --- a/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php +++ b/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php @@ -6,6 +6,7 @@ { public function __construct( private string $module, + private bool $enabled, private array $options, private ?string $description, ) { @@ -25,4 +26,19 @@ public function getOptions(): array { return $this->options; } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function asArray(): array + { + return [ + 'module' => $this->module, + 'enabled' => $this->enabled, + 'options' => $this->options, + 'description' => $this->description, + ]; + } } diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php b/lib/php/rendition-factory/src/Format/AacFormat.php similarity index 82% rename from lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php rename to lib/php/rendition-factory/src/Format/AacFormat.php index 3dd5c5300..16b4188bd 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php +++ b/lib/php/rendition-factory/src/Format/AacFormat.php @@ -1,9 +1,9 @@ getFamily()->value, $mimeType); } - $transformations = $familyBuildConfig->getTransformations(); + $transformations = $familyBuildConfig->getEnabledTransformations(); if (empty($transformations)) { NoBuildConfigException::throwNoTransformation($inputFile->getFamily()->value, $mimeType); } @@ -98,7 +98,7 @@ public function buildHashesDiffer( return true; } - $transformations = $familyBuildConfig->getTransformations(); + $transformations = $familyBuildConfig->getEnabledTransformations(); if (empty($transformations)) { return true; } diff --git a/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php index e1546d0ea..875b7f926 100644 --- a/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php @@ -8,7 +8,9 @@ use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; use Alchemy\RenditionFactory\Transformer\Document\Libreoffice\PdfConverter; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; final readonly class DocumentToPdfTransformerModule implements TransformerModuleInterface { @@ -17,6 +19,28 @@ public static function getName(): string return 'document_to_pdf'; } + public static function getDocumentation(): Documentation + { + static $doc = null; + if (null === $doc) { + $treeBuilder = Documentation::createBaseTree(self::getName()); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
getType()) { diff --git a/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php index 0447d9c16..fbf85a3ca 100644 --- a/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php @@ -7,9 +7,11 @@ use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use Spatie\PdfToImage\Enums\OutputFormat; use Spatie\PdfToImage\Pdf; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; final readonly class PdfToImageTransformerModule implements TransformerModuleInterface { @@ -18,6 +20,28 @@ public static function getName(): string return 'pdf_to_image'; } + public static function getDocumentation(): Documentation + { + static $doc = null; + if (null === $doc) { + $treeBuilder = Documentation::createBaseTree(self::getName()); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
getType()) { diff --git a/lib/php/rendition-factory/src/Transformer/Documentation.php b/lib/php/rendition-factory/src/Transformer/Documentation.php new file mode 100644 index 000000000..e90a4b40b --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Documentation.php @@ -0,0 +1,69 @@ +children = []; + } + + public function addChild(Documentation $child): void + { + $this->children[] = $child; + } + + public function getTreeBuilder(): TreeBuilder + { + return $this->treeBuilder; + } + + public function getChildren(): array + { + return $this->children; + } + + public function getHeader(): string + { + return $this->header; + } + + public function getFooter(): string + { + return $this->footer; + } + + /** + * helper to create a base tree for a module, including common options. + */ + public static function createBaseTree(string $name): TreeBuilder + { + $treeBuilder = new TreeBuilder('root'); + $rootNode = $treeBuilder->getRootNode(); + // @formatter:off + $rootNode + ->children() + ->scalarNode('module') + ->isRequired() + ->defaultValue($name) + ->end() + ->scalarNode('description') + ->info('Description of the module action') + ->end() + ->scalarNode('enabled') + ->defaultTrue() + ->info('Whether to enable this module') + ->end() + ->end() + ; + // @formatter:on + + return $treeBuilder; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Image/Imagine/ImagineTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Image/Imagine/ImagineTransformerModule.php index e7722e3ef..97c428821 100644 --- a/lib/php/rendition-factory/src/Transformer/Image/Imagine/ImagineTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Image/Imagine/ImagineTransformerModule.php @@ -9,8 +9,10 @@ use Alchemy\RenditionFactory\DTO\OutputFileInterface; use Alchemy\RenditionFactory\MimeType\ImageFormatGuesser; use Alchemy\RenditionFactory\Transformer\BuildHashDiffInterface; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use Liip\ImagineBundle\Model\FileBinary; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; final readonly class ImagineTransformerModule implements TransformerModuleInterface, BuildHashDiffInterface { @@ -24,6 +26,28 @@ public static function getName(): string return 'imagine'; } + public static function getDocumentation(): Documentation + { + static $doc = null; + if (null === $doc) { + $treeBuilder = Documentation::createBaseTree(self::getName()); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
getType()); diff --git a/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php index 783d394ca..af4587720 100644 --- a/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php +++ b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php @@ -5,7 +5,6 @@ use Alchemy\RenditionFactory\Context\TransformationContextInterface; use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFileInterface; -use Symfony\Component\Config\Definition\Builder\NodeBuilder; interface TransformerModuleInterface { @@ -15,6 +14,5 @@ public static function getName(): string; public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface; -// public function buildConfiguration(NodeBuilder $builder): void; -// public function getExtraConfigurationBuilders(): iterable; + public static function getDocumentation(): Documentation; } diff --git a/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php b/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php deleted file mode 100644 index 614c6ab92..000000000 --- a/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php +++ /dev/null @@ -1,91 +0,0 @@ -getRootNode()->children()); - $doc = new DocumentationTree( + $doc = new Documentation( $treeBuilder, <<
$builder) { - $tree = new TreeBuilder($name); + $tree = new TreeBuilder('root'); $builder($tree->getRootNode()); - $doc->addChild(new DocumentationTree( + $doc->addChild(new Documentation( $tree, <<
scalarNode('format') - ->info('output format') - ->end() - ->scalarNode('video_codec') - ->info('Change the default video codec used by the output format') - ->end() - ->scalarNode('audio_codec') - ->info('Change the default audio codec used by the output format') - ->end() - ->scalarNode('video_kilobitrate') - ->info('Change the default video_kilobitrate used by the output format') - ->end() - ->scalarNode('audio_kilobitrate') - ->info('Change the default audio_kilobitrate used by the output format') - ->end() - ->scalarNode('passes') - ->defaultValue(2) - ->info('Change the number of ffmpeg passes') - ->end() - ->scalarNode('timeout') - ->info('Change the default timeout used by ffmpeg (defaults to symphony process timeout)') - ->end() - ->scalarNode('threads') - ->info('Change the default number of threads used by ffmpeg') - ->end() - ->arrayNode('filters') - ->info('Filters to apply to the video') - ->arrayPrototype() - ->info('see list of available filters below') - ->validate()->always()->then(function ($x) { - self::validateFilter($x); - })->end() - ->children() - ->scalarNode('name') - ->isRequired() - ->info('Name of the filter') -// ->validate() -// ->ifNotInArray(['pre_clip', 'remove_audio', 'resize', 'rotate', 'pad', 'crop', 'clip', 'synchronize', 'watermark', 'framerate']) -// ->thenInvalid('Invalid filter') -// ->end() - ->end() - ->scalarNode('enabled') - ->defaultTrue() - ->info('Whether to enable the filter') + ->arrayNode('options') + ->info('Options for the module') + ->children() + ->scalarNode('format') + ->info('output format') + ->end() + ->scalarNode('extension') + ->info('extension of the output file') + ->end() + ->scalarNode('video_codec') + ->info('Change the default video codec used by the output format') + ->end() + ->scalarNode('audio_codec') + ->info('Change the default audio codec used by the output format') + ->end() + ->scalarNode('video_kilobitrate') + ->info('Change the default video_kilobitrate used by the output format') + ->end() + ->scalarNode('audio_kilobitrate') + ->info('Change the default audio_kilobitrate used by the output format') + ->end() + ->scalarNode('passes') + ->defaultValue(2) + ->info('Change the number of ffmpeg passes') + ->end() + ->scalarNode('timeout') + ->info('Change the default timeout used by ffmpeg (defaults to symphony process timeout)') + ->end() + ->scalarNode('threads') + ->info('Change the default number of threads used by ffmpeg') + ->end() + ->arrayNode('filters') + ->info('Filters to apply to the video') + ->arrayPrototype() + ->info('see list of available filters below') + ->validate()->always()->then(function ($x) { + self::validateFilter($x); + })->end() + ->children() + ->scalarNode('name') + ->isRequired() + ->info('Name of the filter') + // ->validate() + // ->ifNotInArray(['pre_clip', 'remove_audio', 'resize', 'rotate', 'pad', 'crop', 'clip', 'synchronize', 'watermark', 'framerate']) + // ->thenInvalid('Invalid filter') + // ->end() + ->end() + ->scalarNode('enabled') + ->defaultTrue() + ->info('Whether to enable the filter') + ->end() ->end() + // false: (undocumented) ignore extra keys on general validation, but do NOT suppress (so the validate..then() can check them) + ->ignoreExtraKeys(false) ->end() - // false: (undocumented) ignore extra keys on general validation, but do NOT suppress (so the validate..then() can check them) - ->ignoreExtraKeys(false) ->end() ->end() - ->end() - ->end(); + ->end(); // @formatter:on } @@ -141,12 +157,12 @@ private static function getExtraConfigurationBuilders(): iterable $root ->info('Clip the video before applying other filters') ->children() - ->scalarNode('name')->isRequired()->end() + ->scalarNode('name')->isRequired()->defaultValue('pre_clip')->end() ->scalarNode('enabled')->defaultTrue()->end() ->scalarNode('start') ->defaultValue(0) ->info('Offset of frame in seconds or timecode') - ->example('2.5 ; "00:00:02.500" ; "{{ metadata.start }}"') + ->example('2.5 ; "00:00:02.500" ; "{{ attr.start }}"') ->end() ->scalarNode('duration') ->defaultValue(null) @@ -159,160 +175,160 @@ private static function getExtraConfigurationBuilders(): iterable $root ->info('Clip the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('start') - ->defaultValue(0) - ->info('Offset of frame in seconds or timecode') - ->example('2.5 ; "00:00:02.500" ; "{{ metadata.start }}"') - ->end() - ->scalarNode('duration') - ->defaultValue(null) - ->info('Duration in seconds or timecode') - ->example('30 ; "00:00:30" ; "{{ input.duration/2 }}"') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('clip')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('start') + ->defaultValue(0) + ->info('Offset of frame in seconds or timecode') + ->example('2.5 ; "00:00:02.500" ; "{{ attr.start }}"') + ->end() + ->scalarNode('duration') + ->defaultValue(null) + ->info('Duration in seconds or timecode') + ->example('30 ; "00:00:30" ; "{{ input.duration/2 }}"') + ->end() ->end(); }, 'remove_audio' => function (ArrayNodeDefinition $root): void { $root ->info('Remove the audio from the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('name')->isRequired()->defaultValue('remove_audio')->end() + ->scalarNode('enabled')->defaultTrue()->end() ->end(); }, 'resize' => function (ArrayNodeDefinition $root): void { $root ->info('Resize the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('width') - ->isRequired() - ->info('Width of the video') - ->end() - ->scalarNode('height') - ->isRequired() - ->info('Height of the video') - ->end() - ->scalarNode('mode') - ->defaultValue(FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET) - ->info('Resize mode') - ->example('inset') - ->end() - ->scalarNode('force_standards') - ->defaultValue(true) - ->info('Correct the width/height to the closest "standard" size') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('resize')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('width') + ->isRequired() + ->info('Width of the video') + ->end() + ->scalarNode('height') + ->isRequired() + ->info('Height of the video') + ->end() + ->scalarNode('mode') + ->defaultValue(FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET) + ->info('Resize mode') + ->example('inset') + ->end() + ->scalarNode('force_standards') + ->defaultValue(true) + ->info('Correct the width/height to the closest "standard" size') + ->end() ->end(); }, 'rotate' => function (ArrayNodeDefinition $root): void { $root ->info('Rotate the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('angle') - ->isRequired() - ->info('Angle of rotation [0 | 90 | 180 | 270]') - ->example('90') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('rotate')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('angle') + ->isRequired() + ->info('Angle of rotation [0 | 90 | 180 | 270]') + ->example('90') + ->end() ->end(); }, 'pad' => function (ArrayNodeDefinition $root): void { $root ->info('Pad the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('width') - ->isRequired() - ->info('Width of the video') - ->end() - ->scalarNode('height') - ->isRequired() - ->info('Height of the video') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('pad')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('width') + ->isRequired() + ->info('Width of the video') + ->end() + ->scalarNode('height') + ->isRequired() + ->info('Height of the video') + ->end() ->end(); }, 'crop' => function (ArrayNodeDefinition $root): void { $root ->info('Crop the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('x') - ->isRequired() - ->info('X coordinate') - ->end() - ->scalarNode('y') - ->isRequired() - ->info('Y coordinate') - ->end() - ->scalarNode('width') - ->isRequired() - ->info('Width of the video') - ->end() - ->scalarNode('height') - ->isRequired() - ->info('Height of the video') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('crop')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('x') + ->isRequired() + ->info('X coordinate') + ->end() + ->scalarNode('y') + ->isRequired() + ->info('Y coordinate') + ->end() + ->scalarNode('width') + ->isRequired() + ->info('Width of the video') + ->end() + ->scalarNode('height') + ->isRequired() + ->info('Height of the video') + ->end() ->end(); }, 'watermark' => function (ArrayNodeDefinition $root): void { $root ->info('Apply a watermark on the video') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('position') - ->isRequired() - ->info('"relative" or "absolute" position') - ->end() - ->scalarNode('path') - ->isRequired() - ->info('Path to the watermark image') - ->end() - ->scalarNode('top') - ->info('top coordinate (only if position is "relative", set top OR bottom)') - ->end() - ->scalarNode('bottom') - ->info('bottom coordinate (only if position is "relative", set top OR bottom)') - ->end() - ->scalarNode('left') - ->info('left coordinate (only if position is "relative", set left OR right)') - ->end() - ->scalarNode('right') - ->info('right coordinate (only if position is "relative", set left OR right)') - ->end() - ->scalarNode('x') - ->info('X coordinate (only if position is "absolute")') - ->end() - ->scalarNode('y') - ->info('Y coordinate (only if position is "absolute")') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('watermark')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('position') + ->isRequired() + ->info('"relative" or "absolute" position') + ->end() + ->scalarNode('path') + ->isRequired() + ->info('Path to the watermark image') + ->end() + ->scalarNode('top') + ->info('top coordinate (only if position is "relative", set top OR bottom)') + ->end() + ->scalarNode('bottom') + ->info('bottom coordinate (only if position is "relative", set top OR bottom)') + ->end() + ->scalarNode('left') + ->info('left coordinate (only if position is "relative", set left OR right)') + ->end() + ->scalarNode('right') + ->info('right coordinate (only if position is "relative", set left OR right)') + ->end() + ->scalarNode('x') + ->info('X coordinate (only if position is "absolute")') + ->end() + ->scalarNode('y') + ->info('Y coordinate (only if position is "absolute")') + ->end() ->end(); }, 'framerate' => function (ArrayNodeDefinition $root): void { $root ->info('Change the framerate') ->children() - ->scalarNode('name')->isRequired()->end() - ->scalarNode('enabled')->defaultTrue()->end() - ->scalarNode('framerate') - ->isRequired() - ->info('framerate') - ->end() - ->scalarNode('gop') - ->info('gop') - ->end() + ->scalarNode('name')->isRequired()->defaultValue('framerate')->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('framerate') + ->isRequired() + ->info('framerate') + ->end() + ->scalarNode('gop') + ->info('gop') + ->end() ->end(); }, 'synchronize' => function (ArrayNodeDefinition $root): void { $root ->info('re-synchronize audio and video') ->children() - ->scalarNode('name')->isRequired()->end() + ->scalarNode('name')->isRequired()->defaultValue('synchronize')->end() ->scalarNode('enabled')->defaultTrue()->end() ->end(); }, @@ -322,7 +338,6 @@ private static function getExtraConfigurationBuilders(): iterable return $configurations; } - public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { $context->log("Applying '".self::getName()."' module"); diff --git a/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php b/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php index e2fb15e1e..fd857a819 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php +++ b/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php @@ -4,7 +4,7 @@ use Alchemy\RenditionFactory\Config\ModuleOptionsResolver; use Alchemy\RenditionFactory\Context\TransformationContextInterface; -use Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface; +use Alchemy\RenditionFactory\Format\FormatInterface; use FFMpeg; use Symfony\Component\DependencyInjection\ServiceLocator; diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php index 14a04ade8..a84a837fc 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php @@ -2,53 +2,91 @@ namespace Alchemy\RenditionFactory\Transformer\Video; +use Alchemy\RenditionFactory\Config\ModuleOptionsResolver; use Alchemy\RenditionFactory\Context\TransformationContextInterface; use Alchemy\RenditionFactory\DTO\FamilyEnum; use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Alchemy\RenditionFactory\Format\FormatInterface; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use FFMpeg; use FFMpeg\Coordinate\TimeCode; use FFMpeg\Format\VideoInterface; +use Imagine\Image\ImagineInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; +use Symfony\Component\DependencyInjection\ServiceLocator; -final readonly class VideoSummaryTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface +final readonly class VideoSummaryTransformerModule implements TransformerModuleInterface { + public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] private ServiceLocator $formats, + private ModuleOptionsResolver $optionsResolver, + private ImagineInterface $imagine, + ) { + } + public static function getName(): string { return 'video_summary'; } - public function buildConfiguration(NodeBuilder $builder): void + public static function getDocumentation(): Documentation { + static $doc = null; + if (null === $doc) { + $treeBuilder = Documentation::createBaseTree(self::getName()); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
scalarNode('module') - ->isRequired() - ->defaultValue(self::getName()) - ->end() - ->booleanNode('enabled') - ->defaultTrue() - ->info('Whether to enable this module') - ->end() ->arrayNode('options') ->info('Options for the module') ->children() + ->scalarNode('start') + ->defaultValue(0) + ->info('Skip video start, in seconds or timecode') + ->example('2.5 ; "00:00:02.50" ; "{{ attr.start }}"') + ->end() ->scalarNode('period') ->isRequired() ->info('Extract one video clip every period, in seconds or timecode') ->example('5 ; "00:00:05.00"') - ->end() + ->end() ->scalarNode('duration') ->isRequired() ->info('Duration of each clip, in seconds or timecode') ->example('0.25 ; "00:00:00.25"') - ->end() + ->end() + ->scalarNode('format') + ->isRequired() + ->info('Output format') + ->example('video-mpeg') + ->end() + ->scalarNode('extension') + ->defaultValue('default extension from format') + ->info('extension of the output file') + ->example('mpeg') + ->end() ->end() ->end() ; + // @formatter:on } /** @@ -72,7 +110,7 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo $resolverContext = $context->getTemplatingContext(); $resolverContext['input'] = $video->getStreams()->videos()->first()->all(); - + $period = $this->optionsResolver->resolveOption($options['period'] ?? 0, $resolverContext); $periodAsTimecode = FFMpegHelper::optionAsTimecode($period); if (null === $periodAsTimecode || ($period = FFMpegHelper::timecodeToseconds($periodAsTimecode)) <= 0) { @@ -111,8 +149,7 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo $FFMpegOutputFormat->setAudioCodec($audioCodec); } - // todo: allow to choose other extension - $clipsExtension = $outputFormat->getAllowedExtensions()[0]; + $clipsExtension = $commonArgs->getExtension(); try { $clipsFiles = []; @@ -150,8 +187,7 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo } } } elseif (FamilyEnum::Animation === $outputFormat->getFamily()) { - // todo: allow to choose other extension - $clipsExtension = $outputFormat->getAllowedExtensions()[0]; + $clipsExtension = $commonArgs->getExtension(); try { $clipsFiles = []; $usableInputDuration = ($inputDuration - $start); diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php index d17e0eaa3..a22c145fd 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -2,68 +2,78 @@ namespace Alchemy\RenditionFactory\Transformer\Video; +use Alchemy\RenditionFactory\Config\ModuleOptionsResolver; use Alchemy\RenditionFactory\Context\TransformationContextInterface; use Alchemy\RenditionFactory\DTO\FamilyEnum; use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Alchemy\RenditionFactory\Format\FormatInterface; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use FFMpeg; use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; +use Symfony\Component\DependencyInjection\ServiceLocator; -final readonly class VideoToAnimationTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface +final readonly class VideoToAnimationTransformerModule implements TransformerModuleInterface { + public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] private ServiceLocator $formats, + private ModuleOptionsResolver $optionsResolver, + ) { + } + public static function getName(): string { return 'video_to_animation'; } - public static function getDocumentationHeader(): ?string + public static function getDocumentation(): Documentation { - return 'Converts a video to an animated gif'; + static $doc = null; + if (null === $doc) { + $treeBuilder = Documentation::createBaseTree(self::getName()); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
scalarNode('module') - ->isRequired() - ->defaultValue(self::getName()) - ->end() - ->booleanNode('enabled') - ->defaultTrue() - ->info('Whether to enable this module') - ->end() ->arrayNode('options') ->info('Options for the module') ->children() ->scalarNode('start') ->defaultValue(0) ->info('Start time in seconds or timecode') - ->example('2.5 ; "00:00:02.50" ; "{{ metadata.start }}"') - ->end() + ->example('2.5 ; "00:00:02.50" ; "{{ attr.start }}"') + ->end() ->scalarNode('duration') ->defaultValue(null) ->info('Duration in seconds or timecode') ->example('30 ; "00:00:30.00" ; "{{ input.duration/2 }}"') - ->end() - ->integerNode('fps') + ->end() + ->scalarNode('fps') ->defaultValue(1) ->info('Frames per second') - ->end() - ->integerNode('width') + ->end() + ->scalarNode('width') ->defaultValue(null) ->info('Width in pixels') - ->end() - ->integerNode('height') + ->end() + ->scalarNode('height') ->defaultValue(null) ->info('Height in pixels') - ->end() + ->end() ->enumNode('mode') ->values([ FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET, @@ -74,10 +84,20 @@ public function buildConfiguration(NodeBuilder $builder): void ]) ->defaultValue(FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET) ->info('Resize mode') - ->end() + ->end() + ->scalarNode('format') + ->isRequired() + ->info('Output format') + ->example('animated-png') + ->end() + ->scalarNode('extension') + ->defaultValue('default extension from format') + ->info('extension of the output file') + ->example('apng') + ->end() ->end() - ->end() - ; + ->end(); + // @formatter:on } public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php index b6840f511..4d4239c3f 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -2,44 +2,75 @@ namespace Alchemy\RenditionFactory\Transformer\Video; +use Alchemy\RenditionFactory\Config\ModuleOptionsResolver; use Alchemy\RenditionFactory\Context\TransformationContextInterface; use Alchemy\RenditionFactory\DTO\FamilyEnum; use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Alchemy\RenditionFactory\Format\FormatInterface; +use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use FFMpeg\Media\Video; use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; +use Symfony\Component\DependencyInjection\ServiceLocator; -final readonly class VideoToFrameTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface +final readonly class VideoToFrameTransformerModule implements TransformerModuleInterface { + public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] private ServiceLocator $formats, + private ModuleOptionsResolver $optionsResolver, + ) { + } + public static function getName(): string { return 'video_to_frame'; } - public function buildConfiguration(NodeBuilder $builder): void + public static function getDocumentation(): Documentation { + static $doc = null; + if (null === $doc) { + $treeBuilder = Documentation::createBaseTree(self::getName()); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
scalarNode('module') - ->isRequired() - ->defaultValue(self::getName()) - ->end() - ->booleanNode('enabled') - ->defaultTrue() - ->info('Whether to enable this module') - ->end() ->arrayNode('options') ->info('Options for the module') ->children() ->scalarNode('start') ->defaultValue(0) ->info('Offset of frame in seconds or timecode') - ->example('2.5 ; "00:00:02.50" ; "{{ metadata.start }}"') - ->end() + ->example('2.5 ; "00:00:02.50" ; "{{ attr.start }}"') + ->end() + ->scalarNode('format') + ->isRequired() + ->info('Output format') + ->example('image-jpeg') + ->end() + ->scalarNode('extension') + ->defaultValue('default extension from format') + ->info('extension of the output file') + ->example('jpg') + ->end() ->end() ->end() ; + // @formatter:on } public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface diff --git a/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php b/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php index 1a79150e4..63628771e 100644 --- a/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php @@ -4,16 +4,40 @@ use Alchemy\RenditionFactory\Context\TransformationContextInterface; use Alchemy\RenditionFactory\DTO\InputFileInterface; -use Alchemy\RenditionFactory\DTO\OutputFile; +use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; class VoidTransformerModule implements TransformerModuleInterface { public static function getName(): string { - return 'Void'; + return 'void'; } - public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFile + public static function getDocumentation(): Documentation + { + static $doc = null; + if (null === $doc) { + $treeBuilder = new TreeBuilder('root'); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
createOutputFile(); }