diff --git a/changelog/unreleased/40600 b/changelog/unreleased/40600 new file mode 100644 index 000000000000..7dab8ac57b05 --- /dev/null +++ b/changelog/unreleased/40600 @@ -0,0 +1,12 @@ +Bugfix: Fix orientation of images with exif data + +Some images with a large exif data had problems with the orientation +when they were shown. This was caused by the native function failing +to retrieve the exif data. Images with small exif data didn't have +this problem. + +By making the chunk size of the stream bigger, the native function +is able to load the exif data properly and return the information, and +with such information we can fix the orientation of the image. + +https://github.com/owncloud/core/pull/40600 diff --git a/lib/private/legacy/image.php b/lib/private/legacy/image.php index 9dcda0391ff2..3ed082b72af9 100644 --- a/lib/private/legacy/image.php +++ b/lib/private/legacy/image.php @@ -386,7 +386,55 @@ private function loadExifData($file) { return; } - $this->exifData = @\exif_read_data($file, 'IFD0'); + $detectedType = false; + if (\is_resource($file)) { + $detectedType = $this->detectImageTypeFromStream($file); + } else { + // it should be a string pointing to a valid file + $detectedType = \exif_imagetype($file); + } + + if (\in_array($detectedType, [IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM], true)) { + // as of 24/1/2023, "exif_read_data" only supports those image types + $this->exifData = \exif_read_data($file, 'IFD0'); + } else { + // other image types aren't supported by the exif_read_data function, so + // the function should return false + $this->exifData = false; + } + } + + /** + * Detect the image type from the stream. + * The detection code is copied from the php source code, + * from the exif extension. As such, it only detects jpeg + * and tiff formats. + * This is intended to be used only to check if the stream + * can be used with the "exif_read_data" function or not. + */ + private function detectImageTypeFromStream($stream) { + $detectedType = false; + + \rewind($stream); + $bytes = \fread($stream, 2); + switch ($bytes) { + case "\xff\xd8": + $detectedType = IMAGETYPE_JPEG; + break; + case "II": + $nextBytes = \fread($stream, 2); + if ($nextBytes === "\x2a\x00") { + $detectedType = IMAGETYPE_TIFF_II; + } + break; + case "MM": + $nextBytes = \fread($stream, 2); + if ($nextBytes === "\x00\x2a") { + $detectedType = IMAGETYPE_TIFF_MM; + } + break; + } + return $detectedType; } /** @@ -494,12 +542,61 @@ public function load($imageRef) { public function loadFromFileHandle($handle) { $contents = \stream_get_contents($handle); if ($this->loadFromData($contents)) { + $this->adjustStreamChunkSize($handle); $this->loadExifData($handle); return $this->resource; } return false; } + /** + * Adjust the size of the chunks in the stream. This is required for + * the "exif_read_data" native method to read a whole chunk of metadata + * from the image in one go, otherwise there could be problems with + * the function. + * + * The expected max size of a metadata chunk should be 64 kB, at least for JPEG + * images. + * + * We'll adjust the chunk size only for custom wrappers (the stream_type + * should be "user-space") because those are the ones having this problem. + * Native streams seem to work without this workaround. + * + * This adjustment must be made in all the wrappers. This means that we must + * have access to all the wrapped streams and adjust the chunk size of all + * of them. A "getSource" method must be present in the custom wrapper + * providing access to the underlying stream, otherwise a warning will be + * logged because it could cause problems. + */ + private function adjustStreamChunkSize($handle) { + $stream = $handle; + $metadata = \stream_get_meta_data($stream); + while ($metadata['stream_type'] === 'user-space') { + \stream_set_chunk_size($stream, 64 * 1024); + if (isset($metadata['wrapper_data'])) { + $streamObj = $metadata['wrapper_data']; + if (\method_exists($streamObj, 'getSource')) { + // underlying stream must be adjusted too + $stream = $streamObj->getSource(); + $metadata = \stream_get_meta_data($stream); + } else { + // can't access to the underlying stream, so we'll stop here + $this->logger->warning( + "Cannot get the underlying stream of class " . \get_class($streamObj) . "with uri {$metadata['uri']}", + ['app' => 'core'] + ); + break; + } + } else { + $this->logger->warning( + "No wrapper data found in the metadata of stream {$metadata['uri']}", + ['app' => 'core'] + ); + break; + } + } + } + /** * Loads an image from a local file. * diff --git a/tests/data/bigexif.jpg b/tests/data/bigexif.jpg new file mode 100644 index 000000000000..bd661dc11c22 Binary files /dev/null and b/tests/data/bigexif.jpg differ diff --git a/tests/lib/ImageTest.php b/tests/lib/ImageTest.php index db3c6f8600ec..eb43bcf1b40c 100644 --- a/tests/lib/ImageTest.php +++ b/tests/lib/ImageTest.php @@ -338,4 +338,26 @@ public function testConvert($mimeType) { \unlink($tempFile); $this->assertEquals($mimeType, $actualMimeType); } + + public function exifDataBigStreamProvider() { + return [ + [OC::$SERVERROOT.'/tests/data/bigexif.jpg'], + // TIFF images can't be loaded at the moment + ]; + } + + /** + * @dataProvider exifDataBigStreamProvider + */ + public function testExifDataBigStream($targetFile) { + $img = new \OC_Image(); + + \OC\Files\Stream\Close::registerCallback($targetFile, function () { + }); + $stream = \fopen("close://{$targetFile}", 'rb'); + + $img->loadFromFileHandle($stream); + \fclose($stream); + self::assertSame(6, $img->getOrientation()); // orientation = right, top + } }