From c6532e5327ce6774492ee39e5c6eace95d8880bf Mon Sep 17 00:00:00 2001 From: Mattias Svensson Date: Sat, 13 Jun 2026 07:34:36 +0200 Subject: [PATCH] fix(preview): preserve ICC colour profiles in generated previews GD strips the ICC colour profile when loading an image but keeps the pixel values, so previews of wide-gamut photos (Display P3, Adobe RGB) are served as untagged JPEGs that browsers render as sRGB, looking washed out compared to the original. Remember the source profile in OC\Image, carry it across the resize and crop copies the preview generator uses, and re-embed it into the encoded JPEG (APP2) or PNG (iCCP) output. No colour conversion is done: the pixels already match the profile, they were only mistagged. Only RGB profiles are kept, since GD converts CMYK and grayscale sources to RGB. Also re-attach the profile in the HEIC provider after stripImage() removes it from small thumbnails. Closes #22951 Assisted-by: ClaudeCode:claude-opus-4-8 Signed-off-by: Mattias Svensson --- REUSE.toml | 2 +- lib/private/Image.php | 229 +++++++++++++++++++++++++++++- lib/private/Preview/HEIC.php | 5 + tests/data/testimage-icc-cmyk.jpg | Bin 0 -> 57509 bytes tests/data/testimage-icc-p3.jpg | Bin 0 -> 2106 bytes tests/lib/ImageTest.php | 99 +++++++++++++ 6 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 tests/data/testimage-icc-cmyk.jpg create mode 100644 tests/data/testimage-icc-p3.jpg diff --git a/REUSE.toml b/REUSE.toml index ffc5fed40a8..63555a85551 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -88,7 +88,7 @@ SPDX-FileCopyrightText = "2012 ownCloud, Inc." SPDX-License-Identifier = "AGPL-3.0-only" [[annotations]] -path = ["robots.txt", "tests/data/testavatar.png", "tests/data/testimage.gif", "tests/data/testimage.jpg", "tests/data/testimage.png"] +path = ["robots.txt", "tests/data/testavatar.png", "tests/data/testimage.gif", "tests/data/testimage.jpg", "tests/data/testimage.png", "tests/data/testimage-icc-p3.jpg", "tests/data/testimage-icc-cmyk.jpg"] precedence = "aggregate" SPDX-FileCopyrightText = "2013 ownCloud, Inc." SPDX-License-Identifier = "AGPL-3.0-only" diff --git a/lib/private/Image.php b/lib/private/Image.php index d1a2f164ed5..1397df326f6 100644 --- a/lib/private/Image.php +++ b/lib/private/Image.php @@ -31,6 +31,15 @@ class Image implements IImage { // Default quality for webp images protected const DEFAULT_WEBP_QUALITY = 80; + // ICC profile marker in JPEG APP2 segments + private const JPEG_ICC_IDENTIFIER = "ICC_PROFILE\x00"; + + // Max ICC bytes per APP2 segment: 0xFFFF - 2 (length) - 12 (marker) - 2 (index and count) + private const JPEG_ICC_MAX_CHUNK_SIZE = 65519; + + // ICC profiles precede the image data, so the scan can stop early + private const ICC_SCAN_BYTE_LIMIT = 8 * 1024 * 1024; + // tmp resource. protected GdImage|false $resource = false; // Default to png if file type isn't evident. @@ -43,6 +52,8 @@ class Image implements IImage { private IAppConfig $appConfig; private IConfig $config; private ?array $exif = null; + // Colour profile carried from the source into generated output + private ?string $iccProfile = null; /** * @throws \InvalidArgumentException in case the $imageRef parameter is not null @@ -258,11 +269,19 @@ class Image implements IImage { $retVal = imagegif($this->resource, $filePath); break; case IMAGETYPE_JPEG: - imageinterlace($this->resource, true); - $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality()); + if ($this->iccProfile !== null) { + $retVal = $this->outputWithIccProfile(IMAGETYPE_JPEG, $filePath); + } else { + imageinterlace($this->resource, true); + $retVal = imagejpeg($this->resource, $filePath, $this->getJpegQuality()); + } break; case IMAGETYPE_PNG: - $retVal = imagepng($this->resource, $filePath); + if ($this->iccProfile !== null) { + $retVal = $this->outputWithIccProfile(IMAGETYPE_PNG, $filePath); + } else { + $retVal = imagepng($this->resource, $filePath); + } break; case IMAGETYPE_XBM: if (function_exists('imagexbm')) { @@ -361,7 +380,197 @@ class Image implements IImage { if (!$res) { $this->logger->error('Image->data. Error getting image data.', ['app' => 'core']); } - return ob_get_clean(); + $data = ob_get_clean(); + if ($data !== false) { + $data = $this->embedIccProfile($data); + } + return $data; + } + + /** + * Re-embeds the ICC profile into a freshly encoded image, then writes it to + * $filePath or outputs it directly when no path is given. + */ + private function outputWithIccProfile(int $imageType, ?string $filePath): bool { + ob_start(); + if ($imageType === IMAGETYPE_PNG) { + $res = imagepng($this->resource); + } else { + imageinterlace($this->resource, true); + $res = imagejpeg($this->resource, null, $this->getJpegQuality()); + } + $data = ob_get_clean(); + if (!$res || $data === false) { + return false; + } + $data = $this->embedIccProfile($data); + if ($filePath === null || $filePath === '') { + echo $data; + return true; + } + return file_put_contents($filePath, $data) !== false; + } + + /** + * Remembers the source ICC profile for re-embedding into generated output. + * + * Only RGB profiles are kept: GD converts CMYK and grayscale sources to RGB + * pixel data on load, so their source profiles no longer describe the image. + */ + private function rememberIccProfile(string $data): void { + $this->iccProfile = null; + if (str_starts_with($data, "\xFF\xD8")) { + $profile = self::extractIccProfileFromJpeg($data); + } elseif (str_starts_with($data, "\x89PNG\r\n\x1a\n")) { + $profile = self::extractIccProfileFromPng($data); + } else { + return; + } + if ($profile !== null && self::isUsableRgbProfile($profile)) { + $this->iccProfile = $profile; + } + } + + private static function isUsableRgbProfile(string $profile): bool { + return strlen($profile) >= 132 // ICC header plus tag count + && substr($profile, 36, 4) === 'acsp' // ICC profile signature + && substr($profile, 16, 4) === 'RGB '; // data colour space + } + + private static function extractIccProfileFromJpeg(string $data): ?string { + $len = strlen($data); + $identifierLength = strlen(self::JPEG_ICC_IDENTIFIER); + $pos = 2; + $chunks = []; + $chunkCount = null; + while ($pos + 4 <= $len) { + if ($data[$pos] !== "\xFF") { + return null; + } + $marker = ord($data[$pos + 1]); + if ($marker === 0xFF) { + // fill byte before a marker + $pos++; + continue; + } + if ($marker === 0x01 || ($marker >= 0xD0 && $marker <= 0xD8)) { + // standalone marker without a length field + $pos += 2; + continue; + } + if ($marker === 0xDA || $marker === 0xD9) { + // start of scan or end of image: no more metadata segments + break; + } + $segmentLength = (ord($data[$pos + 2]) << 8) | ord($data[$pos + 3]); + if ($segmentLength < 2 || $pos + 2 + $segmentLength > $len) { + return null; + } + if ($marker === 0xE2 && $segmentLength >= 2 + $identifierLength + 2) { + $payload = substr($data, $pos + 4, $segmentLength - 2); + if (str_starts_with($payload, self::JPEG_ICC_IDENTIFIER)) { + $sequence = ord($payload[$identifierLength]); + $total = ord($payload[$identifierLength + 1]); + if ($total === 0 || ($chunkCount !== null && $total !== $chunkCount)) { + return null; + } + $chunkCount = $total; + $chunks[$sequence] = substr($payload, $identifierLength + 2); + } + } + $pos += 2 + $segmentLength; + } + if ($chunkCount === null || count($chunks) !== $chunkCount) { + return null; + } + ksort($chunks); + return implode('', $chunks); + } + + private static function extractIccProfileFromPng(string $data): ?string { + $len = strlen($data); + $pos = 8; + while ($pos + 8 <= $len) { + $header = unpack('NchunkLength', $data, $pos); + if ($header === false) { + return null; + } + $chunkLength = $header['chunkLength']; + $type = substr($data, $pos + 4, 4); + if ($type === 'IDAT' || $type === 'IEND') { + break; + } + if ($type === 'iCCP') { + if ($pos + 8 + $chunkLength > $len) { + return null; + } + $chunk = substr($data, $pos + 8, $chunkLength); + $separator = strpos($chunk, "\x00"); + if ($separator === false || $separator < 1 || $separator > 79 || strlen($chunk) < $separator + 2) { + return null; + } + if (ord($chunk[$separator + 1]) !== 0) { + // unknown compression method + return null; + } + $profile = @gzuncompress(substr($chunk, $separator + 2)); + return $profile === false ? null : $profile; + } + $pos += 12 + $chunkLength; + } + return null; + } + + private function embedIccProfile(string $data): string { + if ($this->iccProfile === null) { + return $data; + } + if (str_starts_with($data, "\xFF\xD8")) { + return $this->embedIccProfileInJpeg($data); + } + if (str_starts_with($data, "\x89PNG\r\n\x1a\n")) { + return $this->embedIccProfileInPng($data); + } + return $data; + } + + private function embedIccProfileInJpeg(string $data): string { + $chunks = str_split($this->iccProfile, self::JPEG_ICC_MAX_CHUNK_SIZE); + $total = count($chunks); + if ($total > 255) { + return $data; + } + // APP2 segments belong before the image data, after the APP0/APP1 + // (JFIF/EXIF) segments the encoder may have written + $len = strlen($data); + $pos = 2; + while ($pos + 4 <= $len + && $data[$pos] === "\xFF" + && (ord($data[$pos + 1]) === 0xE0 || ord($data[$pos + 1]) === 0xE1)) { + $segmentLength = (ord($data[$pos + 2]) << 8) | ord($data[$pos + 3]); + if ($segmentLength < 2 || $pos + 2 + $segmentLength > $len) { + return $data; + } + $pos += 2 + $segmentLength; + } + $segments = ''; + foreach ($chunks as $index => $chunk) { + $payload = self::JPEG_ICC_IDENTIFIER . chr($index + 1) . chr($total) . $chunk; + $segments .= "\xFF\xE2" . pack('n', strlen($payload) + 2) . $payload; + } + return substr($data, 0, $pos) . $segments . substr($data, $pos); + } + + private function embedIccProfileInPng(string $data): string { + // IHDR is required to be first and has a fixed size; iCCP belongs before PLTE and IDAT + $ihdrEnd = 8 + 8 + 13 + 4; + if (strlen($data) < $ihdrEnd || substr($data, 12, 4) !== 'IHDR') { + return $data; + } + $chunkData = "ICC profile\x00\x00" . gzcompress($this->iccProfile); + $payload = 'iCCP' . $chunkData; + $chunk = pack('N', strlen($chunkData)) . $payload . pack('N', crc32($payload)); + return substr($data, 0, $ihdrEnd) . $chunk . substr($data, $ihdrEnd); } /** @@ -787,6 +996,12 @@ class Image implements IImage { $this->imageType = $iType; $this->mimeType = image_type_to_mime_type($iType); $this->filePath = $imagePath; + if ($iType === IMAGETYPE_JPEG || $iType === IMAGETYPE_PNG) { + $header = @file_get_contents($imagePath, false, null, 0, self::ICC_SCAN_BYTE_LIMIT); + if ($header !== false) { + $this->rememberIccProfile($header); + } + } } return $this->resource; } @@ -806,6 +1021,7 @@ class Image implements IImage { if ($this->valid()) { imagealphablending($this->resource, false); imagesavealpha($this->resource, true); + $this->rememberIccProfile($str); } if (!$this->resource) { @@ -835,6 +1051,7 @@ class Image implements IImage { $this->logger->debug('Image->loadFromBase64, could not load', ['app' => 'core']); return false; } + $this->rememberIccProfile($data); return $this->resource; } else { return false; @@ -1120,6 +1337,7 @@ class Image implements IImage { $this->width(), $this->height() ); + $image->iccProfile = $this->iccProfile; return $image; } @@ -1129,6 +1347,7 @@ class Image implements IImage { $image = new self($this->logger, $this->appConfig, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; + $image->iccProfile = $this->iccProfile; $image->resource = $this->cropNew($x, $y, $w, $h); return $image; @@ -1139,6 +1358,7 @@ class Image implements IImage { $image = new self($this->logger, $this->appConfig, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; + $image->iccProfile = $this->iccProfile; $image->resource = $this->preciseResizeNew($width, $height); return $image; @@ -1149,6 +1369,7 @@ class Image implements IImage { $image = new self($this->logger, $this->appConfig, $this->config); $image->imageType = $this->imageType; $image->mimeType = $this->mimeType; + $image->iccProfile = $this->iccProfile; $image->resource = $this->resizeNew($maxSize); return $image; diff --git a/lib/private/Preview/HEIC.php b/lib/private/Preview/HEIC.php index e56101e8e40..bc68d6316ea 100644 --- a/lib/private/Preview/HEIC.php +++ b/lib/private/Preview/HEIC.php @@ -140,8 +140,13 @@ class HEIC extends ProviderV2 { if ($previewWidth > $maxX || $previewHeight > $maxY) { // If we want a small image (thumbnail) let's be most space- and time-efficient if ($maxX <= 500 && $maxY <= 500) { + $profiles = $bp->getImageProfiles('icc', true); $bp->thumbnailImage($maxY, $maxX, true); $bp->stripImage(); + // keep the colour profile that stripImage() also removed + if (isset($profiles['icc'])) { + $bp->profileImage('icc', $profiles['icc']); + } } else { // A bigger image calls for some better resizing algorithm // According to http://www.imagemagick.org/Usage/filter/#lanczos diff --git a/tests/data/testimage-icc-cmyk.jpg b/tests/data/testimage-icc-cmyk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a33471d6d7339b7c9d179245fbffd0c84a44394c GIT binary patch literal 57509 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 z

WT`Bg}s&cJn!E^ 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 literal 0 HcmV?d00001 diff --git a/tests/lib/ImageTest.php b/tests/lib/ImageTest.php index 22246f8e437..255e8934581 100644 --- a/tests/lib/ImageTest.php +++ b/tests/lib/ImageTest.php @@ -381,4 +381,103 @@ class ImageTest extends \Test\TestCase { $img->loadFromData($data); $this->assertFalse($img->valid()); } + + /** + * Returns the reassembled ICC profile from the APP2 segments of a JPEG, + * or null if there is none. + */ + private static function iccProfileOfJpeg(string $data): ?string { + $chunks = []; + $offset = 0; + while (($pos = strpos($data, "ICC_PROFILE\x00", $offset)) !== false) { + $length = (ord($data[$pos - 2]) << 8) | ord($data[$pos - 1]); + $sequence = ord($data[$pos + 12]); + $chunks[$sequence] = substr($data, $pos + 14, $length - 2 - 12 - 2); + $offset = $pos + 14; + } + if ($chunks === []) { + return null; + } + ksort($chunks); + return implode('', $chunks); + } + + /** + * Returns the ICC profile from the iCCP chunk of a PNG, or null if there is none. + */ + private static function iccProfileOfPng(string $data): ?string { + $pos = strpos($data, 'iCCP'); + if ($pos === false) { + return null; + } + $length = unpack('N', substr($data, $pos - 4, 4))[1]; + $chunk = substr($data, $pos + 4, $length); + $separator = strpos($chunk, "\x00"); + return gzuncompress(substr($chunk, $separator + 2)) ?: null; + } + + public function testIccProfilePreservedInJpeg(): void { + $source = file_get_contents(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $sourceProfile = self::iccProfileOfJpeg($source); + $this->assertNotNull($sourceProfile); + + $img = new Image(); + $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $this->assertTrue($img->valid()); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->data())); + + $img = new Image(); + $img->loadFromData($source); + $this->assertTrue($img->valid()); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->data())); + + $img = new Image(); + $img->loadFromBase64(base64_encode($source)); + $this->assertTrue($img->valid()); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->data())); + } + + public function testIccProfilePreservedAcrossResizeAndCrop(): void { + $source = file_get_contents(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $sourceProfile = self::iccProfileOfJpeg($source); + + $img = new Image(); + $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $img->resize(32); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->data())); + + $img = new Image(); + $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->resizeCopy(32)->data())); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->preciseResizeCopy(32, 16)->data())); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->cropCopy(0, 0, 16, 16)->data())); + $this->assertEquals($sourceProfile, self::iccProfileOfJpeg($img->copy()->data())); + } + + public function testIccProfilePreservedInPngOutput(): void { + $source = file_get_contents(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $sourceProfile = self::iccProfileOfJpeg($source); + + $img = new Image(); + $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage-icc-p3.jpg'); + $tempFile = tempnam(sys_get_temp_dir(), 'img-test'); + $img->save($tempFile, 'image/png'); + $this->assertEquals($sourceProfile, self::iccProfileOfPng(file_get_contents($tempFile))); + unlink($tempFile); + } + + public function testIccProfileNotInventedForUntaggedImage(): void { + $img = new Image(); + $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage.jpg'); + $this->assertTrue($img->valid()); + $this->assertNull(self::iccProfileOfJpeg($img->data())); + } + + public function testNonRgbIccProfileNotPreserved(): void { + // GD converts CMYK pixel data to RGB on load, the source profile no longer applies + $img = new Image(); + $img->loadFromFile(OC::$SERVERROOT . '/tests/data/testimage-icc-cmyk.jpg'); + $this->assertTrue($img->valid()); + $this->assertNull(self::iccProfileOfJpeg($img->data())); + } }