mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Read image data for reports and refactor file model class to make it more re-useable
This commit is contained in:
parent
a7505aa324
commit
260d11e79b
@ -7,9 +7,11 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import posixpath, os
|
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.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')
|
File = namedtuple('File', 'name dir basename size category')
|
||||||
|
|
||||||
@ -39,11 +41,51 @@ def safe_size(container, name):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
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):
|
def file_data(container):
|
||||||
for name, path in container.name_path_map.iteritems():
|
for name, path in container.name_path_map.iteritems():
|
||||||
yield File(name, posixpath.dirname(name), posixpath.basename(name), safe_size(container, name),
|
yield File(name, posixpath.dirname(name), posixpath.basename(name), safe_size(container, name),
|
||||||
get_category(name, container.mime_map.get(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):
|
def gather_data(container):
|
||||||
data = {'files':tuple(file_data(container))}
|
data = {'files':tuple(file_data(container))}
|
||||||
|
img_data = link_data(container)
|
||||||
|
data['images'] = img_data
|
||||||
return data
|
return data
|
||||||
|
@ -129,7 +129,7 @@ class Boss(QObject):
|
|||||||
self.gui.manage_fonts.container_changed.connect(self.apply_container_update_to_gui)
|
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.embed_all_fonts.connect(self.manage_fonts_embed)
|
||||||
self.gui.manage_fonts.subset_all_fonts.connect(self.manage_fonts_subset)
|
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
|
@property
|
||||||
def currently_editing(self):
|
def currently_editing(self):
|
||||||
@ -1157,10 +1157,11 @@ class Boss(QObject):
|
|||||||
self.gui.reports.show()
|
self.gui.reports.show()
|
||||||
self.gui.reports.raise_()
|
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))
|
mt = current_container().mime_map.get(name, guess_type(name))
|
||||||
editor = self.edit_file_requested(name, None, mt)
|
editor = self.edit_file_requested(name, None, mt)
|
||||||
if editor is None and location is not None:
|
if editor is not None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def image_activated(self, name):
|
def image_activated(self, name):
|
||||||
|
@ -16,12 +16,12 @@ from PyQt5.Qt import (
|
|||||||
QListWidgetItem, QLineEdit, QStackedWidget, QSplitter, QByteArray)
|
QListWidgetItem, QLineEdit, QStackedWidget, QSplitter, QByteArray)
|
||||||
|
|
||||||
from calibre import human_readable
|
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 import error_dialog
|
||||||
from calibre.gui2.tweak_book import current_container, tprefs
|
from calibre.gui2.tweak_book import current_container, tprefs
|
||||||
from calibre.gui2.tweak_book.widgets import Dialog
|
from calibre.gui2.tweak_book.widgets import Dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
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):
|
def read_state(name, default=None):
|
||||||
data = tprefs.get('reports-ui-state')
|
data = tprefs.get('reports-ui-state')
|
||||||
@ -66,22 +66,13 @@ class ProxyModel(QSortFilterProxyModel): # {{{
|
|||||||
return QSortFilterProxyModel.headerData(self, section, orientation, role)
|
return QSortFilterProxyModel.headerData(self, section, orientation, role)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Files {{{
|
class FileCollection(QAbstractTableModel):
|
||||||
class FilesModel(QAbstractTableModel):
|
|
||||||
|
|
||||||
COLUMN_HEADERS = [_('Folder'), _('Name'), _('Size (KB)'), _('Type')]
|
COLUMN_HEADERS = ()
|
||||||
CATEGORY_NAMES = {
|
|
||||||
'image':_('Image'),
|
|
||||||
'text': _('Text'),
|
|
||||||
'font': _('Font'),
|
|
||||||
'style': _('Style'),
|
|
||||||
'opf': _('Metadata'),
|
|
||||||
'toc': _('Table of Contents'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
self.files = self.sort_keys = ()
|
self.files = self.sort_keys = ()
|
||||||
self.total_size = self.images_size = self.fonts_size = 0
|
self.total_size = 0
|
||||||
QAbstractTableModel.__init__(self, parent)
|
QAbstractTableModel.__init__(self, parent)
|
||||||
|
|
||||||
def columnCount(self, parent=None):
|
def columnCount(self, parent=None):
|
||||||
@ -98,29 +89,46 @@ class FilesModel(QAbstractTableModel):
|
|||||||
pass
|
pass
|
||||||
return QAbstractTableModel.headerData(self, section, orientation, role)
|
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):
|
def sort_key(self, row, col):
|
||||||
try:
|
try:
|
||||||
return self.sort_keys[row][col]
|
return self.sort_keys[row][col]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def name(self, index):
|
def location(self, index):
|
||||||
try:
|
try:
|
||||||
return self.files[index.row()].name
|
return Location(self.files[index.row()].name)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
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):
|
def data(self, index, role=Qt.DisplayRole):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
col = index.column()
|
col = index.column()
|
||||||
@ -140,7 +148,7 @@ class FilesModel(QAbstractTableModel):
|
|||||||
|
|
||||||
class FilesWidget(QWidget):
|
class FilesWidget(QWidget):
|
||||||
|
|
||||||
edit_requested = pyqtSignal(object, object)
|
edit_requested = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
@ -175,19 +183,83 @@ class FilesWidget(QWidget):
|
|||||||
human_readable, (m.total_size, m.images_size, m.fonts_size))))
|
human_readable, (m.total_size, m.images_size, m.fonts_size))))
|
||||||
|
|
||||||
def double_clicked(self, index):
|
def double_clicked(self, index):
|
||||||
name = self.model.name(self.proxy.mapToSource(index))
|
location = self.model.location(self.proxy.mapToSource(index))
|
||||||
if name is not None:
|
if location is not None:
|
||||||
self.edit_requested.emit(name, None)
|
self.edit_requested.emit(location)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
save_state('all-files-table', bytearray(self.files.horizontalHeader().saveState()))
|
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 {{{
|
# Wrapper UI {{{
|
||||||
class ReportsWidget(QWidget):
|
class ReportsWidget(QWidget):
|
||||||
|
|
||||||
edit_requested = pyqtSignal(object, object)
|
edit_requested = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QWidget.__init__(self, parent)
|
QWidget.__init__(self, parent)
|
||||||
@ -226,7 +298,7 @@ class ReportsWidget(QWidget):
|
|||||||
class Reports(Dialog):
|
class Reports(Dialog):
|
||||||
|
|
||||||
data_gathered = pyqtSignal(object, object)
|
data_gathered = pyqtSignal(object, object)
|
||||||
edit_requested = pyqtSignal(object, object)
|
edit_requested = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
Dialog.__init__(self, _('Reports'), 'reports-dialog', parent=parent)
|
Dialog.__init__(self, _('Reports'), 'reports-dialog', parent=parent)
|
||||||
@ -271,6 +343,7 @@ class Reports(Dialog):
|
|||||||
ok, data = True, gather_data(current_container())
|
ok, data = True, gather_data(current_container())
|
||||||
except Exception:
|
except Exception:
|
||||||
import traceback
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
ok, data = False, traceback.format_exc()
|
ok, data = False, traceback.format_exc()
|
||||||
self.data_gathered.emit(ok, data)
|
self.data_gathered.emit(ok, data)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user