generated from Nodarx/template
feat: add ImageMagick preview provider
Implements the shared preview provider contract using ImageMagick to generate thumbnails for supported source types.
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/lib/vendor/
|
||||||
|
/vendor/
|
||||||
|
coverage/
|
||||||
|
*.cache
|
||||||
|
.phpunit.coverage
|
||||||
|
.phpactor/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "ktxm/provider-preview-imagemagick",
|
||||||
|
"description": "ImageMagick preview provider for Ktrix",
|
||||||
|
"type": "ktrix-module",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"platform": {
|
||||||
|
"php": "8.3"
|
||||||
|
},
|
||||||
|
"autoloader-suffix": "ProviderPreviewImageMagick",
|
||||||
|
"vendor-dir": "lib/vendor"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.3 <=8.5",
|
||||||
|
"ext-imagick": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"KTXM\\ProviderPreviewImageMagick\\": "lib/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "d507794e977605dfd588f099f118506c",
|
||||||
|
"packages": [],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {
|
||||||
|
"php": ">=8.3 <=8.5",
|
||||||
|
"ext-imagick": "*"
|
||||||
|
},
|
||||||
|
"platform-dev": {},
|
||||||
|
"platform-overrides": {
|
||||||
|
"php": "8.3"
|
||||||
|
},
|
||||||
|
"plugin-api-version": "2.6.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXM\ProviderPreviewImageMagick;
|
||||||
|
|
||||||
|
use KTXC\Resource\ProviderManager;
|
||||||
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
use KTXF\Resource\Provider\ProviderInterface;
|
||||||
|
|
||||||
|
final class Module extends ModuleInstanceAbstract
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ProviderManager $providers)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): string
|
||||||
|
{
|
||||||
|
return 'provider_preview_imagemagick';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function version(): string
|
||||||
|
{
|
||||||
|
return '0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'ImageMagick Preview Provider';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Generates image previews using ImageMagick';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author(): string
|
||||||
|
{
|
||||||
|
return 'Ktrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->providers->register(ProviderInterface::TYPE_PREVIEW, 'imagemagick', Provider::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXM\ProviderPreviewImageMagick;
|
||||||
|
|
||||||
|
use Imagick;
|
||||||
|
use ImagickException;
|
||||||
|
use KTXF\Preview\MimeType;
|
||||||
|
use KTXF\Preview\PreviewGenerationException;
|
||||||
|
use KTXF\Preview\PreviewRequest;
|
||||||
|
use KTXF\Preview\PreviewResult;
|
||||||
|
use KTXF\Preview\PreviewSource;
|
||||||
|
use KTXF\Preview\Provider\PreviewProviderInterface;
|
||||||
|
use KTXF\Resource\BinaryResource;
|
||||||
|
use KTXF\Resource\Provider\ProviderInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class Provider implements PreviewProviderInterface
|
||||||
|
{
|
||||||
|
private const INPUT_FORMATS = [
|
||||||
|
'JPEG' => 'image/jpeg',
|
||||||
|
'PNG' => 'image/png',
|
||||||
|
'WEBP' => 'image/webp',
|
||||||
|
'GIF' => 'image/gif',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const OUTPUT_FORMATS = [
|
||||||
|
'image/jpeg' => 'JPEG',
|
||||||
|
'image/png' => 'PNG',
|
||||||
|
'image/webp' => 'WEBP',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function type(): string
|
||||||
|
{
|
||||||
|
return ProviderInterface::TYPE_PREVIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function identifier(): string
|
||||||
|
{
|
||||||
|
return 'imagemagick';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'ImageMagick';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Generates previews for common image formats';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function icon(): string
|
||||||
|
{
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportedMimeTypes(): array
|
||||||
|
{
|
||||||
|
return array_values(self::INPUT_FORMATS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function outputMimeTypes(): array
|
||||||
|
{
|
||||||
|
return array_keys(self::OUTPUT_FORMATS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(string $sourceMimeType, PreviewRequest $request): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$sourceMimeType = MimeType::normalize($sourceMimeType);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($sourceMimeType, $this->supportedMimeTypes(), true)
|
||||||
|
&& isset(self::OUTPUT_FORMATS[$request->preferredMimeType])
|
||||||
|
&& ($request->page === null || $request->page === 1)
|
||||||
|
&& $request->timestamp === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(PreviewSource $source, PreviewRequest $request): PreviewResult
|
||||||
|
{
|
||||||
|
if (!$this->supports($source->mimeType, $request)) {
|
||||||
|
throw new PreviewGenerationException('ImageMagick does not support the requested preview conversion');
|
||||||
|
}
|
||||||
|
if ($source->size !== null && $source->size > $request->maxSourceSize) {
|
||||||
|
throw new PreviewGenerationException('Preview source exceeds the configured size limit');
|
||||||
|
}
|
||||||
|
|
||||||
|
$images = new Imagick();
|
||||||
|
$image = null;
|
||||||
|
try {
|
||||||
|
$images->readImageBlob($this->readSource($source, $request->maxSourceSize));
|
||||||
|
$images->setIteratorIndex(0);
|
||||||
|
$image = $images->getImage();
|
||||||
|
|
||||||
|
$actualMimeType = self::INPUT_FORMATS[strtoupper($image->getImageFormat())] ?? null;
|
||||||
|
if ($actualMimeType === null || $actualMimeType !== $source->mimeType) {
|
||||||
|
throw new PreviewGenerationException('Decoded image type does not match the source MIME type');
|
||||||
|
}
|
||||||
|
|
||||||
|
$image->autoOrient();
|
||||||
|
$this->resize($image, $request->maxWidth, $request->maxHeight);
|
||||||
|
$image->stripImage();
|
||||||
|
$image->setImagePage(0, 0, 0, 0);
|
||||||
|
$this->encode($image, $request->preferredMimeType, $request->quality);
|
||||||
|
|
||||||
|
$content = $image->getImageBlob();
|
||||||
|
if ($content === '') {
|
||||||
|
throw new PreviewGenerationException('ImageMagick produced an empty preview');
|
||||||
|
}
|
||||||
|
|
||||||
|
$mimeType = $request->preferredMimeType;
|
||||||
|
return new PreviewResult(
|
||||||
|
new BinaryResource(
|
||||||
|
'preview.' . $this->extension($mimeType),
|
||||||
|
$mimeType,
|
||||||
|
(static function () use ($content): \Generator { yield $content; })(),
|
||||||
|
),
|
||||||
|
$image->getImageWidth(),
|
||||||
|
$image->getImageHeight(),
|
||||||
|
);
|
||||||
|
} catch (PreviewGenerationException $exception) {
|
||||||
|
throw $exception;
|
||||||
|
} catch (ImagickException $exception) {
|
||||||
|
throw new PreviewGenerationException('ImageMagick could not generate the preview', previous: $exception);
|
||||||
|
} finally {
|
||||||
|
$image?->clear();
|
||||||
|
$images->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generatorVersion(): string
|
||||||
|
{
|
||||||
|
$version = Imagick::getVersion();
|
||||||
|
return $version['versionString'] ?? 'ImageMagick';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function priority(): int
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readSource(PreviewSource $source, int $limit): string
|
||||||
|
{
|
||||||
|
$content = '';
|
||||||
|
foreach ($source->open()->stream() as $chunk) {
|
||||||
|
if (!is_string($chunk)) {
|
||||||
|
throw new PreviewGenerationException('Preview source yielded a non-string chunk');
|
||||||
|
}
|
||||||
|
if (strlen($content) + strlen($chunk) > $limit) {
|
||||||
|
throw new PreviewGenerationException('Preview source exceeds the configured size limit');
|
||||||
|
}
|
||||||
|
$content .= $chunk;
|
||||||
|
}
|
||||||
|
if ($content === '') {
|
||||||
|
throw new PreviewGenerationException('Preview source is empty');
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resize(Imagick $image, int $maxWidth, int $maxHeight): void
|
||||||
|
{
|
||||||
|
$width = $image->getImageWidth();
|
||||||
|
$height = $image->getImageHeight();
|
||||||
|
$scale = min($maxWidth / $width, $maxHeight / $height, 1);
|
||||||
|
if ($scale < 1) {
|
||||||
|
$image->resizeImage(
|
||||||
|
max(1, (int) floor($width * $scale)),
|
||||||
|
max(1, (int) floor($height * $scale)),
|
||||||
|
Imagick::FILTER_LANCZOS,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encode(Imagick $image, string $mimeType, int $quality): void
|
||||||
|
{
|
||||||
|
if ($mimeType === 'image/jpeg' && $image->getImageAlphaChannel()) {
|
||||||
|
$image->setImageBackgroundColor('white');
|
||||||
|
$flattened = $image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
|
||||||
|
$image->clear();
|
||||||
|
$image->readImageBlob($flattened->getImageBlob());
|
||||||
|
$flattened->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
$image->setImageFormat(self::OUTPUT_FORMATS[$mimeType]);
|
||||||
|
$image->setImageCompressionQuality($quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extension(string $mimeType): string
|
||||||
|
{
|
||||||
|
return match ($mimeType) {
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
'image/png' => 'png',
|
||||||
|
default => 'webp',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXT\ProviderPreviewImageMagick\Unit;
|
||||||
|
|
||||||
|
use Imagick;
|
||||||
|
use ImagickPixel;
|
||||||
|
use KTXC\Resource\ProviderManager;
|
||||||
|
use KTXF\Preview\PreviewGenerationException;
|
||||||
|
use KTXF\Preview\PreviewRequest;
|
||||||
|
use KTXF\Preview\PreviewSource;
|
||||||
|
use KTXF\Resource\BinaryResource;
|
||||||
|
use KTXF\Resource\Provider\ProviderInterface;
|
||||||
|
use KTXM\ProviderPreviewImageMagick\Module;
|
||||||
|
use KTXM\ProviderPreviewImageMagick\Provider;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function moduleRegistersThePreviewProvider(): void
|
||||||
|
{
|
||||||
|
$providers = $this->createMock(ProviderManager::class);
|
||||||
|
$providers->expects(self::once())->method('register')->with(
|
||||||
|
ProviderInterface::TYPE_PREVIEW,
|
||||||
|
'imagemagick',
|
||||||
|
Provider::class,
|
||||||
|
);
|
||||||
|
|
||||||
|
(new Module($providers))->boot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function inputFormats(): iterable
|
||||||
|
{
|
||||||
|
yield 'JPEG' => ['JPEG', 'image/jpeg'];
|
||||||
|
yield 'PNG' => ['PNG', 'image/png'];
|
||||||
|
yield 'WebP' => ['WEBP', 'image/webp'];
|
||||||
|
yield 'GIF' => ['GIF', 'image/gif'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('inputFormats')]
|
||||||
|
public function generatesBoundedWebpFromSupportedInputs(string $format, string $mimeType): void
|
||||||
|
{
|
||||||
|
$provider = new Provider();
|
||||||
|
$result = $provider->generate(
|
||||||
|
$this->source($this->image($format, 800, 400), $mimeType),
|
||||||
|
$this->request(),
|
||||||
|
);
|
||||||
|
$output = $this->content($result->resource);
|
||||||
|
$image = new Imagick();
|
||||||
|
$image->readImageBlob($output);
|
||||||
|
|
||||||
|
self::assertSame('image/webp', $result->resource->mimeType());
|
||||||
|
self::assertSame(640, $result->width);
|
||||||
|
self::assertSame(320, $result->height);
|
||||||
|
self::assertSame('WEBP', $image->getImageFormat());
|
||||||
|
self::assertSame(640, $image->getImageWidth());
|
||||||
|
self::assertSame(320, $image->getImageHeight());
|
||||||
|
$image->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function doesNotUpscaleSmallImages(): void
|
||||||
|
{
|
||||||
|
$result = (new Provider())->generate(
|
||||||
|
$this->source($this->image('PNG', 100, 50), 'image/png'),
|
||||||
|
$this->request(),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(100, $result->width);
|
||||||
|
self::assertSame(50, $result->height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function outputFormats(): iterable
|
||||||
|
{
|
||||||
|
yield 'JPEG' => ['image/jpeg', 'JPEG'];
|
||||||
|
yield 'PNG' => ['image/png', 'PNG'];
|
||||||
|
yield 'WebP' => ['image/webp', 'WEBP'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
#[DataProvider('outputFormats')]
|
||||||
|
public function encodesEveryAdvertisedOutputFormat(string $mimeType, string $format): void
|
||||||
|
{
|
||||||
|
$request = new PreviewRequest('popover', 640, 440, $mimeType, 80);
|
||||||
|
$result = (new Provider())->generate(
|
||||||
|
$this->source($this->image('PNG', 20, 20, 'rgba(255, 0, 0, 0.25)'), 'image/png'),
|
||||||
|
$request,
|
||||||
|
);
|
||||||
|
$image = new Imagick();
|
||||||
|
$image->readImageBlob($this->content($result->resource));
|
||||||
|
|
||||||
|
self::assertSame($mimeType, $result->resource->mimeType());
|
||||||
|
self::assertSame($format, $image->getImageFormat());
|
||||||
|
$image->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function preservesTransparencyInPngOutput(): void
|
||||||
|
{
|
||||||
|
$request = new PreviewRequest('popover', 640, 440, 'image/png', 80);
|
||||||
|
$result = (new Provider())->generate(
|
||||||
|
$this->source($this->image('PNG', 20, 20, 'rgba(255, 0, 0, 0.25)'), 'image/png'),
|
||||||
|
$request,
|
||||||
|
);
|
||||||
|
$image = new Imagick();
|
||||||
|
$image->readImageBlob($this->content($result->resource));
|
||||||
|
$color = $image->getImagePixelColor(0, 0)->getColor(1);
|
||||||
|
|
||||||
|
self::assertSame('image/png', $result->resource->mimeType());
|
||||||
|
self::assertLessThan(0.5, $color['a']);
|
||||||
|
$image->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function usesOnlyTheFirstAnimatedFrame(): void
|
||||||
|
{
|
||||||
|
$animation = new Imagick();
|
||||||
|
foreach (['red', 'blue'] as $color) {
|
||||||
|
$frame = new Imagick();
|
||||||
|
$frame->newImage(20, 20, new ImagickPixel($color));
|
||||||
|
$frame->setImageFormat('GIF');
|
||||||
|
$frame->setImageDelay(10);
|
||||||
|
$animation->addImage($frame);
|
||||||
|
$frame->clear();
|
||||||
|
}
|
||||||
|
$content = $animation->getImagesBlob();
|
||||||
|
$animation->clear();
|
||||||
|
|
||||||
|
$result = (new Provider())->generate($this->source($content, 'image/gif'), $this->request());
|
||||||
|
$output = new Imagick();
|
||||||
|
$output->readImageBlob($this->content($result->resource));
|
||||||
|
|
||||||
|
self::assertSame(1, $output->getNumberImages());
|
||||||
|
$color = $output->getImagePixelColor(0, 0)->getColor();
|
||||||
|
self::assertGreaterThan($color['b'], $color['r']);
|
||||||
|
$output->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function rejectsMalformedOrMismatchedImages(): void
|
||||||
|
{
|
||||||
|
$provider = new Provider();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$provider->generate($this->source('not an image', 'image/png'), $this->request());
|
||||||
|
self::fail('Malformed input should fail');
|
||||||
|
} catch (PreviewGenerationException) {
|
||||||
|
self::addToAssertionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->expectException(PreviewGenerationException::class);
|
||||||
|
$this->expectExceptionMessage('does not match');
|
||||||
|
$provider->generate(
|
||||||
|
$this->source($this->image('PNG', 20, 20), 'image/jpeg'),
|
||||||
|
$this->request(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function enforcesTheSourceSizeLimitWhileReading(): void
|
||||||
|
{
|
||||||
|
$content = $this->image('PNG', 20, 20);
|
||||||
|
$request = new PreviewRequest(
|
||||||
|
'popover',
|
||||||
|
640,
|
||||||
|
440,
|
||||||
|
'image/webp',
|
||||||
|
80,
|
||||||
|
maxSourceSize: strlen($content) - 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException(PreviewGenerationException::class);
|
||||||
|
$this->expectExceptionMessage('size limit');
|
||||||
|
(new Provider())->generate($this->source($content, 'image/png', size: null), $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function request(): PreviewRequest
|
||||||
|
{
|
||||||
|
return new PreviewRequest('popover', 640, 440, 'image/webp', 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function source(string $content, string $mimeType, ?int $size = null): PreviewSource
|
||||||
|
{
|
||||||
|
return new PreviewSource(
|
||||||
|
'document',
|
||||||
|
'image',
|
||||||
|
hash('sha256', $content),
|
||||||
|
$mimeType,
|
||||||
|
$size ?? strlen($content),
|
||||||
|
static fn(): BinaryResource => new BinaryResource(
|
||||||
|
'source.bin',
|
||||||
|
$mimeType,
|
||||||
|
(static function () use ($content): \Generator {
|
||||||
|
$middle = intdiv(strlen($content), 2);
|
||||||
|
yield substr($content, 0, $middle);
|
||||||
|
yield substr($content, $middle);
|
||||||
|
})(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function image(string $format, int $width, int $height, string $color = 'red'): string
|
||||||
|
{
|
||||||
|
$image = new Imagick();
|
||||||
|
$image->newImage($width, $height, new ImagickPixel($color));
|
||||||
|
$image->setImageFormat($format);
|
||||||
|
$content = $image->getImageBlob();
|
||||||
|
$image->clear();
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function content(BinaryResource $resource): string
|
||||||
|
{
|
||||||
|
$content = '';
|
||||||
|
foreach ($resource->stream() as $chunk) {
|
||||||
|
$content .= $chunk;
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$loader = require dirname(__DIR__, 4) . '/vendor/autoload.php';
|
||||||
|
$loader->addPsr4('KTXM\\ProviderPreviewImageMagick\\', dirname(__DIR__, 2) . '/lib/');
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="../../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
failOnDeprecation="true"
|
||||||
|
failOnNotice="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
bootstrap="bootstrap.php">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit Tests">
|
||||||
|
<directory>Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>../../lib</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
Reference in New Issue
Block a user