mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Edit metadata: When setting a cover from comic files allow choosing which page to use as the cover. Fixes #2007765 [Set cover from format: Image selection for comic files](https://bugs.launchpad.net/calibre/+bug/2007765)
This commit is contained in:
parent
a86b88c634
commit
7e8dc39ff8
@ -57,7 +57,7 @@ def find_pages(dir_or_items, sort_on_mtime=False, verbose=False):
|
||||
:param sort_on_mtime: If True sort pages based on their last modified time.
|
||||
Otherwise, sort alphabetically.
|
||||
'''
|
||||
extensions = {'jpeg', 'jpg', 'gif', 'png', 'webp'}
|
||||
from calibre.libunzip import comic_exts
|
||||
items = generate_entries_from_dir(dir_or_items) if isinstance(dir_or_items, str) else dir_or_items
|
||||
sep_counts = set()
|
||||
pages = []
|
||||
@ -65,8 +65,8 @@ def find_pages(dir_or_items, sort_on_mtime=False, verbose=False):
|
||||
if '__MACOSX' in path:
|
||||
continue
|
||||
ext = path.rpartition('.')[2].lower()
|
||||
if ext in extensions:
|
||||
sep_counts.add(path.replace(os.sep, '/').count('/'))
|
||||
if ext in comic_exts:
|
||||
sep_counts.add(path.replace('\\', '/').count('/'))
|
||||
pages.append(path)
|
||||
# Use the full path to sort unless the files are in folders of different
|
||||
# levels, in which case simply use the filenames.
|
||||
|
@ -199,3 +199,60 @@ def get_comic_metadata(stream, stream_type, series_index='volume'):
|
||||
comment = get_comment(stream)
|
||||
|
||||
return parse_comic_comment(comment or b'{}', series_index=series_index)
|
||||
|
||||
|
||||
def get_comic_images(path, tdir, first=1, last=0): # first and last use 1 based indexing
|
||||
from functools import partial
|
||||
with open(path, 'rb') as f:
|
||||
fmt = archive_type(f)
|
||||
if fmt not in ('zip', 'rar'):
|
||||
return 0
|
||||
items = {}
|
||||
if fmt == 'rar':
|
||||
from calibre.utils.unrar import headers
|
||||
for h in headers(path):
|
||||
items[h['filename']] = lambda : partial(h.get, 'file_time', 0)
|
||||
else:
|
||||
from zipfile import ZipFile
|
||||
with ZipFile(path) as zf:
|
||||
for i in zf.infolist():
|
||||
items[i.filename] = partial(getattr, i, 'date_time')
|
||||
from calibre.ebooks.comic.input import find_pages
|
||||
pages = find_pages(items)
|
||||
if last <= 0:
|
||||
last = len(pages)
|
||||
pages = pages[first-1:last]
|
||||
|
||||
def make_filename(num, ext):
|
||||
return f'{num:08d}{ext}'
|
||||
|
||||
if fmt == 'rar':
|
||||
all_pages = {p:i+first for i, p in enumerate(pages)}
|
||||
from calibre.utils.unrar import extract_members
|
||||
current = None
|
||||
def callback(x):
|
||||
nonlocal current
|
||||
if isinstance(x, dict):
|
||||
if current is not None:
|
||||
current.close()
|
||||
fname = x['filename']
|
||||
if fname in all_pages:
|
||||
ext = os.path.splitext(fname)[1]
|
||||
num = all_pages[fname]
|
||||
current = open(os.path.join(tdir, make_filename(num, ext)), 'wb')
|
||||
return True
|
||||
return False
|
||||
if isinstance(x, bytes):
|
||||
current.write(x)
|
||||
extract_members(path, callback)
|
||||
if current is not None:
|
||||
current.close()
|
||||
else:
|
||||
import shutil
|
||||
with ZipFile(path) as zf:
|
||||
for i, name in enumerate(pages):
|
||||
num = i + first
|
||||
ext = os.path.splitext(name)[1]
|
||||
with open(os.path.join(tdir, make_filename(num, ext)), 'wb') as dest, zf.open(name) as src:
|
||||
shutil.copyfileobj(src, dest)
|
||||
return len(pages)
|
||||
|
@ -983,13 +983,13 @@ class EditMetadataAction(InterfaceAction):
|
||||
fmt = fmt.lower()
|
||||
cdata = None
|
||||
db = self.gui.current_db.new_api
|
||||
if fmt == 'pdf':
|
||||
pdfpath = db.format_abspath(book_id, fmt)
|
||||
if pdfpath is None:
|
||||
if fmt in ('pdf', 'cbz', 'cbr'):
|
||||
path = db.format_abspath(book_id, fmt)
|
||||
if path is None:
|
||||
return error_dialog(self.gui, _('Format file missing'), _(
|
||||
'Cannot read cover as the %s file is missing from this book') % 'PDF', show=True)
|
||||
'Cannot read cover as the %s file is missing from this book') % fmt.upper(), show=True)
|
||||
from calibre.gui2.metadata.pdf_covers import PDFCovers
|
||||
d = PDFCovers(pdfpath, parent=self.gui)
|
||||
d = PDFCovers(path, parent=self.gui)
|
||||
ret = d.exec()
|
||||
if ret == QDialog.DialogCode.Accepted:
|
||||
cpath = d.cover_path
|
||||
|
@ -5,20 +5,23 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, shutil, os
|
||||
from threading import Thread
|
||||
from glob import glob
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from qt.core import (
|
||||
QDialog, QApplication, QLabel, QVBoxLayout, QDialogButtonBox, Qt, QAbstractItemView, QListView,
|
||||
pyqtSignal, QListWidget, QListWidgetItem, QSize, QPixmap, QStyledItemDelegate, sip
|
||||
QAbstractItemView, QApplication, QDialog, QDialogButtonBox, QLabel, QListView,
|
||||
QListWidget, QListWidgetItem, QPixmap, QSize, QStyledItemDelegate, Qt, QTimer,
|
||||
QVBoxLayout, pyqtSignal, sip,
|
||||
)
|
||||
from threading import Thread
|
||||
|
||||
from calibre import as_unicode
|
||||
from calibre.ebooks.metadata.archive import get_comic_images
|
||||
from calibre.ebooks.metadata.pdf import page_images
|
||||
from calibre.gui2 import error_dialog, file_icon_provider
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.gui2.progress_indicator import WaitLayout
|
||||
from calibre.libunzip import comic_exts
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
|
||||
|
||||
class CoverDelegate(QStyledItemDelegate):
|
||||
@ -42,11 +45,13 @@ class PDFCovers(QDialog):
|
||||
def __init__(self, pdfpath, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.pdfpath = pdfpath
|
||||
self.stack = WaitLayout(_('Rendering PDF pages, please wait...'), parent=self)
|
||||
self.ext = os.path.splitext(pdfpath)[1][1:].lower()
|
||||
self.is_pdf = self.ext == 'pdf'
|
||||
self.stack = WaitLayout(_('Rendering {} pages, please wait...').format('PDF' if self.is_pdf else _('comic book')), parent=self)
|
||||
self.container = self.stack.after
|
||||
|
||||
self.container.l = l = QVBoxLayout(self.container)
|
||||
self.la = la = QLabel(_('Choose a cover from the list of PDF pages below'))
|
||||
self.la = la = QLabel(_('Choose a cover from the list of pages below'))
|
||||
l.addWidget(la)
|
||||
self.covers = c = QListWidget(self)
|
||||
l.addWidget(c)
|
||||
@ -67,16 +72,15 @@ class PDFCovers(QDialog):
|
||||
l.addWidget(bb)
|
||||
self.rendering_done.connect(self.show_pages, type=Qt.ConnectionType.QueuedConnection)
|
||||
self.first = 1
|
||||
self.setWindowTitle(_('Choose cover from PDF'))
|
||||
self.setWindowIcon(file_icon_provider().icon_from_ext('pdf'))
|
||||
self.setWindowTitle(_('Choose cover from book'))
|
||||
self.setWindowIcon(file_icon_provider().icon_from_ext(self.ext))
|
||||
self.resize(QSize(800, 600))
|
||||
self.tdir = PersistentTemporaryDirectory('_pdf_covers')
|
||||
self.start_rendering()
|
||||
QTimer.singleShot(0, self.start_rendering)
|
||||
|
||||
def start_rendering(self):
|
||||
self.hide_pages()
|
||||
self.thread = Thread(target=self.render)
|
||||
self.thread.daemon = True
|
||||
self.thread = Thread(target=self.render, daemon=True, name='RenderPages')
|
||||
self.thread.start()
|
||||
|
||||
@property
|
||||
@ -97,11 +101,14 @@ class PDFCovers(QDialog):
|
||||
self.error = None
|
||||
try:
|
||||
os.mkdir(self.current_tdir)
|
||||
page_images(self.pdfpath, self.current_tdir, first=self.first, last=self.first + PAGES_PER_RENDER - 1)
|
||||
except Exception as e:
|
||||
if self.covers.count():
|
||||
pass
|
||||
if self.is_pdf:
|
||||
page_images(self.pdfpath, self.current_tdir, first=self.first, last=self.first + PAGES_PER_RENDER - 1)
|
||||
else:
|
||||
get_comic_images(self.pdfpath, self.current_tdir, first=self.first, last=self.first + PAGES_PER_RENDER - 1)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if not self.covers.count():
|
||||
self.error = as_unicode(e)
|
||||
if not sip.isdeleted(self) and self.isVisible():
|
||||
self.rendering_done.emit()
|
||||
@ -113,14 +120,14 @@ class PDFCovers(QDialog):
|
||||
def show_pages(self):
|
||||
if self.error is not None:
|
||||
error_dialog(self, _('Failed to render'),
|
||||
_('Could not render this PDF file'), show=True, det_msg=self.error)
|
||||
_('Could not render this file'), show=True, det_msg=self.error)
|
||||
self.reject()
|
||||
return
|
||||
self.stack.stop()
|
||||
files = glob(os.path.join(self.current_tdir, '*.jpg')) + glob(os.path.join(self.current_tdir, '*.jpeg'))
|
||||
files = tuple(x for x in os.listdir(self.current_tdir) if os.path.splitext(x)[1][1:].lower() in comic_exts)
|
||||
if not files and not self.covers.count():
|
||||
error_dialog(self, _('Failed to render'),
|
||||
_('This PDF has no pages'), show=True)
|
||||
_('This book has no pages'), show=True)
|
||||
self.reject()
|
||||
return
|
||||
|
||||
@ -130,13 +137,14 @@ class PDFCovers(QDialog):
|
||||
dpr = self.devicePixelRatio()
|
||||
|
||||
for i, f in enumerate(sorted(files)):
|
||||
p = QPixmap(f).scaled(
|
||||
path = os.path.join(self.current_tdir, f)
|
||||
p = QPixmap(path).scaled(
|
||||
self.covers.iconSize()*dpr, aspectRatioMode=Qt.AspectRatioMode.IgnoreAspectRatio,
|
||||
transformMode=Qt.TransformationMode.SmoothTransformation)
|
||||
p.setDevicePixelRatio(dpr)
|
||||
i = QListWidgetItem(_('page %d') % (self.first + i))
|
||||
i.setData(Qt.ItemDataRole.DecorationRole, p)
|
||||
i.setData(Qt.ItemDataRole.UserRole, f)
|
||||
i.setData(Qt.ItemDataRole.UserRole, path)
|
||||
self.covers.addItem(i)
|
||||
self.first += len(files)
|
||||
if len(files) == PAGES_PER_RENDER:
|
||||
|
@ -415,24 +415,23 @@ class MetadataSingleDialogBase(QDialog):
|
||||
if mi is not None:
|
||||
self.update_from_mi(mi)
|
||||
|
||||
def get_pdf_cover(self):
|
||||
pdfpath = self.formats_manager.get_format_path(self.db, self.book_id,
|
||||
'pdf')
|
||||
def choose_cover_from_pages(self, ext):
|
||||
path = self.formats_manager.get_format_path(self.db, self.book_id, ext.lower())
|
||||
from calibre.gui2.metadata.pdf_covers import PDFCovers
|
||||
d = PDFCovers(pdfpath, parent=self)
|
||||
d = PDFCovers(path, parent=self)
|
||||
if d.exec() == QDialog.DialogCode.Accepted:
|
||||
cpath = d.cover_path
|
||||
if cpath:
|
||||
with open(cpath, 'rb') as f:
|
||||
self.update_cover(f.read(), 'PDF')
|
||||
self.update_cover(f.read(), ext.upper())
|
||||
d.cleanup()
|
||||
|
||||
def cover_from_format(self, *args):
|
||||
ext = self.formats_manager.get_selected_format()
|
||||
if ext is None:
|
||||
return
|
||||
if ext == 'pdf':
|
||||
return self.get_pdf_cover()
|
||||
if ext in ('pdf', 'cbz', 'cbr'):
|
||||
return self.choose_cover_from_pages(ext)
|
||||
try:
|
||||
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
||||
self.book_id)
|
||||
|
@ -94,6 +94,12 @@ def extract_member(
|
||||
return name, data
|
||||
|
||||
|
||||
def extract_members(path_or_stream, callback):
|
||||
from unrardll import extract_members
|
||||
with StreamAsPath(path_or_stream) as path:
|
||||
extract_members(path, callback)
|
||||
|
||||
|
||||
def extract_first_alphabetically(stream):
|
||||
from calibre.libunzip import sort_key
|
||||
names_ = sorted((
|
||||
|
Loading…
x
Reference in New Issue
Block a user