From e0428741fa8b9f8cf4ff64d821ee1e1e3846bfa3 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 14 Nov 2024 16:47:07 +0100 Subject: [PATCH 01/12] WIP DO NOT MERGE - auto doc WIP - more "twigable" options + factorization - almost every "time" options supports float (2.5) or timecode ("00.00.02.50") - video-summary module can generate "phraseanet" animated gif - period sets the gap between frames from the _input video_, e.g. `"{{ input.duration/10 }}"` -> 10 frames extracted. - duration sets the duration in seconds of each frame into the _output animated gif_, e.g. `0.1` -> 10 frames / seconds. - video-summary module supports `start` - ffmpeg module can extract audio: set `format: "audio-wav"` or `audio-mp3`, `audio-aac` ; todo: audio options - ffmpeg module / filter "watermark" supports `path: ` - bypass ffmpeg bugs (timecode / seconds) --- .../Resources/config/services.yaml | 13 + .../src/Command/ConfigCommand.php | 169 +++++++++ .../Video/AbstractVideoTransformer.php | 75 ++++ .../Video/FFMpeg/Format/AacFormat.php | 41 ++ .../Video/FFMpeg/Format/Audio/Aac.php | 25 ++ .../Video/FFMpeg/Format/Mp3Format.php | 41 ++ .../Video/FFMpeg/Format/WavFormat.php | 41 ++ .../src/Transformer/Video/FFMpegHelper.php | 29 +- .../Video/FFMpegTransformerModule.php | 353 +++++++++++++++--- .../Video/VideoSummaryTransformerModule.php | 184 ++++++--- .../VideoToAnimationTransformerModule.php | 89 ++++- .../Video/VideoToFrameTransformerModule.php | 38 +- 12 files changed, 982 insertions(+), 116 deletions(-) create mode 100644 lib/php/rendition-factory/src/Command/ConfigCommand.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Audio/Aac.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mp3Format.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WavFormat.php diff --git a/lib/php/rendition-factory-bundle/Resources/config/services.yaml b/lib/php/rendition-factory-bundle/Resources/config/services.yaml index 16dbfd75c..a0d27ad61 100644 --- a/lib/php/rendition-factory-bundle/Resources/config/services.yaml +++ b/lib/php/rendition-factory-bundle/Resources/config/services.yaml @@ -4,6 +4,7 @@ services: autoconfigure: true Alchemy\RenditionFactory\Command\CreateCommand: ~ + Alchemy\RenditionFactory\Command\ConfigCommand: ~ Alchemy\RenditionFactory\Context\TransformationContextFactory: ~ Alchemy\RenditionFactory\FileFamilyGuesser: ~ @@ -81,6 +82,18 @@ services: tags: - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\WavFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AacFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\Mp3Format: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + Imagine\Imagick\Imagine: ~ Imagine\Image\ImagineInterface: '@Imagine\Imagick\Imagine' diff --git a/lib/php/rendition-factory/src/Command/ConfigCommand.php b/lib/php/rendition-factory/src/Command/ConfigCommand.php new file mode 100644 index 000000000..7ebc66b90 --- /dev/null +++ b/lib/php/rendition-factory/src/Command/ConfigCommand.php @@ -0,0 +1,169 @@ +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.') + ; + } + + 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); + + foreach (FamilyEnum::cases() as $family) { + $familyConfig = $buildConfig->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, $transformation->getOptions())); + } + } + } else { + $transformers = array_flip($this->transformers->getProvidedServices()); + ksort($transformers); + $last_parent = null; + foreach ($transformers as $fqcn => $transformerName) { + /** @var TransformerModuleInterface $transformer */ + $transformer = $this->transformers->get($transformerName); + // $parent = get_parent_class($transformer); + // if ($parent !== $last_parent) { + // if ($last_parent) { + // // $output->writeln("\n\n## parent foot: $last_parent\n"); + // $output->writeln($last_parent::getDocumentationFooter()); + // } + // if ($parent) { + // // $output->writeln("\n\n## parent head: $parent\n"); + // $output->writeln($parent::getDocumentationHeader()); + // } + // $last_parent = $parent; + // } + $output->writeln($this->no_getTransformerDocumentation($fqcn, $transformerName, $transformer)); + } + } + + return 0; + } + + private function getTransformerDocumentation(string $transformerName, TransformerModuleInterface $transformer, array $options): string + { + $doc = "\n\n## $transformerName\n"; + + if (method_exists($transformer, 'getDocumentationHeader')) { + $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"; + // var_dump($options); + + $processor = new Processor(); + $processor->process($treeBuilder->buildTree(), ['root' => $options]); + + } + + if (method_exists($transformer, 'getDocumentationFooter')) { + $doc .= $transformer->getDocumentationFooter()."\n"; + } + + return $doc; + } + + 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"; + } + + return $doc; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php b/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php index bc55d1c62..614c6ab92 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php +++ b/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php @@ -4,6 +4,7 @@ use Alchemy\RenditionFactory\Config\ModuleOptionsResolver; use Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface; +use Imagine\Image\ImagineInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -11,6 +12,80 @@ { public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] protected ServiceLocator $formats, protected ModuleOptionsResolver $optionsResolver, + protected ImagineInterface $imagine, ) { } + + public static function no_getDocumentationHeader(): ?string + { + return <<<_DOC_ + # Rendition Factory for video-input modules (wip) + + ## Common options + + ### `enabled` (optional) + + Used to disable a whole module from the build chain. + + __default__: true + + ### `format` (mandatory) + + A format defines the output file : + - family (image, video, audio, animation, document, unknown) + - mime type (unique mime type for this type of file) + - extension (possible extenstion(s) for this type of file) + + For a specific module, only a subset of formats may be available, e.g.: + Since `video_to_frame` extracts one image from the video, the only supported output format(s) + are ones of family=image. + + see below "Output formats" for the list of available formats. + + -------------------------------------------- + + # Modules + + _DOC_; + } + + public static function no_getDocumentationFooter(): ?string + { + return <<<_DOC_ + -------------------------------------------- + + ## Output formats + + | format | family | mime type | extension(s) | + |-----------------|-----------|------------------|--------------| + | animated-gif | Animation | image/gif | gif | + | animated-png | Animation | image/png | apng, png | + | animated-webp | Animation | image/webp | webp | + | image-jpeg | Image | image/jpeg | jpg, jpeg | + | video-mkv | Video | video/x-matroska | mkv | + | video-mpeg4 | Video | video/mp4 | mp4 | + | video-mpeg | Video | video/mpeg | mpeg | + | video-quicktime | Video | video/quicktime | mov | + | video-webm | Video | video/webm | webm | + + -------------------------------------------- + + ## Resize modes + ### `inset` + The output is garanteed to fit in the requested size (width, height) and the aspect ratio is kept. + - If only one dimension is provided, the other is computed. + - If both dimensions are provided, the output is resize so the biggest dimension fits into the rectangle. + - If no dimension is provided, the output is the same size as the input. + + -------------------------------------------- + + ## twig context + input.width + + input.height + + input.duration + + _DOC_; + } } diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php new file mode 100644 index 000000000..3dd5c5300 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AacFormat.php @@ -0,0 +1,41 @@ +format = new Aac(); + } + + public static function getAllowedExtensions(): array + { + return ['aac', 'm4a']; + } + + public static function getMimeType(): string + { + return 'audio/aac'; + } + + public static function getFormat(): string + { + return 'audio-aac'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Audio; + } + + public function getFFMpegFormat(): Aac + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Audio/Aac.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Audio/Aac.php new file mode 100644 index 000000000..a263976c5 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Audio/Aac.php @@ -0,0 +1,25 @@ +audioCodec = 'aac'; + } + + public function getAvailableAudioCodecs() + { + return ['aac']; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mp3Format.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mp3Format.php new file mode 100644 index 000000000..6a02758e3 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mp3Format.php @@ -0,0 +1,41 @@ +format = new Mp3(); + } + + public static function getAllowedExtensions(): array + { + return ['mp3']; + } + + public static function getMimeType(): string + { + return 'audio/mp3'; + } + + public static function getFormat(): string + { + return 'audio-mp3'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Audio; + } + + public function getFFMpegFormat(): Mp3 + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WavFormat.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WavFormat.php new file mode 100644 index 000000000..99eec3d9a --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WavFormat.php @@ -0,0 +1,41 @@ +format = new Wav(); + } + + public static function getAllowedExtensions(): array + { + return ['wav']; + } + + public static function getMimeType(): string + { + return 'audio/wav'; + } + + public static function getFormat(): string + { + return 'audio-wav'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Audio; + } + + public function getFFMpegFormat(): Wav + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php index c232e1bc6..225209b59 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php @@ -3,6 +3,7 @@ namespace Alchemy\RenditionFactory\Transformer\Video; use FFMpeg; +use FFMpeg\Coordinate\TimeCode; class FFMpegHelper { @@ -39,9 +40,35 @@ public static function coordAsText(array $coord): string { $s = []; foreach ($coord as $k => $v) { - $s[] = sprintf('%s=%d', $k, $v); + $s[] = sprintf('%s=%s', $k, $v); } return '['.implode(', ', $s).']'; } + + public static function optionAsTimecode($value): ?TimeCode + { + if (is_numeric($value) && $value >= 0.0) { + return TimeCode::fromSeconds($value); + } elseif (is_string($value)) { + return TimeCode::fromString($value); + } + + return null; + } + + public static function timecodeToseconds(TimeCode $timecode): float + { + if (preg_match('/^[0-9]+:[0-9]+:[0-9]+\.[0-9]+$/', (string) $timecode)) { + [$hours, $minutes, $seconds, $frames] = sscanf($timecode, '%d:%d:%d.%d'); + } + $s = 0.0; + + $s += $hours * 60 * 60; + $s += $minutes * 60; + $s += $seconds; + $s += $frames / 100; + + return $s; + } } diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index 854d593e7..615eceb31 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -13,6 +13,11 @@ use FFMpeg\Format\FormatInterface as FFMpegFormatInterface; use FFMpeg\Media\Clip; use FFMpeg\Media\Video; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; +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; final readonly class FFMpegTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface { @@ -21,6 +26,274 @@ public static function getName(): string return 'ffmpeg'; } + public function buildConfiguration(NodeBuilder $builder): void + { + // @formatter:off + $builder + ->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() + ->arrayNode('filters') + ->info('Filters to apply to the video') + ->arrayPrototype() + ->info('see list of available filters below') + ->validate()->always()->then(function ($x) { + $this->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() + ->end() + ->end() + ->end(); + // @formatter:on + } + + private function validateFilter(array $filter): void + { + var_dump($filter); + $name = $filter['name']; + unset($filter['enabled']); + if ($conf = $this->getFiltersConfigurations()[$name] ?? null) { + $node = $conf->buildTree(); + $processor = new Processor(); + $processor->process($node, [$name => $filter]); + } else { + throw new InvalidConfigurationException(sprintf('Unknown filter: %s', $name)); + } + } + + private function getFiltersConfigurations() + { + static $configurations = [ + // @formatter:off + 'pre_clip' => (new TreeBuilder('pre_clip')) + ->getRootNode() + ->info('Clip the video before applying other filters') + ->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() + ->end() + ->end(), + 'clip' => (new TreeBuilder('clip')) + ->getRootNode() + ->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() + ->end() + ->end(), + 'remove_audio' => (new TreeBuilder('remove_audio')) + ->getRootNode() + ->info('Remove the audio from the video') + ->children() + ->scalarNode('name')->isRequired()->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->end() + ->end(), + 'resize' => (new TreeBuilder('resize')) + ->getRootNode() + ->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() + ->end() + ->end(), + 'rotate' => (new TreeBuilder('rotate')) + ->getRootNode() + ->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() + ->end() + ->end(), + 'pad' => (new TreeBuilder('pad')) + ->getRootNode() + ->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() + ->end() + ->end(), + 'crop' => (new TreeBuilder('crop')) + ->getRootNode() + ->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() + ->end() + ->end(), + 'watermark' => (new TreeBuilder('watermark')) + ->getRootNode() + ->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() + ->end() + ->end(), + 'framerate' => (new TreeBuilder('framerate')) + ->getRootNode() + ->info('Change the framerate') + ->children() + ->scalarNode('name')->isRequired()->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('framerate') + ->isRequired() + ->info('framerate') + ->end() + ->scalarNode('gop') + ->info('gop') + ->end() + ->end() + ->end(), + 'synchronize' => (new TreeBuilder('synchronize')) + ->getRootNode() + ->info('re-synchronize audio and video') + ->children() + ->scalarNode('name')->isRequired()->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->end() + ->end(), + + // @formatter:on + ]; + + return $configurations; + } + + private function getFiltersDocumentation() + { + /** @var TreeBuilder $treeBuilder */ + foreach ($this->getFiltersConfigurations() as $name => $treeBuilder) { + $node = $treeBuilder->buildTree(); + $dumper = new YamlReferenceDumper(); + $t = $dumper->dumpNode($node); + // $t = preg_replace("#root:(\n( {4})?|\s+\[])#", "-\n", (string)$t); + // $t = preg_replace("#\n {4}#", "\n", $t); + // $t = preg_replace("#\n\n#", "\n", $t); + // $t = trim(preg_replace("#^\n+#", '', $t)); + var_dump($t); + } + } + public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { $context->log("Applying '".self::getName()."' module"); @@ -36,7 +309,7 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo } if (FamilyEnum::Audio === $commonArgs->getOutputFormat()->getFamily()) { - return $this->doAudio($options, $inputFile, $context, $commonArgs); + return $this->doVideo($options, $inputFile, $context, $commonArgs); } throw new \InvalidArgumentException(sprintf('Invalid format %s, only video or audio format supported', $commonArgs->getOutputFormat()->getFormat())); @@ -48,7 +321,7 @@ private function doVideo(array $options, InputFileInterface $inputFile, Transfor $format = $outputFormat->getFormat(); if (!method_exists($outputFormat, 'getFFMpegFormat')) { - throw new \InvalidArgumentException('format %s does not declare FFMpeg format', $format); + throw new \InvalidArgumentException(sprintf('format %s does not declare FFMpeg format', $format)); } /** @var FFMpegFormatInterface $FFMpegFormat */ @@ -126,7 +399,7 @@ function ($filter) use ($resolverContext) { } /* @uses self::resize(), self::rotate(), self::pad(), self::crop(), self::clip(), self::synchronize() - * @uses self::watermark(), self::framerate(), self::remove_audio() + * @uses self::watermark(), self::framerate(), self::remove_audio() */ $this->{$filter['name']}($clip, $filter, $resolverContext, $transformationContext, $isProjection); } @@ -175,37 +448,29 @@ private function doAudio(array $options, InputFileInterface $inputFile, Transfor private function preClip(Video $video, array $options, array $resolverContext, TransformationContextInterface $transformationContext, bool &$isProjection): Clip { $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); - $duration = $this->optionsResolver->resolveOption($options['duration'] ?? null, $resolverContext); - - $startAsTimecode = false; - $durationAsTimecode = null; + $startAsTimecode = FFMpegHelper::optionAsTimecode($start); - if (is_numeric($start) && (float) $start >= 0) { - $startAsTimecode = TimeCode::fromSeconds($start); - } elseif (is_string($start)) { - $startAsTimecode = TimeCode::fromString($start); - } - if (false === $startAsTimecode) { - throw new \InvalidArgumentException('Invalid start for filter "clip"'); + if (null === $startAsTimecode) { + throw new \InvalidArgumentException('Invalid start for filter "pre_clip"'); } - if ($startAsTimecode->toSeconds() > 0) { + $start = FFMpegHelper::timecodeToseconds($startAsTimecode); + if ($start > 0.0) { $isProjection = false; } + $duration = $this->optionsResolver->resolveOption($options['duration'] ?? null, $resolverContext); if (null !== $duration) { - if (is_numeric($duration) && (float) $duration > 0) { - $durationAsTimecode = TimeCode::fromSeconds($duration); - } elseif (is_string($duration)) { - $durationAsTimecode = TimeCode::fromString($duration); - } - if (false === $durationAsTimecode) { + $durationAsTimecode = FFMpegHelper::optionAsTimecode($duration); + if (null === $durationAsTimecode) { throw new \InvalidArgumentException('Invalid duration for filter "pre_clip"'); } $isProjection = false; + $transformationContext->log(sprintf(" Applying 'pre_clip' filter: start=%s (%.02f), duration=%s (%.02f)", $startAsTimecode, $start, $durationAsTimecode, $duration)); + } else { + $durationAsTimecode = null; + $transformationContext->log(sprintf(" Applying 'pre_clip' filter: start=%s (%.02f), duration=null", $startAsTimecode, $start)); } - $transformationContext->log(sprintf(" Applying 'pre_clip' filter: start=%s, duration=%s", $startAsTimecode, $durationAsTimecode)); - return $video->clip($startAsTimecode, $durationAsTimecode); } @@ -288,36 +553,29 @@ private function crop(Clip $clip, array $options, array $resolverContext, Transf private function clip(Clip $clip, array $options, array $resolverContext, TransformationContextInterface $transformationContext, bool &$isProjection): void { $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); - $duration = $this->optionsResolver->resolveOption($options['duration'] ?? null, $resolverContext); - - $startAsTimecode = false; - $durationAsTimecode = null; + $startAsTimecode = FFMpegHelper::optionAsTimecode($start); - if (is_numeric($start) && (float) $start >= 0) { - $startAsTimecode = TimeCode::fromSeconds($start); - } elseif (is_string($start)) { - $startAsTimecode = TimeCode::fromString($start); - } - if (false === $startAsTimecode) { + if (null === $startAsTimecode) { throw new \InvalidArgumentException('Invalid start for filter "clip"'); } - if ($startAsTimecode->toSeconds() > 0) { + $start = FFMpegHelper::timecodeToseconds($startAsTimecode); + if ($start > 0.0) { $isProjection = false; } + $duration = $this->optionsResolver->resolveOption($options['duration'] ?? null, $resolverContext); if (null !== $duration) { - if (is_numeric($duration) && (float) $duration > 0) { - $durationAsTimecode = TimeCode::fromSeconds($duration); - } elseif (is_string($duration)) { - $durationAsTimecode = TimeCode::fromString($duration); - } - if (false === $durationAsTimecode) { - throw new \InvalidArgumentException('Invalid duration for filter "pre_clip"'); + $durationAsTimecode = FFMpegHelper::optionAsTimecode($duration); + if (null === $durationAsTimecode) { + throw new \InvalidArgumentException('Invalid duration for filter "clip"'); } $isProjection = false; + $transformationContext->log(sprintf(" Applying 'clip' filter: start=%s (%.02f), duration=%s (%.02f)", $startAsTimecode, $start, $durationAsTimecode, $duration)); + } else { + $durationAsTimecode = null; + $transformationContext->log(sprintf(" Applying 'clip' filter: start=%s (%.02f), duration=null", $startAsTimecode, $start)); } - $transformationContext->log(sprintf(" Applying 'clip' filter: start=%s, duration=%s", $startAsTimecode, $durationAsTimecode)); $clip->filters()->clip($startAsTimecode, $durationAsTimecode); } @@ -330,6 +588,14 @@ private function synchronize(Clip $clip, array $options, array $resolverContext, private function watermark(Clip $clip, array $options, array $resolverContext, TransformationContextInterface $transformationContext, bool &$isProjection): void { $path = $this->optionsResolver->resolveOption($options['path'] ?? null, $resolverContext); + $path = $transformationContext->getRemoteFile($path); + $wmImage = $this->imagine->open($path); + $wmWidth = $wmImage->getSize()->getWidth(); + $wmHeight = $wmImage->getSize()->getHeight(); + unset($wmImage); + + $resolverContext['watermark'] = ['width' => $wmWidth, 'height' => $wmHeight]; + if (!file_exists($path)) { throw new \InvalidArgumentException('Watermark file for filter "watermark" not found'); } @@ -348,6 +614,7 @@ private function watermark(Clip $clip, array $options, array $resolverContext, T } array_walk($coord, fn (&$v) => $v = (int) $this->optionsResolver->resolveOption($v, $resolverContext)); + $coord['position'] = $position; $transformationContext->log(sprintf(" Applying 'watermark' filter: path=%s, coord=%s", $path, FFMpegHelper::coordAsText($coord))); $clip->filters()->watermark($path, $coord); @@ -359,7 +626,7 @@ private function framerate(Clip $clip, array $options, array $resolverContext, T if ($framerate <= 0) { throw new \InvalidArgumentException('Invalid framerate for filter "framerate"'); } - $gop = (int) ($options['gop'] ?? 0); + $gop = (int) $this->optionsResolver->resolveOption($options['gop'] ?? 0, $resolverContext); $transformationContext->log(sprintf(" Applying 'framerate' filter: framerate=%d, gop=%d", $framerate, $gop)); $clip->filters()->framerate(new FFMpeg\Coordinate\FrameRate($framerate), $gop); diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php index afb172340..29d6d4947 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php @@ -13,6 +13,7 @@ use FFMpeg\Format\VideoInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; final readonly class VideoSummaryTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface { @@ -21,6 +22,35 @@ public static function getName(): string return 'video_summary'; } + public function buildConfiguration(NodeBuilder $builder): void + { + $builder + ->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('period') + ->isRequired() + ->info('Extract one video clip every period, in seconds or timecode') + ->example('5 ; "00:00:05.00"') + ->end() + ->scalarNode('duration') + ->isRequired() + ->info('Duration of each clip, in seconds or timecode') + ->example('0.25 ; "00:00:00.25"') + ->end() + ->end() + ->end() + ; + } + /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface @@ -46,64 +76,122 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo ]; $period = $this->optionsResolver->resolveOption($options['period'] ?? 0, $resolverContext); - if ($period <= 0) { + $periodAsTimecode = FFMpegHelper::optionAsTimecode($period); + if (null === $periodAsTimecode || ($period = FFMpegHelper::timecodeToseconds($periodAsTimecode)) <= 0) { throw new \InvalidArgumentException(sprintf('Invalid period for module "%s"', self::getName())); } + + $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); + $startAsTimecode = FFMpegHelper::optionAsTimecode($start); + if (null === $startAsTimecode || ($start = FFMpegHelper::timecodeToseconds($startAsTimecode)) < 0) { + throw new \InvalidArgumentException('Invalid start'); + } + $clipDuration = $this->optionsResolver->resolveOption($options['duration'] ?? 0, $resolverContext); - if ($clipDuration <= 0 || $clipDuration >= $period) { - throw new \InvalidArgumentException(sprintf('Invalid duration for module "%s"', self::getName())); + $clipDurationAsTimecode = FFMpegHelper::optionAsTimecode($clipDuration); + if (null === $clipDurationAsTimecode || ($clipDuration = FFMpegHelper::timecodeToseconds($clipDurationAsTimecode)) <= 0 || $clipDuration >= $period) { + throw new \InvalidArgumentException('Invalid duration, should be >0 and log(sprintf(' period: %d, duration: %d', $period, $clipDuration)); + $context->log(sprintf(' start=%s (%.02f), period=%s (%.02f), duration=%s (%.02f)', $startAsTimecode, $start, $periodAsTimecode, $period, $clipDurationAsTimecode, $clipDuration)); + + $inputDuration = $video->getFFProbe()->format($inputFile->getPath())->get('duration'); - /** @var VideoInterface $FFMpegOutputFormat */ - $FFMpegOutputFormat = $outputFormat->getFFMpegFormat(); - if ($videoCodec = $this->optionsResolver->resolveOption($options['video_codec'] ?? null, $resolverContext)) { - if (!in_array($videoCodec, $FFMpegOutputFormat->getAvailableVideoCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid video codec %s for format %s', $videoCodec, $format)); + if (FamilyEnum::Video === $outputFormat->getFamily()) { + /** @var VideoInterface $FFMpegOutputFormat */ + $FFMpegOutputFormat = $outputFormat->getFFMpegFormat(); + if ($videoCodec = $this->optionsResolver->resolveOption($options['video_codec'] ?? null, $resolverContext)) { + if (!in_array($videoCodec, $FFMpegOutputFormat->getAvailableVideoCodecs())) { + throw new \InvalidArgumentException(sprintf('Invalid video codec %s for format %s', $videoCodec, $format)); + } + $FFMpegOutputFormat->setVideoCodec($videoCodec); } - $FFMpegOutputFormat->setVideoCodec($videoCodec); - } - if ($audioCodec = $this->optionsResolver->resolveOption($options['audio_codec'] ?? null, $resolverContext)) { - if (!in_array($audioCodec, $FFMpegOutputFormat->getAvailableAudioCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); + if ($audioCodec = $this->optionsResolver->resolveOption($options['audio_codec'] ?? null, $resolverContext)) { + if (!in_array($audioCodec, $FFMpegOutputFormat->getAvailableAudioCodecs())) { + throw new \InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); + } + $FFMpegOutputFormat->setAudioCodec($audioCodec); } - $FFMpegOutputFormat->setAudioCodec($audioCodec); - } - $clipsExtension = $outputFormat->getAllowedExtensions()[0]; - - $clipsFiles = []; - try { - $inputDuration = $video->getFFProbe()->format($inputFile->getPath())->get('duration'); - $nClips = ceil($inputDuration / $period); - - $context->log(sprintf(' Duration duration: %s, extracting %d clips of %d seconds', $inputDuration, $nClips, $clipDuration)); - $clipDuration = TimeCode::fromSeconds($clipDuration); - $removeAudioFilter = new FFMpeg\Filters\Audio\SimpleFilter(['-an']); - for ($i = 0; $i < $nClips; ++$i) { - $start = $i * $period; - $clip = $video->clip(TimeCode::fromSeconds($start), $clipDuration); - $clip->addFilter($removeAudioFilter); - $clipPath = $context->createTmpFilePath($clipsExtension); - $clip->save($FFMpegOutputFormat, $clipPath); - unset($clip); - $clipsFiles[] = realpath($clipPath); + // todo: allow to choose other extension + $clipsExtension = $outputFormat->getAllowedExtensions()[0]; + + try { + $clipsFiles = []; + $gap = $period - $clipDuration; + $usableInputDuration = ($inputDuration - $start) + $gap; + $nClips = floor($usableInputDuration / $period); + + $context->log(sprintf(' Video duration=%.02f, extracting %d clips of %.02f seconds from %s', $inputDuration, $nClips, $clipDuration, $startAsTimecode)); + $removeAudioFilter = new FFMpeg\Filters\Audio\SimpleFilter(['-an']); + for ($i = 0; $i < $nClips; ++$i) { + $startAsTimecode = TimeCode::fromSeconds($start); + $context->log(sprintf(' - Extracting clip %d/%d, start=%s (%.02f)', $i + 1, $nClips, $startAsTimecode, $start)); + $clip = $video->clip($startAsTimecode, $clipDurationAsTimecode); + $clip->addFilter($removeAudioFilter); + $clipPath = $context->createTmpFilePath($clipsExtension); + $clip->save($FFMpegOutputFormat, $clipPath); + unset($clip); + $clipsFiles[] = realpath($clipPath); + $start += $period; + } + unset($removeAudioFilter, $video); + + $outVideo = $commonArgs->getFFMpeg()->open($clipsFiles[0]); + + $outputPath = $context->createTmpFilePath($commonArgs->getExtension()); + + $outVideo + ->concat($clipsFiles) + ->saveFromSameCodecs($outputPath, true); + + unset($outVideo); + } finally { + foreach ($clipsFiles as $clipFile) { + @unlink($clipFile); + } } - unset($removeAudioFilter, $video); - - $outVideo = $commonArgs->getFFMpeg()->open($clipsFiles[0]); - - $outputPath = $context->createTmpFilePath($commonArgs->getExtension()); - - $outVideo - ->concat($clipsFiles) - ->saveFromSameCodecs($outputPath, true); - - unset($outVideo); - } finally { - foreach ($clipsFiles as $clipFile) { - @unlink($clipFile); + } elseif (FamilyEnum::Animation === $outputFormat->getFamily()) { + // todo: allow to choose other extension + $clipsExtension = $outputFormat->getAllowedExtensions()[0]; + try { + $clipsFiles = []; + $usableInputDuration = ($inputDuration - $start); + $nClips = floor($usableInputDuration / $period); + + $context->log(sprintf(' Video duration=%.02f, extracting %d frames from %s', $inputDuration, $nClips, $startAsTimecode)); + + for ($i = 0; $i < $nClips; ++$i) { + $startAsTimecode = TimeCode::fromSeconds($start); + $context->log(sprintf(' - Extracting frame %d/%d, start=%s (%.02f)', $i + 1, $nClips, $startAsTimecode, $start)); + + $frame = $video->frame($startAsTimecode); + $clipPath = $context->createTmpFilePath($clipsExtension); + $frame->save($clipPath); + unset($clip); + $clipsFiles[] = realpath($clipPath); + + $start += $period; + } + unset($video); + + $image = $this->imagine->open(array_shift($clipsFiles)); + foreach ($clipsFiles as $file) { + $image->layers()->add($this->imagine->open($file)); + } + + $outputPath = $context->createTmpFilePath($commonArgs->getExtension()); + $delay = (int) ($clipDuration * 1000); + $image->save($outputPath, [ + 'animated' => true, + 'animated.delay' => $delay, + 'animated.loops' => 0, + ]); + + } finally { + foreach ($clipsFiles as $clipFile) { + @unlink($clipFile); + } } } diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php index 1eefdc96b..e83ee21ff 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -9,7 +9,7 @@ use Alchemy\RenditionFactory\DTO\OutputFileInterface; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use FFMpeg; -use FFMpeg\Coordinate\TimeCode; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; final readonly class VideoToAnimationTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface { @@ -18,6 +18,68 @@ public static function getName(): string return 'video_to_animation'; } + public static function getDocumentationHeader(): ?string + { + return 'Converts a video to an animated gif'; + } + + // public static function getDocumentationFooter(): ?string + // { + // return null; + // } + // + public function buildConfiguration(NodeBuilder $builder): void + { + $builder + ->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() + ->scalarNode('duration') + ->defaultValue(null) + ->info('Duration in seconds or timecode') + ->example('30 ; "00:00:30.00" ; "{{ input.duration/2 }}"') + ->end() + ->integerNode('fps') + ->defaultValue(1) + ->info('Frames per second') + ->end() + ->integerNode('width') + ->defaultValue(null) + ->info('Width in pixels') + ->end() + ->integerNode('height') + ->defaultValue(null) + ->info('Height in pixels') + ->end() + ->enumNode('mode') + ->values([ + FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET, + // todo: implement other modes + // FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_FIT, + // FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_SCALE_WIDTH, + // FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_SCALE_HEIGHT, + ]) + ->defaultValue(FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET) + ->info('Resize mode') + ->end() + ->end() + ->end() + ; + } + public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { $context->log("Applying '".self::getName()."' module"); @@ -38,30 +100,19 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo ]; $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); - $startAsTimecode = false; - if (is_numeric($start) && (float) $start >= 0) { - $startAsTimecode = TimeCode::fromSeconds($start); - } elseif (is_string($start)) { - $startAsTimecode = TimeCode::fromString($start); - } - if (false === $startAsTimecode) { + $startAsTimecode = FFMpegHelper::optionAsTimecode($start); + if (null === $startAsTimecode) { throw new \InvalidArgumentException('Invalid start.'); } - $start = $startAsTimecode->toSeconds(); - + $start = FFMpegHelper::timecodeToseconds($startAsTimecode); $duration = $this->optionsResolver->resolveOption($options['duration'] ?? null, $resolverContext); - $durationAsTimecode = false; - if (is_numeric($duration) && (float) $duration >= 0) { - $durationAsTimecode = TimeCode::fromSeconds($duration); - } elseif (is_string($duration)) { - $durationAsTimecode = TimeCode::fromString($duration); - } - if (null !== $duration ) { - if (false === $durationAsTimecode) { + if (null !== $duration) { + $durationAsTimecode = FFMpegHelper::optionAsTimecode($duration); + if (null === $durationAsTimecode) { throw new \InvalidArgumentException('Invalid duration for filter "clip"'); } - $duration = $durationAsTimecode->toSeconds(); + $duration = FFMpegHelper::timecodeToseconds($durationAsTimecode); } if (($fps = (int) $this->optionsResolver->resolveOption($options['fps'] ?? 1, $resolverContext)) <= 0) { diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php index a8dedc215..45b92891b 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -8,8 +8,8 @@ use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; -use FFMpeg; use FFMpeg\Media\Video; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; final readonly class VideoToFrameTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface { @@ -18,6 +18,30 @@ public static function getName(): string return 'video_to_frame'; } + public function buildConfiguration(NodeBuilder $builder): void + { + $builder + ->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() + ->end() + ->end() + ; + } + public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { $context->log("Applying '".self::getName()."' module"); @@ -37,11 +61,15 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo 'input' => $video->getStreams()->videos()->first()->all(), ]; - $from = FFMpeg\Coordinate\TimeCode::fromSeconds($this->optionsResolver->resolveOption($options['from_seconds'] ?? 0, $resolverContext)); - - $context->log(sprintf(' from=%s', $from)); + $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); + $startAsTimecode = FFMpegHelper::optionAsTimecode($start); + if (null === $startAsTimecode) { + throw new \InvalidArgumentException('Invalid start.'); + } + $start = FFMpegHelper::timecodeToseconds($startAsTimecode); + $context->log(sprintf(' start=%s (%.02f)', $startAsTimecode, $start)); - $frame = $video->frame($from); + $frame = $video->frame($startAsTimecode); $outputPath = $context->createTmpFilePath($commonArgs->getExtension()); $frame->save($outputPath); From e8796c826ab6c6049c700036a258314613087d90 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Mon, 18 Nov 2024 19:38:32 +0100 Subject: [PATCH 02/12] WIP DO NOT MERGE - fix attributes access in twig : use `"{{ attr.myfield }}"` where `myfield` is the *slugified* attribute name --- .../src/Transformer/Video/FFMpegTransformerModule.php | 10 +++------- .../src/Transformer/Video/ModuleCommonArgs.php | 4 +--- .../Video/VideoSummaryTransformerModule.php | 8 +++----- .../Video/VideoToAnimationTransformerModule.php | 6 ++---- .../Video/VideoToFrameTransformerModule.php | 6 ++---- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index 615eceb31..9bdacba44 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -330,10 +330,8 @@ private function doVideo(array $options, InputFileInterface $inputFile, Transfor /** @var Video $video */ $video = $commonArgs->getFFMpeg()->open($inputFile->getPath()); - $resolverContext = [ - 'metadata' => $transformationContext->getTemplatingContext(), - 'input' => $video->getStreams()->videos()->first()->all(), - ]; + $resolverContext = $transformationContext->getTemplatingContext(); + $resolverContext['input'] = $video->getStreams()->videos()->first()->all(); if ($videoCodec = $this->optionsResolver->resolveOption($options['video_codec'] ?? null, $resolverContext)) { if (!in_array($videoCodec, $FFMpegFormat->getAvailableVideoCodecs())) { @@ -424,9 +422,7 @@ function ($filter) use ($resolverContext) { */ private function doAudio(array $options, InputFileInterface $inputFile, TransformationContextInterface $context, ModuleCommonArgs $commonArgs): OutputFileInterface { - $resolverContext = [ - 'metadata' => $context->getTemplatingContext(), - ]; + $resolverContext = $context->getTemplatingContext(); $format = $commonArgs->getOutputFormat()->getFormat(); if (!method_exists($commonArgs->getOutputFormat(), 'getFFMpegFormat')) { diff --git a/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php b/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php index c9a275563..e2fb15e1e 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php +++ b/lib/php/rendition-factory/src/Transformer/Video/ModuleCommonArgs.php @@ -20,9 +20,7 @@ public function __construct( TransformationContextInterface $context, ModuleOptionsResolver $optionsResolver) { - $resolverContext = [ - 'metadata' => $context->getTemplatingContext(), - ]; + $resolverContext = $context->getTemplatingContext(); $format = $optionsResolver->resolveOption($options['format'] ?? null, $resolverContext); if (!$format) { diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php index 29d6d4947..14a04ade8 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php @@ -70,11 +70,9 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo /** @var FFMpeg\Media\Video $video */ $video = $commonArgs->getFFMpeg()->open($inputFile->getPath()); - $resolverContext = [ - 'metadata' => $context->getTemplatingContext(), - 'input' => $video->getStreams()->videos()->first()->all(), - ]; - + $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) { diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php index e83ee21ff..d17e0eaa3 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -94,10 +94,8 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo /** @var FFMpeg\Media\Video $video */ $video = $commonArgs->getFFMpeg()->open($inputFile->getPath()); - $resolverContext = [ - 'metadata' => $context->getTemplatingContext(), - 'input' => $video->getStreams()->videos()->first()->all(), - ]; + $resolverContext = $context->getTemplatingContext(); + $resolverContext['input'] = $video->getStreams()->videos()->first()->all(); $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); $startAsTimecode = FFMpegHelper::optionAsTimecode($start); diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php index 45b92891b..b6840f511 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -56,10 +56,8 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo /** @var Video $video */ $video = $commonArgs->getFFMpeg()->open($inputFile->getPath()); - $resolverContext = [ - 'metadata' => $context->getTemplatingContext(), - 'input' => $video->getStreams()->videos()->first()->all(), - ]; + $resolverContext = $context->getTemplatingContext(); + $resolverContext['input'] = $video->getStreams()->videos()->first()->all(); $start = $this->optionsResolver->resolveOption($options['start'] ?? 0, $resolverContext); $startAsTimecode = FFMpegHelper::optionAsTimecode($start); From 4f417f881fb6fee9653d0740eb43e971bfb35ff7 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Mon, 25 Nov 2024 09:34:39 +0100 Subject: [PATCH 03/12] WIP --- .../src/Command/ConfigCommand.php | 83 ++-- .../TransformerModuleInterface.php | 4 + .../Video/FFMpeg/Filter/ResizeFilter.php | 145 +++++++ .../Video/FFMpegTransformerModule.php | 357 ++++++++++-------- 4 files changed, 380 insertions(+), 209 deletions(-) create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Filter/ResizeFilter.php diff --git a/lib/php/rendition-factory/src/Command/ConfigCommand.php b/lib/php/rendition-factory/src/Command/ConfigCommand.php index 7ebc66b90..b6023beab 100644 --- a/lib/php/rendition-factory/src/Command/ConfigCommand.php +++ b/lib/php/rendition-factory/src/Command/ConfigCommand.php @@ -6,6 +6,7 @@ use Alchemy\RenditionFactory\Config\YamlLoader; use Alchemy\RenditionFactory\DTO\FamilyEnum; +use Alchemy\RenditionFactory\Transformer\DocumentationTree; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; @@ -57,54 +58,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - $output->writeln(sprintf('Family "%s":', $family->name)); + $output->writeln(sprintf('# Family `%s`:', $family->name)); foreach ($familyConfig->getTransformations() as $transformation) { $transformerName = $transformation->getModule(); - $output->writeln(sprintf(' - %s', $transformerName)); + // $output->writeln(sprintf(' - %s', $transformerName)); /** @var TransformerModuleInterface $transformer */ $transformer = $this->transformers->get($transformerName); - $output->writeln($this->getTransformerDocumentation($transformerName, $transformer, $transformation->getOptions())); + $output->writeln($this->getTransformerDocumentation($transformerName, $transformer)); + + $this->checkTransformerConfiguration($transformerName, $transformer, $transformation->getOptions()); } } - } else { - $transformers = array_flip($this->transformers->getProvidedServices()); - ksort($transformers); - $last_parent = null; - foreach ($transformers as $fqcn => $transformerName) { - /** @var TransformerModuleInterface $transformer */ - $transformer = $this->transformers->get($transformerName); - // $parent = get_parent_class($transformer); - // if ($parent !== $last_parent) { - // if ($last_parent) { - // // $output->writeln("\n\n## parent foot: $last_parent\n"); - // $output->writeln($last_parent::getDocumentationFooter()); - // } - // if ($parent) { - // // $output->writeln("\n\n## parent head: $parent\n"); - // $output->writeln($parent::getDocumentationHeader()); - // } - // $last_parent = $parent; - // } - $output->writeln($this->no_getTransformerDocumentation($fqcn, $transformerName, $transformer)); - } } return 0; } - private function getTransformerDocumentation(string $transformerName, TransformerModuleInterface $transformer, array $options): string + private function getTransformerDocumentation(string $transformerName, TransformerModuleInterface $transformer): string { - $doc = "\n\n## $transformerName\n"; - - if (method_exists($transformer, 'getDocumentationHeader')) { - $doc .= $transformer->getDocumentationHeader()."\n"; - } + $docToText = function(DocumentationTree $documentation, int $depth=0) use (&$docToText): string { - if (method_exists($transformer, 'buildConfiguration')) { - $treeBuilder = new TreeBuilder('root'); - $transformer->buildConfiguration($treeBuilder->getRootNode()->children()); + $text = ''; + if($t = $documentation->getHeader()) { + $text .= $t . "\n"; + } + $treeBuilder = $documentation->getTreeBuilder(); $node = $treeBuilder->buildTree(); $dumper = new YamlReferenceDumper(); @@ -112,26 +92,41 @@ private function getTransformerDocumentation(string $transformerName, Transforme $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"; - // var_dump($options); + $text .= "```yaml\n" . $t . "\n```\n"; - $processor = new Processor(); - $processor->process($treeBuilder->buildTree(), ['root' => $options]); + foreach ($documentation->getChildren() as $child) { + $text .= $docToText($child, $depth+1); + } - } + if($t = $documentation->getFooter()) { + $text .= $t . "\n"; + } - if (method_exists($transformer, 'getDocumentationFooter')) { - $doc .= $transformer->getDocumentationFooter()."\n"; + return $text; + }; + + + $doc = "\n\n## $transformerName transformer module\n"; + if (method_exists($transformer, 'getDocumentation')) { + $documentation = $transformer->getDocumentation(); + $doc .= $docToText($documentation); } return $doc; } + 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"; diff --git a/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php index 2966c982b..783d394ca 100644 --- a/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php +++ b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php @@ -5,6 +5,7 @@ use Alchemy\RenditionFactory\Context\TransformationContextInterface; use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; interface TransformerModuleInterface { @@ -13,4 +14,7 @@ interface TransformerModuleInterface 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; } diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Filter/ResizeFilter.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Filter/ResizeFilter.php new file mode 100644 index 000000000..0de275ccb --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Filter/ResizeFilter.php @@ -0,0 +1,145 @@ +dimension = $dimension; + $this->mode = $mode; + $this->forceStandards = $forceStandards; + $this->priority = $priority; + } + + public function getPriority() + { + return $this->priority; + } + + /** + * @return Dimension + */ + public function getDimension() + { + return $this->dimension; + } + + /** + * @return string + */ + public function getMode() + { + return $this->mode; + } + + /** + * @return bool + */ + public function areStandardsForced() + { + return $this->forceStandards; + } + + public function apply(Video $video, VideoInterface $format) + { + $rotation = 0; + + try { + $command = [ + '-loglevel', 'error', + '-select_streams', 'v:0', + '-print_format', 'json', + '-show_entries', 'stream_side_data=rotation', + '-i', $video->getPathfile(), + ]; + $r = json_decode($video->getFFProbe()->getFFProbeDriver()->command($command), true, 16, JSON_THROW_ON_ERROR); + $rotation = (int) $r['streams'][0]['side_data_list'][0]['rotation']; + } catch (\Exception $e) { + // ignore (failed to get orientation) + } + + $dimensions = null; + $commands = []; + + foreach ($video->getStreams() as $stream) { + if ($stream->isVideo()) { + try { + $dimensions = $stream->getDimensions(); + if (90 === $rotation || -90 === $rotation) { + $dimensions = new Dimension($dimensions->getHeight(), $dimensions->getWidth()); + } + break; + } catch (RuntimeException $e) { + } + } + } + + if (null !== $dimensions) { + $dimensions = $this->getComputedDimensions($dimensions, $format->getModulus()); + + // Using Filter to have ordering + $commands[] = '-vf'; + $commands[] = '[in]scale='.$dimensions->getWidth().':'.$dimensions->getHeight().' [out]'; + } + + return $commands; + } + + private function getComputedDimensions(Dimension $dimension, $modulus) + { + $originalRatio = $dimension->getRatio($this->forceStandards); + switch ($this->mode) { + case self::RESIZEMODE_SCALE_WIDTH: + $height = $this->dimension->getHeight(); + $width = $originalRatio->calculateWidth($height, $modulus); + break; + case self::RESIZEMODE_SCALE_HEIGHT: + $width = $this->dimension->getWidth(); + $height = $originalRatio->calculateHeight($width, $modulus); + break; + case self::RESIZEMODE_INSET: + $targetRatio = $this->dimension->getRatio($this->forceStandards); + + if ($targetRatio->getValue() > $originalRatio->getValue()) { + $height = $this->dimension->getHeight(); + $width = $originalRatio->calculateWidth($height, $modulus); + } else { + $width = $this->dimension->getWidth(); + $height = $originalRatio->calculateHeight($width, $modulus); + } + break; + case self::RESIZEMODE_FIT: + default: + $width = $this->dimension->getWidth(); + $height = $this->dimension->getHeight(); + break; + } + + return new Dimension($width, $height); + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index 9bdacba44..a416c8382 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -7,13 +7,17 @@ use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; +use Alchemy\RenditionFactory\Transformer\DocumentationTree; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; +use Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Filter\ResizeFilter; use FFMpeg; use FFMpeg\Coordinate\TimeCode; use FFMpeg\Format\FormatInterface as FFMpegFormatInterface; use FFMpeg\Media\Clip; use FFMpeg\Media\Video; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -26,7 +30,37 @@ public static function getName(): string return 'ffmpeg'; } - public function buildConfiguration(NodeBuilder $builder): void + public static function getDocumentation(): DocumentationTree + { + static $doc = null; + if(null === $doc) { + $treeBuilder = new TreeBuilder('root'); + self::buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new DocumentationTree( + $treeBuilder, + <<
$builder) { + $tree = new TreeBuilder($name); + $builder($tree->getRootNode()); + $doc->addChild(new DocumentationTree( + $tree, + <<
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) { - $this->validateFilter($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() +// ->validate() +// ->ifNotInArray(['pre_clip', 'remove_audio', 'resize', 'rotate', 'pad', 'crop', 'clip', 'synchronize', 'watermark', 'framerate']) +// ->thenInvalid('Invalid filter') +// ->end() ->end() ->scalarNode('enabled') ->defaultTrue() @@ -79,26 +119,26 @@ public function buildConfiguration(NodeBuilder $builder): void // @formatter:on } - private function validateFilter(array $filter): void + private static function validateFilter(array $filter): void { - var_dump($filter); $name = $filter['name']; unset($filter['enabled']); - if ($conf = $this->getFiltersConfigurations()[$name] ?? null) { - $node = $conf->buildTree(); + if ($builder = self::getExtraConfigurationBuilders()[$name] ?? null) { + $tree = new TreeBuilder($name); + $builder($tree->getRootNode()); $processor = new Processor(); - $processor->process($node, [$name => $filter]); + $processor->process($tree->buildTree(), [$name => $filter]); } else { throw new InvalidConfigurationException(sprintf('Unknown filter: %s', $name)); } } - private function getFiltersConfigurations() + private static function getExtraConfigurationBuilders(): iterable { static $configurations = [ // @formatter:off - 'pre_clip' => (new TreeBuilder('pre_clip')) - ->getRootNode() + 'pre_clip' => function (ArrayNodeDefinition $root): void { + $root ->info('Clip the video before applying other filters') ->children() ->scalarNode('name')->isRequired()->end() @@ -113,186 +153,175 @@ private function getFiltersConfigurations() ->info('Duration in seconds or timecode') ->example('30 ; "00:00:30" ; "{{ input.duration/2 }}"') ->end() - ->end() - ->end(), - 'clip' => (new TreeBuilder('clip')) - ->getRootNode() + ->end(); + }, + 'clip' => function (ArrayNodeDefinition $root): void { + $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()->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() - ->end(), - 'remove_audio' => (new TreeBuilder('remove_audio')) - ->getRootNode() + ->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() - ->end() - ->end(), - 'resize' => (new TreeBuilder('resize')) - ->getRootNode() + ->scalarNode('name')->isRequired()->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('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() - ->end(), - 'rotate' => (new TreeBuilder('rotate')) - ->getRootNode() + ->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()->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('angle') + ->isRequired() + ->info('Angle of rotation [0 | 90 | 180 | 270]') + ->example('90') ->end() - ->end(), - 'pad' => (new TreeBuilder('pad')) - ->getRootNode() + ->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()->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('width') + ->isRequired() + ->info('Width of the video') ->end() - ->end(), - 'crop' => (new TreeBuilder('crop')) - ->getRootNode() + ->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()->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' => (new TreeBuilder('watermark')) - ->getRootNode() + ->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()->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() - ->end(), - 'framerate' => (new TreeBuilder('framerate')) - ->getRootNode() + ->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()->end() + ->scalarNode('enabled')->defaultTrue()->end() + ->scalarNode('framerate') + ->isRequired() + ->info('framerate') + ->end() + ->scalarNode('gop') + ->info('gop') ->end() - ->end(), - 'synchronize' => (new TreeBuilder('synchronize')) - ->getRootNode() + ->end(); + }, + 'synchronize' => function (ArrayNodeDefinition $root): void { + $root ->info('re-synchronize audio and video') ->children() ->scalarNode('name')->isRequired()->end() ->scalarNode('enabled')->defaultTrue()->end() - ->end() - ->end(), - + ->end(); + }, // @formatter:on ]; return $configurations; } - private function getFiltersDocumentation() - { - /** @var TreeBuilder $treeBuilder */ - foreach ($this->getFiltersConfigurations() as $name => $treeBuilder) { - $node = $treeBuilder->buildTree(); - $dumper = new YamlReferenceDumper(); - $t = $dumper->dumpNode($node); - // $t = preg_replace("#root:(\n( {4})?|\s+\[])#", "-\n", (string)$t); - // $t = preg_replace("#\n {4}#", "\n", $t); - // $t = preg_replace("#\n\n#", "\n", $t); - // $t = trim(preg_replace("#^\n+#", '', $t)); - var_dump($t); - } - } public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { @@ -481,6 +510,7 @@ private function resize(Clip $clip, array $options, array $resolverContext, Tran { $dimension = $this->getDimension($options, $resolverContext, 'resize'); $mode = $this->optionsResolver->resolveOption($options['mode'] ?? FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET, $resolverContext); + $forceStandards = $this->optionsResolver->resolveOption($options['force_standards'] ?? true, $resolverContext); if (!in_array( $mode, [ @@ -494,10 +524,7 @@ private function resize(Clip $clip, array $options, array $resolverContext, Tran } $transformationContext->log(sprintf(" Applying 'resize' filter: dimension=[width=%s, height=%s], mode=%s", $dimension->getWidth(), $dimension->getHeight(), $mode)); - $clip->filters()->resize( - $dimension, - $mode - ); + $clip->addFilter(new ResizeFilter($dimension, $mode, $forceStandards)); $isProjection = false; } From 82ea1d539bba72a92dc760d96d824bae89cffa8d Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 28 Nov 2024 11:11:37 +0100 Subject: [PATCH 04/12] 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(); } From e9a96268cbd49559d883f575bd0672c2f60abf1b Mon Sep 17 00:00:00 2001 From: jygaulier Date: Tue, 3 Dec 2024 09:37:19 +0100 Subject: [PATCH 05/12] documentation & config validator as services, used by databox. --- databox/api/config/validator/validation.yaml | 5 +- .../Command/DocumentationDumperCommand.php | 41 ++++++ .../ValidRenditionDefinitionConstraint.php | 15 +++ ...RenditionDefinitionConstraintValidator.php | 36 +++++ .../Resources/config/services.yaml | 8 +- .../src/Command/ConfigCommand.php | 123 ------------------ .../Command/ConfigurationValidateCommand.php | 43 ++++++ .../src/Config/Validator.php | 58 +++++++++ .../src/DTO/BuildConfig/Transformation.php | 2 +- .../src/DocumentationDumper.php | 108 +++++++++++++++ .../DocumentToPdfTransformerModule.php | 32 ++--- .../Document/PdfToImageTransformerModule.php | 32 ++--- .../Imagine/ImagineTransformerModule.php | 34 ++--- .../Transformer/TransformerConfigHelper.php | 64 +++++++++ .../TransformerModuleInterface.php | 2 +- .../Video/FFMpegTransformerModule.php | 60 ++++----- .../Video/VideoSummaryTransformerModule.php | 37 +++--- .../VideoToAnimationTransformerModule.php | 37 +++--- .../Video/VideoToFrameTransformerModule.php | 37 +++--- .../src/Transformer/VoidTransformerModule.php | 26 ++-- 20 files changed, 528 insertions(+), 272 deletions(-) create mode 100644 databox/api/src/Command/DocumentationDumperCommand.php create mode 100644 databox/api/src/Validator/ValidRenditionDefinitionConstraint.php create mode 100644 databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php delete mode 100644 lib/php/rendition-factory/src/Command/ConfigCommand.php create mode 100644 lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php create mode 100644 lib/php/rendition-factory/src/Config/Validator.php create mode 100644 lib/php/rendition-factory/src/DocumentationDumper.php create mode 100644 lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php diff --git a/databox/api/config/validator/validation.yaml b/databox/api/config/validator/validation.yaml index 74292be91..240e6e38b 100644 --- a/databox/api/config/validator/validation.yaml +++ b/databox/api/config/validator/validation.yaml @@ -127,6 +127,7 @@ App\Entity\Core\RenditionDefinition: - workspace - class.workspace - parent.workspace + - App\Validator\ValidRenditionDefinitionConstraint: ~ properties: class: - NotNull: ~ @@ -176,8 +177,8 @@ App\Entity\Core\Tag: errorPath: name App\Entity\Integration\WorkspaceIntegration: - constraints: - - App\Validator\ValidIntegrationOptionsConstraint: ~ + constraints: + - App\Validator\ValidIntegrationOptionsConstraint: ~ App\Entity\Integration\IntegrationData: properties: diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php new file mode 100644 index 000000000..2f8c49a99 --- /dev/null +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -0,0 +1,41 @@ +setName('app:documentation:dump') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln('# rendition factory'); + $output->writeln($this->renditionFactoryDocumentationDumper->dump()); + + return 0; + } +} diff --git a/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php b/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php new file mode 100644 index 000000000..75b0096e1 --- /dev/null +++ b/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php @@ -0,0 +1,15 @@ +yamlLoader->parse($value->getDefinition()); + $this->validator->validate($config); + } catch (\Throwable $e) { + $this->context + ->buildViolation($e->getMessage()) + ->addViolation(); + } + } +} diff --git a/lib/php/rendition-factory-bundle/Resources/config/services.yaml b/lib/php/rendition-factory-bundle/Resources/config/services.yaml index 1e2d8def2..47522f5a9 100644 --- a/lib/php/rendition-factory-bundle/Resources/config/services.yaml +++ b/lib/php/rendition-factory-bundle/Resources/config/services.yaml @@ -4,7 +4,7 @@ services: autoconfigure: true Alchemy\RenditionFactory\Command\CreateCommand: ~ - Alchemy\RenditionFactory\Command\ConfigCommand: ~ + Alchemy\RenditionFactory\Command\ConfigurationValidateCommand: ~ Alchemy\RenditionFactory\Context\TransformationContextFactory: ~ Alchemy\RenditionFactory\FileFamilyGuesser: ~ @@ -102,3 +102,9 @@ services: Alchemy\RenditionFactory\Format\FormatGuesser: ~ Alchemy\RenditionFactory\Format\FormatFactory: ~ Alchemy\RenditionFactory\Config\ModuleOptionsResolver: ~ + + Alchemy\RenditionFactory\DocumentationDumper: + tags: + - { name: 'documentation.dumper' } + + Alchemy\RenditionFactory\Config\Validator: ~ diff --git a/lib/php/rendition-factory/src/Command/ConfigCommand.php b/lib/php/rendition-factory/src/Command/ConfigCommand.php deleted file mode 100644 index 7ccad1a40..000000000 --- a/lib/php/rendition-factory/src/Command/ConfigCommand.php +++ /dev/null @@ -1,123 +0,0 @@ -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 (null !== ($configPath = $input->getArgument('config'))) { - $config = $this->yamlLoader->load($configPath); - - foreach (FamilyEnum::cases() as $family) { - $familyConfig = $config->getFamily($family); - if (null === $familyConfig) { - continue; - } - foreach ($familyConfig->getTransformations() as $transformation) { - $transformerName = $transformation->getModule(); - - /** @var TransformerModuleInterface $transformer */ - $transformer = $this->transformers->get($transformerName); - - 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; - } - - private function getTransformerDocumentation(string $transformerName, TransformerModuleInterface $transformer): string - { - $docToText = function (Documentation $documentation, int $depth = 0) use (&$docToText): string { - - $text = ''; - if ($t = $documentation->getHeader()) { - $text .= $t."\n"; - } - - $treeBuilder = $documentation->getTreeBuilder(); - $node = $treeBuilder->buildTree(); - $dumper = new YamlReferenceDumper(); - - $t = $dumper->dumpNode($node); - $t = preg_replace("#^root:($|(\s+)\[]$)#m", "-\n", (string) $t); - $t = preg_replace("#\n+#", "\n", $t); - $t = trim($t); - - $text .= "```yaml\n".$t."\n```\n"; - - if ($t = $documentation->getFooter()) { - $text .= $t."\n"; - } - - foreach ($documentation->getChildren() as $child) { - $text .= $docToText($child, $depth + 1); - } - - return $text; - }; - - $documentation = $transformer->getDocumentation(); - - return "## `$transformerName` transformer module\n".$docToText($documentation); - } - - private function checkTransformerConfiguration(string $transformerName, TransformerModuleInterface $transformer, array $options): void - { - $documentation = $transformer->getDocumentation(); - $treeBuilder = $documentation->getTreeBuilder(); - - $processor = new Processor(); - $processor->process($treeBuilder->buildTree(), ['root' => $options]); - } -} diff --git a/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php b/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php new file mode 100644 index 000000000..6d8785b58 --- /dev/null +++ b/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php @@ -0,0 +1,43 @@ +addArgument('config', InputArgument::REQUIRED, 'A build config YAML file to validate') + ->setHelp('Validate a config file.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $config = $this->yamlLoader->load($input->getArgument('config')); + $this->validator->validate($config); + + $output->writeln('Configuration is valid.'); + + return 0; + } +} diff --git a/lib/php/rendition-factory/src/Config/Validator.php b/lib/php/rendition-factory/src/Config/Validator.php new file mode 100644 index 000000000..81ee26dcd --- /dev/null +++ b/lib/php/rendition-factory/src/Config/Validator.php @@ -0,0 +1,58 @@ +transformers; + } + + public function validate(BuildConfig $config): void + { + foreach (FamilyEnum::cases() as $family) { + $familyConfig = $config->getFamily($family); + if (null === $familyConfig) { + continue; + } + foreach ($familyConfig->getTransformations() as $transformation) { + $transformerName = $transformation->getModule(); + + /** @var TransformerModuleInterface $transformer */ + $transformer = $this->transformers->get($transformerName); + + try { + $this->checkTransformerConfiguration($transformer, $transformation->toArray()); + } catch (\Throwable $e) { + $msg = sprintf("Error in module \"%s\"\n%s", $transformerName, $e->getMessage()); + throw new InvalidConfigurationException($msg); + } + } + } + } + + private function checkTransformerConfiguration(TransformerModuleInterface $transformer, array $options): void + { + $documentation = $transformer->getDocumentation(); + $treeBuilder = $documentation->getTreeBuilder(); + + $processor = new Processor(); + $processor->process($treeBuilder->buildTree(), ['root' => $options]); + } +} diff --git a/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php b/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php index 202656432..90fca1916 100644 --- a/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php +++ b/lib/php/rendition-factory/src/DTO/BuildConfig/Transformation.php @@ -32,7 +32,7 @@ public function isEnabled(): bool return $this->enabled; } - public function asArray(): array + public function toArray(): array { return [ 'module' => $this->module, diff --git a/lib/php/rendition-factory/src/DocumentationDumper.php b/lib/php/rendition-factory/src/DocumentationDumper.php new file mode 100644 index 000000000..e4c01fdd8 --- /dev/null +++ b/lib/php/rendition-factory/src/DocumentationDumper.php @@ -0,0 +1,108 @@ +transformers->getProvidedServices() as $transformerName => $transformerFqcn) { + /** @var TransformerModuleInterface $transformer */ + $transformer = $this->transformers->get($transformerName); + $text .= $this->getTransformerDocumentation($transformerName, $transformer); + } + $text .= $this->listFormats(); + + return $text; + } + + private function listFormats(): string + { + $formats = []; + foreach ($this->formats->getProvidedServices() as $formatName => $formatFqcn) { + /** @var FormatInterface $format */ + $format = $this->formats->get($formatName); + $family = $format->getFamily()->value; + if (!array_key_exists($family, $formats)) { + $formats[$family] = []; + } + $formats[$family][] = $format; + } + ksort($formats); + + $text = '## Supported formats'."\n"; + $text .= "| Family | Format | Mime type | Extensions |\n"; + $text .= "|-|-|-|-|\n"; + foreach ($formats as $familyFormats) { + $text .= sprintf("| %s ||||\n", + $familyFormats[0]->getFamily()->value, + ); + foreach ($familyFormats as $format) { + $text .= sprintf("|| %s | %s | %s |\n", + $format->getFormat(), + $format->getMimeType(), + implode(', ', $format->getAllowedExtensions()) + ); + } + } + + return $text; + } + + private function getTransformerDocumentation(string $transformerName, TransformerModuleInterface $transformer): string + { + $docToText = function (Documentation $documentation, int $depth = 0) use (&$docToText): string { + + $text = ''; + if ($t = $documentation->getHeader()) { + $text .= $t."\n"; + } + + $treeBuilder = $documentation->getTreeBuilder(); + $node = $treeBuilder->buildTree(); + $dumper = new YamlReferenceDumper(); + + $t = $dumper->dumpNode($node); + $t = preg_replace("#^root:($|(\s+)\[]$)#m", "-\n", (string) $t); + $t = preg_replace("#\n+#", "\n", $t); + $t = trim($t); + + $text .= "```yaml\n".$t."\n```\n"; + + if ($t = $documentation->getFooter()) { + $text .= $t."\n"; + } + + foreach ($documentation->getChildren() as $child) { + $text .= $docToText($child, $depth + 1); + } + + return $text; + }; + + $documentation = $transformer->getDocumentation(); + + return "## `$transformerName` transformer module\n".$docToText($documentation); + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php index 875b7f926..a14b976e9 100644 --- a/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php @@ -19,26 +19,28 @@ public static function getName(): string return 'document_to_pdf'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); - return $doc; + return new Documentation( + $treeBuilder, + <<
arrayNode('options') + ->ignoreExtraKeys(false) + ->end() + ; + // @formatter:on } public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface diff --git a/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php index fbf85a3ca..03aa1fcd6 100644 --- a/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php @@ -20,26 +20,28 @@ public static function getName(): string return 'pdf_to_image'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); - return $doc; + return new Documentation( + $treeBuilder, + <<
arrayNode('options') + ->ignoreExtraKeys(false) + ->end() + ; + // @formatter:on } public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface 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 97c428821..61dbeab5c 100644 --- a/lib/php/rendition-factory/src/Transformer/Image/Imagine/ImagineTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Image/Imagine/ImagineTransformerModule.php @@ -26,26 +26,28 @@ public static function getName(): string return 'imagine'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); + + return new Documentation( + $treeBuilder, + <<
arrayNode('options') + ->ignoreExtraKeys(false) + ->end() + ; + // @formatter:on } public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface diff --git a/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php b/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php new file mode 100644 index 000000000..d71896ef2 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php @@ -0,0 +1,64 @@ +children = []; + } + + public function addChild(TransformerConfigHelper $child): void + { + $this->children[] = $child; + } + + 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/TransformerModuleInterface.php b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php index af4587720..97d703bf3 100644 --- a/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php +++ b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php @@ -14,5 +14,5 @@ public static function getName(): string; public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface; - public static function getDocumentation(): Documentation; + public function getDocumentation(): Documentation; } diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index deb36e966..71b423bf5 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -39,43 +39,39 @@ public static function getName(): string return 'ffmpeg'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, + $treeBuilder = Documentation::createBaseTree(self::getName()); + $this->buildConfiguration($treeBuilder->getRootNode()->children()); + $doc = new Documentation( + $treeBuilder, + <<
getExtraConfigurationBuilders() as $name => $builder) { + $tree = new TreeBuilder('root'); + $builder($tree->getRootNode()); + $doc->addChild(new Documentation( + $tree, <<
$builder) { - $tree = new TreeBuilder('root'); - $builder($tree->getRootNode()); - $doc->addChild(new Documentation( - $tree, - <<
arrayNode('options') - ->info('Options for the module') ->children() ->scalarNode('format') ->info('output format') @@ -110,16 +106,12 @@ private static function buildConfiguration(NodeBuilder $builder): void ->arrayPrototype() ->info('see list of available filters below') ->validate()->always()->then(function ($x) { - self::validateFilter($x); + $this->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() @@ -135,11 +127,11 @@ private static function buildConfiguration(NodeBuilder $builder): void // @formatter:on } - private static function validateFilter(array $filter): void + private function validateFilter(array $filter): void { $name = $filter['name']; unset($filter['enabled']); - if ($builder = self::getExtraConfigurationBuilders()[$name] ?? null) { + if ($builder = $this->getExtraConfigurationBuilders()[$name] ?? null) { $tree = new TreeBuilder($name); $builder($tree->getRootNode()); $processor = new Processor(); @@ -149,7 +141,7 @@ private static function validateFilter(array $filter): void } } - private static function getExtraConfigurationBuilders(): iterable + private function getExtraConfigurationBuilders(): iterable { static $configurations = [ // @formatter:off diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php index a84a837fc..8e0f61dc2 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php @@ -34,29 +34,24 @@ public static function getName(): string return 'video_summary'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); + + return new Documentation( + $treeBuilder, + <<
arrayNode('options') - ->info('Options for the module') ->children() ->scalarNode('start') ->defaultValue(0) @@ -83,6 +78,16 @@ private static function buildConfiguration(NodeBuilder $builder): void ->info('extension of the output file') ->example('mpeg') ->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() ->end() ->end() ; diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php index a22c145fd..bba56dfb2 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -28,29 +28,24 @@ public static function getName(): string return 'video_to_animation'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); + + return new Documentation( + $treeBuilder, + <<
arrayNode('options') - ->info('Options for the module') ->children() ->scalarNode('start') ->defaultValue(0) @@ -95,6 +90,16 @@ private static function buildConfiguration(NodeBuilder $builder): void ->info('extension of the output file') ->example('apng') ->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() ->end() ->end(); // @formatter:on diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php index 4d4239c3f..cebf7dfd1 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -28,29 +28,24 @@ public static function getName(): string return 'video_to_frame'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = Documentation::createBaseTree(self::getName()); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); + + return new Documentation( + $treeBuilder, + <<
arrayNode('options') - ->info('Options for the module') ->children() ->scalarNode('start') ->defaultValue(0) @@ -67,6 +62,16 @@ private static function buildConfiguration(NodeBuilder $builder): void ->info('extension of the output file') ->example('jpg') ->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() ->end() ->end() ; diff --git a/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php b/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php index 63628771e..7e5147db2 100644 --- a/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/VoidTransformerModule.php @@ -6,7 +6,6 @@ use Alchemy\RenditionFactory\DTO\InputFileInterface; use Alchemy\RenditionFactory\DTO\OutputFileInterface; use Symfony\Component\Config\Definition\Builder\NodeBuilder; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; class VoidTransformerModule implements TransformerModuleInterface { @@ -15,26 +14,21 @@ public static function getName(): string return 'void'; } - public static function getDocumentation(): Documentation + public function getDocumentation(): Documentation { - static $doc = null; - if (null === $doc) { - $treeBuilder = new TreeBuilder('root'); - self::buildConfiguration($treeBuilder->getRootNode()->children()); - $doc = new Documentation( - $treeBuilder, - <<
buildConfiguration($treeBuilder->getRootNode()->children()); - return $doc; + return new Documentation( + $treeBuilder, + <<
Date: Tue, 3 Dec 2024 10:48:07 +0100 Subject: [PATCH 06/12] allow empty definition --- .../ValidRenditionDefinitionConstraintValidator.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php b/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php index 6578cccca..0bb44994f 100644 --- a/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php +++ b/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php @@ -24,8 +24,11 @@ public function __construct(private readonly YamlLoader $yamlLoader, private rea */ public function validate($value, Constraint $constraint): void { + if(!($definition = $value->getDefinition())) { + return; + } try { - $config = $this->yamlLoader->parse($value->getDefinition()); + $config = $this->yamlLoader->parse($definition); $this->validator->validate($config); } catch (\Throwable $e) { $this->context From 272c5ff192c6a1a1eaa116c34da4042ecd81d0de Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 4 Dec 2024 09:37:34 +0100 Subject: [PATCH 07/12] move video "formats" to video namespace ; fix documentation --- .../Command/DocumentationDumperCommand.php | 3 +- .../Resources/config/services.yaml | 48 +++++++++---------- .../src/DocumentationDumper.php | 4 +- .../Video/FFMpegTransformerModule.php | 4 +- .../Video}/Format/AacFormat.php | 4 +- .../Video}/Format/AnimatedGifFormat.php | 2 +- .../Video}/Format/AnimatedPngFormat.php | 2 +- .../Video}/Format/AnimatedWebpFormat.php | 2 +- .../Video}/Format/Audio/Aac.php | 2 +- .../Video}/Format/FormatInterface.php | 2 +- .../Video}/Format/JpegFormat.php | 2 +- .../Video}/Format/MkvFormat.php | 2 +- .../Video}/Format/Mp3Format.php | 2 +- .../Video}/Format/Mpeg4Format.php | 2 +- .../Video}/Format/MpegFormat.php | 2 +- .../Video}/Format/QuicktimeFormat.php | 2 +- .../Video}/Format/WavFormat.php | 2 +- .../Video}/Format/WebmFormat.php | 2 +- .../Transformer/Video/ModuleCommonArgs.php | 2 +- .../Video/VideoSummaryTransformerModule.php | 4 +- .../VideoToAnimationTransformerModule.php | 4 +- .../Video/VideoToFrameTransformerModule.php | 4 +- 22 files changed, 51 insertions(+), 52 deletions(-) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/AacFormat.php (84%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/AnimatedGifFormat.php (88%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/AnimatedPngFormat.php (89%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/AnimatedWebpFormat.php (88%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/Audio/Aac.php (84%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/FormatInterface.php (85%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/JpegFormat.php (88%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/MkvFormat.php (92%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/Mp3Format.php (91%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/Mpeg4Format.php (92%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/MpegFormat.php (92%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/QuicktimeFormat.php (92%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/WavFormat.php (91%) rename lib/php/rendition-factory/src/{ => Transformer/Video}/Format/WebmFormat.php (92%) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 2f8c49a99..d08905d68 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -8,7 +8,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - use Alchemy\RenditionFactory\DocumentationDumper as RenditionFactoryDocumentationDumper; @@ -33,7 +32,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('# rendition factory'); + $output->writeln('# ' . $this->renditionFactoryDocumentationDumper::getName()); $output->writeln($this->renditionFactoryDocumentationDumper->dump()); return 0; diff --git a/lib/php/rendition-factory-bundle/Resources/config/services.yaml b/lib/php/rendition-factory-bundle/Resources/config/services.yaml index 47522f5a9..36fff1986 100644 --- a/lib/php/rendition-factory-bundle/Resources/config/services.yaml +++ b/lib/php/rendition-factory-bundle/Resources/config/services.yaml @@ -46,53 +46,53 @@ services: - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } # Output "formats" - Alchemy\RenditionFactory\Format\JpegFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\JpegFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\MkvFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\MkvFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\Mpeg4Format: + Alchemy\RenditionFactory\Transformer\Video\Format\Mpeg4Format: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\MpegFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\MpegFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\QuicktimeFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\QuicktimeFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\WebmFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\WebmFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\AnimatedGifFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedGifFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\AnimatedPngFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedPngFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\AnimatedWebpFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedWebpFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\WavFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\WavFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\AacFormat: + Alchemy\RenditionFactory\Transformer\Video\Format\AacFormat: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } - Alchemy\RenditionFactory\Format\Mp3Format: + Alchemy\RenditionFactory\Transformer\Video\Format\Mp3Format: tags: - - { name: !php/const Alchemy\RenditionFactory\Format\FormatInterface::TAG } + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } Imagine\Imagick\Imagine: ~ diff --git a/lib/php/rendition-factory/src/DocumentationDumper.php b/lib/php/rendition-factory/src/DocumentationDumper.php index e4c01fdd8..5bec29b1b 100644 --- a/lib/php/rendition-factory/src/DocumentationDumper.php +++ b/lib/php/rendition-factory/src/DocumentationDumper.php @@ -2,9 +2,9 @@ namespace Alchemy\RenditionFactory; -use Alchemy\RenditionFactory\Format\FormatInterface; use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; +use Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -51,7 +51,7 @@ private function listFormats(): string } ksort($formats); - $text = '## Supported formats'."\n"; + $text = "## Video transformers output `format`s.\n"; $text .= "| Family | Format | Mime type | Extensions |\n"; $text .= "|-|-|-|-|\n"; foreach ($formats as $familyFormats) { diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index 71b423bf5..e803112b2 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -8,10 +8,10 @@ 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 Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Filter\ResizeFilter; +use Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface; use FFMpeg; use FFMpeg\Coordinate\TimeCode; use FFMpeg\Format\FormatInterface as FFMpegFormatInterface; @@ -74,7 +74,7 @@ public function buildConfiguration(NodeBuilder $builder): void ->arrayNode('options') ->children() ->scalarNode('format') - ->info('output format') + ->info('output format (see Video transformers output `format`s)') ->end() ->scalarNode('extension') ->info('extension of the output file') diff --git a/lib/php/rendition-factory/src/Format/AacFormat.php b/lib/php/rendition-factory/src/Transformer/Video/Format/AacFormat.php similarity index 84% rename from lib/php/rendition-factory/src/Format/AacFormat.php rename to lib/php/rendition-factory/src/Transformer/Video/Format/AacFormat.php index 16b4188bd..acb738e29 100644 --- a/lib/php/rendition-factory/src/Format/AacFormat.php +++ b/lib/php/rendition-factory/src/Transformer/Video/Format/AacFormat.php @@ -1,9 +1,9 @@ end() ->scalarNode('format') ->isRequired() - ->info('Output format') + ->info('output format (see Video transformers output `format`s)') ->example('video-mpeg') ->end() ->scalarNode('extension') diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php index bba56dfb2..0cd18d354 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -8,9 +8,9 @@ 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 Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface; use FFMpeg; use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; @@ -82,7 +82,7 @@ public function buildConfiguration(NodeBuilder $builder): void ->end() ->scalarNode('format') ->isRequired() - ->info('Output format') + ->info('output format (see Video transformers output `format`s)') ->example('animated-png') ->end() ->scalarNode('extension') diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php index cebf7dfd1..7937bed50 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -8,9 +8,9 @@ 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 Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface; use FFMpeg\Media\Video; use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; @@ -54,7 +54,7 @@ public function buildConfiguration(NodeBuilder $builder): void ->end() ->scalarNode('format') ->isRequired() - ->info('Output format') + ->info('output format (see Video transformers output `format`s)') ->example('image-jpeg') ->end() ->scalarNode('extension') From 1d697799d9bbdee21488bebc463969675742534f Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 5 Dec 2024 19:10:53 +0100 Subject: [PATCH 08/12] move video output "formats" documentation to each related transformer module. move conf validator to attribute (not full entity) --- databox/api/config/services.yaml | 2 + databox/api/config/validator/validation.yaml | 5 +- .../Command/DocumentationDumperCommand.php | 12 ++-- .../src/Entity/Core/RenditionDefinition.php | 7 +- .../ValidRenditionDefinitionConstraint.php | 4 +- ...RenditionDefinitionConstraintValidator.php | 16 ++--- .../Resources/config/services.yaml | 7 +- .../Command/ConfigurationValidateCommand.php | 4 +- ...Validator.php => buildConfigValidator.php} | 5 +- ...tionBuilderConfigurationDocumentation.php} | 41 +----------- .../Transformer/TransformerConfigHelper.php | 64 ------------------- .../Video/FFMpegTransformerModule.php | 27 +++++++- .../Format/OutputFormatsDocumentation.php | 51 +++++++++++++++ .../Transformer/Video/ModuleCommonArgs.php | 3 +- .../Video/VideoSummaryTransformerModule.php | 25 +++++++- .../VideoToAnimationTransformerModule.php | 21 +++++- .../Video/VideoToFrameTransformerModule.php | 15 ++++- 17 files changed, 161 insertions(+), 148 deletions(-) rename lib/php/rendition-factory/src/Config/{Validator.php => buildConfigValidator.php} (96%) rename lib/php/rendition-factory/src/{DocumentationDumper.php => RenditionBuilderConfigurationDocumentation.php} (60%) delete mode 100644 lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/Format/OutputFormatsDocumentation.php diff --git a/databox/api/config/services.yaml b/databox/api/config/services.yaml index 4c13a3c69..b00efb993 100644 --- a/databox/api/config/services.yaml +++ b/databox/api/config/services.yaml @@ -131,3 +131,5 @@ services: $decorated: '@.inner' Alchemy\RenditionFactory\Templating\TemplateResolverInterface: '@App\Asset\Attribute\TemplateResolver' + + App\Validator\ValidRenditionDefinitionConstraintValidator: ~ diff --git a/databox/api/config/validator/validation.yaml b/databox/api/config/validator/validation.yaml index 240e6e38b..74292be91 100644 --- a/databox/api/config/validator/validation.yaml +++ b/databox/api/config/validator/validation.yaml @@ -127,7 +127,6 @@ App\Entity\Core\RenditionDefinition: - workspace - class.workspace - parent.workspace - - App\Validator\ValidRenditionDefinitionConstraint: ~ properties: class: - NotNull: ~ @@ -177,8 +176,8 @@ App\Entity\Core\Tag: errorPath: name App\Entity\Integration\WorkspaceIntegration: - constraints: - - App\Validator\ValidIntegrationOptionsConstraint: ~ + constraints: + - App\Validator\ValidIntegrationOptionsConstraint: ~ App\Entity\Integration\IntegrationData: properties: diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index d08905d68..6d28742b0 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -8,14 +8,14 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Alchemy\RenditionFactory\DocumentationDumper as RenditionFactoryDocumentationDumper; +use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command { public function __construct( - private readonly RenditionFactoryDocumentationDumper $renditionFactoryDocumentationDumper, + private readonly RenditionBuilderConfigurationDocumentation $renditionBuilderConfigurationDocumentation, ) { parent::__construct(); @@ -24,16 +24,12 @@ public function __construct( protected function configure(): void { parent::configure(); - - $this - ->setName('app:documentation:dump') - ; } protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('# ' . $this->renditionFactoryDocumentationDumper::getName()); - $output->writeln($this->renditionFactoryDocumentationDumper->dump()); + $output->writeln('# ' . $this->renditionBuilderConfigurationDocumentation::getName()); + $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); return 0; } diff --git a/databox/api/src/Entity/Core/RenditionDefinition.php b/databox/api/src/Entity/Core/RenditionDefinition.php index e0231b57c..d0df77b6b 100644 --- a/databox/api/src/Entity/Core/RenditionDefinition.php +++ b/databox/api/src/Entity/Core/RenditionDefinition.php @@ -5,6 +5,8 @@ namespace App\Entity\Core; use Alchemy\CoreBundle\Entity\AbstractUuidEntity; +use Alchemy\CoreBundle\Entity\Traits\CreatedAtTrait; +use Alchemy\CoreBundle\Entity\Traits\UpdatedAtTrait; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; @@ -16,10 +18,8 @@ use App\Api\Model\Input\RenditionDefinitionInput; use App\Api\Provider\RenditionDefinitionCollectionProvider; use App\Controller\Core\RenditionDefinitionSortAction; - -use Alchemy\CoreBundle\Entity\Traits\CreatedAtTrait; -use Alchemy\CoreBundle\Entity\Traits\UpdatedAtTrait; use App\Entity\Traits\WorkspaceTrait; +use App\Validator as CustomAssert; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection as DoctrineCollection; use Doctrine\DBAL\Types\Types; @@ -153,6 +153,7 @@ class RenditionDefinition extends AbstractUuidEntity implements \Stringable #[Groups([RenditionDefinition::GROUP_LIST, RenditionDefinition::GROUP_READ, RenditionDefinition::GROUP_WRITE])] #[ORM\Column(type: Types::TEXT, nullable: true)] #[ApiProperty(security: self::GRANT_ADMIN_PROP)] + #[CustomAssert\ValidRenditionDefinitionConstraint] private ?string $definition = null; #[Groups([RenditionDefinition::GROUP_READ])] diff --git a/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php b/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php index 75b0096e1..ecf01a68c 100644 --- a/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php +++ b/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php @@ -6,10 +6,12 @@ use Symfony\Component\Validator\Constraint; +/** @uses ValidRenditionDefinitionConstraintValidator */ +#[\Attribute] class ValidRenditionDefinitionConstraint extends Constraint { public function getTargets(): string|array { - return self::CLASS_CONSTRAINT; + return self::PROPERTY_CONSTRAINT; } } diff --git a/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php b/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php index 0bb44994f..ff74af98b 100644 --- a/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php +++ b/databox/api/src/Validator/ValidRenditionDefinitionConstraintValidator.php @@ -4,33 +4,31 @@ namespace App\Validator; - -use Alchemy\RenditionFactory\Config\Validator; +use Alchemy\RenditionFactory\Config\buildConfigValidator; use Alchemy\RenditionFactory\Config\YamlLoader; -use App\Entity\Core\RenditionDefinition; -use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class ValidRenditionDefinitionConstraintValidator extends ConstraintValidator { - public function __construct(private readonly YamlLoader $yamlLoader, private readonly Validator $validator) + /** @uses buildConfigValidator */ + public function __construct(private readonly YamlLoader $yamlLoader, private readonly buildConfigValidator $validator) { } /** - * @param RenditionDefinition $value + * @param string $value * @param ValidRenditionDefinitionConstraint $constraint */ public function validate($value, Constraint $constraint): void { - if(!($definition = $value->getDefinition())) { + if(!$value) { return; } try { - $config = $this->yamlLoader->parse($definition); + $config = $this->yamlLoader->parse($value); $this->validator->validate($config); - } catch (\Throwable $e) { + } catch (\Exception $e) { $this->context ->buildViolation($e->getMessage()) ->addViolation(); diff --git a/lib/php/rendition-factory-bundle/Resources/config/services.yaml b/lib/php/rendition-factory-bundle/Resources/config/services.yaml index 36fff1986..48489bec2 100644 --- a/lib/php/rendition-factory-bundle/Resources/config/services.yaml +++ b/lib/php/rendition-factory-bundle/Resources/config/services.yaml @@ -94,6 +94,7 @@ services: tags: - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG } + Alchemy\RenditionFactory\Transformer\Video\Format\OutputFormatsDocumentation: ~ Imagine\Imagick\Imagine: ~ Imagine\Image\ImagineInterface: '@Imagine\Imagick\Imagine' @@ -103,8 +104,6 @@ services: Alchemy\RenditionFactory\Format\FormatFactory: ~ Alchemy\RenditionFactory\Config\ModuleOptionsResolver: ~ - Alchemy\RenditionFactory\DocumentationDumper: - tags: - - { name: 'documentation.dumper' } + Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation: ~ - Alchemy\RenditionFactory\Config\Validator: ~ + Alchemy\RenditionFactory\Config\buildConfigValidator: ~ diff --git a/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php b/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php index 6d8785b58..c6d3d8d8b 100644 --- a/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php +++ b/lib/php/rendition-factory/src/Command/ConfigurationValidateCommand.php @@ -4,7 +4,7 @@ namespace Alchemy\RenditionFactory\Command; -use Alchemy\RenditionFactory\Config\Validator; +use Alchemy\RenditionFactory\Config\buildConfigValidator; use Alchemy\RenditionFactory\Config\YamlLoader; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -17,7 +17,7 @@ class ConfigurationValidateCommand extends Command { public function __construct( private readonly YamlLoader $yamlLoader, - private readonly Validator $validator, + private readonly buildConfigValidator $validator, ) { parent::__construct(); } diff --git a/lib/php/rendition-factory/src/Config/Validator.php b/lib/php/rendition-factory/src/Config/buildConfigValidator.php similarity index 96% rename from lib/php/rendition-factory/src/Config/Validator.php rename to lib/php/rendition-factory/src/Config/buildConfigValidator.php index 81ee26dcd..3421df9ce 100644 --- a/lib/php/rendition-factory/src/Config/Validator.php +++ b/lib/php/rendition-factory/src/Config/buildConfigValidator.php @@ -10,13 +10,12 @@ use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\ServiceLocator; -class Validator +readonly class buildConfigValidator { public function __construct( #[TaggedLocator(TransformerModuleInterface::TAG, defaultIndexMethod: 'getName')] - private readonly ServiceLocator $transformers, + private ServiceLocator $transformers, ) { - } public function getTransformers(): ServiceLocator diff --git a/lib/php/rendition-factory/src/DocumentationDumper.php b/lib/php/rendition-factory/src/RenditionBuilderConfigurationDocumentation.php similarity index 60% rename from lib/php/rendition-factory/src/DocumentationDumper.php rename to lib/php/rendition-factory/src/RenditionBuilderConfigurationDocumentation.php index 5bec29b1b..5ff25d4fc 100644 --- a/lib/php/rendition-factory/src/DocumentationDumper.php +++ b/lib/php/rendition-factory/src/RenditionBuilderConfigurationDocumentation.php @@ -4,18 +4,15 @@ use Alchemy\RenditionFactory\Transformer\Documentation; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; -use Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\ServiceLocator; -final readonly class DocumentationDumper +final readonly class RenditionBuilderConfigurationDocumentation { public function __construct( #[TaggedLocator(TransformerModuleInterface::TAG, defaultIndexMethod: 'getName')] private ServiceLocator $transformers, - #[TaggedLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] - private ServiceLocator $formats, ) { } @@ -24,7 +21,7 @@ public static function getName(): string return 'rendition-factory'; } - public function dump(): string + public function generate(): string { $text = ''; foreach ($this->transformers->getProvidedServices() as $transformerName => $transformerFqcn) { @@ -32,40 +29,6 @@ public function dump(): string $transformer = $this->transformers->get($transformerName); $text .= $this->getTransformerDocumentation($transformerName, $transformer); } - $text .= $this->listFormats(); - - return $text; - } - - private function listFormats(): string - { - $formats = []; - foreach ($this->formats->getProvidedServices() as $formatName => $formatFqcn) { - /** @var FormatInterface $format */ - $format = $this->formats->get($formatName); - $family = $format->getFamily()->value; - if (!array_key_exists($family, $formats)) { - $formats[$family] = []; - } - $formats[$family][] = $format; - } - ksort($formats); - - $text = "## Video transformers output `format`s.\n"; - $text .= "| Family | Format | Mime type | Extensions |\n"; - $text .= "|-|-|-|-|\n"; - foreach ($formats as $familyFormats) { - $text .= sprintf("| %s ||||\n", - $familyFormats[0]->getFamily()->value, - ); - foreach ($familyFormats as $format) { - $text .= sprintf("|| %s | %s | %s |\n", - $format->getFormat(), - $format->getMimeType(), - implode(', ', $format->getAllowedExtensions()) - ); - } - } return $text; } diff --git a/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php b/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php deleted file mode 100644 index d71896ef2..000000000 --- a/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php +++ /dev/null @@ -1,64 +0,0 @@ -children = []; - } - - public function addChild(TransformerConfigHelper $child): void - { - $this->children[] = $child; - } - - 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/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index e803112b2..db6dac5f1 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -12,6 +12,14 @@ use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; use Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Filter\ResizeFilter; use Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface; +use Alchemy\RenditionFactory\Transformer\Video\Format\MkvFormat; +use Alchemy\RenditionFactory\Transformer\Video\Format\Mp3Format; +use Alchemy\RenditionFactory\Transformer\Video\Format\Mpeg4Format; +use Alchemy\RenditionFactory\Transformer\Video\Format\MpegFormat; +use Alchemy\RenditionFactory\Transformer\Video\Format\OutputFormatsDocumentation; +use Alchemy\RenditionFactory\Transformer\Video\Format\QuicktimeFormat; +use Alchemy\RenditionFactory\Transformer\Video\Format\WavFormat; +use Alchemy\RenditionFactory\Transformer\Video\Format\WebmFormat; use FFMpeg; use FFMpeg\Coordinate\TimeCode; use FFMpeg\Format\FormatInterface as FFMpegFormatInterface; @@ -31,6 +39,7 @@ public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] private ServiceLocator $formats, private ModuleOptionsResolver $optionsResolver, private ImagineInterface $imagine, + private OutputFormatsDocumentation $outputFormatsDocumentation, ) { } @@ -39,6 +48,19 @@ public static function getName(): string return 'ffmpeg'; } + private static function getSupportedOutputFormats(): array + { + return [ + MkvFormat::getFormat(), + Mpeg4Format::getFormat(), + MpegFormat::getFormat(), + QuicktimeFormat::getFormat(), + WebmFormat::getFormat(), + WavFormat::getFormat(), + Mp3Format::getFormat(), + ]; + } + public function getDocumentation(): Documentation { $treeBuilder = Documentation::createBaseTree(self::getName()); @@ -48,6 +70,7 @@ public function getDocumentation(): Documentation <<
outputFormatsDocumentation->listFormats(self::getSupportedOutputFormats()). <<