diff --git a/composer.json b/composer.json index 7f9efe4..423df53 100644 --- a/composer.json +++ b/composer.json @@ -8,23 +8,26 @@ "email": "riley.ross@gmail.com" } ], - + "minimum-stability": "dev", - + "require": { "symfony/http-foundation": "~2.5", "ext-gd": "*" }, - - + + "require-dev": { + "bolt/bolt": "~2.0" + }, + "autoload": { "psr-4": { "Bolt\\Thumbs\\": "src", "Bolt\\Thumbs\\Tests\\": "tests" } }, + "extra": { "branch-alias": { "dev-master": "2.1-dev" } } - } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0d11db1..1a282a1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,7 +7,7 @@ verbose="false"> - + tests/ diff --git a/src/ThumbnailCreator.php b/src/ThumbnailCreator.php index abf1c84..3070d6f 100644 --- a/src/ThumbnailCreator.php +++ b/src/ThumbnailCreator.php @@ -174,9 +174,13 @@ protected function doResize($src, $width, $height, $crop = false, $fit = false, break; case 'jpg': $img = imagecreatefromjpeg($src); + // Handle exif orientation $exif = $this->exifOrientation ? exif_read_data($src) : false; - if ($exif !== false && isset($exif['Orientation'])) { - $img = self::imageSetOrientationFromExif($img, $exif['Orientation']); + $modes = array(2 => 'H-', 3 => '-T', 4 => 'V-', 5 => 'VL', 6 => '-L', 7 => 'HL', 8 => '-R'); + $orientation = isset($exif['Orientation']) ? $exif['Orientation'] : 0; + if (isset($modes[$orientation])) { + $mode = $modes[$orientation]; + $img = self::imageFlipRotate($img, $mode[0], $mode[1]); $w = imagesx($img); $h = imagesy($img); } @@ -291,88 +295,58 @@ protected function getOutput($imageContent, $type) } /** - * Sets orientation of image based on exif data - * - * @param $img image resource - * @param $orientation the exif image orientation - **/ - public static function imageSetOrientationFromExif($img, $orientation) - { - switch ($orientation) { - case 2: // horizontal flip - $img = self::imageFlip($img, 1); - break; - case 3: // 180 rotate left - $img = imagerotate($img, 180, 0); - break; - case 4: // vertical flip - $img = self::imageFlip($img, 0); - break; - case 5: // vertical flip + 90 rotate right - $img = self::imageFlip($img, 0); - $img = imagerotate($img, -90, 0); - break; - case 6: // 90 rotate right - $img = imagerotate($img, -90, 0); - break; - case 7: // horizontal flip + 90 rotate right - $img = self::imageFlip($img, 1); - $img = imagerotate($img, -90, 0); - break; - case 8: // 90 rotate left - $img = imagerotate($img, 90, 0); - break; - } - - return $img; - } - - /** - * Image Flip + * Image flip and rotate * * Based on http://stackoverflow.com/a/10001884/1136593 * Thanks Jon Grant * - * @param $imgsrc (image to flip) - * @param $mode (0 = vertical, 1 = horizontal, 2 = both) - defaults to vertical flip + * @param $img (image to flip and/or rotate) + * @param $mode ('V' = vertical, 'H' = horizontal, 'HV' = both) + * @param $angle ('L' = -90°, 'R' = +90°, 'T' = 180°) * */ - public static function imageFlip($imgsrc, $mode = 0) + public static function imageFlipRotate($img, $mode, $angle) { - $width = imagesx($imgsrc); - $height = imagesy($imgsrc); + // Flip the image + if ($mode === 'V' || $mode === 'H' || $mode === 'HV') { + $width = imagesx($img); + $height = imagesy($img); + + $srcX = 0; + $srcY = 0; + $srcWidth = $width; + $srcHeight = $height; + + switch ($mode) { + case 'V': // Vertical + $srcY = $height - 1; + $srcHeight = -$height; + break; + case 'H': // Horizontal + $srcX = $width - 1; + $srcWidth = -$width; + break; + case 'HV': // Both + $srcX = $width - 1; + $srcY = $height - 1; + $srcWidth = -$width; + $srcHeight = -$height; + break; + } - $src_x = 0; - $src_y = 0; - $src_width = $width; - $src_height = $height; + $imgdest = imagecreatetruecolor($width, $height); - switch ( $mode ) - { - default: - case '0': //vertical - $src_y = $height -1; - $src_height= -$height; - break; - case '1': //horizontal - $src_x = $width -1; - $src_width = -$width; - break; - case '2': //both - $src_x = $width -1; - $src_y = $height -1; - $src_width = -$width; - $src_height = -$height; - break; + if (imagecopyresampled($imgdest, $img, 0, 0, $srcX, $srcY, $width, $height, $srcWidth, $srcHeight)) { + $img = $imgdest; + } } - $imgdest = imagecreatetruecolor ( $width, $height ); - - if (imagecopyresampled($imgdest, $imgsrc, 0, 0, $src_x, $src_y , $width, $height, $src_width, $src_height)) - { - return $imgdest; + // Rotate the image + if ($angle === 'L' || $angle === 'R' || $angle === 'T') { + $rotate = array('L' => 270, 'R' => 90, 'T' => 180); + $img = imagerotate($img, $rotate[$angle], 0); } - return $imgsrc; + return $img; } } diff --git a/src/ThumbnailResponder.php b/src/ThumbnailResponder.php index 944f188..ac897dc 100644 --- a/src/ThumbnailResponder.php +++ b/src/ThumbnailResponder.php @@ -51,12 +51,12 @@ public function __construct(Application $app, Request $request, ResizeInterface public function initialize() { if (null !== $this->app['config']->get('general/thumbnails/notfound_image')) { - $file = $this->app['resources']->getPath('app'). '/' .$this->app['config']->get('general/thumbnails/notfound_image'); + $file = $this->app['resources']->getPath('app') . '/' . $this->app['config']->get('general/thumbnails/notfound_image'); $this->resizer->setDefaultSource(new File($file, false)); } if (null !== $this->app['config']->get('general/thumbnails/error_image')) { - $file = $this->app['resources']->getPath('app'). '/' .$this->app['config']->get('general/thumbnails/error_image'); + $file = $this->app['resources']->getPath('app') . '/' . $this->app['config']->get('general/thumbnails/error_image'); $this->resizer->setErrorSource(new File($file, false)); } @@ -184,11 +184,11 @@ public function saveStatic($imageContent) $path = urldecode($this->request->getPathInfo()); try { $webroot = dirname($this->request->server->get('SCRIPT_FILENAME')); - $savePath = dirname($webroot.$path); + $savePath = dirname($webroot . $path); if (!is_dir($savePath)) { mkdir($savePath, 0777, true); } - file_put_contents($webroot.$path, $imageContent); + file_put_contents($webroot . $path, $imageContent); } catch (\Exception $e) { } diff --git a/tests/ThumbnailCreatorTest.php b/tests/ThumbnailCreatorTest.php index ce5de1a..644ee11 100644 --- a/tests/ThumbnailCreatorTest.php +++ b/tests/ThumbnailCreatorTest.php @@ -1,13 +1,7 @@ jpg = __DIR__ . '/images/generic-logo.jpg'; $this->gif = __DIR__ . '/images/generic-logo.gif'; $this->png = __DIR__ . '/images/generic-logo.png'; - require_once __DIR__ . '/../vendor/bolt/bolt/app/lib.php'; } public function testSetup() @@ -41,23 +34,23 @@ public function testFallbacksForBadDimensions() $creator = new ThumbnailCreator(); $creator->setSource($src); - $ok_width = 624; - $ok_height = 351; + $okWidth = 624; + $okHeight = 351; $testcases = array( array(), - array('width' => $ok_width, 'height' => -20), - array('width' => $ok_width), - array('height' => $ok_height), - array('width' => 'A', 'height' => $ok_height), - array('width' => 123.456, 'height' => $ok_height), + array('width' => $okWidth, 'height' => -20), + array('width' => $okWidth), + array('height' => $okHeight), + array('width' => 'A', 'height' => $okHeight), + array('width' => 123.456, 'height' => $okHeight), array('width' => 'both', 'height' => 'wrong'), ); foreach ($testcases as $parameters) { $creator->verify($parameters); - $this->assertEquals($ok_width, $creator->targetWidth); - $this->assertEquals($ok_height, $creator->targetHeight); + $this->assertEquals($okWidth, $creator->targetWidth); + $this->assertEquals($okHeight, $creator->targetHeight); } } @@ -171,8 +164,15 @@ public function testPortraitResize() $result = $creator->resize(array('width' => 200, 'height' => 500)); $compare = __DIR__ . '/images/timthumbs/resize_sample2_200_500.jpg'; file_put_contents(__DIR__ . '/tmp/test.jpg', $result); + + // Original compare image is with v80, v90 creates a 2 byte smaller image (perhaps only on windows?) + $correction = 0; + if (preg_match('%CREATOR: gd-jpeg v1\.0 \(using IJG JPEG v(\d+)\)%', $result, $pm) && $pm[1] == '90') { + $correction = 2; + } + $this->assertEquals(getimagesize($compare), getimagesize(__DIR__ . '/tmp/test.jpg')); - $this->assertEquals(filesize($compare), filesize(__DIR__ . '/tmp/test.jpg')); + $this->assertEquals(filesize($compare), filesize(__DIR__ . '/tmp/test.jpg') + $correction); } public function testPortraitFit() @@ -199,12 +199,44 @@ public function testPortraitBorder() $this->assertEquals(filesize($compare), filesize(__DIR__ . '/tmp/test.jpg')); } + public function testExifOrientation() + { + $images = array( + '1-top-left', + '2-top-right', + '3-bottom-right', + '4-bottom-left', + '5-left-top', + '6-right-top', + '7-right-bottom', + '8-left-bottom', + ); + $resize = array('width' => 200, 'height' => 100); + + foreach ($images as $name) { + $path = __DIR__ . '/tmp/' . $name . '.jpg'; + // Create test image + $creator = new ThumbnailCreator(); + $creator->setSource(new File(__DIR__ . '/images/exif-orientation/' . $name . '.jpg')); + $result = $creator->resize($resize); + file_put_contents($path, $result); + // Read test image + $img = imagecreatefromjpeg($path); + $width = imagesx($img); + $height = imagesy($img); + $rgb = imagecolorsforindex($img, imagecolorat($img, 0, 0)); + // Assert image size and red color (fuzzy) in the upper left corner + $this->assertEquals($resize['width'], $width, 'Wrong width!'); + $this->assertEquals($resize['height'], $height, 'Wrong height!'); + $this->assertTrue($rgb['red'] > 250 && $rgb['green'] < 5 && $rgb['blue'] < 5, 'Wrong orientation!'); + } + } + public function tearDown() { $tmp = __DIR__ . '/tmp/test.jpg'; if (is_readable($tmp)) { unlink($tmp); } - } } diff --git a/tests/ThumbnailResponderTest.php b/tests/ThumbnailResponderTest.php index d70fbbd..d949a5b 100644 --- a/tests/ThumbnailResponderTest.php +++ b/tests/ThumbnailResponderTest.php @@ -1,13 +1,16 @@ assertEquals('240', $responder->height); $this->assertEquals('crop', $responder->action); $this->assertEquals('generic-logo.jpg', $responder->file); - } public function testParseWithSubdirectory() @@ -44,7 +45,6 @@ public function testParseWithSubdirectory() $responder = $this->initializeResponder($request); $this->assertEquals('subdir/generic-logo.jpg', $responder->file); - } public function testResponse() @@ -61,13 +61,20 @@ public function testResponse() protected function initializeResponder($request) { - $config = new ResourceManager(__DIR__); + $container = new Pimple( + array( + 'rootpath' => __DIR__, + 'pathmanager' => new PlatformFileSystemPathFactory() + ) + ); + + $config = new ResourceManager($container); $config->setPath('cache', 'tmp/cache'); $config->setPath('files', 'images'); $config->compat(); $app = new Application(array('resources' => $config)); - $app->register(new \Bolt\Provider\CacheServiceProvider()); + $app->register(new CacheServiceProvider()); $responder = new ThumbnailResponder($app, $request); $responder->initialize(); @@ -78,13 +85,13 @@ protected function initializeResponder($request) public function tearDown() { $this->rmdir(__DIR__ . '/tmp'); - @rmdir(__DIR__ .'/tmp'); + @rmdir(__DIR__ . '/tmp'); } protected function rmdir($dir) { $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $file) { diff --git a/tests/images/exif-orientation/1-top-left.jpg b/tests/images/exif-orientation/1-top-left.jpg new file mode 100644 index 0000000..27a0d8b Binary files /dev/null and b/tests/images/exif-orientation/1-top-left.jpg differ diff --git a/tests/images/exif-orientation/2-top-right.jpg b/tests/images/exif-orientation/2-top-right.jpg new file mode 100644 index 0000000..85cac3d Binary files /dev/null and b/tests/images/exif-orientation/2-top-right.jpg differ diff --git a/tests/images/exif-orientation/3-bottom-right.jpg b/tests/images/exif-orientation/3-bottom-right.jpg new file mode 100644 index 0000000..e60f603 Binary files /dev/null and b/tests/images/exif-orientation/3-bottom-right.jpg differ diff --git a/tests/images/exif-orientation/4-bottom-left.jpg b/tests/images/exif-orientation/4-bottom-left.jpg new file mode 100644 index 0000000..5f62b55 Binary files /dev/null and b/tests/images/exif-orientation/4-bottom-left.jpg differ diff --git a/tests/images/exif-orientation/5-left-top.jpg b/tests/images/exif-orientation/5-left-top.jpg new file mode 100644 index 0000000..8a1671b Binary files /dev/null and b/tests/images/exif-orientation/5-left-top.jpg differ diff --git a/tests/images/exif-orientation/6-right-top.jpg b/tests/images/exif-orientation/6-right-top.jpg new file mode 100644 index 0000000..fdc9e2c Binary files /dev/null and b/tests/images/exif-orientation/6-right-top.jpg differ diff --git a/tests/images/exif-orientation/7-right-bottom.jpg b/tests/images/exif-orientation/7-right-bottom.jpg new file mode 100644 index 0000000..600c255 Binary files /dev/null and b/tests/images/exif-orientation/7-right-bottom.jpg differ diff --git a/tests/images/exif-orientation/8-left-bottom.jpg b/tests/images/exif-orientation/8-left-bottom.jpg new file mode 100644 index 0000000..d9044ee Binary files /dev/null and b/tests/images/exif-orientation/8-left-bottom.jpg differ