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)