diff --git a/Config/Filter/Argument/Point.php b/Config/Filter/Argument/Point.php
new file mode 100644
index 000000000..33b0d8a6d
--- /dev/null
+++ b/Config/Filter/Argument/Point.php
@@ -0,0 +1,44 @@
+x = $x;
+ $this->y = $y;
+ }
+
+ public function getX(): ?int
+ {
+ return $this->x;
+ }
+
+ public function getY(): ?int
+ {
+ return $this->y;
+ }
+}
diff --git a/Config/Filter/Argument/Size.php b/Config/Filter/Argument/Size.php
new file mode 100644
index 000000000..e3ec10213
--- /dev/null
+++ b/Config/Filter/Argument/Size.php
@@ -0,0 +1,48 @@
+width = $width;
+ $this->height = $height;
+ }
+
+ public function getWidth(): ?int
+ {
+ return $this->width;
+ }
+
+ public function getHeight(): ?int
+ {
+ return $this->height;
+ }
+}
diff --git a/Config/Filter/Type/AutoRotate.php b/Config/Filter/Type/AutoRotate.php
new file mode 100644
index 000000000..7eaafc73a
--- /dev/null
+++ b/Config/Filter/Type/AutoRotate.php
@@ -0,0 +1,20 @@
+color = $color;
+ $this->transparency = $transparency;
+ $this->position = $position;
+ $this->size = $size;
+ }
+
+ public function getColor(): ?string
+ {
+ return $this->color;
+ }
+
+ public function getTransparency(): ?string
+ {
+ return $this->transparency;
+ }
+
+ public function getPosition(): ?string
+ {
+ return $this->position;
+ }
+
+ public function getSize(): Size
+ {
+ return $this->size;
+ }
+}
diff --git a/Config/Filter/Type/Crop.php b/Config/Filter/Type/Crop.php
new file mode 100644
index 000000000..633a9182f
--- /dev/null
+++ b/Config/Filter/Type/Crop.php
@@ -0,0 +1,49 @@
+startPoint = $startPoint;
+ $this->size = $size;
+ }
+
+ public function getStartPoint(): Point
+ {
+ return $this->startPoint;
+ }
+
+ public function getSize(): Size
+ {
+ return $this->size;
+ }
+}
diff --git a/Config/Filter/Type/Downscale.php b/Config/Filter/Type/Downscale.php
new file mode 100644
index 000000000..4d2732928
--- /dev/null
+++ b/Config/Filter/Type/Downscale.php
@@ -0,0 +1,52 @@
+max = $max;
+ $this->by = $by;
+ }
+
+ public function getMax(): ?Size
+ {
+ return $this->max;
+ }
+
+ public function getBy(): ?float
+ {
+ return $this->by;
+ }
+}
diff --git a/Config/Filter/Type/FilterAbstract.php b/Config/Filter/Type/FilterAbstract.php
new file mode 100644
index 000000000..3b1eb2782
--- /dev/null
+++ b/Config/Filter/Type/FilterAbstract.php
@@ -0,0 +1,29 @@
+axis = $axis;
+ }
+
+ public function getAxis(): string
+ {
+ return $this->axis;
+ }
+}
diff --git a/Config/Filter/Type/Grayscale.php b/Config/Filter/Type/Grayscale.php
new file mode 100644
index 000000000..5bb5a82bd
--- /dev/null
+++ b/Config/Filter/Type/Grayscale.php
@@ -0,0 +1,20 @@
+mode = $mode;
+ }
+
+ public function getMode(): string
+ {
+ return $this->mode;
+ }
+}
diff --git a/Config/Filter/Type/Paste.php b/Config/Filter/Type/Paste.php
new file mode 100644
index 000000000..c92906e93
--- /dev/null
+++ b/Config/Filter/Type/Paste.php
@@ -0,0 +1,37 @@
+start = $start;
+ }
+
+ public function getStart(): Point
+ {
+ return $this->start;
+ }
+}
diff --git a/Config/Filter/Type/RelativeResize.php b/Config/Filter/Type/RelativeResize.php
new file mode 100644
index 000000000..a93ff07a9
--- /dev/null
+++ b/Config/Filter/Type/RelativeResize.php
@@ -0,0 +1,72 @@
+heighten = $heighten;
+ $this->widen = $widen;
+ $this->increase = $increase;
+ $this->scale = $scale;
+ }
+
+ public function getHeighten(): ?float
+ {
+ return $this->heighten;
+ }
+
+ public function getWiden(): ?float
+ {
+ return $this->widen;
+ }
+
+ public function getIncrease(): ?float
+ {
+ return $this->increase;
+ }
+
+ public function getScale(): ?float
+ {
+ return $this->scale;
+ }
+}
diff --git a/Config/Filter/Type/Resize.php b/Config/Filter/Type/Resize.php
new file mode 100644
index 000000000..9edb27cdd
--- /dev/null
+++ b/Config/Filter/Type/Resize.php
@@ -0,0 +1,37 @@
+size = $size;
+ }
+
+ public function getSize(): Size
+ {
+ return $this->size;
+ }
+}
diff --git a/Config/Filter/Type/Rotate.php b/Config/Filter/Type/Rotate.php
new file mode 100644
index 000000000..ef4cd3851
--- /dev/null
+++ b/Config/Filter/Type/Rotate.php
@@ -0,0 +1,35 @@
+angle = $angle;
+ }
+
+ public function getAngle(): int
+ {
+ return $this->angle;
+ }
+}
diff --git a/Config/Filter/Type/Scale.php b/Config/Filter/Type/Scale.php
new file mode 100644
index 000000000..7f0447602
--- /dev/null
+++ b/Config/Filter/Type/Scale.php
@@ -0,0 +1,52 @@
+dimensions = $dimensions;
+ $this->to = $to;
+ }
+
+ public function getDimensions(): Size
+ {
+ return $this->dimensions;
+ }
+
+ public function getTo(): ?float
+ {
+ return $this->to;
+ }
+}
diff --git a/Config/Filter/Type/Strip.php b/Config/Filter/Type/Strip.php
new file mode 100644
index 000000000..bfdf3f019
--- /dev/null
+++ b/Config/Filter/Type/Strip.php
@@ -0,0 +1,20 @@
+size = $size;
+ $this->mode = $mode;
+ $this->allowUpscale = $allowUpscale;
+ $this->filter = $filter;
+ }
+
+ public function getSize(): Size
+ {
+ return $this->size;
+ }
+
+ public function getMode(): ?string
+ {
+ return $this->mode;
+ }
+
+ public function isAllowUpscale(): ?bool
+ {
+ return $this->allowUpscale;
+ }
+
+ public function getFilter(): ?string
+ {
+ return $this->filter;
+ }
+}
diff --git a/Config/Filter/Type/Upscale.php b/Config/Filter/Type/Upscale.php
new file mode 100644
index 000000000..acd227208
--- /dev/null
+++ b/Config/Filter/Type/Upscale.php
@@ -0,0 +1,52 @@
+min = $min;
+ $this->by = $by;
+ }
+
+ public function getMin(): Size
+ {
+ return $this->min;
+ }
+
+ public function getBy(): ?float
+ {
+ return $this->by;
+ }
+}
diff --git a/Config/Filter/Type/Watermark.php b/Config/Filter/Type/Watermark.php
new file mode 100644
index 000000000..aff94ddb0
--- /dev/null
+++ b/Config/Filter/Type/Watermark.php
@@ -0,0 +1,57 @@
+image = $image;
+ $this->position = $position;
+ $this->size = $size;
+ }
+
+ public function getImage(): string
+ {
+ return $this->image;
+ }
+
+ public function getPosition(): string
+ {
+ return $this->position;
+ }
+
+ public function getSize(): ?float
+ {
+ return $this->size;
+ }
+}
diff --git a/Config/FilterFactoryCollection.php b/Config/FilterFactoryCollection.php
new file mode 100644
index 000000000..c46c3e35c
--- /dev/null
+++ b/Config/FilterFactoryCollection.php
@@ -0,0 +1,57 @@
+filterFactories[$filterFactory->getName()] = $filterFactory;
+ }
+ }
+
+ /**
+ * @param string $name
+ *
+ * @throws NotFoundException
+ *
+ * @return FilterFactoryInterface
+ */
+ public function getFilterFactoryByName(string $name): FilterFactoryInterface
+ {
+ if (!isset($this->filterFactories[$name])) {
+ throw new NotFoundException(sprintf("Filter factory with name '%s' was not found.", $name));
+ }
+
+ return $this->filterFactories[$name];
+ }
+
+ /**
+ * @return FilterFactoryInterface[]
+ */
+ public function getAll()
+ {
+ return $this->filterFactories;
+ }
+}
diff --git a/Config/FilterInterface.php b/Config/FilterInterface.php
new file mode 100644
index 000000000..6b45461ef
--- /dev/null
+++ b/Config/FilterInterface.php
@@ -0,0 +1,28 @@
+name = $name;
+ $this->dataLoader = $dataLoader;
+ $this->quality = $quality;
+ $this->setFilters($filters);
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getDataLoader(): ?string
+ {
+ return $this->dataLoader;
+ }
+
+ public function getQuality(): ?int
+ {
+ return $this->quality;
+ }
+
+ /**
+ * @return FilterInterface[]
+ */
+ public function getFilters(): array
+ {
+ return $this->filters;
+ }
+
+ /**
+ * @param FilterInterface[] $filters
+ */
+ private function setFilters(array $filters): void
+ {
+ foreach ($filters as $filter) {
+ if (!($filter instanceof FilterInterface)) {
+ throw new InvalidArgumentException('Unknown filter provided.');
+ }
+ }
+ $this->filters = $filters;
+ }
+}
diff --git a/Config/StackBuilder.php b/Config/StackBuilder.php
new file mode 100644
index 000000000..d399bd537
--- /dev/null
+++ b/Config/StackBuilder.php
@@ -0,0 +1,51 @@
+stackFactory = $stackFactory;
+ $this->filterFactoryCollection = $filterFactoryCollection;
+ }
+
+ public function build(string $stackName, array $stackData): StackInterface
+ {
+ $filters = [];
+ if (!empty($stackData['filters'])) {
+ foreach ($stackData['filters'] as $filterName => $filterData) {
+ $filterFactory = $this->filterFactoryCollection->getFilterFactoryByName($filterName);
+ $filters[] = $filterFactory->create($filterData);
+ }
+ }
+
+ return $this->stackFactory->create(
+ $stackName,
+ $stackData['data_loader'],
+ $stackData['quality'],
+ $filters
+ );
+ }
+}
diff --git a/Config/StackBuilderInterface.php b/Config/StackBuilderInterface.php
new file mode 100644
index 000000000..56319d230
--- /dev/null
+++ b/Config/StackBuilderInterface.php
@@ -0,0 +1,17 @@
+stackBuilder = $stackBuilder;
+ $this->filtersConfiguration = $filtersConfiguration;
+ }
+
+ /**
+ * @return StackInterface[]
+ */
+ public function getStacks()
+ {
+ if (!empty($this->stacks)) {
+ return $this->stacks;
+ }
+
+ foreach ($this->filtersConfiguration as $filterSetName => $filterSetData) {
+ $this->stacks[] = $this->stackBuilder->build($filterSetName, $filterSetData);
+ }
+
+ return $this->stacks;
+ }
+}
diff --git a/Config/StackInterface.php b/Config/StackInterface.php
new file mode 100644
index 000000000..9275b7fb9
--- /dev/null
+++ b/Config/StackInterface.php
@@ -0,0 +1,26 @@
+sizeFactory = $sizeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Background::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ $color = $options['color'] ?? null;
+ $transparency = $options['transparency'] ?? null;
+ $position = $options['position'] ?? null;
+ $size = $this->sizeFactory->createFromOptions($options);
+
+ return new Background($color, $transparency, $position, $size);
+ }
+}
diff --git a/Factory/Config/Filter/CropFactory.php b/Factory/Config/Filter/CropFactory.php
new file mode 100644
index 000000000..7660e2a40
--- /dev/null
+++ b/Factory/Config/Filter/CropFactory.php
@@ -0,0 +1,60 @@
+sizeFactory = $sizeFactory;
+ $this->pointFactory = $pointFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Crop::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ return new Crop(
+ $this->pointFactory->createFromOptions($options, 'start'),
+ $this->sizeFactory->createFromOptions($options)
+ );
+ }
+}
diff --git a/Factory/Config/Filter/DownscaleFactory.php b/Factory/Config/Filter/DownscaleFactory.php
new file mode 100644
index 000000000..cf9b05c20
--- /dev/null
+++ b/Factory/Config/Filter/DownscaleFactory.php
@@ -0,0 +1,53 @@
+sizeFactory = $sizeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Downscale::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ $max = $this->sizeFactory->createFromOptions($options, 'max');
+ $by = isset($options['by']) ? (float) $options['by'] : null;
+
+ return new Downscale($max, $by);
+ }
+}
diff --git a/Factory/Config/Filter/FlipFactory.php b/Factory/Config/Filter/FlipFactory.php
new file mode 100644
index 000000000..546478529
--- /dev/null
+++ b/Factory/Config/Filter/FlipFactory.php
@@ -0,0 +1,39 @@
+pointFactory = $pointFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Paste::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ return new Paste($this->pointFactory->createFromOptions($options, 'start'));
+ }
+}
diff --git a/Factory/Config/Filter/RelativeResizeFactory.php b/Factory/Config/Filter/RelativeResizeFactory.php
new file mode 100644
index 000000000..95fe036bf
--- /dev/null
+++ b/Factory/Config/Filter/RelativeResizeFactory.php
@@ -0,0 +1,44 @@
+sizeFactory = $sizeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Resize::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ return new Resize($this->sizeFactory->createFromOptions($options));
+ }
+}
diff --git a/Factory/Config/Filter/RotateFactory.php b/Factory/Config/Filter/RotateFactory.php
new file mode 100644
index 000000000..7dc12beb5
--- /dev/null
+++ b/Factory/Config/Filter/RotateFactory.php
@@ -0,0 +1,41 @@
+sizeFactory = $sizeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Scale::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ $dimensions = $this->sizeFactory->createFromOptions($options, 'dim');
+ $to = isset($options['to']) ? (float) $options['to'] : null;
+
+ return new Scale($dimensions, $to);
+ }
+}
diff --git a/Factory/Config/Filter/StripFactory.php b/Factory/Config/Filter/StripFactory.php
new file mode 100644
index 000000000..9eef666d2
--- /dev/null
+++ b/Factory/Config/Filter/StripFactory.php
@@ -0,0 +1,39 @@
+sizeFactory = $sizeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Thumbnail::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ $size = $this->sizeFactory->createFromOptions($options);
+ $mode = $options['mode'] ?? null;
+ $allowUpscale = $options['allow_upscale'] ?? null;
+ $filter = $options['filter'] ?? null;
+
+ return new Thumbnail($size, $mode, $allowUpscale, $filter);
+ }
+}
diff --git a/Factory/Config/Filter/UpscaleFactory.php b/Factory/Config/Filter/UpscaleFactory.php
new file mode 100644
index 000000000..296022490
--- /dev/null
+++ b/Factory/Config/Filter/UpscaleFactory.php
@@ -0,0 +1,53 @@
+sizeFactory = $sizeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return Upscale::NAME;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create(array $options): FilterInterface
+ {
+ $min = $this->sizeFactory->createFromOptions($options, 'min');
+ $by = isset($options['by']) ? (float) $options['by'] : null;
+
+ return new Upscale($min, $by);
+ }
+}
diff --git a/Factory/Config/Filter/WatermarkFactory.php b/Factory/Config/Filter/WatermarkFactory.php
new file mode 100644
index 000000000..74e152ebe
--- /dev/null
+++ b/Factory/Config/Filter/WatermarkFactory.php
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %liip_imagine.filter_sets%
+
+
+
diff --git a/Tests/Config/FilterSetBuilderTest.php b/Tests/Config/FilterSetBuilderTest.php
new file mode 100644
index 000000000..8280c49bd
--- /dev/null
+++ b/Tests/Config/FilterSetBuilderTest.php
@@ -0,0 +1,112 @@
+filterSetFactoryMock = $this->createMock(StackFactoryInterface::class);
+ $this->filterFactoryCollectionMock = $this->createMock(FilterFactoryCollection::class);
+ $this->model = new StackBuilder($this->filterSetFactoryMock, $this->filterFactoryCollectionMock);
+ }
+
+ public function testBuildWithEmptyFilters()
+ {
+ $name = 'foo';
+ $dataLoader = 'bar';
+ $quality = 42;
+ $filters = [];
+
+ $filterSetMock = $this->createMock(StackInterface::class);
+
+ $this->filterSetFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($name, $dataLoader, $quality, $filters)
+ ->will($this->returnValue($filterSetMock));
+
+ $this->filterFactoryCollectionMock->expects($this->never())
+ ->method('getFilterFactoryByName');
+
+ $filterSet = $this->model->build($name, [
+ 'data_loader' => $dataLoader,
+ 'quality' => $quality,
+ 'filters' => $filters,
+ ]);
+ $this->assertSame($filterSetMock, $filterSet);
+ }
+
+ public function testBuildWithFilters()
+ {
+ $name = 'foo';
+ $dataLoader = 'bar';
+ $quality = 42;
+
+ $filterCode = 'foo_filter';
+ $filterData = ['foo_data'];
+ $filters = [
+ $filterCode => $filterData,
+ ];
+
+ $filterMock = $this->createMock(FilterInterface::class);
+ $filterFactoryMock = $this->createMock(FilterFactoryInterface::class);
+ $filterSetMock = $this->createMock(StackInterface::class);
+
+ $filterFactoryMock->expects($this->once())
+ ->method('create')
+ ->with($filterData)
+ ->will($this->returnValue($filterMock));
+
+ $this->filterSetFactoryMock->expects($this->once())
+ ->method('create')
+ ->will($this->returnValue($filterSetMock));
+
+ $this->filterFactoryCollectionMock->expects($this->once())
+ ->method('getFilterFactoryByName')
+ ->with($filterCode)
+ ->will($this->returnValue($filterFactoryMock));
+
+ $filterSet = $this->model->build($name, [
+ 'data_loader' => $dataLoader,
+ 'quality' => $quality,
+ 'filters' => $filters,
+ ]);
+ $this->assertSame($filterSetMock, $filterSet);
+ }
+}
diff --git a/Tests/Config/FilterSetCollectionTest.php b/Tests/Config/FilterSetCollectionTest.php
new file mode 100644
index 000000000..8b82aa51b
--- /dev/null
+++ b/Tests/Config/FilterSetCollectionTest.php
@@ -0,0 +1,41 @@
+createMock(StackInterface::class);
+
+ $stackBuilderMock = $this->createMock(StackBuilderInterface::class);
+ $stackBuilderMock->expects($this->once())
+ ->method('build')
+ ->with($filterSetName, $filterSetData)
+ ->will($this->returnValue($filterSetMock));
+
+ $model = new StackCollection($stackBuilderMock, [$filterSetName => $filterSetData]);
+ $this->assertSame([$filterSetMock], $model->getStacks());
+ $this->assertSame([$filterSetMock], $model->getStacks());
+ }
+}
diff --git a/Tests/Config/FilterSetTest.php b/Tests/Config/FilterSetTest.php
new file mode 100644
index 000000000..30c159295
--- /dev/null
+++ b/Tests/Config/FilterSetTest.php
@@ -0,0 +1,44 @@
+expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Unknown filter provided.');
+
+ $this->buildFilterSet(['not_a_filter']);
+ }
+
+ public function testSetFiltersWithValidFilterSuccess()
+ {
+ $filterMock = $this->createMock(FilterInterface::class);
+ $this->buildFilterSet([$filterMock]);
+ }
+
+ /**
+ * @param array $filters
+ */
+ private function buildFilterSet(array $filters)
+ {
+ new Stack('filter_name', 'data_loader', 42, $filters);
+ }
+}