diff --git a/core_pe/app.py b/core_pe/app.py
deleted file mode 100644
index bce62610..00000000
--- a/core_pe/app.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
-#
-# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
-# which should be included with this package. The terms are also available at
-# http://www.gnu.org/licenses/gpl-3.0.html
-
-import os.path as op
-
-from core.app import DupeGuru as DupeGuruBase, cmp_value
-from .scanner import ScannerPE
-from . import prioritize
-from . import __appname__
-from .photo import get_delta_dimensions
-from .result_table import ResultTable
-
-class DupeGuru(DupeGuruBase):
- NAME = __appname__
- METADATA_TO_READ = ['size', 'mtime', 'dimensions', 'exif_timestamp']
- SCANNER_CLASS = ScannerPE
-
- def __init__(self, view):
- DupeGuruBase.__init__(self, view)
- self.options['cache_path'] = op.join(self.appdata, 'cached_pictures.db')
-
- def _get_dupe_sort_key(self, dupe, get_group, key, delta):
- if key == 'folder_path':
- dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
- return str(dupe_folder_path).lower()
- if delta and key == 'dimensions':
- r = cmp_value(dupe, key)
- ref_value = cmp_value(get_group().ref, key)
- return get_delta_dimensions(r, ref_value)
- return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
-
- def _get_group_sort_key(self, group, key):
- if key == 'folder_path':
- dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
- return str(dupe_folder_path).lower()
- return DupeGuruBase._get_group_sort_key(self, group, key)
-
- def _prioritization_categories(self):
- return prioritize.all_categories()
-
- def _create_result_table(self):
- return ResultTable(self)
diff --git a/core_pe/photo.py b/core_pe/photo.py
index 6d9c9126..5f76dadf 100644
--- a/core_pe/photo.py
+++ b/core_pe/photo.py
@@ -1,6 +1,4 @@
-# Created By: Virgil Dupras
-# Created On: 2011-05-29
-# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
+# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
@@ -13,6 +11,9 @@ from core.app import format_timestamp, format_perc, format_dupe_count
from core import fs
from . import exif
+# This global value is set by the platform-specific subclasser of the Photo base class
+PLAT_SPECIFIC_PHOTO_CLASS = None
+
def format_dimensions(dimensions):
return '%d x %d' % (dimensions[0], dimensions[1])
diff --git a/core_se/app.py b/core_se/app.py
index 375e6851..a72377bf 100644
--- a/core_se/app.py
+++ b/core_se/app.py
@@ -4,12 +4,19 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
-from core.app import DupeGuru as DupeGuruBase, AppMode
+import os.path as op
+
+from core.app import DupeGuru as DupeGuruBase, AppMode, cmp_value
from core import prioritize
import core_me.fs
import core_me.prioritize
import core_me.result_table
import core_me.scanner
+import core_pe.photo
+import core_pe.prioritize
+import core_pe.result_table
+import core_pe.scanner
+from core_pe.photo import get_delta_dimensions
from . import __appname__, fs, scanner
from .result_table import ResultTable
@@ -19,50 +26,66 @@ class DupeGuru(DupeGuruBase):
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.folderclass = fs.Folder
+ self.options['cache_path'] = op.join(self.appdata, 'cached_pictures.db')
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
- if self.app_mode == AppMode.Music:
+ if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == 'folder_path':
dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path)
return str(dupe_folder_path).lower()
+ if self.app_mode == AppMode.Picture:
+ if delta and key == 'dimensions':
+ r = cmp_value(dupe, key)
+ ref_value = cmp_value(get_group().ref, key)
+ return get_delta_dimensions(r, ref_value)
return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta)
def _get_group_sort_key(self, group, key):
- if self.app_mode == AppMode.Music:
+ if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == 'folder_path':
dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path)
return str(dupe_folder_path).lower()
return DupeGuruBase._get_group_sort_key(self, group, key)
def _prioritization_categories(self):
- if self.app_mode == AppMode.Music:
- return prioritize.all_categories()
+ if self.app_mode == AppMode.Picture:
+ return core_pe.prioritize.all_categories()
+ elif self.app_mode == AppMode.Music:
+ return core_me.prioritize.all_categories()
else:
return prioritize.all_categories()
def _create_result_table(self):
- if self.app_mode == AppMode.Music:
+ if self.app_mode == AppMode.Picture:
+ return core_pe.result_table.ResultTable(self)
+ elif self.app_mode == AppMode.Music:
return core_me.result_table.ResultTable(self)
else:
return ResultTable(self)
@property
def fileclasses(self):
- if self.app_mode == AppMode.Music:
+ if self.app_mode == AppMode.Picture:
+ return [core_pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]
+ elif self.app_mode == AppMode.Music:
return [core_me.fs.MusicFile]
else:
return [fs.File]
@property
def SCANNER_CLASS(self):
- if self.app_mode == AppMode.Music:
+ if self.app_mode == AppMode.Picture:
+ return core_pe.scanner.ScannerPE
+ elif self.app_mode == AppMode.Music:
return core_me.scanner.ScannerME
else:
return scanner.ScannerSE
@property
def METADATA_TO_READ(self):
- if self.app_mode == AppMode.Music:
+ if self.app_mode == AppMode.Picture:
+ return ['size', 'mtime', 'dimensions', 'exif_timestamp']
+ elif self.app_mode == AppMode.Music:
return [
'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
diff --git a/qt/base/directories_dialog.py b/qt/base/directories_dialog.py
index e5a738c4..95eaf71d 100644
--- a/qt/base/directories_dialog.py
+++ b/qt/base/directories_dialog.py
@@ -127,7 +127,11 @@ class DirectoriesDialog(QMainWindow):
label = QLabel(tr("Application Mode:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label)
- self.appModeRadioBox = RadioBox(self, items=[tr("Standard"), tr("Music")], spread=False)
+ self.appModeRadioBox = RadioBox(
+ self,
+ items=[tr("Standard"), tr("Music"), tr("Picture")],
+ spread=False
+ )
hl.addWidget(self.appModeRadioBox)
self.verticalLayout.addLayout(hl)
hl = QHBoxLayout()
@@ -240,7 +244,9 @@ class DirectoriesDialog(QMainWindow):
self.recentFolders.insertItem(dirpath)
def appModeButtonSelected(self, index):
- if index == 1:
+ if index == 2:
+ mode = AppMode.Picture
+ elif index == 1:
mode = AppMode.Music
else:
mode = AppMode.Standard
diff --git a/qt/base/preferences.py b/qt/base/preferences.py
index cd319a44..24fc5dc2 100644
--- a/qt/base/preferences.py
+++ b/qt/base/preferences.py
@@ -92,13 +92,17 @@ class Preferences(PreferencesBase):
# scan_type is special because we save it immediately when we set it.
def get_scan_type(self, app_mode):
- if app_mode == AppMode.Music:
+ if app_mode == AppMode.Picture:
+ return self.get_value('ScanTypePicture', ScanType.FuzzyBlock)
+ elif app_mode == AppMode.Music:
return self.get_value('ScanTypeMusic', ScanType.Tag)
else:
return self.get_value('ScanTypeStandard', ScanType.Contents)
def set_scan_type(self, app_mode, value):
- if app_mode == AppMode.Music:
+ if app_mode == AppMode.Picture:
+ self.set_value('ScanTypePicture', value)
+ elif app_mode == AppMode.Music:
self.set_value('ScanTypeMusic', value)
else:
self.set_value('ScanTypeStandard', value)
diff --git a/qt/pe/installer.aip b/qt/pe/installer.aip
deleted file mode 100644
index 16e5b7fd..00000000
--- a/qt/pe/installer.aip
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/qt/pe/photo.py b/qt/pe/photo.py
new file mode 100644
index 00000000..16caf338
--- /dev/null
+++ b/qt/pe/photo.py
@@ -0,0 +1,56 @@
+# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
+#
+# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
+# which should be included with this package. The terms are also available at
+# http://www.gnu.org/licenses/gpl-3.0.html
+
+import logging
+
+from PyQt5.QtGui import QImage, QImageReader, QTransform
+
+from core_pe.photo import Photo as PhotoBase
+
+from .block import getblocks
+
+class File(PhotoBase):
+ 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 _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)
+
diff --git a/qt/pe/preferences.py b/qt/pe/preferences.py
deleted file mode 100644
index 27bc4915..00000000
--- a/qt/pe/preferences.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
-#
-# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
-# which should be included with this package. The terms are also available at
-# http://www.gnu.org/licenses/gpl-3.0.html
-
-from core.scanner import ScanType
-
-from ..base.preferences import Preferences as PreferencesBase
-
-class Preferences(PreferencesBase):
- DEFAULT_SCAN_TYPE = ScanType.FuzzyBlock
-
- def _load_specific(self, settings):
- get = self.get_value
- self.match_scaled = get('MatchScaled', self.match_scaled)
-
- def _reset_specific(self):
- self.filter_hardness = 95
- self.match_scaled = False
-
- def _save_specific(self, settings):
- set_ = self.set_value
- set_('MatchScaled', self.match_scaled)
-
diff --git a/qt/pe/preferences_dialog.py b/qt/pe/preferences_dialog.py
index cd513412..c8a18cd9 100644
--- a/qt/pe/preferences_dialog.py
+++ b/qt/pe/preferences_dialog.py
@@ -6,9 +6,10 @@
from hscommon.trans import trget
from core.scanner import ScanType
+from core.app import AppMode
from ..base.preferences_dialog import PreferencesDialogBase
-from . import preferences
+from ..se import preferences
tr = trget('ui')
@@ -34,7 +35,7 @@ class PreferencesDialog(PreferencesDialogBase):
setchecked(self.matchScaledBox, prefs.match_scaled)
# Update UI state based on selected scan type
- scan_type = prefs.scan_type
+ scan_type = prefs.get_scan_type(AppMode.Picture)
fuzzy_scan = scan_type == ScanType.FuzzyBlock
self.filterHardnessSlider.setEnabled(fuzzy_scan)
diff --git a/qt/se/app.py b/qt/se/app.py
index 70941433..72c2cf51 100644
--- a/qt/se/app.py
+++ b/qt/se/app.py
@@ -8,15 +8,20 @@ from core_se import __appname__
from core_se.app import DupeGuru as DupeGuruModel
from core.directories import Directories as DirectoriesBase, DirectoryState
from core.app import AppMode
+import core_pe.photo
from ..base.app import DupeGuru as DupeGuruBase
from .details_dialog import DetailsDialog as DetailsDialogStandard
from ..me.details_dialog import DetailsDialog as DetailsDialogMusic
+from ..pe.details_dialog import DetailsDialog as DetailsDialogPicture
from .results_model import ResultsModel as ResultsModelStandard
from ..me.results_model import ResultsModel as ResultsModelMusic
+from ..pe.results_model import ResultsModel as ResultsModelPicture
from .preferences import Preferences
from .preferences_dialog import PreferencesDialog as PreferencesDialogStandard
from ..me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic
+from ..pe.preferences_dialog import PreferencesDialog as PreferencesDialogPicture
+from ..pe.photo import File as PlatSpecificPhoto
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
@@ -39,6 +44,7 @@ class DupeGuru(DupeGuruBase):
def _setup(self):
self.directories = Directories()
DupeGuruBase._setup(self)
+ core_pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto
def _update_options(self):
DupeGuruBase._update_options(self)
@@ -61,24 +67,31 @@ class DupeGuru(DupeGuruBase):
if self.prefs.scan_tag_year:
scanned_tags.add('year')
self.model.options['scanned_tags'] = scanned_tags
+ self.model.options['match_scaled'] = self.prefs.match_scaled
@property
def DETAILS_DIALOG_CLASS(self):
- if self.model.app_mode == AppMode.Music:
+ if self.model.app_mode == AppMode.Picture:
+ return DetailsDialogPicture
+ elif self.model.app_mode == AppMode.Music:
return DetailsDialogMusic
else:
return DetailsDialogStandard
@property
def RESULT_MODEL_CLASS(self):
- if self.model.app_mode == AppMode.Music:
+ if self.model.app_mode == AppMode.Picture:
+ return ResultsModelPicture
+ elif self.model.app_mode == AppMode.Music:
return ResultsModelMusic
else:
return ResultsModelStandard
@property
def PREFERENCES_DIALOG_CLASS(self):
- if self.model.app_mode == AppMode.Music:
+ if self.model.app_mode == AppMode.Picture:
+ return PreferencesDialogPicture
+ elif self.model.app_mode == AppMode.Music:
return PreferencesDialogMusic
else:
return PreferencesDialogStandard
diff --git a/qt/se/installer.aip b/qt/se/installer.aip
deleted file mode 100644
index 0e348ba8..00000000
--- a/qt/se/installer.aip
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/qt/se/preferences.py b/qt/se/preferences.py
index 27bb5e0c..2b5d92d7 100644
--- a/qt/se/preferences.py
+++ b/qt/se/preferences.py
@@ -19,9 +19,10 @@ class Preferences(PreferencesBase):
self.scan_tag_title = get('ScanTagTitle', self.scan_tag_title)
self.scan_tag_genre = get('ScanTagGenre', self.scan_tag_genre)
self.scan_tag_year = get('ScanTagYear', self.scan_tag_year)
+ self.match_scaled = get('MatchScaled', self.match_scaled)
def _reset_specific(self):
- self.filter_hardness = 80
+ self.filter_hardness = 95
self.word_weighting = True
self.match_similar = False
self.ignore_small_files = True
@@ -32,6 +33,7 @@ class Preferences(PreferencesBase):
self.scan_tag_title = True
self.scan_tag_genre = False
self.scan_tag_year = False
+ self.match_scaled = False
def _save_specific(self, settings):
set_ = self.set_value
@@ -45,4 +47,5 @@ class Preferences(PreferencesBase):
set_('ScanTagTitle', self.scan_tag_title)
set_('ScanTagGenre', self.scan_tag_genre)
set_('ScanTagYear', self.scan_tag_year)
+ set_('MatchScaled', self.match_scaled)