[#154 state:fixed] Added exif orientation support.

This commit is contained in:
Virgil Dupras 2011-05-31 10:05:12 -04:00
parent cc7ccff48e
commit 1f26fbeacc
4 changed files with 134 additions and 35 deletions

View File

@ -30,14 +30,12 @@ class Photo(PhotoBase):
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy() HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
HANDLED_EXTS.update({'psd', 'nef', 'cr2'}) HANDLED_EXTS.update({'psd', 'nef', 'cr2'})
def _read_info(self, field): def _plat_get_dimensions(self):
PhotoBase._read_info(self, field) return _block_osx.get_image_size(str(self.path))
if field == 'dimensions':
self.dimensions = _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: 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: except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e))) raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
if not blocks: if not blocks:

View File

@ -11,6 +11,8 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#define RADIANS( degrees ) ( degrees * M_PI / 180 )
static CFStringRef static CFStringRef
pystring2cfstring(PyObject *pystring) pystring2cfstring(PyObject *pystring)
{ {
@ -149,12 +151,10 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
CFURLRef image_url; CFURLRef image_url;
CGImageSourceRef source; CGImageSourceRef source;
CGImageRef image; CGImageRef image;
size_t width, height; size_t width, height, image_width, image_height;
int block_count, block_width, block_height, i; int block_count, block_width, block_height, orientation, i;
width = 0; if (!PyArg_ParseTuple(args, "Oii", &path, &block_count, &orientation)) {
height = 0;
if (!PyArg_ParseTuple(args, "Oi", &path, &block_count)) {
return NULL; return NULL;
} }
@ -163,6 +163,10 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
return NULL; 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); image_path = pystring2cfstring(path);
if (image_path == NULL) { if (image_path == NULL) {
return PyErr_NoMemory(); return PyErr_NoMemory();
@ -182,13 +186,61 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
return PyErr_NoMemory(); return PyErr_NoMemory();
} }
width = CGImageGetWidth(image);
height = CGImageGetHeight(image); width = image_width = CGImageGetWidth(image);
CGContextRef myContext = MyCreateBitmapContext(width, height); height = image_height = CGImageGetHeight(image);
CGRect myBoundingBox = CGRectMake(0, 0, width, height); if (orientation >= 5) {
CGContextDrawImage(myContext, myBoundingBox, image); // orientations 5-8 rotate the photo sideways, so we have to swap width and height
unsigned char *bitmapData = CGBitmapContextGetData(myContext); width = image_height;
CGContextRelease(myContext); 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); CGImageRelease(image);
CFRelease(source); CFRelease(source);
if (bitmapData == NULL) { if (bitmapData == NULL) {

View File

@ -6,8 +6,10 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from hscommon import io
from hscommon.util import get_file_ext from hscommon.util import get_file_ext
from core import fs from core import fs
from . import exif
class Photo(fs.File): class Photo(fs.File):
INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO = fs.File.INITIAL_INFO.copy()
@ -17,10 +19,35 @@ class Photo(fs.File):
# These extensions are supported on all platforms # These extensions are supported on all platforms
HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'} 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 @classmethod
def can_handle(cls, path): def can_handle(cls, path):
return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS
def get_blocks(self, block_count_per_side): def _read_info(self, field):
raise NotImplementedError() 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())

View File

@ -9,7 +9,7 @@
import os.path as op import os.path as op
import logging 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 import data as data_pe, __appname__
from core_pe.photo import Photo as PhotoBase from core_pe.photo import Photo as PhotoBase
@ -23,23 +23,45 @@ from .preferences import Preferences
from .preferences_dialog import PreferencesDialog from .preferences_dialog import PreferencesDialog
class File(PhotoBase): class File(PhotoBase):
def _read_info(self, field): def _plat_get_dimensions(self):
PhotoBase._read_info(self, field) try:
if field == 'dimensions': ir = QImageReader(str(self.path))
try: size = ir.size()
ir = QImageReader(str(self.path)) if size.isValid():
size = ir.size() return (size.width(), size.height())
if size.isValid(): else:
self.dimensions = (size.width(), size.height()) return (0, 0)
else: except EnvironmentError:
self.dimensions = (0, 0) logging.warning("Could not read image '%s'", str(self.path))
except EnvironmentError: return (0, 0)
self.dimensions = (0, 0)
logging.warning("Could not read image '%s'", str(self.path))
def get_blocks(self, block_count_per_side): def _plat_get_blocks(self, block_count_per_side, orientation):
image = QImage(str(self.path)) image = QImage(str(self.path))
image = image.convertToFormat(QImage.Format_RGB888) 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) return getblocks(image, block_count_per_side)