From 1f26fbeacc0571a71c3abfd54b78a6d8f649d220 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Tue, 31 May 2011 10:05:12 -0400 Subject: [PATCH] [#154 state:fixed] Added exif orientation support. --- core_pe/app_cocoa.py | 10 ++--- core_pe/modules/block_osx.m | 76 +++++++++++++++++++++++++++++++------ core_pe/photo.py | 31 ++++++++++++++- qt/pe/app.py | 52 +++++++++++++++++-------- 4 files changed, 134 insertions(+), 35 deletions(-) diff --git a/core_pe/app_cocoa.py b/core_pe/app_cocoa.py index b9b7d6c8..787c43ad 100644 --- a/core_pe/app_cocoa.py +++ b/core_pe/app_cocoa.py @@ -30,14 +30,12 @@ class Photo(PhotoBase): HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy() HANDLED_EXTS.update({'psd', 'nef', 'cr2'}) - def _read_info(self, field): - PhotoBase._read_info(self, field) - if field == 'dimensions': - self.dimensions = _block_osx.get_image_size(str(self.path)) + def _plat_get_dimensions(self): + return _block_osx.get_image_size(str(self.path)) - def get_blocks(self, block_count_per_side): + def _plat_get_blocks(self, block_count_per_side, orientation): try: - blocks = _block_osx.getblocks(str(self.path), block_count_per_side) + blocks = _block_osx.getblocks(str(self.path), block_count_per_side, orientation) except Exception as e: raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e))) if not blocks: diff --git a/core_pe/modules/block_osx.m b/core_pe/modules/block_osx.m index fe50feb6..37b8bfeb 100644 --- a/core_pe/modules/block_osx.m +++ b/core_pe/modules/block_osx.m @@ -11,6 +11,8 @@ #import +#define RADIANS( degrees ) ( degrees * M_PI / 180 ) + static CFStringRef pystring2cfstring(PyObject *pystring) { @@ -149,12 +151,10 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args) CFURLRef image_url; CGImageSourceRef source; CGImageRef image; - size_t width, height; - int block_count, block_width, block_height, i; + size_t width, height, image_width, image_height; + int block_count, block_width, block_height, orientation, i; - width = 0; - height = 0; - if (!PyArg_ParseTuple(args, "Oi", &path, &block_count)) { + if (!PyArg_ParseTuple(args, "Oii", &path, &block_count, &orientation)) { return NULL; } @@ -163,6 +163,10 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args) return NULL; } + if ((orientation > 8) || (orientation < 0)) { + orientation = 0; // simplifies checks later since we can only have values in 0-8 + } + image_path = pystring2cfstring(path); if (image_path == NULL) { return PyErr_NoMemory(); @@ -182,13 +186,61 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args) return PyErr_NoMemory(); } - width = CGImageGetWidth(image); - height = CGImageGetHeight(image); - CGContextRef myContext = MyCreateBitmapContext(width, height); - CGRect myBoundingBox = CGRectMake(0, 0, width, height); - CGContextDrawImage(myContext, myBoundingBox, image); - unsigned char *bitmapData = CGBitmapContextGetData(myContext); - CGContextRelease(myContext); + + width = image_width = CGImageGetWidth(image); + height = image_height = CGImageGetHeight(image); + if (orientation >= 5) { + // orientations 5-8 rotate the photo sideways, so we have to swap width and height + width = image_height; + height = image_width; + } + + CGContextRef context = MyCreateBitmapContext(width, height); + + if (orientation == 2) { + // Flip X + CGContextTranslateCTM(context, width, 0); + CGContextScaleCTM(context, -1, 1); + } + else if (orientation == 3) { + // Rot 180 + CGContextTranslateCTM(context, width, height); + CGContextRotateCTM(context, RADIANS(180)); + } + else if (orientation == 4) { + // Flip Y + CGContextTranslateCTM(context, 0, height); + CGContextScaleCTM(context, 1, -1); + } + else if (orientation == 5) { + // Flip X + Rot CW 90 + CGContextTranslateCTM(context, width, 0); + CGContextScaleCTM(context, -1, 1); + CGContextTranslateCTM(context, 0, height); + CGContextRotateCTM(context, RADIANS(-90)); + } + else if (orientation == 6) { + // Rot CW 90 + CGContextTranslateCTM(context, 0, height); + CGContextRotateCTM(context, RADIANS(-90)); + } + else if (orientation == 7) { + // Rot CCW 90 + Flip X + CGContextTranslateCTM(context, width, 0); + CGContextScaleCTM(context, -1, 1); + CGContextTranslateCTM(context, width, 0); + CGContextRotateCTM(context, RADIANS(90)); + } + else if (orientation == 8) { + // Rot CCW 90 + CGContextTranslateCTM(context, width, 0); + CGContextRotateCTM(context, RADIANS(90)); + } + CGRect myBoundingBox = CGRectMake(0, 0, image_width, image_height); + CGContextDrawImage(context, myBoundingBox, image); + unsigned char *bitmapData = CGBitmapContextGetData(context); + CGContextRelease(context); + CGImageRelease(image); CFRelease(source); if (bitmapData == NULL) { diff --git a/core_pe/photo.py b/core_pe/photo.py index 3e47dac9..b200b3ac 100644 --- a/core_pe/photo.py +++ b/core_pe/photo.py @@ -6,8 +6,10 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license +from hscommon import io from hscommon.util import get_file_ext from core import fs +from . import exif class Photo(fs.File): INITIAL_INFO = fs.File.INITIAL_INFO.copy() @@ -17,10 +19,35 @@ class Photo(fs.File): # These extensions are supported on all platforms HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'} + def _plat_get_dimensions(self): + raise NotImplementedError() + + def _plat_get_blocks(self, block_count_per_side, orientation): + raise NotImplementedError() + + def _get_orientation(self): + if not hasattr(self, '_cached_orientation'): + try: + with io.open(self.path, 'rb') as fp: + exifdata = exif.get_fields(fp) + # the value is a list (probably one-sized) of ints + orientations = exifdata['Orientation'] + self._cached_orientation = orientations[0] + except Exception: # Couldn't read EXIF data, no transforms + self._cached_orientation = 0 + return self._cached_orientation + @classmethod def can_handle(cls, path): return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS - def get_blocks(self, block_count_per_side): - raise NotImplementedError() + def _read_info(self, field): + fs.File._read_info(self, field) + if field == 'dimensions': + self.dimensions = self._plat_get_dimensions() + if self._get_orientation() in {5, 6, 7, 8}: + self.dimensions = (self.dimensions[1], self.dimensions[0]) + + def get_blocks(self, block_count_per_side): + return self._plat_get_blocks(block_count_per_side, self._get_orientation()) diff --git a/qt/pe/app.py b/qt/pe/app.py index 457915b3..a7b45387 100644 --- a/qt/pe/app.py +++ b/qt/pe/app.py @@ -9,7 +9,7 @@ import os.path as op import logging -from PyQt4.QtGui import QImage, QImageReader +from PyQt4.QtGui import QImage, QImageReader, QTransform from core_pe import data as data_pe, __appname__ from core_pe.photo import Photo as PhotoBase @@ -23,23 +23,45 @@ from .preferences import Preferences from .preferences_dialog import PreferencesDialog class File(PhotoBase): - def _read_info(self, field): - PhotoBase._read_info(self, field) - if field == 'dimensions': - try: - ir = QImageReader(str(self.path)) - size = ir.size() - if size.isValid(): - self.dimensions = (size.width(), size.height()) - else: - self.dimensions = (0, 0) - except EnvironmentError: - self.dimensions = (0, 0) - logging.warning("Could not read image '%s'", str(self.path)) + def _plat_get_dimensions(self): + try: + ir = QImageReader(str(self.path)) + size = ir.size() + if size.isValid(): + return (size.width(), size.height()) + else: + return (0, 0) + except EnvironmentError: + logging.warning("Could not read image '%s'", str(self.path)) + return (0, 0) - def get_blocks(self, block_count_per_side): + def _plat_get_blocks(self, block_count_per_side, orientation): image = QImage(str(self.path)) image = image.convertToFormat(QImage.Format_RGB888) + # MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for + # duplicate scanning. The transforms seems to work fine (if I try to save the image after + # the transform, we see that the image has been correctly flipped and rotated), but the + # analysis part yields wrong blocks. I spent enought time with this feature, so I'll leave + # like that for now. (by the way, orientations 5 and 7 work fine under Cocoa) + if 2 <= orientation <= 8: + t = QTransform() + if orientation == 2: + t.scale(-1, 1) + elif orientation == 3: + t.rotate(180) + elif orientation == 4: + t.scale(1, -1) + elif orientation == 5: + t.scale(-1, 1) + t.rotate(90) + elif orientation == 6: + t.rotate(90) + elif orientation == 7: + t.scale(-1, 1) + t.rotate(270) + elif orientation == 8: + t.rotate(270) + image = image.transformed(t) return getblocks(image, block_count_per_side)