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/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php
new file mode 100644
index 000000000..32aded42d
--- /dev/null
+++ b/databox/api/src/Command/DocumentationDumperCommand.php
@@ -0,0 +1,31 @@
+writeln('# ' . $this->renditionBuilderConfigurationDocumentation::getName());
+ $output->writeln($this->renditionBuilderConfigurationDocumentation->generate());
+
+ return Command::SUCCESS;
+ }
+}
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
new file mode 100644
index 000000000..ecf01a68c
--- /dev/null
+++ b/databox/api/src/Validator/ValidRenditionDefinitionConstraint.php
@@ -0,0 +1,17 @@
+yamlLoader->parse($value);
+ $this->validator->validate($config);
+ } 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 16dbfd75c..b52b3d83b 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\ConfigurationValidateCommand: ~
Alchemy\RenditionFactory\Context\TransformationContextFactory: ~
Alchemy\RenditionFactory\FileFamilyGuesser: ~
@@ -44,43 +45,56 @@ services:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG }
- # FFMpeg "formats"
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\JpegFormat:
+ # Output "formats"
+ Alchemy\RenditionFactory\Transformer\Video\Format\JpegFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MkvFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\MkvFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\Mpeg4Format:
+ Alchemy\RenditionFactory\Transformer\Video\Format\Mpeg4Format:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MpegFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\MpegFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\QuicktimeFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\QuicktimeFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\WebmFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\WebmFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedGifFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedGifFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedPngFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedPngFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
- Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedWebpFormat:
+ Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedWebpFormat:
tags:
- - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
+ Alchemy\RenditionFactory\Transformer\Video\Format\WavFormat:
+ tags:
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
+
+ Alchemy\RenditionFactory\Transformer\Video\Format\AacFormat:
+ tags:
+ - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }
+
+ Alchemy\RenditionFactory\Transformer\Video\Format\Mp3Format:
+ 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'
@@ -89,3 +103,7 @@ services:
Alchemy\RenditionFactory\Format\FormatGuesser: ~
Alchemy\RenditionFactory\Format\FormatFactory: ~
Alchemy\RenditionFactory\Config\ModuleOptionsResolver: ~
+
+ Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation: ~
+
+ Alchemy\RenditionFactory\Config\BuildConfigValidator: ~
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..b52b425bf
--- /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 Command::SUCCESS;
+ }
+}
diff --git a/lib/php/rendition-factory/src/Command/CreateCommand.php b/lib/php/rendition-factory/src/Command/CreateCommand.php
index 2317a9ff1..2e88ffbc8 100644
--- a/lib/php/rendition-factory/src/Command/CreateCommand.php
+++ b/lib/php/rendition-factory/src/Command/CreateCommand.php
@@ -45,34 +45,38 @@ protected function configure(): void
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $ret = 0;
+ $ret = Command::SUCCESS;
$src = $input->getArgument('src');
if (is_dir($src)) {
if (false === ($od = opendir($src))) {
$output->writeln(sprintf('Directory "%s" could not be opened.', $src));
- return 1;
+ return Command::FAILURE;
}
while ($f = readdir($od)) {
if ('.' === $f || '..' === $f) {
continue;
}
- $ret |= $this->doFile($input, $output, $src.'/'.$f);
+ if(false === $this->doFile($input, $output, $src.'/'.$f)) {
+ $ret = Command::FAILURE;
+ }
}
closedir($od);
} else {
- $ret = $this->doFile($input, $output, $src);
+ if(false === $this->doFile($input, $output, $src)) {
+ $ret = Command::FAILURE;
+ }
}
return $ret;
}
- protected function doFile(InputInterface $input, OutputInterface $output, string $src): int
+ protected function doFile(InputInterface $input, OutputInterface $output, string $src): bool
{
if (!file_exists($src)) {
$output->writeln(sprintf('File "%s" does not exist.', $src));
- return 1;
+ return false;
}
$time = microtime(true);
@@ -104,7 +108,7 @@ protected function doFile(InputInterface $input, OutputInterface $output, string
} catch (\InvalidArgumentException $e) {
$output->writeln(sprintf('%s', $e->getMessage()));
- return 1;
+ return false;
}
if ($outputPath = $input->getOption('output')) {
@@ -121,7 +125,7 @@ protected function doFile(InputInterface $input, OutputInterface $output, string
if ($src === $outputFile->getPath()) {
$output->writeln('No transformation needed');
- return 1;
+ return false;
}
if (!$input->getOption('debug')) {
@@ -130,6 +134,6 @@ protected function doFile(InputInterface $input, OutputInterface $output, string
$output->writeln(sprintf('Execution time: %0.2f', microtime(true) - $time));
- return 0;
+ return true;
}
}
diff --git a/lib/php/rendition-factory/src/Config/BuildConfigValidator.php b/lib/php/rendition-factory/src/Config/BuildConfigValidator.php
new file mode 100644
index 000000000..fb9acdbf2
--- /dev/null
+++ b/lib/php/rendition-factory/src/Config/BuildConfigValidator.php
@@ -0,0 +1,57 @@
+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/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..90fca1916 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 toArray(): array
+ {
+ return [
+ 'module' => $this->module,
+ 'enabled' => $this->enabled,
+ 'options' => $this->options,
+ 'description' => $this->description,
+ ];
+ }
}
diff --git a/lib/php/rendition-factory/src/RenditionBuilderConfigurationDocumentation.php b/lib/php/rendition-factory/src/RenditionBuilderConfigurationDocumentation.php
new file mode 100644
index 000000000..5ff25d4fc
--- /dev/null
+++ b/lib/php/rendition-factory/src/RenditionBuilderConfigurationDocumentation.php
@@ -0,0 +1,71 @@
+transformers->getProvidedServices() as $transformerName => $transformerFqcn) {
+ /** @var TransformerModuleInterface $transformer */
+ $transformer = $this->transformers->get($transformerName);
+ $text .= $this->getTransformerDocumentation($transformerName, $transformer);
+ }
+
+ 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/RenditionCreator.php b/lib/php/rendition-factory/src/RenditionCreator.php
index 86cea27ef..179a6d879 100644
--- a/lib/php/rendition-factory/src/RenditionCreator.php
+++ b/lib/php/rendition-factory/src/RenditionCreator.php
@@ -43,7 +43,7 @@ public function createRendition(
NoBuildConfigException::throwNoFamily($inputFile->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..fb8932ba4 100644
--- a/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php
+++ b/lib/php/rendition-factory/src/Transformer/Document/DocumentToPdfTransformerModule.php
@@ -8,7 +8,10 @@
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\TransformerConfigHelper;
use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface;
+use Symfony\Component\Config\Definition\Builder\NodeBuilder;
final readonly class DocumentToPdfTransformerModule implements TransformerModuleInterface
{
@@ -17,6 +20,30 @@ public static function getName(): string
return 'document_to_pdf';
}
+ public function getDocumentation(): Documentation
+ {
+ $treeBuilder = TransformerConfigHelper::createBaseTree(self::getName());
+ $this->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
{
if ('application/pdf' === $inputFile->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..bc03b9013 100644
--- a/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php
+++ b/lib/php/rendition-factory/src/Transformer/Document/PdfToImageTransformerModule.php
@@ -7,9 +7,12 @@
use Alchemy\RenditionFactory\DTO\InputFileInterface;
use Alchemy\RenditionFactory\DTO\OutputFile;
use Alchemy\RenditionFactory\DTO\OutputFileInterface;
+use Alchemy\RenditionFactory\Transformer\Documentation;
+use Alchemy\RenditionFactory\Transformer\TransformerConfigHelper;
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 +21,30 @@ public static function getName(): string
return 'pdf_to_image';
}
+ public function getDocumentation(): Documentation
+ {
+ $treeBuilder = TransformerConfigHelper::createBaseTree(self::getName());
+ $this->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
{
if ('application/pdf' !== $inputFile->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..68a2fe920
--- /dev/null
+++ b/lib/php/rendition-factory/src/Transformer/Documentation.php
@@ -0,0 +1,41 @@
+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;
+ }
+}
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..b5679e802 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,11 @@
use Alchemy\RenditionFactory\DTO\OutputFileInterface;
use Alchemy\RenditionFactory\MimeType\ImageFormatGuesser;
use Alchemy\RenditionFactory\Transformer\BuildHashDiffInterface;
+use Alchemy\RenditionFactory\Transformer\Documentation;
+use Alchemy\RenditionFactory\Transformer\TransformerConfigHelper;
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 +27,30 @@ public static function getName(): string
return 'imagine';
}
+ public function getDocumentation(): Documentation
+ {
+ $treeBuilder = TransformerConfigHelper::createBaseTree(self::getName());
+ $this->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
{
$inputFormat = ImageFormatGuesser::getFormat($inputFile->getType());
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..d70de0192
--- /dev/null
+++ b/lib/php/rendition-factory/src/Transformer/TransformerConfigHelper.php
@@ -0,0 +1,36 @@
+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 2966c982b..97d703bf3 100644
--- a/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php
+++ b/lib/php/rendition-factory/src/Transformer/TransformerModuleInterface.php
@@ -13,4 +13,6 @@ interface TransformerModuleInterface
public static function getName(): string;
public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface;
+
+ public 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 bc55d1c62..000000000
--- a/lib/php/rendition-factory/src/Transformer/Video/AbstractVideoTransformer.php
+++ /dev/null
@@ -1,16 +0,0 @@
-priority;
+ }
+
+ public function getDimension(): Dimension
+ {
+ return $this->dimension;
+ }
+
+ public function getMode(): string
+ {
+ return $this->mode;
+ }
+
+ public function areStandardsForced(): bool
+ {
+ return $this->forceStandards;
+ }
+
+ public function apply(Video $video, VideoInterface $format): array
+ {
+ $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): Dimension
+ {
+ $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/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..f3a2c2396 100644
--- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php
+++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php
@@ -2,25 +2,358 @@
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\Transformer\Documentation;
+use Alchemy\RenditionFactory\Transformer\TransformerConfigHelper;
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;
use FFMpeg\Media\Clip;
use FFMpeg\Media\Video;
-
-final readonly class FFMpegTransformerModule extends AbstractVideoTransformer implements TransformerModuleInterface
+use Imagine\Image\ImagineInterface;
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+use Symfony\Component\Config\Definition\Builder\NodeBuilder;
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
+use Symfony\Component\Config\Definition\Processor;
+use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
+use Symfony\Component\DependencyInjection\ServiceLocator;
+
+final readonly class FFMpegTransformerModule implements TransformerModuleInterface
{
+ public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] private ServiceLocator $formats,
+ private ModuleOptionsResolver $optionsResolver,
+ private ImagineInterface $imagine,
+ private OutputFormatsDocumentation $outputFormatsDocumentation,
+ ) {
+ }
+
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 = TransformerConfigHelper::createBaseTree(self::getName());
+ $this->buildConfiguration($treeBuilder->getRootNode()->children());
+ $doc = new Documentation(
+ $treeBuilder,
+ <<outputFormatsDocumentation->listFormats(self::getSupportedOutputFormats()).
+ <<