From 260d11e79bac17e2cd809a4ff6af1b5b70262431 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 Jan 2015 14:38:04 +0530 Subject: [PATCH] Read image data for reports and refactor file model class to make it more re-useable --- src/calibre/ebooks/oeb/polish/report.py | 44 +++++++- src/calibre/gui2/tweak_book/boss.py | 7 +- src/calibre/gui2/tweak_book/reports.py | 139 ++++++++++++++++++------ 3 files changed, 153 insertions(+), 37 deletions(-) diff --git a/src/calibre/ebooks/oeb/polish/report.py b/src/calibre/ebooks/oeb/polish/report.py index 571fa1f449..e17e008ff6 100644 --- a/src/calibre/ebooks/oeb/polish/report.py +++ b/src/calibre/ebooks/oeb/polish/report.py @@ -7,9 +7,11 @@ __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' import posixpath, os -from collections import namedtuple +from collections import namedtuple, defaultdict from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, OEB_FONTS +from calibre.utils.icu import numeric_sort_key +from calibre.utils.magick.draw import identify File = namedtuple('File', 'name dir basename size category') @@ -39,11 +41,51 @@ def safe_size(container, name): except Exception: return 0 +def safe_img_data(container, name, mt): + if 'svg' in mt: + return 0, 0 + try: + width, height, fmt = identify(container.name_to_abspath(name)) + except Exception: + width = height = 0 + return width, height + def file_data(container): for name, path in container.name_path_map.iteritems(): yield File(name, posixpath.dirname(name), posixpath.basename(name), safe_size(container, name), get_category(name, container.mime_map.get(name, ''))) +Image = namedtuple('Image', 'name mime_type usage size width height') +L = namedtuple('Location', 'name line_number offset word') +def Location(name, line_number=None, offset=0, word=None): + return L(name, line_number, offset, word) + +def sort_locations(locations): + def sort_key(l): + return (numeric_sort_key(l.name), l.line_number, l.offset, l.word) + return sorted(locations, key=sort_key) + +def link_data(container): + image_usage = defaultdict(set) + link_sources = OEB_STYLES | OEB_DOCS + for name, mt in container.mime_map.iteritems(): + if mt in link_sources: + for href, line_number, offset in container.iterlinks(name): + target = container.href_to_name(href, name) + if target and container.exists(target): + mt = container.mime_map.get(target) + if mt and mt.startswith('image/'): + image_usage[target].add(Location(name, line_number, offset)) + + image_data = [] + for name, mt in container.mime_map.iteritems(): + if mt.startswith('image/') and container.exists(name): + image_data.append(Image(name, mt, sort_locations(image_usage.get(name, set())), safe_size(container, name), + *safe_img_data(container, name, mt))) + return tuple(image_data) + def gather_data(container): data = {'files':tuple(file_data(container))} + img_data = link_data(container) + data['images'] = img_data return data diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index a606e63de7..9190b7cce4 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -129,7 +129,7 @@ class Boss(QObject): self.gui.manage_fonts.container_changed.connect(self.apply_container_update_to_gui) self.gui.manage_fonts.embed_all_fonts.connect(self.manage_fonts_embed) self.gui.manage_fonts.subset_all_fonts.connect(self.manage_fonts_subset) - self.gui.reports.edit_requested.connect(self.report_edit_requested) + self.gui.reports.edit_requested.connect(self.reports_edit_requested) @property def currently_editing(self): @@ -1157,10 +1157,11 @@ class Boss(QObject): self.gui.reports.show() self.gui.reports.raise_() - def report_edit_requested(self, name, location=None): + def reports_edit_requested(self, location): + name = location.name mt = current_container().mime_map.get(name, guess_type(name)) editor = self.edit_file_requested(name, None, mt) - if editor is None and location is not None: + if editor is not None: pass def image_activated(self, name): diff --git a/src/calibre/gui2/tweak_book/reports.py b/src/calibre/gui2/tweak_book/reports.py index b398d51610..d93b1e9dec 100644 --- a/src/calibre/gui2/tweak_book/reports.py +++ b/src/calibre/gui2/tweak_book/reports.py @@ -16,12 +16,12 @@ from PyQt5.Qt import ( QListWidgetItem, QLineEdit, QStackedWidget, QSplitter, QByteArray) from calibre import human_readable -from calibre.ebooks.oeb.polish.report import gather_data +from calibre.ebooks.oeb.polish.report import gather_data, Location from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import current_container, tprefs from calibre.gui2.tweak_book.widgets import Dialog from calibre.gui2.progress_indicator import ProgressIndicator -from calibre.utils.icu import primary_contains, primary_sort_key +from calibre.utils.icu import primary_contains, numeric_sort_key def read_state(name, default=None): data = tprefs.get('reports-ui-state') @@ -66,22 +66,13 @@ class ProxyModel(QSortFilterProxyModel): # {{{ return QSortFilterProxyModel.headerData(self, section, orientation, role) # }}} -# Files {{{ -class FilesModel(QAbstractTableModel): +class FileCollection(QAbstractTableModel): - COLUMN_HEADERS = [_('Folder'), _('Name'), _('Size (KB)'), _('Type')] - CATEGORY_NAMES = { - 'image':_('Image'), - 'text': _('Text'), - 'font': _('Font'), - 'style': _('Style'), - 'opf': _('Metadata'), - 'toc': _('Table of Contents'), - } + COLUMN_HEADERS = () def __init__(self, parent=None): self.files = self.sort_keys = () - self.total_size = self.images_size = self.fonts_size = 0 + self.total_size = 0 QAbstractTableModel.__init__(self, parent) def columnCount(self, parent=None): @@ -98,29 +89,46 @@ class FilesModel(QAbstractTableModel): pass return QAbstractTableModel.headerData(self, section, orientation, role) - def __call__(self, data): - self.beginResetModel() - self.files = data['files'] - self.total_size = sum(map(itemgetter(3), self.files)) - self.images_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'image'))) - self.fonts_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'font'))) - psk = primary_sort_key - self.sort_keys = tuple((psk(entry.dir), psk(entry.basename), entry.size, psk(self.CATEGORY_NAMES.get(entry.category, ''))) - for entry in self.files) - self.endResetModel() - def sort_key(self, row, col): try: return self.sort_keys[row][col] except IndexError: pass - def name(self, index): + def location(self, index): try: - return self.files[index.row()].name + return Location(self.files[index.row()].name) except IndexError: pass +# Files {{{ +class FilesModel(FileCollection): + + COLUMN_HEADERS = (_('Folder'), _('Name'), _('Size (KB)'), _('Type')) + CATEGORY_NAMES = { + 'image':_('Image'), + 'text': _('Text'), + 'font': _('Font'), + 'style': _('Style'), + 'opf': _('Metadata'), + 'toc': _('Table of Contents'), + } + + def __init__(self, parent=None): + FileCollection.__init__(self, parent) + self.images_size = self.fonts_size = 0 + + def __call__(self, data): + self.beginResetModel() + self.files = data['files'] + self.total_size = sum(map(itemgetter(3), self.files)) + self.images_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'image'))) + self.fonts_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'font'))) + psk = numeric_sort_key + self.sort_keys = tuple((psk(entry.dir), psk(entry.basename), entry.size, psk(self.CATEGORY_NAMES.get(entry.category, ''))) + for entry in self.files) + self.endResetModel() + def data(self, index, role=Qt.DisplayRole): if role == Qt.DisplayRole: col = index.column() @@ -140,7 +148,7 @@ class FilesModel(QAbstractTableModel): class FilesWidget(QWidget): - edit_requested = pyqtSignal(object, object) + edit_requested = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -175,19 +183,83 @@ class FilesWidget(QWidget): human_readable, (m.total_size, m.images_size, m.fonts_size)))) def double_clicked(self, index): - name = self.model.name(self.proxy.mapToSource(index)) - if name is not None: - self.edit_requested.emit(name, None) + location = self.model.location(self.proxy.mapToSource(index)) + if location is not None: + self.edit_requested.emit(location) def save(self): save_state('all-files-table', bytearray(self.files.horizontalHeader().saveState())) # }}} +class ImagesModel(QAbstractTableModel): + + COLUMN_HEADERS = [_('Name'), _('Size (KB)'), _('Times used'), _('Width'), _('Height'), _('Image')] + + def __init__(self, parent=None): + self.files = self.sort_keys = () + self.total_size = 0 + QAbstractTableModel.__init__(self, parent) + + def columnCount(self, parent=None): + return len(self.COLUMN_HEADERS) + + def rowCount(self, parent=None): + return len(self.files) + + def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + try: + return self.COLUMN_HEADERS[section] + except IndexError: + pass + return QAbstractTableModel.headerData(self, section, orientation, role) + + def __call__(self, data): + self.beginResetModel() + self.files = data['files'] + self.total_size = sum(map(itemgetter(3), self.files)) + self.images_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'image'))) + self.fonts_size = sum(map(itemgetter(3), (f for f in self.files if f.category == 'font'))) + psk = numeric_sort_key + self.sort_keys = tuple((psk(entry.dir), psk(entry.basename), entry.size, psk(self.CATEGORY_NAMES.get(entry.category, ''))) + for entry in self.files) + self.endResetModel() + + def sort_key(self, row, col): + try: + return self.sort_keys[row][col] + except IndexError: + pass + + def name(self, index): + try: + return self.files[index.row()].name + except IndexError: + pass + + def data(self, index, role=Qt.DisplayRole): + if role == Qt.DisplayRole: + col = index.column() + try: + entry = self.files[index.row()] + except IndexError: + return None + if col == 0: + return entry.dir + if col == 1: + return entry.basename + if col == 2: + sz = entry.size / 1024. + return ('%.2f' % sz if int(sz) != sz else type('')(sz)) + if col == 3: + return self.CATEGORY_NAMES.get(entry.category) + + # Wrapper UI {{{ class ReportsWidget(QWidget): - edit_requested = pyqtSignal(object, object) + edit_requested = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -226,7 +298,7 @@ class ReportsWidget(QWidget): class Reports(Dialog): data_gathered = pyqtSignal(object, object) - edit_requested = pyqtSignal(object, object) + edit_requested = pyqtSignal(object) def __init__(self, parent=None): Dialog.__init__(self, _('Reports'), 'reports-dialog', parent=parent) @@ -271,6 +343,7 @@ class Reports(Dialog): ok, data = True, gather_data(current_container()) except Exception: import traceback + traceback.print_exc() ok, data = False, traceback.format_exc() self.data_gathered.emit(ok, data)