mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54: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.
|
:param sort_on_mtime: If True sort pages based on their last modified time.
|
||||||
Otherwise, sort alphabetically.
|
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
|
items = generate_entries_from_dir(dir_or_items) if isinstance(dir_or_items, str) else dir_or_items
|
||||||
sep_counts = set()
|
sep_counts = set()
|
||||||
pages = []
|
pages = []
|
||||||
@ -65,8 +65,8 @@ def find_pages(dir_or_items, sort_on_mtime=False, verbose=False):
|
|||||||
if '__MACOSX' in path:
|
if '__MACOSX' in path:
|
||||||
continue
|
continue
|
||||||
ext = path.rpartition('.')[2].lower()
|
ext = path.rpartition('.')[2].lower()
|
||||||
if ext in extensions:
|
if ext in comic_exts:
|
||||||
sep_counts.add(path.replace(os.sep, '/').count('/'))
|
sep_counts.add(path.replace('\\', '/').count('/'))
|
||||||
pages.append(path)
|
pages.append(path)
|
||||||
# Use the full path to sort unless the files are in folders of different
|
# Use the full path to sort unless the files are in folders of different
|
||||||
# levels, in which case simply use the filenames.
|
# 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)
|
comment = get_comment(stream)
|
||||||
|
|
||||||
return parse_comic_comment(comment or b'{}', series_index=series_index)
|
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()
|
fmt = fmt.lower()
|
||||||
cdata = None
|
cdata = None
|
||||||
db = self.gui.current_db.new_api
|
db = self.gui.current_db.new_api
|
||||||
if fmt == 'pdf':
|
if fmt in ('pdf', 'cbz', 'cbr'):
|
||||||
pdfpath = db.format_abspath(book_id, fmt)
|
path = db.format_abspath(book_id, fmt)
|
||||||
if pdfpath is None:
|
if path is None:
|
||||||
return error_dialog(self.gui, _('Format file missing'), _(
|
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
|
from calibre.gui2.metadata.pdf_covers import PDFCovers
|
||||||
d = PDFCovers(pdfpath, parent=self.gui)
|
d = PDFCovers(path, parent=self.gui)
|
||||||
ret = d.exec()
|
ret = d.exec()
|
||||||
if ret == QDialog.DialogCode.Accepted:
|
if ret == QDialog.DialogCode.Accepted:
|
||||||
cpath = d.cover_path
|
cpath = d.cover_path
|
||||||
|
@ -5,20 +5,23 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import sys, shutil, os
|
import os
|
||||||
from threading import Thread
|
import shutil
|
||||||
from glob import glob
|
import sys
|
||||||
|
|
||||||
from qt.core import (
|
from qt.core import (
|
||||||
QDialog, QApplication, QLabel, QVBoxLayout, QDialogButtonBox, Qt, QAbstractItemView, QListView,
|
QAbstractItemView, QApplication, QDialog, QDialogButtonBox, QLabel, QListView,
|
||||||
pyqtSignal, QListWidget, QListWidgetItem, QSize, QPixmap, QStyledItemDelegate, sip
|
QListWidget, QListWidgetItem, QPixmap, QSize, QStyledItemDelegate, Qt, QTimer,
|
||||||
|
QVBoxLayout, pyqtSignal, sip,
|
||||||
)
|
)
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from calibre import as_unicode
|
from calibre import as_unicode
|
||||||
|
from calibre.ebooks.metadata.archive import get_comic_images
|
||||||
from calibre.ebooks.metadata.pdf import page_images
|
from calibre.ebooks.metadata.pdf import page_images
|
||||||
from calibre.gui2 import error_dialog, file_icon_provider
|
from calibre.gui2 import error_dialog, file_icon_provider
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
|
||||||
from calibre.gui2.progress_indicator import WaitLayout
|
from calibre.gui2.progress_indicator import WaitLayout
|
||||||
|
from calibre.libunzip import comic_exts
|
||||||
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
class CoverDelegate(QStyledItemDelegate):
|
class CoverDelegate(QStyledItemDelegate):
|
||||||
@ -42,11 +45,13 @@ class PDFCovers(QDialog):
|
|||||||
def __init__(self, pdfpath, parent=None):
|
def __init__(self, pdfpath, parent=None):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.pdfpath = pdfpath
|
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 = self.stack.after
|
||||||
|
|
||||||
self.container.l = l = QVBoxLayout(self.container)
|
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)
|
l.addWidget(la)
|
||||||
self.covers = c = QListWidget(self)
|
self.covers = c = QListWidget(self)
|
||||||
l.addWidget(c)
|
l.addWidget(c)
|
||||||
@ -67,16 +72,15 @@ class PDFCovers(QDialog):
|
|||||||
l.addWidget(bb)
|
l.addWidget(bb)
|
||||||
self.rendering_done.connect(self.show_pages, type=Qt.ConnectionType.QueuedConnection)
|
self.rendering_done.connect(self.show_pages, type=Qt.ConnectionType.QueuedConnection)
|
||||||
self.first = 1
|
self.first = 1
|
||||||
self.setWindowTitle(_('Choose cover from PDF'))
|
self.setWindowTitle(_('Choose cover from book'))
|
||||||
self.setWindowIcon(file_icon_provider().icon_from_ext('pdf'))
|
self.setWindowIcon(file_icon_provider().icon_from_ext(self.ext))
|
||||||
self.resize(QSize(800, 600))
|
self.resize(QSize(800, 600))
|
||||||
self.tdir = PersistentTemporaryDirectory('_pdf_covers')
|
self.tdir = PersistentTemporaryDirectory('_pdf_covers')
|
||||||
self.start_rendering()
|
QTimer.singleShot(0, self.start_rendering)
|
||||||
|
|
||||||
def start_rendering(self):
|
def start_rendering(self):
|
||||||
self.hide_pages()
|
self.hide_pages()
|
||||||
self.thread = Thread(target=self.render)
|
self.thread = Thread(target=self.render, daemon=True, name='RenderPages')
|
||||||
self.thread.daemon = True
|
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -97,11 +101,14 @@ class PDFCovers(QDialog):
|
|||||||
self.error = None
|
self.error = None
|
||||||
try:
|
try:
|
||||||
os.mkdir(self.current_tdir)
|
os.mkdir(self.current_tdir)
|
||||||
|
if self.is_pdf:
|
||||||
page_images(self.pdfpath, self.current_tdir, first=self.first, last=self.first + PAGES_PER_RENDER - 1)
|
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
|
|
||||||
else:
|
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)
|
self.error = as_unicode(e)
|
||||||
if not sip.isdeleted(self) and self.isVisible():
|
if not sip.isdeleted(self) and self.isVisible():
|
||||||
self.rendering_done.emit()
|
self.rendering_done.emit()
|
||||||
@ -113,14 +120,14 @@ class PDFCovers(QDialog):
|
|||||||
def show_pages(self):
|
def show_pages(self):
|
||||||
if self.error is not None:
|
if self.error is not None:
|
||||||
error_dialog(self, _('Failed to render'),
|
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()
|
self.reject()
|
||||||
return
|
return
|
||||||
self.stack.stop()
|
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():
|
if not files and not self.covers.count():
|
||||||
error_dialog(self, _('Failed to render'),
|
error_dialog(self, _('Failed to render'),
|
||||||
_('This PDF has no pages'), show=True)
|
_('This book has no pages'), show=True)
|
||||||
self.reject()
|
self.reject()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -130,13 +137,14 @@ class PDFCovers(QDialog):
|
|||||||
dpr = self.devicePixelRatio()
|
dpr = self.devicePixelRatio()
|
||||||
|
|
||||||
for i, f in enumerate(sorted(files)):
|
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,
|
self.covers.iconSize()*dpr, aspectRatioMode=Qt.AspectRatioMode.IgnoreAspectRatio,
|
||||||
transformMode=Qt.TransformationMode.SmoothTransformation)
|
transformMode=Qt.TransformationMode.SmoothTransformation)
|
||||||
p.setDevicePixelRatio(dpr)
|
p.setDevicePixelRatio(dpr)
|
||||||
i = QListWidgetItem(_('page %d') % (self.first + i))
|
i = QListWidgetItem(_('page %d') % (self.first + i))
|
||||||
i.setData(Qt.ItemDataRole.DecorationRole, p)
|
i.setData(Qt.ItemDataRole.DecorationRole, p)
|
||||||
i.setData(Qt.ItemDataRole.UserRole, f)
|
i.setData(Qt.ItemDataRole.UserRole, path)
|
||||||
self.covers.addItem(i)
|
self.covers.addItem(i)
|
||||||
self.first += len(files)
|
self.first += len(files)
|
||||||
if len(files) == PAGES_PER_RENDER:
|
if len(files) == PAGES_PER_RENDER:
|
||||||
|
@ -415,24 +415,23 @@ class MetadataSingleDialogBase(QDialog):
|
|||||||
if mi is not None:
|
if mi is not None:
|
||||||
self.update_from_mi(mi)
|
self.update_from_mi(mi)
|
||||||
|
|
||||||
def get_pdf_cover(self):
|
def choose_cover_from_pages(self, ext):
|
||||||
pdfpath = self.formats_manager.get_format_path(self.db, self.book_id,
|
path = self.formats_manager.get_format_path(self.db, self.book_id, ext.lower())
|
||||||
'pdf')
|
|
||||||
from calibre.gui2.metadata.pdf_covers import PDFCovers
|
from calibre.gui2.metadata.pdf_covers import PDFCovers
|
||||||
d = PDFCovers(pdfpath, parent=self)
|
d = PDFCovers(path, parent=self)
|
||||||
if d.exec() == QDialog.DialogCode.Accepted:
|
if d.exec() == QDialog.DialogCode.Accepted:
|
||||||
cpath = d.cover_path
|
cpath = d.cover_path
|
||||||
if cpath:
|
if cpath:
|
||||||
with open(cpath, 'rb') as f:
|
with open(cpath, 'rb') as f:
|
||||||
self.update_cover(f.read(), 'PDF')
|
self.update_cover(f.read(), ext.upper())
|
||||||
d.cleanup()
|
d.cleanup()
|
||||||
|
|
||||||
def cover_from_format(self, *args):
|
def cover_from_format(self, *args):
|
||||||
ext = self.formats_manager.get_selected_format()
|
ext = self.formats_manager.get_selected_format()
|
||||||
if ext is None:
|
if ext is None:
|
||||||
return
|
return
|
||||||
if ext == 'pdf':
|
if ext in ('pdf', 'cbz', 'cbr'):
|
||||||
return self.get_pdf_cover()
|
return self.choose_cover_from_pages(ext)
|
||||||
try:
|
try:
|
||||||
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
|
||||||
self.book_id)
|
self.book_id)
|
||||||
|
@ -94,6 +94,12 @@ def extract_member(
|
|||||||
return name, data
|
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):
|
def extract_first_alphabetically(stream):
|
||||||
from calibre.libunzip import sort_key
|
from calibre.libunzip import sort_key
|
||||||
names_ = sorted((
|
names_ = sorted((
|
||||||
|
Loading…
x
Reference in New Issue
Block a user