Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelog/unreleased/40600
Original file line number Diff line number Diff line change
@@ -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
99 changes: 98 additions & 1 deletion lib/private/legacy/image.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error handling?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok - not really - it only fails if the chunk size is negative or bigger then php max int ....https://www.php.net/manual/en/function.stream-set-chunk-size.php

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.
*
Expand Down
Binary file added tests/data/bigexif.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions tests/lib/ImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}