From 4b10d060dd50b8165da67d8b7e92fa37d9cfb060 Mon Sep 17 00:00:00 2001 From: Thomas Deinhamer Date: Mon, 21 Apr 2025 12:21:54 +0200 Subject: [PATCH 1/4] Add imglab integration --- Classes/ImageService/ImageServiceFactory.php | 12 ++ Classes/ImageService/ImglabImageService.php | 112 ++++++++++++++++ Classes/UriBuilder/ImglabUri.php | 131 +++++++++++++++++++ Classes/UriBuilder/ImglabUriSource.php | 23 ++++ README.md | 13 ++ Resources/Private/Language/settings.xlf | 4 + composer.json | 1 + ext_conf_template.txt | 7 +- 8 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 Classes/ImageService/ImglabImageService.php create mode 100644 Classes/UriBuilder/ImglabUri.php create mode 100644 Classes/UriBuilder/ImglabUriSource.php diff --git a/Classes/ImageService/ImageServiceFactory.php b/Classes/ImageService/ImageServiceFactory.php index ce15255..91329de 100644 --- a/Classes/ImageService/ImageServiceFactory.php +++ b/Classes/ImageService/ImageServiceFactory.php @@ -16,6 +16,7 @@ use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImagorUriSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgixFolderSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgixProxySource; +use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabUriSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgProxyFileSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgProxyUriSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\OptimoleUriSource; @@ -52,6 +53,7 @@ public function __invoke(): ?ImageServiceInterface CloudinaryImageService::getIdentifier() => $this->getCloudinaryImageService($options), CloudImageImageService::getIdentifier() => $this->getCloudImageImageService($options), GumletImageService::getIdentifier() => $this->getGumletImageService($options), + ImglabImageService::getIdentifier() => $this->getImglabImageService($options), }; } @@ -236,4 +238,14 @@ private function getGumletImageService(array $options): GumletImageService $options, ); } + + private function getImglabImageService(array $options): ImglabImageService + { + $source = new ImglabUriSource(); + + return new ImglabImageService( + $source, + $options, + ); + } } diff --git a/Classes/ImageService/ImglabImageService.php b/Classes/ImageService/ImglabImageService.php new file mode 100644 index 0000000..6e069b6 --- /dev/null +++ b/Classes/ImageService/ImglabImageService.php @@ -0,0 +1,112 @@ +configureOptions($resolver); + $this->options = $resolver->resolve($options); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'api_endpoint' => null, + ]); + } + + public function getEndpoint(): string + { + return $this->options['api_endpoint']; + } + + public function hasConfiguration(): bool + { + return filter_var($this->getEndpoint(), FILTER_VALIDATE_URL) !== false; + } + + public function getSupportedMimeTypes(): array + { + return [ + 'image/jpeg', + 'image/jp2', + 'image/jpx', + 'image/jpm', + 'image/png', + 'image/gif', + 'image/webp', + 'image/heic', + 'application/pdf', + 'video/youtube', + 'video/vimeo', + ]; + } + + public function canProcessTask(TaskInterface $task): bool + { + return + in_array($task->getName(), ['Preview', 'CropScaleMask'], true) && + in_array($task->getSourceFile()->getMimeType(), $this->getSupportedMimeTypes(), true); + } + + public function processTask(TaskInterface $task): ImageServiceResult + { + $file = $task->getSourceFile(); + $configuration = $task->getTargetFile()->getProcessingConfiguration(); + $dimension = ImageDimension::fromProcessingTask($task); + + $uri = new ImglabUri($this->getEndpoint()); + $uri->setSource($this->source->getSource($file)); + + $mode = (static function ($configuration) { + switch (true) { + default: + return 'clip'; + + case str_ends_with((string) ($configuration['width'] ?? ''), 'c'): + case str_ends_with((string) ($configuration['height'] ?? ''), 'c'): + return 'crop'; + } + })($configuration); + + $uri->setMode($mode); + + if (isset($configuration['crop'])) { + $uri->setCrop( + (int) $configuration['crop']->getWidth(), + (int) $configuration['crop']->getHeight(), + (int) $configuration['crop']->getOffsetLeft(), + (int) $configuration['crop']->getOffsetTop(), + ); + } + + $width = (int) ($configuration['width'] ?? $configuration['maxWidth'] ?? null); + $height = (int) ($configuration['height'] ?? $configuration['maxHeight'] ?? null); + + $uri->setWidth($width); + $uri->setHeight($height); + + return new ImageServiceResult( + $uri, + $dimension, + ); + } +} diff --git a/Classes/UriBuilder/ImglabUri.php b/Classes/UriBuilder/ImglabUri.php new file mode 100644 index 0000000..122eeac --- /dev/null +++ b/Classes/UriBuilder/ImglabUri.php @@ -0,0 +1,131 @@ +build(); + } + + public function __toString(): string + { + return $this->build(); + } + + public function setSource(string $source): self + { + $this->source = $source; + + return $this; + } + + public function getSource(): ?string + { + return $this->source; + } + + public function setMode(string $mode): self + { + $this->mode = $mode; + + return $this; + } + + public function getMode(): ?string + { + return $this->mode; + } + + public function setWidth(int $width): self + { + $this->width = $width; + + return $this; + } + + public function getWidth(): ?int + { + return $this->width; + } + + public function setHeight(int $height): self + { + $this->height = $height; + + return $this; + } + + public function getHeight(): ?int + { + return $this->height; + } + + public function setCrop(int $width, int $height, int $horizontal, int $vertical): self + { + $this->crop = [ + $width, + $height, + $horizontal, + $vertical, + ]; + + return $this; + } + + public function getCrop(): ?array + { + return $this->crop; + } + + private function build(): string + { + $path = $this->buildPath(); + + return strtr('%endpoint%/%path%', [ + '%endpoint%' => trim($this->endpoint, '/'), + '%path%' => $path, + ]); + } + + private function buildPath(): string + { + $parameters = array_filter([ + 'mode' => $this->getMode(), + 'width' => $this->getWidth(), + 'height' => $this->getHeight(), + ]); + + $options = implode('&', array_map(static function ($name, $value) { + return strtr('%name%=%value%', [ + '%name%' => $name, + '%value%' => $value, + ]); + }, array_keys($parameters), $parameters)); + + $source = rawurlencode(trim($this->getSource(), '/')); + + return strtr('%source%?%options%', [ + '%source%' => $source, + '%options%' => $options, + ]); + } +} diff --git a/Classes/UriBuilder/ImglabUriSource.php b/Classes/UriBuilder/ImglabUriSource.php new file mode 100644 index 0000000..9c3bb32 --- /dev/null +++ b/Classes/UriBuilder/ImglabUriSource.php @@ -0,0 +1,23 @@ +build($file); + } + + private function build(FileInterface $source): string + { + $path = parse_url($source->getPublicUrl(), PHP_URL_PATH); + $query = parse_url($source->getPublicUrl(), PHP_URL_QUERY) ?? ''; + + return implode('?', array_filter([trim($path, '/'), trim($query)])); + } +} diff --git a/README.md b/README.md index 09ccd2f..d788653 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ libraries like `ImageMagick` for image processing operations. | [**cloudinary.com** ](https://cloudinary.com/) | 🟢 | 🟢 | 🟡 | 🟢 | | [**cloudimage.io** ](https://cloudimage.io/) | 🟢 | 🟢 | 🟡 | 🟢 | | [**gumlet.com** ](https://www.gumlet.com/) | 🟢 | 🟢 | 🟢 | 🟢 | +| [**imglab.io** ](https://www.imglab.io/) | 🟢 | 🟢 | 🟢 | 🟢 | * `resize`: Integration supports resize operations. * `crop`: Integration supports crop operations. @@ -224,6 +225,17 @@ for more information. See also the official [`gumlet.com` documentation](https://docs.gumlet.com/) for more information. +**imglab.io** `integration.imglab` + +| option | type | description | default | +|---------------|--------|----------------------------------------------|---------| +| api_endpoint | string | The Gumlet URL of the imglab.io service. | `null` | +| signature | bool | Enable signature of the imglab.io service. | `false` | +| signature_key | string | The signature key of the imglab.io service. | `null` | + +See also the official [`imglab.io` documentation](https://imglab.io/docs/overview/introduction/) +for more information. + ## Usage 🪄 ## API @@ -275,6 +287,7 @@ Version **1.0.0** 🏷️ `developing` * ✅ Integration for [**cloudinary.com** `service`](https://cloudinary.com/). * ✅ Integration for [**cloudimage.io** `service`](https://www.cloudimage.io/). * ✅ Integration for [**gumlet.com** `service`](https://www.gumlet.com/). +* ✅ Integration for [**imglab.io** `service`](https://www.imglab.io/). * Release. Version **2.0.0** 🏷️ `planning` diff --git a/Resources/Private/Language/settings.xlf b/Resources/Private/Language/settings.xlf index 441f886..6ae7dd0 100644 --- a/Resources/Private/Language/settings.xlf +++ b/Resources/Private/Language/settings.xlf @@ -198,6 +198,10 @@ URL Signature Key + + + API Endpoint URL + diff --git a/composer.json b/composer.json index f8048af..c222a5e 100755 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "imagekit", "bunny", "sirv", + "imglab", "avif", "webp", "heic", diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 61cfe76..c13e643 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -1,5 +1,5 @@ common { - # cat=Common; type=options[ImgProxy=imgproxy, imagor=imagor, Thumbor=thumbor, optimole.com=optimole, bunny.net=bunny, cloudflare.com=cloudflare, imagekit.io=imagekit, sirv.com=sirv, imgix.com=imgix, cloudinary.com=cloudinary, cloudimage.io=cloudimage, gumlet.com=gumlet]; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:common.integration + # cat=Common; type=options[ImgProxy=imgproxy, imagor=imagor, Thumbor=thumbor, optimole.com=optimole, bunny.net=bunny, cloudflare.com=cloudflare, imagekit.io=imagekit, sirv.com=sirv, imgix.com=imgix, cloudinary.com=cloudinary, cloudimage.io=cloudimage, gumlet.com=gumlet, imglab.io=imglab]; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:common.integration integration = # cat=Common; type=boolean; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:common.storage @@ -207,4 +207,9 @@ integration { # cat=gumlet; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.gumlet.signature_key signature_key = } + + imglab { + # cat=imglab; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.api_endpoint + api_endpoint = + } } From d6a690b81e8cd663929385e601e4187048b5372e Mon Sep 17 00:00:00 2001 From: Thomas Deinhamer Date: Tue, 22 Apr 2025 15:43:21 +0200 Subject: [PATCH 2/4] Add URL signing --- Classes/ImageService/ImglabImageService.php | 27 ++++++++++++- Classes/UriBuilder/ImglabUri.php | 43 +++++++++++++++++++-- Resources/Private/Language/settings.xlf | 9 +++++ ext_conf_template.txt | 9 +++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/Classes/ImageService/ImglabImageService.php b/Classes/ImageService/ImglabImageService.php index 6e069b6..5fe0bfc 100644 --- a/Classes/ImageService/ImglabImageService.php +++ b/Classes/ImageService/ImglabImageService.php @@ -30,6 +30,9 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'api_endpoint' => null, + 'signature' => false, + 'signature_key' => null, + 'signature_salt' => null, ]); } @@ -38,6 +41,21 @@ public function getEndpoint(): string return $this->options['api_endpoint']; } + public function hasSignature(): bool + { + return (bool) $this->options['signature']; + } + + public function getSignatureKey(): ?string + { + return $this->options['signature_key'] ?: null; + } + + public function getSignatureSalt(): ?string + { + return $this->options['signature_salt'] ?: null; + } + public function hasConfiguration(): bool { return filter_var($this->getEndpoint(), FILTER_VALIDATE_URL) !== false; @@ -67,13 +85,18 @@ public function canProcessTask(TaskInterface $task): bool in_array($task->getSourceFile()->getMimeType(), $this->getSupportedMimeTypes(), true); } - public function processTask(TaskInterface $task): ImageServiceResult + public function processTask(TaskInterface $task): ImageServiceResultInterface { $file = $task->getSourceFile(); $configuration = $task->getTargetFile()->getProcessingConfiguration(); $dimension = ImageDimension::fromProcessingTask($task); - $uri = new ImglabUri($this->getEndpoint()); + $uri = new ImglabUri( + $this->getEndpoint(), + $this->hasSignature() ? $this->getSignatureKey() : null, + $this->hasSignature() ? $this->getSignatureSalt() : null, + ); + $uri->setSource($this->source->getSource($file)); $mode = (static function ($configuration) { diff --git a/Classes/UriBuilder/ImglabUri.php b/Classes/UriBuilder/ImglabUri.php index 122eeac..600a60b 100644 --- a/Classes/UriBuilder/ImglabUri.php +++ b/Classes/UriBuilder/ImglabUri.php @@ -6,6 +6,8 @@ class ImglabUri implements UriInterface { + public const SIGNATURE_ALGORITHM = 'sha256'; + private ?string $source = null; private ?string $mode = null; @@ -17,7 +19,9 @@ class ImglabUri implements UriInterface private ?array $crop = null; public function __construct( - private readonly ?string $endpoint, + private readonly string $endpoint, + private readonly ?string $key, + private readonly ?string $salt, ) { } @@ -31,6 +35,21 @@ public function __toString(): string return $this->build(); } + public function getEndpoint(): string + { + return $this->endpoint; + } + + public function getKey(): ?string + { + return $this->key; + } + + public function getSalt(): ?string + { + return $this->salt; + } + public function setSource(string $source): self { $this->source = $source; @@ -100,10 +119,15 @@ private function build(): string { $path = $this->buildPath(); - return strtr('%endpoint%/%path%', [ + $signature = $this->getKey() + ? $this->calculateSignature($path) + : null; + + return strtr($signature ? '%endpoint%/%path%&signature=%signature%' : '%endpoint%/%path%', array_filter([ '%endpoint%' => trim($this->endpoint, '/'), '%path%' => $path, - ]); + '%signature%' => $signature, + ])); } private function buildPath(): string @@ -128,4 +152,17 @@ private function buildPath(): string '%options%' => $options, ]); } + + private function calculateSignature(string $path): string + { + $data = strtr('%salt%/%path%', [ + '%salt%' => base64_decode($this->getSalt(), true), + '%path%' => rawurldecode($path), + ]); + + $hash = hash_hmac(static::SIGNATURE_ALGORITHM, $data, base64_decode($this->getKey(), true), true); + $digest = base64_encode($hash); + + return rtrim(strtr($digest, '+/', '-_'), '='); + } } diff --git a/Resources/Private/Language/settings.xlf b/Resources/Private/Language/settings.xlf index 6ae7dd0..5c66de1 100644 --- a/Resources/Private/Language/settings.xlf +++ b/Resources/Private/Language/settings.xlf @@ -202,6 +202,15 @@ API Endpoint URL + + Enable URL signing + + + URL Signature Key + + + URL Signature Salt + diff --git a/ext_conf_template.txt b/ext_conf_template.txt index c13e643..6a7059d 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -211,5 +211,14 @@ integration { imglab { # cat=imglab; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.api_endpoint api_endpoint = + + # cat=imglab; type=boolean; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.signature + signature = 0 + + # cat=imglab; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.signature_key + signature_key = + + # cat=imglab; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.signature_salt + signature_salt = } } From 879753e536002bc139a2c5b84c07908c81574e59 Mon Sep 17 00:00:00 2001 From: Thomas Deinhamer Date: Tue, 22 Apr 2025 16:11:22 +0200 Subject: [PATCH 3/4] Update URI source class --- Classes/UriBuilder/ImglabUriSource.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Classes/UriBuilder/ImglabUriSource.php b/Classes/UriBuilder/ImglabUriSource.php index 9c3bb32..5caad21 100644 --- a/Classes/UriBuilder/ImglabUriSource.php +++ b/Classes/UriBuilder/ImglabUriSource.php @@ -4,19 +4,22 @@ namespace SomehowDigital\Typo3\MediaProcessing\UriBuilder; +use SomehowDigital\Typo3\MediaProcessing\Utility\OnlineMediaUtility; use TYPO3\CMS\Core\Resource\FileInterface; class ImglabUriSource implements UriSourceInterface { public function getSource(FileInterface $file): string { - return $this->build($file); + $url = OnlineMediaUtility::getPreviewImage($file) ?? $file->getPublicUrl(); + + return $this->build($url); } - private function build(FileInterface $source): string + private function build(string $url): string { - $path = parse_url($source->getPublicUrl(), PHP_URL_PATH); - $query = parse_url($source->getPublicUrl(), PHP_URL_QUERY) ?? ''; + $path = parse_url($url, PHP_URL_PATH); + $query = parse_url($url, PHP_URL_QUERY) ?? ''; return implode('?', array_filter([trim($path, '/'), trim($query)])); } From ec1d85d73312777ed5dcfb51c0cfde4d50d9afcb Mon Sep 17 00:00:00 2001 From: Thomas Deinhamer Date: Wed, 23 Apr 2025 10:22:40 +0200 Subject: [PATCH 4/4] Support web and proxy source loaders --- Classes/ImageService/ImageServiceFactory.php | 13 +++++- Classes/ImageService/ImglabImageService.php | 2 + Classes/UriBuilder/ImglabProxySource.php | 41 +++++++++++++++++++ ...mglabUriSource.php => ImglabWebSource.php} | 4 +- Resources/Private/Language/settings.xlf | 6 +++ ext_conf_template.txt | 6 +++ 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 Classes/UriBuilder/ImglabProxySource.php rename Classes/UriBuilder/{ImglabUriSource.php => ImglabWebSource.php} (87%) diff --git a/Classes/ImageService/ImageServiceFactory.php b/Classes/ImageService/ImageServiceFactory.php index 91329de..047ab97 100644 --- a/Classes/ImageService/ImageServiceFactory.php +++ b/Classes/ImageService/ImageServiceFactory.php @@ -16,7 +16,9 @@ use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImagorUriSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgixFolderSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgixProxySource; +use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabProxySource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabUriSource; +use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImglabWebSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgProxyFileSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\ImgProxyUriSource; use SomehowDigital\Typo3\MediaProcessing\UriBuilder\OptimoleUriSource; @@ -241,7 +243,16 @@ private function getGumletImageService(array $options): GumletImageService private function getImglabImageService(array $options): ImglabImageService { - $source = new ImglabUriSource(); + $source = match ($options['source_loader']) { + ImglabWebSource::IDENTIFIER => (static function () use ($options): ImglabWebSource { + return new ImglabWebSource(); + })(), + ImglabProxySource::IDENTIFIER => (static function () use ($options): ImglabProxySource { + return new ImglabProxySource( + $options['source_uri'] ?: GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST'), + ); + })(), + }; return new ImglabImageService( $source, diff --git a/Classes/ImageService/ImglabImageService.php b/Classes/ImageService/ImglabImageService.php index 5fe0bfc..4e751fc 100644 --- a/Classes/ImageService/ImglabImageService.php +++ b/Classes/ImageService/ImglabImageService.php @@ -30,6 +30,8 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'api_endpoint' => null, + 'source_loader' => 'web', + 'source_uri' => null, 'signature' => false, 'signature_key' => null, 'signature_salt' => null, diff --git a/Classes/UriBuilder/ImglabProxySource.php b/Classes/UriBuilder/ImglabProxySource.php new file mode 100644 index 0000000..8e86db3 --- /dev/null +++ b/Classes/UriBuilder/ImglabProxySource.php @@ -0,0 +1,41 @@ +host; + } + + public function getSource(FileInterface $file): string + { + $url = OnlineMediaUtility::getPreviewImage($file) ?? $file->getPublicUrl(); + + return $this->build($url); + } + + private function build(string $url): string + { + $path = parse_url($url, PHP_URL_PATH); + $query = parse_url($url, PHP_URL_QUERY) ?? ''; + + return strtr('%host%/%path%', [ + '%host%' => trim($this->getHost(), '/'), + '%path%' => implode('?', array_filter([trim($path, '/'), trim($query)])), + ]); + } +} diff --git a/Classes/UriBuilder/ImglabUriSource.php b/Classes/UriBuilder/ImglabWebSource.php similarity index 87% rename from Classes/UriBuilder/ImglabUriSource.php rename to Classes/UriBuilder/ImglabWebSource.php index 5caad21..f1f02a8 100644 --- a/Classes/UriBuilder/ImglabUriSource.php +++ b/Classes/UriBuilder/ImglabWebSource.php @@ -7,8 +7,10 @@ use SomehowDigital\Typo3\MediaProcessing\Utility\OnlineMediaUtility; use TYPO3\CMS\Core\Resource\FileInterface; -class ImglabUriSource implements UriSourceInterface +class ImglabWebSource implements UriSourceInterface { + public const IDENTIFIER = 'web'; + public function getSource(FileInterface $file): string { $url = OnlineMediaUtility::getPreviewImage($file) ?? $file->getPublicUrl(); diff --git a/Resources/Private/Language/settings.xlf b/Resources/Private/Language/settings.xlf index 5c66de1..0aeabe6 100644 --- a/Resources/Private/Language/settings.xlf +++ b/Resources/Private/Language/settings.xlf @@ -202,6 +202,12 @@ API Endpoint URL + + Source Loader + + + Origin Host URL + Enable URL signing diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 6a7059d..605bba7 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -212,6 +212,12 @@ integration { # cat=imglab; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.api_endpoint api_endpoint = + # cat=imglab; type=options[Web=web, Proxy=proxy]; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.source_loader + source_loader = web + + # cat=imglab; type=string; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.source_uri + source_uri = + # cat=imglab; type=boolean; label=LLL:EXT:media_processing/Resources/Private/Language/settings.xlf:integration.imglab.signature signature = 0