Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Introduce image metadata #266

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions lib/Imagine/Image/AbstractImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

abstract class AbstractImage implements ImageInterface
{
/**
* @var Metadata\MetadataInterface
*/
protected $metadata;

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -73,4 +78,26 @@ public function thumbnail(BoxInterface $size, $mode = ImageInterface::THUMBNAIL_

return $thumbnail;
}

/**
* {@inheritdoc}
*/
public function metadata()
{
if ($this->metadata === null) {
$this->metadata = new Metadata\ExifMetadata($this);
}
return $this->metadata;
}

/**
* Assures the metadata instance will be cloned, too
*/
function __clone()
{
if ($this->metadata !== null) {
$this->metadata = clone $this->metadata;
}
}

}
7 changes: 7 additions & 0 deletions lib/Imagine/Image/ImageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,11 @@ public function usePalette(PaletteInterface $palette);
* @throws RuntimeException
*/
public function profile(ProfileInterface $profile);

/**
* Returns the Image's meta data
*
* @return Metadata\MetadataInterface
*/
public function metadata();
}
64 changes: 64 additions & 0 deletions lib/Imagine/Image/Metadata/ExifMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
namespace Imagine\Image\Metadata;

/*
* This file is part of the Imagine package.
*
* (c) Bulat Shakirzyanov <mallluhuct@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Imagine\Image\ImageInterface;

/**
* Metadata driven by Exif information
*/
class ExifMetadata implements MetadataInterface
{
/**
* @var ImageInterface
*/
protected $image;

/**
* @var array
*/
protected $exifData;

/**
* {@inheritdoc}
*/
public function __construct(ImageInterface $image)
{
$this->image = $image;
}

/**
* {@inheritdoc}
*/
public function getOrientation()
{
if ($this->exifData === null) {
$this->initialize();
}
if (isset($this->exifData['Orientation'])) {
return $this->exifData['Orientation'];
}
return null;
}

/**
* Prepares the EXIF data, used for lazy-loading.
* Should be called at first of any getter operation to make sure the exifData array is filled.
*/
protected function initialize()
{
$exifData = exif_read_data('data://image/jpeg;base64,' . base64_encode($this->image->get('jpg')));
Copy link
Contributor

Choose a reason for hiding this comment

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

in case the image is big, this will use a lot of memory (base64 will use 33% more memory than the image has in size).
Would it make sense to dump the image to a file (if it is not already one) and let exif_read_data read from the file? This would not eat php's memory, but might be slower.

Copy link
Contributor

Choose a reason for hiding this comment

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

another alternative would be to use a stream-filter for base64-encoding. this will help against memory limitations without the need to dump the file.. use something like the following with an in-memory stream:
stream_filter_append($fh, 'convert.base64-encode');

Copy link
Collaborator

Choose a reason for hiding this comment

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

yep, that's right.

I've propose an enhancement on top of this PR (see https://github.com/afoeder/Imagine/pull/1) that address this comment (in case ImagineInterface::open is used)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Your proposition is smart. I'll fix my PR with it

Copy link
Collaborator

Choose a reason for hiding this comment

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

@staabm I've tried your proposition, but as long as exif_read_data does not accept streams, the data URI must be computed before calling this function. I can't figure how we could you stream_filter_append as you mentioned. See :

$stream = fopen('php://memory','r+');
fwrite($stream, $data);
rewind($stream);
stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_READ);

exif_read_data('data://image/jpg;base64,' . stream_get_contents($stream));

Copy link
Contributor

Choose a reason for hiding this comment

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

@evert any idea whats going on in the example above?

Copy link

Choose a reason for hiding this comment

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

Using the stream filter doesn't make much sense here. You're not saving any memory by letting a stream filter do the encoding, rather than base64 encode, because the input ($data) and the output (the data:// url) are still strings.

However, i think the reason the stream filter is failing, is because you probably need to use STREAM_FILTER_WRITE instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohhh right, it won't save memory until exif_read_data will accept a stream.... Will try your fix nevertheless, thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

tested it now and it works when used like this:

$data = str_repeat('lalaaaa', 5000);

$stream = fopen('php://memory','r+');
stream_filter_append($stream, 'convert.base64-encode', STREAM_FILTER_WRITE);
fwrite($stream, $data);
rewind($stream);

var_dump(stream_get_contents($stream));

I measured memory consumption with the teststring $data = str_repeat('lalaaaa', 5000); and it seems that using the in-memory stream consumes even more memory - I think mainly because of the buffer not beeing free'd yet.

To put it into a nutshell: atm there is no benefit when using streams here. I would leave it as is, until exif_read_data supports streams or someone encounters the problem of insufficient memory and then one could reconsider using a tempfile for this to cut memory consumption by ~75%

Copy link

Choose a reason for hiding this comment

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

I'm available for contract work to write a pure-php implemenation for reading exif data using streams ;) should be no more than 5 hours

if ($exifData !== false) {
$this->exifData = $exifData;
}
}

}
45 changes: 45 additions & 0 deletions lib/Imagine/Image/Metadata/MetadataInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
namespace Imagine\Image\Metadata;

/*
* This file is part of the Imagine package.
*
* (c) Bulat Shakirzyanov <mallluhuct@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Imagine\Image\ImageInterface;

/**
* An interface for Image Metadata
*/
interface MetadataInterface
{
/**
* Constants representing how the orientation of the actual scene is mapped onto the presented image,
* inspired by Exif's Orientation tag
* @see http://sylvana.net/jpegcrop/exif_orientation.html
*/
const ORIENTATION_NORMAL = 1;
const ORIENTATION_FLIPPED_HORIZONTALLY = 2;
const ORIENTATION_ROTATED_180 = 3;
const ORIENTATION_FLIPPED_VERTICALLY = 4;
const ORIENTATION_ROTATED_MINUS90_FLIPPED_VERTICALLY = 5;
const ORIENTATION_ROTATED_MINUS90 = 6;
const ORIENTATION_ROTATED_90_FLIPPED_VERTICALLY = 7;
const ORIENTATION_ROTATED_90 = 8;

/**
* @param ImageInterface $image
*/
public function __construct(ImageInterface $image);

/**
* Returns an orientation integer inspired by Exif's Orientation tag
*
* @return integer
*/
public function getOrientation();
}
Binary file added tests/Imagine/Fixtures/exifOrientation/90.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions tests/Imagine/Test/Image/AbstractImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,19 @@ public function testStripGBRImageHasGoodColors()
$this->assertEquals('#d07560', (string) $color);
}

public function testMetadataReturnsMetadataInstance()
{
$this->assertInstanceOf('Imagine\Image\Metadata\MetadataInterface', $this->getMonoLayeredImage()->metadata());
}

public function testCloningImageResultsInNewMetadataInstance()
{
$image = $this->getMonoLayeredImage();
$originalMetadata = $image->metadata();
$clone = clone $image;
$this->assertNotSame($originalMetadata, $clone->metadata(), 'The image\'s metadata is the same after cloning the image, but must be a new instance.');
}

private function getMonoLayeredImage()
{
return $this->getImagine()->open('tests/Imagine/Fixtures/google.png');
Expand All @@ -587,6 +600,13 @@ protected function processInOut($file, $in, $out)

}

/**
* @return \Imagine\Image\ImagineInterface
*/
abstract protected function getImagine();

/**
* @return boolean
*/
abstract protected function supportMultipleLayers();
}
50 changes: 50 additions & 0 deletions tests/Imagine/Test/Image/Metadata/ExifMetadataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
namespace Imagine\Test\Image;

/*
* This file is part of the Imagine package.
*
* (c) Bulat Shakirzyanov <mallluhuct@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Imagine\Image\Metadata\ExifMetadata;
use Imagine\Image\Metadata\MetadataInterface;

/**
*/
class ExifMetadataTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function getOrientationReturnsNullIfNoRotationIsGiven()
{
$metadata = new ExifMetadata($this->getImageMock('large.jpg'));
$this->assertNull($metadata->getOrientation());
}

/**
* @test
*/
public function getOrientationReturnsCorrectRotationIfExifDataSaysSo()
{
$metadata = new ExifMetadata($this->getImageMock('exifOrientation/90.jpg'));
$this->assertSame(MetadataInterface::ORIENTATION_ROTATED_MINUS90, $metadata->getOrientation());
}

/**
* Gets an image mock for use with the ExifMetadata constructor
*
* @param string $fixtureImage
* @return \Imagine\Image\ImageInterface
*/
protected function getImageMock($fixtureImage)
{
$mock = $this->getMock('Imagine\Image\ImageInterface');
$mock->expects($this->atLeastOnce())->method('get')->with('jpg')->will($this->returnValue(file_get_contents('tests/Imagine/Fixtures/' . $fixtureImage)));
return $mock;
}
}