mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
285 lines
11 KiB
Python
285 lines
11 KiB
Python
#!/usr/bin/env python2
|
|
# vim:fileencoding=utf-8
|
|
from __future__ import (unicode_literals, division, absolute_import,
|
|
print_function)
|
|
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
|
|
import re
|
|
from threading import Thread
|
|
|
|
from PyQt5.Qt import (
|
|
QTextBrowser, QVBoxLayout, QDialog, QDialogButtonBox, QIcon, QLabel,
|
|
QCheckBox, Qt, QListWidgetItem, QHBoxLayout, QListWidget, QPixmap,
|
|
QSpinBox, QStyledItemDelegate, QSize, QStyle, QPen,
|
|
QProgressBar, pyqtSignal, QApplication
|
|
)
|
|
|
|
from calibre import human_readable, fit_image, force_unicode
|
|
from calibre.ebooks.oeb.polish.main import CUSTOMIZATION
|
|
from calibre.gui2 import empty_index
|
|
from calibre.gui2.tweak_book import tprefs, current_container, set_current_container
|
|
from calibre.gui2.tweak_book.widgets import Dialog
|
|
from calibre.utils.icu import numeric_sort_key
|
|
|
|
|
|
class Abort(Exception):
|
|
pass
|
|
|
|
|
|
def customize_remove_unused_css(name, parent, ans):
|
|
d = QDialog(parent)
|
|
d.l = l = QVBoxLayout()
|
|
d.setLayout(d.l)
|
|
d.setWindowTitle(_('Remove unused CSS'))
|
|
|
|
def label(text):
|
|
la = QLabel(text)
|
|
la.setWordWrap(True), l.addWidget(la), la.setMinimumWidth(450)
|
|
l.addWidget(la)
|
|
return la
|
|
|
|
d.la = label(_(
|
|
'This will remove all CSS rules that do not match any actual content.'
|
|
' There are a couple of additional cleanups you can enable, below:'))
|
|
d.c = c = QCheckBox(_('Remove unused &class attributes'))
|
|
c.setChecked(tprefs['remove_unused_classes'])
|
|
l.addWidget(c)
|
|
d.la2 = label('<span style="font-size:small; font-style: italic">' + _(
|
|
'Remove all class attributes from the HTML that do not match any existing CSS rules'))
|
|
d.m = m = QCheckBox(_('Merge identical CSS rules'))
|
|
m.setChecked(tprefs['merge_identical_selectors'])
|
|
l.addWidget(m)
|
|
d.la3 = label('<span style="font-size:small; font-style: italic">' + _(
|
|
'Merge CSS rules in the same stylesheet that have identical selectors.'
|
|
' Note that in rare cases merging can result in a change to the effective styling'
|
|
' of the book, so use with care.'))
|
|
d.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
d.l.addWidget(d.bb)
|
|
d.bb.rejected.connect(d.reject)
|
|
d.bb.accepted.connect(d.accept)
|
|
if d.exec_() != d.Accepted:
|
|
raise Abort()
|
|
ans['remove_unused_classes'] = tprefs['remove_unused_classes'] = c.isChecked()
|
|
ans['merge_identical_selectors'] = tprefs['merge_identical_selectors'] = m.isChecked()
|
|
|
|
|
|
def get_customization(action, name, parent):
|
|
ans = CUSTOMIZATION.copy()
|
|
try:
|
|
if action == 'remove_unused_css':
|
|
customize_remove_unused_css(name, parent, ans)
|
|
except Abort:
|
|
return None
|
|
return ans
|
|
|
|
|
|
def format_report(title, report):
|
|
from calibre.ebooks.markdown import markdown
|
|
report = [force_unicode(line) for line in report]
|
|
return markdown('# %s\n\n'%force_unicode(title) + '\n\n'.join(report), output_format='html4')
|
|
|
|
|
|
def show_report(changed, title, report, parent, show_current_diff):
|
|
report = format_report(title, report)
|
|
d = QDialog(parent)
|
|
d.setWindowTitle(_('Action report'))
|
|
d.l = QVBoxLayout()
|
|
d.setLayout(d.l)
|
|
d.e = QTextBrowser(d)
|
|
d.l.addWidget(d.e)
|
|
d.e.setHtml(report)
|
|
d.bb = QDialogButtonBox(QDialogButtonBox.Close)
|
|
d.show_changes = False
|
|
if changed:
|
|
b = d.b = d.bb.addButton(_('See what &changed'), d.bb.AcceptRole)
|
|
b.setIcon(QIcon(I('diff.png'))), b.setAutoDefault(False)
|
|
connect_lambda(b.clicked, d, lambda d: setattr(d, 'show_changes', True))
|
|
b = d.bb.addButton(_('&Copy to clipboard'), d.bb.ActionRole)
|
|
b.setIcon(QIcon(I('edit-copy.png'))), b.setAutoDefault(False)
|
|
|
|
def copy_report():
|
|
text = re.sub(r'</.+?>', '\n', report)
|
|
text = re.sub(r'<.+?>', '', text)
|
|
cp = QApplication.instance().clipboard()
|
|
cp.setText(text)
|
|
|
|
b.clicked.connect(copy_report)
|
|
d.bb.button(d.bb.Close).setDefault(True)
|
|
d.l.addWidget(d.bb)
|
|
d.bb.rejected.connect(d.reject)
|
|
d.bb.accepted.connect(d.accept)
|
|
d.resize(600, 400)
|
|
d.exec_()
|
|
b.clicked.disconnect()
|
|
if d.show_changes:
|
|
show_current_diff(allow_revert=True)
|
|
|
|
# CompressImages {{{
|
|
|
|
|
|
class ImageItemDelegate(QStyledItemDelegate):
|
|
|
|
def sizeHint(self, option, index):
|
|
return QSize(300, 100)
|
|
|
|
def paint(self, painter, option, index):
|
|
name = index.data(Qt.DisplayRole)
|
|
sz = human_readable(index.data(Qt.UserRole))
|
|
pmap = index.data(Qt.UserRole+1)
|
|
irect = option.rect.adjusted(0, 5, 0, -5)
|
|
irect.setRight(irect.left() + 70)
|
|
if pmap is None:
|
|
pmap = QPixmap(current_container().get_file_path_for_processing(name))
|
|
scaled, nwidth, nheight = fit_image(pmap.width(), pmap.height(), irect.width(), irect.height())
|
|
if scaled:
|
|
pmap = pmap.scaled(nwidth, nheight, transformMode=Qt.SmoothTransformation)
|
|
index.model().setData(index, pmap, Qt.UserRole+1)
|
|
x, y = (irect.width() - pmap.width())//2, (irect.height() - pmap.height())//2
|
|
r = irect.adjusted(x, y, -x, -y)
|
|
QStyledItemDelegate.paint(self, painter, option, empty_index)
|
|
painter.drawPixmap(r, pmap)
|
|
trect = irect.adjusted(irect.width() + 10, 0, 0, 0)
|
|
trect.setRight(option.rect.right())
|
|
painter.save()
|
|
if option.state & QStyle.State_Selected:
|
|
painter.setPen(QPen(option.palette.color(option.palette.HighlightedText)))
|
|
painter.drawText(trect, Qt.AlignVCenter | Qt.AlignLeft, name + '\n' + sz)
|
|
painter.restore()
|
|
|
|
|
|
class CompressImages(Dialog):
|
|
|
|
def __init__(self, parent=None):
|
|
Dialog.__init__(self, _('Compress images'), 'compress-images', parent=parent)
|
|
|
|
def setup_ui(self):
|
|
from calibre.ebooks.oeb.polish.images import get_compressible_images
|
|
self.setWindowIcon(QIcon(I('compress-image.png')))
|
|
self.h = h = QHBoxLayout(self)
|
|
self.images = i = QListWidget(self)
|
|
h.addWidget(i)
|
|
self.l = l = QVBoxLayout()
|
|
h.addLayout(l)
|
|
c = current_container()
|
|
for name in sorted(get_compressible_images(c), key=numeric_sort_key):
|
|
x = QListWidgetItem(name, i)
|
|
x.setData(Qt.UserRole, c.filesize(name))
|
|
i.setSelectionMode(i.ExtendedSelection)
|
|
i.setMinimumHeight(500), i.setMinimumWidth(350)
|
|
i.selectAll(), i.setSpacing(5)
|
|
self.delegate = ImageItemDelegate(self)
|
|
i.setItemDelegate(self.delegate)
|
|
self.la = la = QLabel(_(
|
|
'You can compress the images in this book losslessly, reducing the file size of the book,'
|
|
' without affecting image quality. Typically image size is reduced by 5 - 15%.'))
|
|
la.setWordWrap(True)
|
|
la.setMinimumWidth(250)
|
|
l.addWidget(la), l.addSpacing(30)
|
|
|
|
self.enable_lossy = el = QCheckBox(_('Enable &lossy compression of JPEG images'))
|
|
el.setToolTip(_('This allows you to change the quality factor used for JPEG images.\nBy lowering'
|
|
' the quality you can greatly reduce file size, at the expense of the image looking blurred.'))
|
|
l.addWidget(el)
|
|
self.h2 = h = QHBoxLayout()
|
|
l.addLayout(h)
|
|
self.jq = jq = QSpinBox(self)
|
|
jq.setMinimum(0), jq.setMaximum(100), jq.setValue(80), jq.setEnabled(False)
|
|
jq.setToolTip(_('The compression quality, 1 is high compression, 100 is low compression.\nImage'
|
|
' quality is inversely correlated with compression quality.'))
|
|
el.toggled.connect(jq.setEnabled)
|
|
self.jql = la = QLabel(_('Compression &quality:'))
|
|
la.setBuddy(jq)
|
|
h.addWidget(la), h.addWidget(jq)
|
|
l.addStretch(10)
|
|
l.addWidget(self.bb)
|
|
|
|
@property
|
|
def names(self):
|
|
return {item.text() for item in self.images.selectedItems()}
|
|
|
|
@property
|
|
def jpeg_quality(self):
|
|
if not self.enable_lossy.isChecked():
|
|
return None
|
|
return self.jq.value()
|
|
|
|
|
|
class CompressImagesProgress(Dialog):
|
|
|
|
gui_loop = pyqtSignal(object, object, object)
|
|
cidone = pyqtSignal()
|
|
|
|
def __init__(self, names=None, jpeg_quality=None, parent=None):
|
|
self.names, self.jpeg_quality = names, jpeg_quality
|
|
self.keep_going = True
|
|
self.result = (None, '')
|
|
Dialog.__init__(self, _('Compressing images...'), 'compress-images-progress', parent=parent)
|
|
self.gui_loop.connect(self.update_progress, type=Qt.QueuedConnection)
|
|
self.cidone.connect(self.accept, type=Qt.QueuedConnection)
|
|
t = Thread(name='RunCompressImages', target=self.run_compress)
|
|
t.daemon = True
|
|
t.start()
|
|
|
|
def run_compress(self):
|
|
from calibre.gui2.tweak_book import current_container
|
|
from calibre.ebooks.oeb.polish.images import compress_images
|
|
report = []
|
|
try:
|
|
self.result = (compress_images(
|
|
current_container(), report=report.append, names=self.names, jpeg_quality=self.jpeg_quality,
|
|
progress_callback=self.progress_callback
|
|
)[0], report)
|
|
except Exception:
|
|
import traceback
|
|
self.result = (None, traceback.format_exc())
|
|
self.cidone.emit()
|
|
|
|
def setup_ui(self):
|
|
self.setWindowIcon(QIcon(I('compress-image.png')))
|
|
self.setCursor(Qt.BusyCursor)
|
|
self.setMinimumWidth(350)
|
|
self.l = l = QVBoxLayout(self)
|
|
self.la = la = QLabel(_('Compressing images, please wait...'))
|
|
la.setStyleSheet('QLabel { font-weight: bold }'), la.setAlignment(Qt.AlignCenter), la.setTextFormat(Qt.PlainText)
|
|
l.addWidget(la)
|
|
self.progress = p = QProgressBar(self)
|
|
p.setMinimum(0), p.setMaximum(0)
|
|
l.addWidget(p)
|
|
self.msg = la = QLabel('\xa0')
|
|
la.setAlignment(Qt.AlignCenter), la.setTextFormat(Qt.PlainText)
|
|
l.addWidget(la)
|
|
|
|
self.bb.setStandardButtons(self.bb.Cancel)
|
|
l.addWidget(self.bb)
|
|
|
|
def reject(self):
|
|
self.keep_going = False
|
|
self.bb.button(self.bb.Cancel).setEnabled(False)
|
|
Dialog.reject(self)
|
|
|
|
def progress_callback(self, num, total, name):
|
|
self.gui_loop.emit(num, total, name)
|
|
return self.keep_going
|
|
|
|
def update_progress(self, num, total, name):
|
|
self.progress.setMaximum(total), self.progress.setValue(num)
|
|
self.msg.setText(name)
|
|
|
|
# }}}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
from calibre.gui2 import Application
|
|
app = Application([])
|
|
import sys, sip
|
|
from calibre.ebooks.oeb.polish.container import get_container
|
|
c = get_container(sys.argv[-1], tweak_mode=True)
|
|
set_current_container(c)
|
|
d = CompressImages()
|
|
if d.exec_() == d.Accepted:
|
|
pass
|
|
sip.delete(app)
|
|
del app
|