diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index ea90757..99cb75d 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -23,6 +23,7 @@ jobs: with: php-version: 8.1 tools: composer:v2 + extensions: imagick - name: Install dependencies run: composer update --no-progress --prefer-dist --prefer-stable --optimize-autoloader --quiet @@ -42,6 +43,7 @@ jobs: with: php-version: 8.1 tools: composer:v2 + extensions: imagick - name: Install dependencies run: composer update --no-progress --prefer-dist --prefer-stable --optimize-autoloader --quiet diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 79ab344..dd8323a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -23,7 +23,7 @@ jobs: with: php-version: 8.1 coverage: none - extensions: tokenizer + extensions: imagick, tokenizer tools: composer:v2 - name: Install dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d3ba86..d1aaf8e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['8.1', '8.2'] + php-versions: ['8.1', '8.2', '8.3'] steps: - name: Checkout uses: actions/checkout@v3 @@ -26,6 +26,7 @@ jobs: with: php-version: ${{ matrix.php-versions }} tools: composer:v2 + extensions: imagick - name: Install dependencies run: composer update --no-progress --prefer-dist --prefer-stable --optimize-autoloader --quiet diff --git a/Dockerfile b/Dockerfile index 1586b5c..c54b568 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,14 +6,27 @@ WORKDIR /var/www/html RUN apk add --no-cache --update git RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -RUN apk add --no-cache ${PHPIZE_DEPS} \ - && apk add --no-cache imagemagick imagemagick-dev \ - && pecl install imagick \ - && docker-php-ext-enable imagick +RUN set -ex \ + # Build dependencies + && apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + && apk add --no-cache \ + libgomp \ + freetype-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + libpng-dev \ + libavif-dev \ + imagemagick \ + imagemagick-dev \ + && pecl install imagick-3.7.0 \ + && docker-php-ext-enable \ + imagick \ + && apk del .build-deps CMD tail -f /dev/null -FROM php:8.2.0RC6-cli-alpine3.16 AS php82 +FROM php:8.2.21-cli-alpine3.20 AS php82 CMD ["/bin/sh"] WORKDIR /var/www/html @@ -21,9 +34,50 @@ WORKDIR /var/www/html RUN apk add --no-cache --update git RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -RUN apk add --no-cache ${PHPIZE_DEPS} \ - && apk add --no-cache imagemagick imagemagick-dev \ - && pecl install imagick \ - && docker-php-ext-enable imagick +RUN set -ex \ + # Build dependencies + && apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + && apk add --no-cache \ + libgomp \ + freetype-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + libpng-dev \ + libavif-dev \ + imagemagick \ + imagemagick-dev \ + && pecl install imagick-3.7.0 \ + && docker-php-ext-enable \ + imagick \ + && apk del .build-deps + +CMD tail -f /dev/null + +FROM php:8.3.9-cli-alpine3.20 AS php83 + +CMD ["/bin/sh"] +WORKDIR /var/www/html + +RUN apk add --no-cache --update git +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +RUN set -ex \ + # Build dependencies + && apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + && apk add --no-cache \ + libgomp \ + freetype-dev \ + libjpeg-turbo-dev \ + libwebp-dev \ + libpng-dev \ + libavif-dev \ + imagemagick \ + imagemagick-dev \ + && pecl install imagick-3.7.0 \ + && docker-php-ext-enable \ + imagick \ + && apk del .build-deps CMD tail -f /dev/null diff --git a/Makefile b/Makefile index 33c3def..99d30c0 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ restart: tests.all: PHP=81 make tests.run PHP=82 make tests.run + PHP=83 make tests.run cs.fix: PHP=81 make composer.update diff --git a/docker-compose.yml b/docker-compose.yml index bbac3fa..192fd04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,3 +18,12 @@ services: container_name: 68publishers.image-storage.82 volumes: - .:/var/www/html:cached + + php83: + build: + context: . + dockerfile: Dockerfile + target: php83 + container_name: 68publishers.image-storage.83 + volumes: + - .:/var/www/html:cached diff --git a/src/Bridge/Nette/DI/ImageStorageExtension.php b/src/Bridge/Nette/DI/ImageStorageExtension.php index 001ba4d..6852e5d 100644 --- a/src/Bridge/Nette/DI/ImageStorageExtension.php +++ b/src/Bridge/Nette/DI/ImageStorageExtension.php @@ -373,7 +373,6 @@ public function createFileStorage(string $name, FileStorageConfig $config): Serv ->setFactory(ImagePersister::class, [ new Reference($this->prefix('filesystem.' . $name)), new Reference($this->prefix('config.' . $name)), - new Reference($this->prefix('modifier_facade.' . $name)), ]) ->setAutowired(false); diff --git a/src/FileInfo.php b/src/FileInfo.php index 8f97275..de39d14 100644 --- a/src/FileInfo.php +++ b/src/FileInfo.php @@ -10,6 +10,7 @@ use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface as ImageLinkGeneratorInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use function assert; final class FileInfo extends BaseFileInfo implements FileInfoInterface @@ -19,7 +20,7 @@ public function __construct(ImageLinkGeneratorInterface $linkGenerator, PathInfo parent::__construct($linkGenerator, $pathInfo, $imageStorageName); } - public function srcSet(DescriptorInterface $descriptor): string + public function srcSet(DescriptorInterface $descriptor): SrcSet { assert($this->linkGenerator instanceof ImageLinkGeneratorInterface); diff --git a/src/FileInfoInterface.php b/src/FileInfoInterface.php index 0e5f0b2..d7a38ae 100644 --- a/src/FileInfoInterface.php +++ b/src/FileInfoInterface.php @@ -6,8 +6,9 @@ use SixtyEightPublishers\FileStorage\FileInfoInterface as BaseFileInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; interface FileInfoInterface extends BaseFileInfoInterface, PathInfoInterface { - public function srcSet(DescriptorInterface $descriptor): string; + public function srcSet(DescriptorInterface $descriptor): SrcSet; } diff --git a/src/ImageStorage.php b/src/ImageStorage.php index 650f725..f1a73f3 100644 --- a/src/ImageStorage.php +++ b/src/ImageStorage.php @@ -19,6 +19,7 @@ use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; use function assert; @@ -89,7 +90,7 @@ public function resolveNoImage(string $path): ImagePathInfoInterface return $this->noImageResolver->resolveNoImage($path); } - public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor): string + public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor): SrcSet { assert($this->linkGenerator instanceof ImageLinkGeneratorInterface); diff --git a/src/LinkGenerator/LinkGenerator.php b/src/LinkGenerator/LinkGenerator.php index ac607cb..620a00f 100644 --- a/src/LinkGenerator/LinkGenerator.php +++ b/src/LinkGenerator/LinkGenerator.php @@ -12,6 +12,7 @@ use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGenerator; use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGeneratorFactoryInterface; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; @@ -49,7 +50,7 @@ public function link(FilePathInfoInterface $pathInfo): string return parent::link($pathInfo); } - public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor): string + public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor): SrcSet { if (null === $this->srcSetGenerator) { $this->srcSetGenerator = $this->srcSetGeneratorFactory->create($this, $this->modifierFacade); diff --git a/src/LinkGenerator/LinkGeneratorInterface.php b/src/LinkGenerator/LinkGeneratorInterface.php index d201372..db788e9 100644 --- a/src/LinkGenerator/LinkGeneratorInterface.php +++ b/src/LinkGenerator/LinkGeneratorInterface.php @@ -7,11 +7,12 @@ use SixtyEightPublishers\FileStorage\LinkGenerator\LinkGeneratorInterface as BaseLinkGeneratorInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; interface LinkGeneratorInterface extends BaseLinkGeneratorInterface { - public function srcSet(PathInfoInterface $info, DescriptorInterface $descriptor): string; + public function srcSet(PathInfoInterface $info, DescriptorInterface $descriptor): SrcSet; public function getSignatureStrategy(): ?SignatureStrategyInterface; } diff --git a/src/Modifier/Applicator/Format.php b/src/Modifier/Applicator/Format.php index b110adf..e405847 100644 --- a/src/Modifier/Applicator/Format.php +++ b/src/Modifier/Applicator/Format.php @@ -4,6 +4,7 @@ namespace SixtyEightPublishers\ImageStorage\Modifier\Applicator; +use Imagick; use Intervention\Image\Image; use SixtyEightPublishers\FileStorage\Config\ConfigInterface; use SixtyEightPublishers\FileStorage\PathInfoInterface; @@ -12,17 +13,24 @@ use SixtyEightPublishers\ImageStorage\Helper\SupportedType; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierValues; use SixtyEightPublishers\ImageStorage\Modifier\Quality; -use function assert; use function in_array; -use function is_int; final class Format implements ModifierApplicatorInterface { - public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): Image + public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): ?Image { $extension = $this->getFileExtension($image, $pathInfo); - $quality = $values->getOptional(Quality::class, $config[Config::ENCODE_QUALITY]); - assert(is_int($quality)); + $quality = $values->getOptional(Quality::class); + $needEncode = null !== $quality || SupportedType::getTypeByExtension($extension) !== $image->mime(); + + if (!$needEncode && 'pjpg' === $extension) { + $core = $image->getCore(); + $needEncode = !($core instanceof Imagick) || in_array($core->getInterlaceScheme(), [Imagick::INTERLACE_UNDEFINED, Imagick::INTERLACE_NO], true); + } + + if (!$needEncode) { + return null; + } if (in_array($extension, ['jpg', 'pjpg'], true)) { $image = $image->getDriver() @@ -35,7 +43,7 @@ public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues } } - return $image->encode($extension, $quality); + return $image->encode($extension, (int) ($quality ?? $config[Config::ENCODE_QUALITY])); } private function getFileExtension(Image $image, PathInfoInterface $pathInfo): string diff --git a/src/Modifier/Applicator/ModifierApplicatorInterface.php b/src/Modifier/Applicator/ModifierApplicatorInterface.php index 72fc7cf..c29b307 100644 --- a/src/Modifier/Applicator/ModifierApplicatorInterface.php +++ b/src/Modifier/Applicator/ModifierApplicatorInterface.php @@ -11,5 +11,8 @@ interface ModifierApplicatorInterface { - public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): Image; + /** + * Returns NULL of image is not modified + */ + public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): ?Image; } diff --git a/src/Modifier/Applicator/Orientation.php b/src/Modifier/Applicator/Orientation.php index 097ee74..def0b76 100644 --- a/src/Modifier/Applicator/Orientation.php +++ b/src/Modifier/Applicator/Orientation.php @@ -14,14 +14,24 @@ final class Orientation implements ModifierApplicatorInterface { - public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): Image + public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): ?Image { $orientation = $values->getOptional(OrientationModifier::class); if (!is_string($orientation) && !is_numeric($orientation)) { - return $image; + return null; } - return ($orientation === 'auto') ? $image->orientate() : $image->rotate((float) $orientation); + if ('auto' === $orientation) { + $exifOrientation = $image->exif('Orientation'); + + if (2 <= $exifOrientation && 8 >= $exifOrientation) { + return $image->orientate(); + } + + return null; + } + + return $image->rotate((float) $orientation); } } diff --git a/src/Modifier/Applicator/Resize.php b/src/Modifier/Applicator/Resize.php index 4bc2062..f0a08cf 100644 --- a/src/Modifier/Applicator/Resize.php +++ b/src/Modifier/Applicator/Resize.php @@ -27,7 +27,7 @@ final class Resize implements ModifierApplicatorInterface { - public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): Image + public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $values, ConfigInterface $config): ?Image { $width = $values->getOptional(Width::class); $height = $values->getOptional(Height::class); @@ -70,7 +70,7 @@ public function apply(Image $image, PathInfoInterface $pathInfo, ModifierValues $height = (int) ($height * $pd); if ($width === $imageWidth && $height === $imageHeight) { - return $image; + return null; } switch ($fit) { diff --git a/src/Modifier/Facade/ModifierFacade.php b/src/Modifier/Facade/ModifierFacade.php index b42e3cd..1454b48 100644 --- a/src/Modifier/Facade/ModifierFacade.php +++ b/src/Modifier/Facade/ModifierFacade.php @@ -102,7 +102,7 @@ public function getCodec(): CodecInterface return $this->codec; } - public function modifyImage(Image $image, PathInfoInterface $info, string|array $modifiers): Image + public function modifyImage(Image $image, PathInfoInterface $info, string|array $modifiers): ModifyResult { if (!is_array($modifiers)) { $modifiers = $this->getCodec()->decode(new PresetValue($modifiers)); @@ -118,10 +118,20 @@ public function modifyImage(Image $image, PathInfoInterface $info, string|array $validator->validate($values, $this->config); } + $modified = false; + foreach ($this->applicators as $applicator) { - $image = $applicator->apply($image, $info, $values, $this->config); + $modifiedImage = $applicator->apply($image, $info, $values, $this->config); + + if (null !== $modifiedImage) { + $image = $modifiedImage; + $modified = true; + } } - return $image; + return new ModifyResult( + image: $image, + modified: $modified, + ); } } diff --git a/src/Modifier/Facade/ModifierFacadeInterface.php b/src/Modifier/Facade/ModifierFacadeInterface.php index fe38451..199c3e7 100644 --- a/src/Modifier/Facade/ModifierFacadeInterface.php +++ b/src/Modifier/Facade/ModifierFacadeInterface.php @@ -41,5 +41,5 @@ public function getCodec(): CodecInterface; /** * @param string|array $modifiers */ - public function modifyImage(Image $image, PathInfoInterface $info, string|array $modifiers): Image; + public function modifyImage(Image $image, PathInfoInterface $info, string|array $modifiers): ModifyResult; } diff --git a/src/Modifier/Facade/ModifyResult.php b/src/Modifier/Facade/ModifyResult.php new file mode 100644 index 0000000..bbf1522 --- /dev/null +++ b/src/Modifier/Facade/ModifyResult.php @@ -0,0 +1,15 @@ +assertPathInfo($resource->getPathInfo(), __METHOD__); - $source = $resource->getSource(); - - if (!($source instanceof Image)) { - throw new InvalidArgumentException(sprintf( - 'A source must be instance of %s.', - Image::class, - )); - } if (null !== $pathInfo->getModifiers()) { - $source = $this->modifierFacade->modifyImage($source, $pathInfo, $pathInfo->getModifiers()); + $resource = $resource->modifyImage($pathInfo->getModifiers()); $prefix = self::FILESYSTEM_PREFIX_CACHE; } else { @@ -73,7 +64,7 @@ public function save(ResourceInterface $resource, array $config = []): string $flushCache = self::FILESYSTEM_PREFIX_SOURCE === $prefix && $this->exists($pathInfo); try { - $this->filesystemOperator->write($prefix . $path, $this->encodeImage($source), $config); + $this->filesystemOperator->write($prefix . $path, $this->encodeImage($resource), $config); if ($flushCache) { $this->delete($pathInfo, [ @@ -137,10 +128,19 @@ public function delete(FilePathInfoInterface $pathInfo, array $config = []): voi $this->deleteFile(self::FILESYSTEM_PREFIX_SOURCE . $pathInfo->getPath(), $config); } - private function encodeImage(Image $image): string + private function encodeImage(ImageResourceInterface $resource): string { + if (!$resource->hasBeenModified()) { + $contents = @file_get_contents($resource->getLocalFilename()); + + if (false !== $contents) { + return $contents; + } + } + $quality = $this->config[Config::ENCODE_QUALITY]; - $image = $image->isEncoded() ? $image : $image->encode(null, is_scalar($quality) ? (int) $quality : 90); + $image = $resource->getSource(); + $image = $image->isEncoded() ? $image : $image->encode('', is_scalar($quality) ? (int) $quality : 90); return $image->getEncoded(); } diff --git a/src/Resource/ImageResource.php b/src/Resource/ImageResource.php index c18f318..ccaee45 100644 --- a/src/Resource/ImageResource.php +++ b/src/Resource/ImageResource.php @@ -10,9 +10,12 @@ class ImageResource implements ResourceInterface { + private bool $modified = false; + public function __construct( private PathInfoInterface $pathInfo, private Image $image, + private readonly string $localFilename, private readonly ModifierFacadeInterface $modifierFacade, ) {} @@ -26,6 +29,16 @@ public function getSource(): Image return $this->image; } + public function getLocalFilename(): string + { + return $this->localFilename; + } + + public function hasBeenModified(): bool + { + return $this->modified; + } + public function withPathInfo(PathInfoInterface $pathInfo): self { $resource = clone $this; @@ -37,7 +50,9 @@ public function withPathInfo(PathInfoInterface $pathInfo): self public function modifyImage(string|array $modifiers): self { $resource = clone $this; - $resource->image = $this->modifierFacade->modifyImage($this->image, $this->pathInfo, $modifiers); + $modifyResult = $this->modifierFacade->modifyImage($this->image, $this->pathInfo, $modifiers); + $resource->image = $modifyResult->image; + $resource->modified = $modifyResult->modified; return $resource; } diff --git a/src/Resource/ResourceFactory.php b/src/Resource/ResourceFactory.php index c4ab9b1..e9b0d36 100644 --- a/src/Resource/ResourceFactory.php +++ b/src/Resource/ResourceFactory.php @@ -81,6 +81,7 @@ public function createResourceFromFile(PathInfoInterface $pathInfo, string $file source: $filename, location: $filename, ), + localFilename: $filename, modifierFacade: $this->modifierFacade, ); } diff --git a/src/Resource/ResourceInterface.php b/src/Resource/ResourceInterface.php index 0c74bb7..74ddefd 100644 --- a/src/Resource/ResourceInterface.php +++ b/src/Resource/ResourceInterface.php @@ -11,6 +11,10 @@ interface ResourceInterface extends FileResourceInterface { public function getSource(): Image; + public function getLocalFilename(): string; + + public function hasBeenModified(): bool; + /** * @param string|array $modifiers */ diff --git a/src/Resource/TmpFile.php b/src/Resource/TmpFile.php index 9915f28..fad961c 100644 --- a/src/Resource/TmpFile.php +++ b/src/Resource/TmpFile.php @@ -11,7 +11,7 @@ final class TmpFile private bool $unlinked = false; public function __construct( - private readonly string $filename, + public readonly string $filename, ) {} /** diff --git a/src/Resource/TmpFileImageResource.php b/src/Resource/TmpFileImageResource.php index 1a30a9f..f33dbed 100644 --- a/src/Resource/TmpFileImageResource.php +++ b/src/Resource/TmpFileImageResource.php @@ -19,6 +19,7 @@ public function __construct( parent::__construct( pathInfo: $pathInfo, image: $image, + localFilename: $this->tmpFile->filename, modifierFacade: $modifierFacade, ); } diff --git a/src/Responsive/Descriptor/DescriptorInterface.php b/src/Responsive/Descriptor/DescriptorInterface.php index d3366a1..bc44cac 100644 --- a/src/Responsive/Descriptor/DescriptorInterface.php +++ b/src/Responsive/Descriptor/DescriptorInterface.php @@ -4,9 +4,10 @@ namespace SixtyEightPublishers\ImageStorage\Responsive\Descriptor; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use Stringable; interface DescriptorInterface extends Stringable { - public function createSrcSet(ArgsFacade $args): string; + public function createSrcSet(ArgsFacade $args): SrcSet; } diff --git a/src/Responsive/Descriptor/WDescriptor.php b/src/Responsive/Descriptor/WDescriptor.php index 2ec8d9f..b0ad80a 100644 --- a/src/Responsive/Descriptor/WDescriptor.php +++ b/src/Responsive/Descriptor/WDescriptor.php @@ -6,6 +6,7 @@ use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\Modifier\Width; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use function array_map; use function array_unique; use function array_values; @@ -60,25 +61,38 @@ public function __toString(): string return sprintf('W(%s)', implode(',', $this->widths)); } - public function createSrcSet(ArgsFacade $args): string + public function createSrcSet(ArgsFacade $args): SrcSet { $wAlias = $args->getModifierAlias(Width::class); $modifiers = $args->getDefaultModifiers() ?? []; if (null === $wAlias) { - return empty($modifiers) ? '' : $args->createLink($modifiers); + $link = empty($modifiers) ? '' : $args->createLink($modifiers); + + return new SrcSet( + descriptor: 'w', + links: '' !== $link ? [ 0 => $link ] : [], + value: $link, + ); } - $links = array_map(static function (int $w) use ($args, $wAlias, $modifiers) { + $links = []; + $parts = array_map(static function (int $w) use ($args, $wAlias, $modifiers, &$links) { $modifiers[$wAlias] = $w; + $link = $args->createLink($modifiers); + $links[$w] = $link; return sprintf( '%s %dw', - $args->createLink($modifiers), + $link, $w, ); }, $this->widths); - return implode(', ', $links); + return new SrcSet( + descriptor: 'w', + links: $links, + value: implode(', ', $parts), + ); } } diff --git a/src/Responsive/Descriptor/XDescriptor.php b/src/Responsive/Descriptor/XDescriptor.php index bdec676..40a1fde 100644 --- a/src/Responsive/Descriptor/XDescriptor.php +++ b/src/Responsive/Descriptor/XDescriptor.php @@ -5,6 +5,7 @@ namespace SixtyEightPublishers\ImageStorage\Responsive\Descriptor; use SixtyEightPublishers\ImageStorage\Modifier\PixelDensity; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use function array_map; use function array_unshift; use function implode; @@ -41,25 +42,39 @@ public function __toString(): string return sprintf('X(%s)', implode(',', $this->pixelDensities)); } - public function createSrcSet(ArgsFacade $args): string + public function createSrcSet(ArgsFacade $args): SrcSet { $pdAlias = $args->getModifierAlias(PixelDensity::class); $modifiers = $args->getDefaultModifiers() ?? []; if (null === $pdAlias) { - return empty($modifiers) ? '' : $args->createLink($modifiers); + $link = empty($modifiers) ? '' : $args->createLink($modifiers); + + return new SrcSet( + descriptor: 'x', + links: '' !== $link ? [ '1.0' => $link ] : [], + value: $link, + ); } - $links = array_map(static function (float $pd) use ($args, $pdAlias, $modifiers) { + $links = []; + $parts = array_map(static function (float $pd) use ($args, $pdAlias, $modifiers, &$links) { $modifiers[$pdAlias] = $pd; + $link = $args->createLink($modifiers); + $formattedPd = number_format($pd, 1, '.', ''); + $links[$formattedPd] = $link; return sprintf( '%s%s', - $args->createLink($modifiers), - 1.0 === $pd ? '' : (' ' . number_format($pd, 1, '.', '') . 'x'), + $link, + 1.0 === $pd ? '' : (' ' . $formattedPd . 'x'), ); }, $this->pixelDensities); - return implode(', ', $links); + return new SrcSet( + descriptor: 'x', + links: $links, + value: implode(', ', $parts), + ); } } diff --git a/src/Responsive/SrcSet.php b/src/Responsive/SrcSet.php new file mode 100644 index 0000000..bb76d27 --- /dev/null +++ b/src/Responsive/SrcSet.php @@ -0,0 +1,30 @@ + $links + */ + public function __construct( + public readonly string $descriptor, + public readonly array $links, + public readonly string $value, + ) {} + + public function toString(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Responsive/SrcSetGenerator.php b/src/Responsive/SrcSetGenerator.php index 34d8262..8a7e0f6 100644 --- a/src/Responsive/SrcSetGenerator.php +++ b/src/Responsive/SrcSetGenerator.php @@ -13,7 +13,7 @@ final class SrcSetGenerator { - /** @var array */ + /** @var array */ private array $results = []; public function __construct( @@ -21,7 +21,7 @@ public function __construct( private readonly ModifierFacadeInterface $modifierFacade, ) {} - public function generate(DescriptorInterface $descriptor, PathInfoInterface $pathInfo): string + public function generate(DescriptorInterface $descriptor, PathInfoInterface $pathInfo): SrcSet { $key = $descriptor . '::' . (empty($pathInfo->getModifiers()) ? $pathInfo->withModifiers(['original' => true]) : $pathInfo); diff --git a/tests/FileInfoTest.phpt b/tests/FileInfoTest.phpt index a536623..8272f1e 100644 --- a/tests/FileInfoTest.phpt +++ b/tests/FileInfoTest.phpt @@ -12,6 +12,7 @@ use SixtyEightPublishers\ImageStorage\FileInfo; use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use Tester\Assert; use Tester\TestCase; use function call_user_func; @@ -42,13 +43,21 @@ final class FileInfoTest extends TestCase $pathInfo = Mockery::mock(ImagePathInfoInterface::class); $descriptor = Mockery::mock(DescriptorInterface::class); $fileInfo = new FileInfo($linkGenerator, $pathInfo, 'default'); + $srcSet = new SrcSet( + descriptor: 'w', + links: [ + 100 => 'var/www/h:100,w:100/file.png', + 200 => 'var/www/h:100,w:200/file.png', + ], + value: 'var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w', + ); $linkGenerator->shouldReceive('srcSet') ->once() ->with($fileInfo, $descriptor) - ->andReturn('srcset'); + ->andReturn($srcSet); - Assert::same('srcset', $fileInfo->srcSet($descriptor)); + Assert::same($srcSet, $fileInfo->srcSet($descriptor)); } public function testModifiersShouldBeNullIfFilePathInfoPassed(): void diff --git a/tests/ImageStorageTest.phpt b/tests/ImageStorageTest.phpt index 74030b6..6605687 100644 --- a/tests/ImageStorageTest.phpt +++ b/tests/ImageStorageTest.phpt @@ -20,6 +20,7 @@ use SixtyEightPublishers\ImageStorage\NoImage\NoImageResolverInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; use Tester\Assert; use Tester\TestCase; @@ -237,15 +238,23 @@ final class ImageStorageTest extends TestCase $linkGenerator = Mockery::mock(LinkGeneratorInterface::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); $descriptor = Mockery::mock(DescriptorInterface::class); + $srcSet = new SrcSet( + descriptor: 'w', + links: [ + 100 => 'var/www/h:100,w:100/file.png', + 200 => 'var/www/h:100,w:200/file.png', + ], + value: 'var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w', + ); $linkGenerator->shouldReceive('srcSet') ->once() ->with($pathInfo, $descriptor) - ->andReturn('srcset'); + ->andReturn($srcSet); $imageStorage = $this->createImageStorage(linkGenerator: $linkGenerator); - Assert::same('srcset', $imageStorage->srcSet($pathInfo, $descriptor)); + Assert::same($srcSet, $imageStorage->srcSet($pathInfo, $descriptor)); } public function testSignatureStrategyShouldBeReturned(): void diff --git a/tests/LinkGenerator/LinkGeneratorTest.phpt b/tests/LinkGenerator/LinkGeneratorTest.phpt index 0c213d7..2792ea5 100644 --- a/tests/LinkGenerator/LinkGeneratorTest.phpt +++ b/tests/LinkGenerator/LinkGeneratorTest.phpt @@ -13,6 +13,7 @@ use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGenerator; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGenerator; use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGeneratorFactoryInterface; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; @@ -133,6 +134,13 @@ final class LinkGeneratorTest extends TestCase $pathInfo = Mockery::mock(ImagePathInfoInterface::class); $descriptor = Mockery::mock(DescriptorInterface::class); $linkGenerator = new LinkGenerator(new Config([]), $modifierFacade, $srcSetGeneratorFactory); + $srcSet = new SrcSet( + descriptor: 'test', + links: [ + 1 => 'srcset', + ], + value: 'srcset', + ); $srcSetGeneratorFactory->shouldReceive('create') ->once() @@ -142,10 +150,10 @@ final class LinkGeneratorTest extends TestCase $srcSetGenerator->shouldReceive('generate') ->times(2) ->with($descriptor, $pathInfo) - ->andReturn('srcset'); + ->andReturn($srcSet); - Assert::same('srcset', $linkGenerator->srcSet($pathInfo, $descriptor)); - Assert::same('srcset', $linkGenerator->srcSet($pathInfo, $descriptor)); + Assert::same($srcSet, $linkGenerator->srcSet($pathInfo, $descriptor)); + Assert::same($srcSet, $linkGenerator->srcSet($pathInfo, $descriptor)); } public function tearDown(): void diff --git a/tests/Modifier/Applicator/FormatTest.phpt b/tests/Modifier/Applicator/FormatTest.phpt index 14644dd..a1337ff 100644 --- a/tests/Modifier/Applicator/FormatTest.phpt +++ b/tests/Modifier/Applicator/FormatTest.phpt @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SixtyEightPublishers\ImageStorage\Tests\Modifier\Applicator; +use Imagick; use Intervention\Image\AbstractDriver; use Intervention\Image\Image; use Mockery; @@ -14,6 +15,7 @@ use SixtyEightPublishers\ImageStorage\Modifier\Applicator\Format; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierValues; use SixtyEightPublishers\ImageStorage\Modifier\Quality as QualityModifier; use SixtyEightPublishers\ImageStorage\PathInfoInterface; +use stdClass; use Tester\Assert; use Tester\TestCase; @@ -21,17 +23,54 @@ require __DIR__ . '/../../bootstrap.php'; final class FormatTest extends TestCase { - public function testImageShouldBeFormattedWithPathInfoExtension(): void + public function testNullShouldBeReturnedIfQualityNotSpecifiedAndPathInfoHasSameExtensionAsImage(): void { $image = Mockery::mock(Image::class); - $modifiedImage = Mockery::mock(Image::class); $pathInfo = $this->createPathInfo('png'); - $modifierValues = $this->createModifierValues(); - $config = $this->createConfig(); + $modifierValues = $this->createModifierValues(null); + $config = Mockery::mock(ConfigInterface::class); - $image->shouldReceive('encode') + $image->shouldReceive('mime') + ->withNoArgs() + ->andReturn('image/png'); + + $applicator = new Format(); + + Assert::null($applicator->apply($image, $pathInfo, $modifierValues, $config)); + } + + public function testNullShouldBeReturnedIfQualityIsNotSpecifiedAndPathInfoExtensionIsNull(): void + { + $image = Mockery::mock(Image::class); + $pathInfo = $this->createPathInfo(null); + $modifierValues = $this->createModifierValues(null); + $config = Mockery::mock(ConfigInterface::class); + + $image->shouldReceive('mime') + ->withNoArgs() + ->andReturn('image/png'); + + $applicator = new Format(); + + Assert::null($applicator->apply($image, $pathInfo, $modifierValues, $config)); + } + + public function testImageShouldBeEncodedInDefaultFormatIfPathInfoExtensionIsNullAndImageMimeTypeIsUnsupported(): void + { + $image = Mockery::mock(Image::class); + $pathInfo = $this->createPathInfo(null); + $modifierValues = $this->createModifierValues(null); + $config = $this->createConfigForEncode(); + + $image->shouldReceive('mime') + ->withNoArgs() + ->andReturn('image/unsupported'); + + $modifiedImage = $this->setupJpgExpectationsOnImage($image, false); + + $modifiedImage->shouldReceive('encode') ->once() - ->with('png', 90) + ->with('jpg', 90) ->andReturn($modifiedImage); $applicator = new Format(); @@ -39,22 +78,21 @@ final class FormatTest extends TestCase Assert::same($modifiedImage, $applicator->apply($image, $pathInfo, $modifierValues, $config)); } - public function testImageShouldBeFormattedWithImageMimeType(): void + public function testImageShouldBeEncodedIfPathInfoExtensionIsDifferentThanImageMimeType(): void { $image = Mockery::mock(Image::class); $modifiedImage = Mockery::mock(Image::class); - $pathInfo = $this->createPathInfo(null); - $modifierValues = $this->createModifierValues(); - $config = $this->createConfig(); + $pathInfo = $this->createPathInfo('webp'); + $modifierValues = $this->createModifierValues(null); + $config = $this->createConfigForEncode(); $image->shouldReceive('mime') - ->once() ->withNoArgs() - ->andReturn('image/png'); + ->andReturn('image/jpeg'); $image->shouldReceive('encode') ->once() - ->with('png', 90) + ->with('webp', 90) ->andReturn($modifiedImage); $applicator = new Format(); @@ -62,23 +100,21 @@ final class FormatTest extends TestCase Assert::same($modifiedImage, $applicator->apply($image, $pathInfo, $modifierValues, $config)); } - public function testImageShouldBeFormattedWithDefaultExtension(): void + public function testImageShouldBeEncodedIfQualityIsSpecified(): void { $image = Mockery::mock(Image::class); + $modifiedImage = Mockery::mock(Image::class); $pathInfo = $this->createPathInfo(null); - $modifierValues = $this->createModifierValues(); - $config = $this->createConfig(); + $modifierValues = $this->createModifierValues(75); + $config = Mockery::mock(ConfigInterface::class); $image->shouldReceive('mime') - ->once() ->withNoArgs() - ->andReturn('image/unsupported'); - - $modifiedImage = $this->setupJpgExpectationsOnImage($image, false); + ->andReturn('image/png'); - $modifiedImage->shouldReceive('encode') + $image->shouldReceive('encode') ->once() - ->with('jpg', 90) + ->with('png', 75) ->andReturn($modifiedImage); $applicator = new Format(); @@ -86,12 +122,16 @@ final class FormatTest extends TestCase Assert::same($modifiedImage, $applicator->apply($image, $pathInfo, $modifierValues, $config)); } - public function testImageShouldBeFormattedIfPathInfoExtensionIsJpg(): void + public function testImageShouldBeEncodedToJpegFromDifferentFormat(): void { $image = Mockery::mock(Image::class); $pathInfo = $this->createPathInfo('jpg'); - $modifierValues = $this->createModifierValues(); - $config = $this->createConfig(); + $modifierValues = $this->createModifierValues(null); + $config = $this->createConfigForEncode(); + + $image->shouldReceive('mime') + ->withNoArgs() + ->andReturn('image/png'); $modifiedImage = $this->setupJpgExpectationsOnImage($image, false); @@ -105,12 +145,23 @@ final class FormatTest extends TestCase Assert::same($modifiedImage, $applicator->apply($image, $pathInfo, $modifierValues, $config)); } - public function testImageShouldBeFormattedIfPathInfoExtensionIsPjpg(): void + /** + * @dataProvider provideImageCoresForProgressiveJpegWithInvalidInterlaceScheme + */ + public function testImageShouldBeEncodedToProgressiveJpeg(object $core): void { $image = Mockery::mock(Image::class); $pathInfo = $this->createPathInfo('pjpg'); - $modifierValues = $this->createModifierValues(); - $config = $this->createConfig(); + $modifierValues = $this->createModifierValues(null); + $config = $this->createConfigForEncode(); + + $image->shouldReceive('mime') + ->withNoArgs() + ->andReturn('image/jpeg'); + + $image->shouldReceive('getCore') + ->withNoArgs() + ->andReturn($core); $modifiedImage = $this->setupJpgExpectationsOnImage($image, true); @@ -124,12 +175,63 @@ final class FormatTest extends TestCase Assert::same($modifiedImage, $applicator->apply($image, $pathInfo, $modifierValues, $config)); } + public function testImageShouldNotBeEncodedToProgressiveJpeg(): void + { + $image = Mockery::mock(Image::class); + $pathInfo = $this->createPathInfo('pjpg'); + $modifierValues = $this->createModifierValues(null); + $config = Mockery::mock(ConfigInterface::class); + $core = new class extends Imagick { + public function getInterlaceScheme(): int + { + return Imagick::INTERLACE_JPEG; + } + }; + + $image->shouldReceive('mime') + ->withNoArgs() + ->andReturn('image/jpeg'); + + $image->shouldReceive('getCore') + ->withNoArgs() + ->andReturn($core); + + $applicator = new Format(); + + Assert::null($applicator->apply($image, $pathInfo, $modifierValues, $config)); + } + + public function provideImageCoresForProgressiveJpegWithInvalidInterlaceScheme(): array + { + return [ + 'Non imagick core' => [ + new stdClass(), + ], + 'Imagick core with INTERLACE_UNDEFINED' => [ + new class extends Imagick { + public function getInterlaceScheme(): int + { + return Imagick::INTERLACE_UNDEFINED; + } + }, + ], + 'Imagick core with INTERLACE_NO' => [ + new class extends Imagick { + public function getInterlaceScheme(): int + { + return Imagick::INTERLACE_NO; + } + }, + ], + ]; + } + protected function tearDown(): void { Mockery::close(); } - private function createConfig(): ConfigInterface + private function createConfigForEncode(): ConfigInterface { $config = Mockery::mock(ConfigInterface::class); @@ -141,14 +243,14 @@ final class FormatTest extends TestCase return $config; } - private function createModifierValues(): ModifierValues + private function createModifierValues(?int $qualityReturn): ModifierValues { $modifierValues = Mockery::mock(ModifierValues::class); $modifierValues->shouldReceive('getOptional') ->once() - ->with(QualityModifier::class, 90) - ->andReturn(90); + ->with(QualityModifier::class) + ->andReturn($qualityReturn); return $modifierValues; } diff --git a/tests/Modifier/Applicator/OrientationTest.phpt b/tests/Modifier/Applicator/OrientationTest.phpt index 56f5cbf..f991a46 100644 --- a/tests/Modifier/Applicator/OrientationTest.phpt +++ b/tests/Modifier/Applicator/OrientationTest.phpt @@ -18,7 +18,7 @@ require __DIR__ . '/../../bootstrap.php'; final class OrientationTest extends TestCase { - public function testImageShouldNotBeModifiedIfValueIsNotStringOrNumber(): void + public function testNullShouldBeReturnedIfValueIsNotStringOrNumber(): void { $image = Mockery::mock(Image::class); $pathInfo = Mockery::mock(PathInfoInterface::class); @@ -32,10 +32,13 @@ final class OrientationTest extends TestCase $applicator = new Orientation(); - Assert::same($image, $applicator->apply($image, $pathInfo, $modifierValues, $config)); + Assert::null($applicator->apply($image, $pathInfo, $modifierValues, $config)); } - public function testImageShouldBeAutomaticallyOrientated(): void + /** + * @dataProvider provideNormalExifOrientations + */ + public function testNullShouldBeReturnedIfImageHasNormalOrientation(int $exifOrientation): void { $image = Mockery::mock(Image::class); $modifiedImage = Mockery::mock(Image::class); @@ -48,6 +51,37 @@ final class OrientationTest extends TestCase ->with(OrientationModifier::class) ->andReturn('auto'); + $image->shouldReceive('exif') + ->once() + ->with('Orientation') + ->andReturn($exifOrientation); + + $applicator = new Orientation(); + + Assert::null($applicator->apply($image, $pathInfo, $modifierValues, $config)); + } + + /** + * @dataProvider provideNonNormalExifOrientations + */ + public function testImageShouldBeAutomaticallyOrientated(int $exifOrientation): void + { + $image = Mockery::mock(Image::class); + $modifiedImage = Mockery::mock(Image::class); + $pathInfo = Mockery::mock(PathInfoInterface::class); + $modifierValues = Mockery::mock(ModifierValues::class); + $config = Mockery::mock(ConfigInterface::class); + + $modifierValues->shouldReceive('getOptional') + ->once() + ->with(OrientationModifier::class) + ->andReturn('auto'); + + $image->shouldReceive('exif') + ->once() + ->with('Orientation') + ->andReturn($exifOrientation); + $image->shouldReceive('orientate') ->once() ->withNoArgs() @@ -69,7 +103,7 @@ final class OrientationTest extends TestCase $modifierValues->shouldReceive('getOptional') ->once() ->with(OrientationModifier::class) - ->andReturn(-90); + ->andReturn('-90'); $image->shouldReceive('rotate') ->once() @@ -81,6 +115,27 @@ final class OrientationTest extends TestCase Assert::same($modifiedImage, $applicator->apply($image, $pathInfo, $modifierValues, $config)); } + public function provideNormalExifOrientations(): array + { + return [ + [0], # unknown? + [1], # normal + ]; + } + + public function provideNonNormalExifOrientations(): array + { + return [ + [2], + [3], + [4], + [5], + [6], + [7], + [8], + ]; + } + protected function tearDown(): void { Mockery::close(); diff --git a/tests/Modifier/Applicator/ResizeTest.phpt b/tests/Modifier/Applicator/ResizeTest.phpt index c35bc48..5278c5e 100644 --- a/tests/Modifier/Applicator/ResizeTest.phpt +++ b/tests/Modifier/Applicator/ResizeTest.phpt @@ -61,7 +61,7 @@ final class ResizeTest extends TestCase /** * @dataProvider getSameImageDimensionsData */ - public function testImageShouldNotBeModifiedIfCalculatedDimensionsAreSameAsOriginal(int $imageWidth, int $imageHeight, ?int $widthValue, ?int $heightValue, float $pd, array $aspectRatio): void + public function testNullShouldBeReturnedIfCalculatedDimensionsAreSameAsOriginal(int $imageWidth, int $imageHeight, ?int $widthValue, ?int $heightValue, float $pd, array $aspectRatio): void { $image = Mockery::mock(Image::class); $pathInfo = Mockery::mock(PathInfoInterface::class); @@ -80,7 +80,7 @@ final class ResizeTest extends TestCase $applicator = new Resize(); - Assert::same($image, $applicator->apply($image, $pathInfo, $modifierValues, $config)); + Assert::null($applicator->apply($image, $pathInfo, $modifierValues, $config)); } public function testImageShouldBeModifiedWithContainFit(): void diff --git a/tests/Modifier/Facade/ModifierFacadeTest.phpt b/tests/Modifier/Facade/ModifierFacadeTest.phpt index bf600c0..7772ef3 100644 --- a/tests/Modifier/Facade/ModifierFacadeTest.phpt +++ b/tests/Modifier/Facade/ModifierFacadeTest.phpt @@ -16,6 +16,7 @@ use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierValues; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacade; +use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifyResult; use SixtyEightPublishers\ImageStorage\Modifier\ModifierInterface; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Validator\ValidatorInterface; @@ -223,7 +224,53 @@ final class ModifierFacadeTest extends TestCase $facade->setApplicators([$applicator]); $facade->setValidators([$validator]); - Assert::same($image, $facade->modifyImage($image, $pathInfo, $modifiers)); + Assert::equal( + new ModifyResult( + image: $image, + modified: true, + ), + $facade->modifyImage($image, $pathInfo, $modifiers), + ); + } + + public function testImageShouldNotBeModifiedWithArrayModifiers(): void + { + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); + $validator = Mockery::mock(ValidatorInterface::class); + $applicator = Mockery::mock(ModifierApplicatorInterface::class); + + $modifierValues = Mockery::mock(ModifierValues::class); + $image = Mockery::mock(Image::class); + $pathInfo = Mockery::mock(PathInfoInterface::class); + $modifiers = ['w' => 100, 'h' => 200]; + + $modifierCollection->shouldReceive('parseValues') + ->once() + ->with($modifiers) + ->andReturn($modifierValues); + + $validator->shouldReceive('validate') + ->once() + ->with($modifierValues, $config); + + $applicator->shouldReceive('apply') + ->once() + ->with($image, $pathInfo, $modifierValues, $config) + ->andReturn(null); + + $facade = $this->createModifierFacade(config: $config, modifierCollection: $modifierCollection); + + $facade->setApplicators([$applicator]); + $facade->setValidators([$validator]); + + Assert::equal( + new ModifyResult( + image: $image, + modified: false, + ), + $facade->modifyImage($image, $pathInfo, $modifiers), + ); } public function testImageShouldBeModifiedWithPresetModifiers(): void @@ -268,7 +315,64 @@ final class ModifierFacadeTest extends TestCase $facade->setApplicators([$applicator]); $facade->setValidators([$validator]); - Assert::same($image, $facade->modifyImage($image, $pathInfo, $preset)); + Assert::equal( + new ModifyResult( + image: $image, + modified: true, + ), + $facade->modifyImage($image, $pathInfo, $preset), + ); + } + + public function testImageShouldNotBeModifiedWithPresetModifiers(): void + { + $config = Mockery::mock(ConfigInterface::class); + $codec = Mockery::mock(CodecInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); + $validator = Mockery::mock(ValidatorInterface::class); + $applicator = Mockery::mock(ModifierApplicatorInterface::class); + + $modifierValues = Mockery::mock(ModifierValues::class); + $image = Mockery::mock(Image::class); + $pathInfo = Mockery::mock(PathInfoInterface::class); + $modifiers = ['w' => 100, 'h' => 200]; + $preset = 'preset'; + + $codec->shouldReceive('decode') + ->once() + ->with(Mockery::type(PresetValue::class)) + ->andReturnUsing(static function (PresetValue $value) use ($preset, $modifiers): array { + Assert::same($preset, $value->presetName); + + return $modifiers; + }); + + $modifierCollection->shouldReceive('parseValues') + ->once() + ->with($modifiers) + ->andReturn($modifierValues); + + $validator->shouldReceive('validate') + ->once() + ->with($modifierValues, $config); + + $applicator->shouldReceive('apply') + ->once() + ->with($image, $pathInfo, $modifierValues, $config) + ->andReturn(null); + + $facade = $this->createModifierFacade(config: $config, codec: $codec, modifierCollection: $modifierCollection); + + $facade->setApplicators([$applicator]); + $facade->setValidators([$validator]); + + Assert::equal( + new ModifyResult( + image: $image, + modified: false, + ), + $facade->modifyImage($image, $pathInfo, $preset), + ); } protected function tearDown(): void diff --git a/tests/Persistence/ImagePersisterTest.phpt b/tests/Persistence/ImagePersisterTest.phpt index 44f5b54..98bef71 100644 --- a/tests/Persistence/ImagePersisterTest.phpt +++ b/tests/Persistence/ImagePersisterTest.phpt @@ -21,10 +21,8 @@ use SixtyEightPublishers\FileStorage\Config\ConfigInterface; use SixtyEightPublishers\FileStorage\Exception\FilesystemException; use SixtyEightPublishers\FileStorage\PathInfoInterface as FilePathInfoInterface; use SixtyEightPublishers\FileStorage\Persistence\FilePersisterInterface; -use SixtyEightPublishers\FileStorage\Resource\ResourceInterface as FileResourceInterface; use SixtyEightPublishers\ImageStorage\Config\Config; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; -use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Persistence\ImagePersister; use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface; @@ -32,6 +30,10 @@ use SixtyEightPublishers\ImageStorage\Resource\ResourceInterface as ImageResourc use SixtyEightPublishers\ImageStorage\Resource\TmpFileImageResource; use Tester\Assert; use Tester\TestCase; +use function file_put_contents; +use function sys_get_temp_dir; +use function uniqid; +use function unlink; require __DIR__ . '/../bootstrap.php'; @@ -42,7 +44,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $this->createFilesystem(), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); Assert::type(FilesystemOperator::class, $persister->getFilesystem()); @@ -53,7 +54,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $this->createFilesystem(), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); Assert::exception( @@ -70,7 +70,6 @@ final class ImagePersisterTest extends TestCase 'path/image' => '... image content ...', ]), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -94,7 +93,6 @@ final class ImagePersisterTest extends TestCase 'path/w:100,h:200/image.png' => '... image content ...', ]), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -116,7 +114,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $this->createFilesystem(), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -138,7 +135,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $this->createFilesystem(), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -161,7 +157,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -188,7 +183,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $this->createFilesystem(), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $resource = Mockery::mock(ImageResourceInterface::class); @@ -206,39 +200,11 @@ final class ImagePersisterTest extends TestCase ); } - public function testErrorShouldBeThrownIfSourceForSavingIsNotImage(): void - { - $persister = new ImagePersister( - $this->createFilesystem(), - Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), - ); - - $resource = Mockery::mock(FileResourceInterface::class); - $pathInfo = Mockery::mock(ImagePathInfoInterface::class); - - $resource->shouldReceive('getPathInfo') - ->once() - ->withNoArgs() - ->andReturn($pathInfo); - - $resource->shouldReceive('getSource') - ->withNoArgs() - ->andReturn('source'); - - Assert::exception( - static fn () => $persister->save($resource), - InvalidArgumentException::class, - 'A source must be instance of Intervention\Image\Image.', - ); - } - - public function testNewSourceImageShouldBeSaved(): void + public function testNewModifiedSourceShouldBeEncodedAndSaved(): void { $filesystem = $this->createFilesystem(); $config = Mockery::mock(ConfigInterface::class); - $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); - $persister = new ImagePersister($filesystem, $config, $modifierFacade); + $persister = new ImagePersister($filesystem, $config); $resource = Mockery::mock(ImageResourceInterface::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -249,6 +215,11 @@ final class ImagePersisterTest extends TestCase ->withNoArgs() ->andReturn($pathInfo); + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(true); + $resource->shouldReceive('getSource') ->once() ->withNoArgs() @@ -273,6 +244,50 @@ final class ImagePersisterTest extends TestCase Assert::same('... image content ...', $filesystem->read('source://path/image')); } + public function testNewNonModifiedSourceShouldBeSavedFromOriginal(): void + { + $localFilename = sys_get_temp_dir() . '/' . uniqid('68publishers:ImageStorage', true) . '_testNewNonModifiedSourceShouldBeSavedFromOriginal'; + + file_put_contents(filename: $localFilename, data: '...the original content...'); + + try { + $filesystem = $this->createFilesystem(); + $config = Mockery::mock(ConfigInterface::class); + $persister = new ImagePersister($filesystem, $config); + + $resource = Mockery::mock(ImageResourceInterface::class); + $pathInfo = Mockery::mock(ImagePathInfoInterface::class); + + $resource->shouldReceive('getPathInfo') + ->once() + ->withNoArgs() + ->andReturn($pathInfo); + + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(false); + + $resource->shouldReceive('getLocalFilename') + ->once() + ->withNoArgs() + ->andReturn($localFilename); + + $pathInfo->shouldReceive('getModifiers') + ->withNoArgs() + ->andReturn(null); + + $pathInfo->shouldReceive('getPath') + ->andReturn('path/image'); + + Assert::same('path/image', $persister->save($resource)); + Assert::true($filesystem->fileExists('source://path/image')); + Assert::same('...the original content...', $filesystem->read('source://path/image')); + } finally { + @unlink($localFilename); + } + } + public function testSourceImageShouldBeUpdatedAndCachedImagesShouldBeDeleted(): void { $filesystem = $this->createFilesystem([ @@ -282,8 +297,7 @@ final class ImagePersisterTest extends TestCase 'path/w:100,pd:2/image.png' => '... image content ...', ]); $config = Mockery::mock(ConfigInterface::class); - $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); - $persister = new ImagePersister($filesystem, $config, $modifierFacade); + $persister = new ImagePersister($filesystem, $config); $resource = Mockery::mock(ImageResourceInterface::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -299,6 +313,11 @@ final class ImagePersisterTest extends TestCase ->withNoArgs() ->andReturn($image); + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(true); + $pathInfo->shouldReceive('getModifiers') ->withNoArgs() ->andReturn(null); @@ -333,8 +352,7 @@ final class ImagePersisterTest extends TestCase { $filesystem = $this->createFilesystem(); $config = Mockery::mock(ConfigInterface::class); - $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); - $persister = new ImagePersister($filesystem, $config, $modifierFacade); + $persister = new ImagePersister($filesystem, $config); $resource = Mockery::mock(TmpFileImageResource::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -350,6 +368,11 @@ final class ImagePersisterTest extends TestCase ->withNoArgs() ->andReturn($image); + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(true); + $resource->shouldReceive('unlink') ->once() ->withNoArgs() @@ -378,13 +401,11 @@ final class ImagePersisterTest extends TestCase { $filesystem = $this->createFilesystem(); $config = Mockery::mock(ConfigInterface::class); - $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); - $persister = new ImagePersister($filesystem, $config, $modifierFacade); + $persister = new ImagePersister($filesystem, $config); $resource = Mockery::mock(ImageResourceInterface::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); $image = Mockery::mock(Image::class); - $modifiedImage = Mockery::mock(Image::class); $resource->shouldReceive('getPathInfo') ->once() @@ -396,6 +417,11 @@ final class ImagePersisterTest extends TestCase ->withNoArgs() ->andReturn($image); + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(true); + $pathInfo->shouldReceive('getModifiers') ->withNoArgs() ->andReturn(['w' => 100, 'h' => 200]); @@ -403,17 +429,17 @@ final class ImagePersisterTest extends TestCase $pathInfo->shouldReceive('getPath') ->andReturn('path/w:100,h:200/image.png'); - $modifierFacade->shouldReceive('modifyImage') + $resource->shouldReceive('modifyImage') ->once() - ->with($image, $pathInfo, ['w' => 100, 'h' => 200]) - ->andReturn($modifiedImage); + ->with(['w' => 100, 'h' => 200]) + ->andReturnSelf(); $config->shouldReceive('offsetGet') ->once() ->with(Config::ENCODE_QUALITY) ->andReturn(90); - $this->setupImageSaveExpectations($modifiedImage); + $this->setupImageSaveExpectations($image); Assert::same('path/w:100,h:200/image.png', $persister->save($resource)); Assert::true($filesystem->fileExists('cache://path/w:100,h:200/image.png')); @@ -424,8 +450,7 @@ final class ImagePersisterTest extends TestCase { $filesystem = Mockery::instanceMock($this->createFilesystem()); $config = Mockery::mock(ConfigInterface::class); - $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); - $persister = new ImagePersister($filesystem, $config, $modifierFacade); + $persister = new ImagePersister($filesystem, $config); $resource = Mockery::mock(ImageResourceInterface::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -441,6 +466,11 @@ final class ImagePersisterTest extends TestCase ->withNoArgs() ->andReturn($image); + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(true); + $pathInfo->shouldReceive('getModifiers') ->withNoArgs() ->andReturn(null); @@ -471,8 +501,7 @@ final class ImagePersisterTest extends TestCase { $filesystem = Mockery::instanceMock($this->createFilesystem()); $config = Mockery::mock(ConfigInterface::class); - $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); - $persister = new ImagePersister($filesystem, $config, $modifierFacade); + $persister = new ImagePersister($filesystem, $config); $resource = Mockery::mock(ImageResourceInterface::class); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -488,6 +517,11 @@ final class ImagePersisterTest extends TestCase ->withNoArgs() ->andReturn($image); + $resource->shouldReceive('hasBeenModified') + ->once() + ->withNoArgs() + ->andReturn(true); + $pathInfo->shouldReceive('getModifiers') ->withNoArgs() ->andReturn(null); @@ -520,7 +554,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $this->createFilesystem(), Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); Assert::exception( @@ -539,7 +572,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -566,7 +598,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -599,7 +630,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -635,7 +665,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -679,7 +708,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -727,7 +755,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -771,7 +798,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); @@ -815,7 +841,6 @@ final class ImagePersisterTest extends TestCase $persister = new ImagePersister( $filesystem, Mockery::mock(ConfigInterface::class), - Mockery::mock(ModifierFacadeInterface::class), ); $pathInfo = Mockery::mock(ImagePathInfoInterface::class); diff --git a/tests/Resource/ImageResourceTest.phpt b/tests/Resource/ImageResourceTest.phpt index e1573cb..bea4517 100644 --- a/tests/Resource/ImageResourceTest.phpt +++ b/tests/Resource/ImageResourceTest.phpt @@ -8,6 +8,7 @@ use Intervention\Image\Image; use Mockery; use SixtyEightPublishers\FileStorage\PathInfoInterface as FilePathInfoInterface; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifyResult; use SixtyEightPublishers\ImageStorage\Resource\ImageResource; use Tester\Assert; use Tester\TestCase; @@ -23,12 +24,15 @@ final class ImageResourceTest extends TestCase $pathInfo1 = Mockery::mock(FilePathInfoInterface::class); $pathInfo2 = Mockery::mock(FilePathInfoInterface::class); - $resource1 = new ImageResource($pathInfo1, $image, $modifierFacade); + $resource1 = new ImageResource($pathInfo1, $image, '/tmp/image.png', $modifierFacade); $resource2 = $resource1->withPathInfo($pathInfo2); Assert::notSame($resource1, $resource2); Assert::same($pathInfo1, $resource1->getPathInfo()); Assert::same($pathInfo2, $resource2->getPathInfo()); + + Assert::same('/tmp/image.png', $resource1->getLocalFilename()); + Assert::same('/tmp/image.png', $resource2->getLocalFilename()); } public function testImageShouldBeModified(): void @@ -37,18 +41,24 @@ final class ImageResourceTest extends TestCase $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); $image = Mockery::mock(Image::class); $modifiedImage = Mockery::mock(Image::class); + $modifyResult = new ModifyResult( + image: $modifiedImage, + modified: true, + ); $modifierFacade->shouldReceive('modifyImage') ->once() ->with($image, $pathInfo, ['w' => 300]) - ->andReturn($modifiedImage); + ->andReturn($modifyResult); - $resource1 = new ImageResource($pathInfo, $image, $modifierFacade); + $resource1 = new ImageResource($pathInfo, $image, '/tmp/image.png', $modifierFacade); $resource2 = $resource1->modifyImage(['w' => 300]); Assert::notSame($resource1, $resource2); Assert::same($image, $resource1->getSource()); Assert::same($modifiedImage, $resource2->getSource()); + Assert::false($resource1->hasBeenModified()); + Assert::true($resource2->hasBeenModified()); } protected function tearDown(): void diff --git a/tests/Resource/TmpFileImageResourceTest.phpt b/tests/Resource/TmpFileImageResourceTest.phpt index 4a9fd28..abc2893 100644 --- a/tests/Resource/TmpFileImageResourceTest.phpt +++ b/tests/Resource/TmpFileImageResourceTest.phpt @@ -9,6 +9,7 @@ use Mockery; use Nette\Utils\FileSystem; use SixtyEightPublishers\FileStorage\PathInfoInterface as FilePathInfoInterface; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifyResult; use SixtyEightPublishers\ImageStorage\Resource\TmpFile; use SixtyEightPublishers\ImageStorage\Resource\TmpFileImageResource; use Tester\Assert; @@ -28,12 +29,15 @@ final class TmpFileImageResourceTest extends TestCase $pathInfo1 = Mockery::mock(FilePathInfoInterface::class); $pathInfo2 = Mockery::mock(FilePathInfoInterface::class); - $resource1 = new TmpFileImageResource($pathInfo1, $image, $modifierFacade, new TmpFile('fake')); + $resource1 = new TmpFileImageResource($pathInfo1, $image, $modifierFacade, new TmpFile('/tmp/fake')); $resource2 = $resource1->withPathInfo($pathInfo2); Assert::notSame($resource1, $resource2); Assert::same($pathInfo1, $resource1->getPathInfo()); Assert::same($pathInfo2, $resource2->getPathInfo()); + + Assert::same('/tmp/fake', $resource1->getLocalFilename()); + Assert::same('/tmp/fake', $resource2->getLocalFilename()); } public function testImageShouldBeModified(): void @@ -42,18 +46,24 @@ final class TmpFileImageResourceTest extends TestCase $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); $image = Mockery::mock(Image::class); $modifiedImage = Mockery::mock(Image::class); + $modifyResult = new ModifyResult( + image: $modifiedImage, + modified: true, + ); $modifierFacade->shouldReceive('modifyImage') ->once() ->with($image, $pathInfo, ['w' => 300]) - ->andReturn($modifiedImage); + ->andReturn($modifyResult); - $resource1 = new TmpFileImageResource($pathInfo, $image, $modifierFacade, new TmpFile('fake')); + $resource1 = new TmpFileImageResource($pathInfo, $image, $modifierFacade, new TmpFile('/tmp/fake')); $resource2 = $resource1->modifyImage(['w' => 300]); Assert::notSame($resource1, $resource2); Assert::same($image, $resource1->getSource()); Assert::same($modifiedImage, $resource2->getSource()); + Assert::false($resource1->hasBeenModified()); + Assert::true($resource2->hasBeenModified()); } public function testTmpFileShouldBeUnlinked(): void diff --git a/tests/Responsive/Descriptor/WDescriptorTest.phpt b/tests/Responsive/Descriptor/WDescriptorTest.phpt index da06721..d6125a5 100644 --- a/tests/Responsive/Descriptor/WDescriptorTest.phpt +++ b/tests/Responsive/Descriptor/WDescriptorTest.phpt @@ -10,6 +10,7 @@ use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\Modifier\Width; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\ArgsFacade; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\WDescriptor; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use Tester\Assert; use Tester\TestCase; use function call_user_func; @@ -69,7 +70,7 @@ final class WDescriptorTest extends TestCase Assert::same('W(100,200,300)', (string) new WDescriptor(100, 200, 300)); } - public function testSrcSetShouldBeEmptyStringIfWidthModifierNotFoundAndDefaultModifiersAreNull(): void + public function testSrcSetShouldBeEmptyIfWidthModifierNotFoundAndDefaultModifiersAreNull(): void { $argsFacade = Mockery::mock(ArgsFacade::class); @@ -85,10 +86,17 @@ final class WDescriptorTest extends TestCase $descriptor = new WDescriptor(100, 200, 300); - Assert::same('', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'w', + links: [], + value: '', + ), + $descriptor->createSrcSet($argsFacade), + ); } - public function testSrcSetShouldBeEmptyStringIfWidthModifierNotFoundAndDefaultModifiersAreEmptyArray(): void + public function testSrcSetShouldBeEmptyIfWidthModifierNotFoundAndDefaultModifiersAreEmptyArray(): void { $argsFacade = Mockery::mock(ArgsFacade::class); @@ -104,7 +112,14 @@ final class WDescriptorTest extends TestCase $descriptor = new WDescriptor(100, 200, 300); - Assert::same('', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'w', + links: [], + value: '', + ), + $descriptor->createSrcSet($argsFacade), + ); } public function testSrcSetShouldBeSingleLinkIfWidthModifierNotFound(): void @@ -128,10 +143,19 @@ final class WDescriptorTest extends TestCase $descriptor = new WDescriptor(100, 200, 300); - Assert::same('var/www/h:100/file.png', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'w', + links: [ + 0 => 'var/www/h:100/file.png', + ], + value: 'var/www/h:100/file.png', + ), + $descriptor->createSrcSet($argsFacade), + ); } - public function testSrcSetShouldBeEmptyStringIfNoWidthsDefined(): void + public function testSrcSetShouldBeEmptyIfNoWidthsDefined(): void { $argsFacade = Mockery::mock(ArgsFacade::class); @@ -147,7 +171,14 @@ final class WDescriptorTest extends TestCase $descriptor = new WDescriptor(); - Assert::same('', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'w', + links: [], + value: '', + ), + $descriptor->createSrcSet($argsFacade), + ); } public function testSrcSetShouldContainMultipleLinksWithoutDefaultModifiers(): void @@ -181,7 +212,18 @@ final class WDescriptorTest extends TestCase $descriptor = new WDescriptor(100, 200, 300); - Assert::same('var/www/w:100/file.png 100w, var/www/w:200/file.png 200w, var/www/w:300/file.png 300w', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'w', + links: [ + 100 => 'var/www/w:100/file.png', + 200 => 'var/www/w:200/file.png', + 300 => 'var/www/w:300/file.png', + ], + value: 'var/www/w:100/file.png 100w, var/www/w:200/file.png 200w, var/www/w:300/file.png 300w', + ), + $descriptor->createSrcSet($argsFacade), + ); } public function testSrcSetShouldContainMultipleLinksWithDefaultModifiers(): void @@ -215,7 +257,18 @@ final class WDescriptorTest extends TestCase $descriptor = new WDescriptor(100, 200, 300); - Assert::same('var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w, var/www/h:100,w:300/file.png 300w', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'w', + links: [ + 100 => 'var/www/h:100,w:100/file.png', + 200 => 'var/www/h:100,w:200/file.png', + 300 => 'var/www/h:100,w:300/file.png', + ], + value: 'var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w, var/www/h:100,w:300/file.png 300w', + ), + $descriptor->createSrcSet($argsFacade), + ); } public function getInvalidRangeData(): array diff --git a/tests/Responsive/Descriptor/XDescriptorTest.phpt b/tests/Responsive/Descriptor/XDescriptorTest.phpt index 57e8aff..d6d8710 100644 --- a/tests/Responsive/Descriptor/XDescriptorTest.phpt +++ b/tests/Responsive/Descriptor/XDescriptorTest.phpt @@ -9,6 +9,7 @@ use Mockery; use SixtyEightPublishers\ImageStorage\Modifier\PixelDensity; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\ArgsFacade; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\XDescriptor; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use Tester\Assert; use Tester\TestCase; use function call_user_func; @@ -37,7 +38,7 @@ final class XDescriptorTest extends TestCase Assert::same('X(1,2,3)', (string) new XDescriptor(1.0, 2.0, 3.0)); } - public function testSrcSetShouldBeEmptyStringIfPixelDensityModifierNotFoundAndDefaultModifiersAreNull(): void + public function testSrcSetShouldBeEmptyIfPixelDensityModifierNotFoundAndDefaultModifiersAreNull(): void { $argsFacade = Mockery::mock(ArgsFacade::class); @@ -53,10 +54,13 @@ final class XDescriptorTest extends TestCase $descriptor = new XDescriptor(1, 2, 2.5); - Assert::same('', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet(descriptor: 'x', links: [], value: ''), + $descriptor->createSrcSet($argsFacade), + ); } - public function testSrcSetShouldBeEmptyStringIfPixelDensityModifierNotFoundAndDefaultModifiersAreEmptyArray(): void + public function testSrcSetShouldBeEmptyIfPixelDensityModifierNotFoundAndDefaultModifiersAreEmptyArray(): void { $argsFacade = Mockery::mock(ArgsFacade::class); @@ -72,7 +76,10 @@ final class XDescriptorTest extends TestCase $descriptor = new XDescriptor(1, 2, 2.5); - Assert::same('', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet(descriptor: 'x', links: [], value: ''), + $descriptor->createSrcSet($argsFacade), + ); } public function testSrcSetShouldBeSingleLinkIfWidthModifierNotFound(): void @@ -96,7 +103,14 @@ final class XDescriptorTest extends TestCase $descriptor = new XDescriptor(1, 2, 2.5); - Assert::same('var/www/h:100/file.png', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'x', + links: ['1.0' => 'var/www/h:100/file.png'], + value: 'var/www/h:100/file.png', + ), + $descriptor->createSrcSet($argsFacade), + ); } public function testSrcSetShouldContainMultipleLinksWithoutDefaultModifiers(): void @@ -130,7 +144,18 @@ final class XDescriptorTest extends TestCase $descriptor = new XDescriptor(1, 2, 2.5); - Assert::same('var/www/pd:1/file.png, var/www/pd:2/file.png 2.0x, var/www/pd:2.5/file.png 2.5x', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'x', + links: [ + '1.0' => 'var/www/pd:1/file.png', + '2.0' => 'var/www/pd:2/file.png', + '2.5' => 'var/www/pd:2.5/file.png', + ], + value: 'var/www/pd:1/file.png, var/www/pd:2/file.png 2.0x, var/www/pd:2.5/file.png 2.5x', + ), + $descriptor->createSrcSet($argsFacade), + ); } public function testSrcSetShouldContainMultipleLinksWithDefaultModifiers(): void @@ -164,7 +189,18 @@ final class XDescriptorTest extends TestCase $descriptor = new XDescriptor(1, 2, 2.5); - Assert::same('var/www/h:100,pd:1/file.png, var/www/h:100,pd:2/file.png 2.0x, var/www/h:100,pd:2.5/file.png 2.5x', $descriptor->createSrcSet($argsFacade)); + Assert::equal( + new SrcSet( + descriptor: 'x', + links: [ + '1.0' => 'var/www/h:100,pd:1/file.png', + '2.0' => 'var/www/h:100,pd:2/file.png', + '2.5' => 'var/www/h:100,pd:2.5/file.png', + ], + value: 'var/www/h:100,pd:1/file.png, var/www/h:100,pd:2/file.png 2.0x, var/www/h:100,pd:2.5/file.png 2.5x', + ), + $descriptor->createSrcSet($argsFacade), + ); } protected function tearDown(): void diff --git a/tests/Responsive/SrcSetGeneratorTest.phpt b/tests/Responsive/SrcSetGeneratorTest.phpt index 00aaca3..dbed00a 100644 --- a/tests/Responsive/SrcSetGeneratorTest.phpt +++ b/tests/Responsive/SrcSetGeneratorTest.phpt @@ -11,6 +11,7 @@ use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\ArgsFacade; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; +use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGenerator; use Tester\Assert; use Tester\TestCase; @@ -27,6 +28,13 @@ final class SrcSetGeneratorTest extends TestCase $descriptor = Mockery::mock(DescriptorInterface::class); $pathInfo = Mockery::mock(PathInfoInterface::class); $modifiedPathInfo = Mockery::mock(PathInfoInterface::class); + $srcSet = new SrcSet( + descriptor: 'test', + links: [ + 1 => 'srcset', + ], + value: 'srcset', + ); $descriptor->shouldReceive('__toString') ->times(2) @@ -51,26 +59,26 @@ final class SrcSetGeneratorTest extends TestCase $descriptor->shouldReceive('createSrcSet') ->times(1) ->with(Mockery::type(ArgsFacade::class)) - ->andReturnUsing(function (ArgsFacade $facade) use ($linkGenerator, $modifierFacade, $pathInfo): string { + ->andReturnUsing(function (ArgsFacade $facade) use ($linkGenerator, $modifierFacade, $pathInfo, $srcSet): SrcSet { $this->assertFacadeProperties($facade, $linkGenerator, $modifierFacade, $pathInfo); - return 'srcset'; + return $srcSet; }); $generator = new SrcSetGenerator($linkGenerator, $modifierFacade); $this->assertCache($generator, []); - Assert::same('srcset', $generator->generate($descriptor, $pathInfo)); + Assert::same($srcSet, $generator->generate($descriptor, $pathInfo)); $this->assertCache($generator, [ - 'TEST()::var/www/original/file.png' => 'srcset', + 'TEST()::var/www/original/file.png' => $srcSet, ]); - Assert::same('srcset', $generator->generate($descriptor, $pathInfo)); + Assert::same($srcSet, $generator->generate($descriptor, $pathInfo)); $this->assertCache($generator, [ - 'TEST()::var/www/original/file.png' => 'srcset', + 'TEST()::var/www/original/file.png' => $srcSet, ]); } @@ -80,6 +88,13 @@ final class SrcSetGeneratorTest extends TestCase $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); $descriptor = Mockery::mock(DescriptorInterface::class); $pathInfo = Mockery::mock(PathInfoInterface::class); + $srcSet = new SrcSet( + descriptor: 'test', + links: [ + 1 => 'srcset', + ], + value: 'srcset', + ); $descriptor->shouldReceive('__toString') ->times(2) @@ -99,26 +114,26 @@ final class SrcSetGeneratorTest extends TestCase $descriptor->shouldReceive('createSrcSet') ->times(1) ->with(Mockery::type(ArgsFacade::class)) - ->andReturnUsing(function (ArgsFacade $facade) use ($linkGenerator, $modifierFacade, $pathInfo): string { + ->andReturnUsing(function (ArgsFacade $facade) use ($linkGenerator, $modifierFacade, $pathInfo, $srcSet): SrcSet { $this->assertFacadeProperties($facade, $linkGenerator, $modifierFacade, $pathInfo); - return 'srcset'; + return $srcSet; }); $generator = new SrcSetGenerator($linkGenerator, $modifierFacade); $this->assertCache($generator, []); - Assert::same('srcset', $generator->generate($descriptor, $pathInfo)); + Assert::same($srcSet, $generator->generate($descriptor, $pathInfo)); $this->assertCache($generator, [ - 'TEST()::var/www/h:100/file.png' => 'srcset', + 'TEST()::var/www/h:100/file.png' => $srcSet, ]); - Assert::same('srcset', $generator->generate($descriptor, $pathInfo)); + Assert::same($srcSet, $generator->generate($descriptor, $pathInfo)); $this->assertCache($generator, [ - 'TEST()::var/www/h:100/file.png' => 'srcset', + 'TEST()::var/www/h:100/file.png' => $srcSet, ]); } diff --git a/tests/Responsive/SrcSetTest.phpt b/tests/Responsive/SrcSetTest.phpt new file mode 100644 index 0000000..97d2389 --- /dev/null +++ b/tests/Responsive/SrcSetTest.phpt @@ -0,0 +1,32 @@ + 'var/www/h:100,w:100/file.png', + 200 => 'var/www/h:100,w:200/file.png', + 300 => 'var/www/h:100,w:300/file.png', + ], + value: 'var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w, var/www/h:100,w:300/file.png 300w', + ); + + Assert::same('var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w, var/www/h:100,w:300/file.png 300w', $srcSet->toString()); + Assert::same('var/www/h:100,w:100/file.png 100w, var/www/h:100,w:200/file.png 200w, var/www/h:100,w:300/file.png 300w', (string) $srcSet); + } +} + +(new SrcSetTest())->run();