mirror of
https://github.com/arsenetar/dupeguru.git
synced 2024-11-04 15:29:02 +00:00
[#154 state:fixed] Added exif orientation support.
This commit is contained in:
parent
cc7ccff48e
commit
1f26fbeacc
@ -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:
|
||||||
|
@ -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) {
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
38
qt/pe/app.py
38
qt/pe/app.py
@ -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)
|
|
||||||
if field == 'dimensions':
|
|
||||||
try:
|
try:
|
||||||
ir = QImageReader(str(self.path))
|
ir = QImageReader(str(self.path))
|
||||||
size = ir.size()
|
size = ir.size()
|
||||||
if size.isValid():
|
if size.isValid():
|
||||||
self.dimensions = (size.width(), size.height())
|
return (size.width(), size.height())
|
||||||
else:
|
else:
|
||||||
self.dimensions = (0, 0)
|
return (0, 0)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
self.dimensions = (0, 0)
|
|
||||||
logging.warning("Could not read image '%s'", str(self.path))
|
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 = 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)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user