mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
dbc4860ac5
@ -5,12 +5,14 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
from io import BytesIO
|
||||||
import math
|
import math
|
||||||
import operator
|
import operator
|
||||||
import os
|
import os
|
||||||
import weakref
|
import weakref
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from PIL import Image
|
||||||
from qt.core import (
|
from qt.core import (
|
||||||
QAbstractItemView, QApplication, QBuffer, QByteArray, QColor, QDrag, QEasingCurve,
|
QAbstractItemView, QApplication, QBuffer, QByteArray, QColor, QDrag, QEasingCurve,
|
||||||
QEvent, QFont, QHelpEvent, QIcon, QImage, QIODevice, QItemSelection,
|
QEvent, QFont, QHelpEvent, QIcon, QImage, QIODevice, QItemSelection,
|
||||||
@ -22,18 +24,17 @@ from qt.core import (
|
|||||||
from textwrap import wrap
|
from textwrap import wrap
|
||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
|
|
||||||
from calibre import fit_image, human_readable, prepare_string_for_xml, prints
|
from calibre import fit_image, human_readable, prepare_string_for_xml
|
||||||
from calibre.constants import DEBUG, config_dir, islinux
|
from calibre.constants import DEBUG, config_dir, islinux
|
||||||
from calibre.ebooks.metadata import fmt_sidx, rating_to_stars
|
from calibre.ebooks.metadata import fmt_sidx, rating_to_stars
|
||||||
from calibre.gui2 import (
|
from calibre.gui2 import config, empty_index, gprefs, rating_font
|
||||||
FunctionDispatcher, config, empty_index, gprefs, is_gui_thread, rating_font,
|
|
||||||
)
|
|
||||||
from calibre.gui2.dnd import path_from_qurl
|
from calibre.gui2.dnd import path_from_qurl
|
||||||
from calibre.gui2.gestures import GestureManager
|
from calibre.gui2.gestures import GestureManager
|
||||||
from calibre.gui2.library.caches import CoverCache, ThumbnailCache
|
from calibre.gui2.library.caches import CoverCache, ThumbnailCache
|
||||||
from calibre.gui2.pin_columns import PinContainer
|
from calibre.gui2.pin_columns import PinContainer
|
||||||
from calibre.utils import join_with_timeout
|
from calibre.utils import join_with_timeout
|
||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
|
from calibre.utils.img import convert_PIL_image_to_pixmap
|
||||||
from polyglot.builtins import itervalues
|
from polyglot.builtins import itervalues
|
||||||
from polyglot.queue import LifoQueue
|
from polyglot.queue import LifoQueue
|
||||||
|
|
||||||
@ -86,6 +87,8 @@ def handle_enter_press(self, ev, special_action=None, has_edit_cell=True):
|
|||||||
|
|
||||||
|
|
||||||
def image_to_data(image): # {{{
|
def image_to_data(image): # {{{
|
||||||
|
# Although this function is no longer used in this file, it is used in
|
||||||
|
# other places in calibre. Don't delete it.
|
||||||
ba = QByteArray()
|
ba = QByteArray()
|
||||||
buf = QBuffer(ba)
|
buf = QBuffer(ba)
|
||||||
buf.open(QIODevice.OpenModeFlag.WriteOnly)
|
buf.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||||
@ -718,7 +721,7 @@ CoverTuple = namedtuple('CoverTuple', ['book_id', 'has_cover', 'cache_valid',
|
|||||||
@setup_dnd_interface
|
@setup_dnd_interface
|
||||||
class GridView(QListView):
|
class GridView(QListView):
|
||||||
|
|
||||||
update_item = pyqtSignal(object)
|
update_item = pyqtSignal(object, object)
|
||||||
files_dropped = pyqtSignal(object)
|
files_dropped = pyqtSignal(object)
|
||||||
books_dropped = pyqtSignal(object)
|
books_dropped = pyqtSignal(object)
|
||||||
|
|
||||||
@ -902,7 +905,6 @@ class GridView(QListView):
|
|||||||
def shown(self):
|
def shown(self):
|
||||||
self.update_memory_cover_cache_size()
|
self.update_memory_cover_cache_size()
|
||||||
if self.render_thread is None:
|
if self.render_thread is None:
|
||||||
self.render_cover_dispatcher = FunctionDispatcher(self.render_cover)
|
|
||||||
self.fetch_thread = Thread(target=self.fetch_covers)
|
self.fetch_thread = Thread(target=self.fetch_covers)
|
||||||
self.fetch_thread.daemon = True
|
self.fetch_thread.daemon = True
|
||||||
self.fetch_thread.start()
|
self.fetch_thread.start()
|
||||||
@ -917,22 +919,17 @@ class GridView(QListView):
|
|||||||
if self.ignore_render_requests.is_set():
|
if self.ignore_render_requests.is_set():
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
# Fetch the cover from the cache or file system on this non-
|
# Fetch the cover from the cache or file system
|
||||||
# GUI thread, putting all file system waits outside the GUI
|
|
||||||
cover_tuple = self.fetch_cover_from_cache(book_id)
|
cover_tuple = self.fetch_cover_from_cache(book_id)
|
||||||
|
|
||||||
# Render the cover on the GUI thread. There is no limit on
|
# Render/resize the cover.
|
||||||
# the length of a signal connection queue. Using a
|
thumb = self.make_thumbnail(cover_tuple)
|
||||||
# dispatcher instead of a queued connection prevents
|
|
||||||
# overloading the GUI with paint requests, which could make
|
|
||||||
# performance sluggish.
|
|
||||||
self.render_cover_dispatcher(cover_tuple)
|
|
||||||
|
|
||||||
# Tell the GUI to redisplay the cover. These can queue, but
|
# Tell the GUI to redisplay the cover. These can queue, but
|
||||||
# the work is limited to painting the cover if it is visible
|
# the work is limited to painting the cover if it is visible
|
||||||
# so there shouldn't be much performance lag. Using a
|
# so there shouldn't be much performance lag. Using a
|
||||||
# dispatcher to eliminate the queue would probably be worse.
|
# dispatcher to eliminate the queue would probably be worse.
|
||||||
self.update_item.emit(book_id)
|
self.update_item.emit(book_id, thumb)
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -941,30 +938,40 @@ class GridView(QListView):
|
|||||||
|
|
||||||
def fetch_cover_from_cache(self, book_id):
|
def fetch_cover_from_cache(self, book_id):
|
||||||
'''
|
'''
|
||||||
This method is called on the cover thread, not the GUI thread. It
|
This method fetches the cover from the cache if it exists, otherwise the
|
||||||
returns a namedTuple of cover and cache data:
|
cover.jpg stored in the library.
|
||||||
|
|
||||||
|
It is called on the cover thread.
|
||||||
|
|
||||||
|
It returns a CoverTuple containing the following cover and cache data:
|
||||||
|
|
||||||
book_id: The id of the book for which a cover is wanted.
|
book_id: The id of the book for which a cover is wanted.
|
||||||
has_cover: True if the book has an associated cover image file.
|
has_cover: True if the book has an associated cover image file.
|
||||||
cdata: Cover data. Can be None (no cover data), jpg string data, or a
|
cdata: Cover data. Can be None (no cover data), or a rendered cover image.
|
||||||
previously rendered cover.
|
|
||||||
cache_valid: True if the cache has correct data, False if a cover exists
|
cache_valid: True if the cache has correct data, False if a cover exists
|
||||||
but isn't in the cache, None if the cache has data but the
|
but isn't in the cache, None if the cache has data but the
|
||||||
cover has been deleted.
|
cover has been deleted.
|
||||||
timestamp: the cover file modtime if the cover came from the file system,
|
timestamp: the cover file modtime if the cover came from the file system,
|
||||||
the timestamp in the cache if a valid cover is in the cache,
|
the timestamp in the cache if a valid cover is in the cache,
|
||||||
None otherwise.
|
otherwise None.
|
||||||
'''
|
'''
|
||||||
if self.ignore_render_requests.is_set():
|
if self.ignore_render_requests.is_set():
|
||||||
return
|
return
|
||||||
db = self.dbref()
|
db = self.dbref()
|
||||||
if db is None:
|
if db is None:
|
||||||
return
|
return
|
||||||
cdata, timestamp = self.thumbnail_cache[book_id] # None, None if not cached.
|
tc = self.thumbnail_cache
|
||||||
|
cdata, timestamp = tc[book_id] # None, None if not cached.
|
||||||
if timestamp is None:
|
if timestamp is None:
|
||||||
# Cover not in cache Get the cover from the file system if it exists.
|
# Cover not in cache. Try to read the cover from the library.
|
||||||
has_cover, cdata, timestamp = db.new_api.cover_or_cache(book_id, 0)
|
has_cover, cdata, timestamp = db.new_api.cover_or_cache(book_id, 0)
|
||||||
cache_valid = False if has_cover else None
|
if has_cover:
|
||||||
|
# There is a cover.jpg. Convert the byte string to an image.
|
||||||
|
cache_valid = False
|
||||||
|
cdata = Image.open(BytesIO(cdata))
|
||||||
|
else:
|
||||||
|
# No cover.jpg
|
||||||
|
cache_valid = None
|
||||||
else:
|
else:
|
||||||
# A cover is in the cache. Check whether it is up to date.
|
# A cover is in the cache. Check whether it is up to date.
|
||||||
has_cover, tcdata, timestamp = db.new_api.cover_or_cache(book_id, timestamp)
|
has_cover, tcdata, timestamp = db.new_api.cover_or_cache(book_id, timestamp)
|
||||||
@ -972,23 +979,22 @@ class GridView(QListView):
|
|||||||
if tcdata is None:
|
if tcdata is None:
|
||||||
# The cached cover is up-to-date.
|
# The cached cover is up-to-date.
|
||||||
cache_valid = True
|
cache_valid = True
|
||||||
|
cdata = Image.open(BytesIO(cdata))
|
||||||
else:
|
else:
|
||||||
# The cached cover is stale
|
# The cached cover is stale
|
||||||
cache_valid = False
|
cache_valid = False
|
||||||
# Replace the cached cover data with the data from the cover file
|
# Convert the bytes from the cover.jpg. The image will be
|
||||||
cdata = tcdata
|
# resized later.
|
||||||
|
cdata = Image.open(BytesIO(tcdata))
|
||||||
else:
|
else:
|
||||||
# We found a cached cover for a book without a cover. This can
|
# We found a cached cover for a book without a cover. This can
|
||||||
# happen in older version of calibres that can reuse book_ids
|
# happen in older version of calibre that can reuse book_ids
|
||||||
# between libraries and the books in one library have covers
|
# between libraries and books in one library have covers where
|
||||||
# where they don't in another library. Removing the cover from
|
# they don't in another library. Versions of calibre with this
|
||||||
# the cache isn't strictly necessary, but seems better than
|
# source code don't have this problem because the cache UUID is
|
||||||
# leaving incorrect data in the cache. It will get regenarated
|
# set when the database changes instead of when the cache thread
|
||||||
# if the user switches to the other library. Versions of calibre
|
# is created.
|
||||||
# with this source code don't have this problem because the
|
tc.invalidate((book_id,))
|
||||||
# cache UUID is set when the database changes instead of when
|
|
||||||
# the cache thread is created.
|
|
||||||
self.thumbnail_cache.invalidate((book_id,))
|
|
||||||
cache_valid = None
|
cache_valid = None
|
||||||
cdata = None
|
cdata = None
|
||||||
if has_cover and cdata is None:
|
if has_cover and cdata is None:
|
||||||
@ -996,66 +1002,82 @@ class GridView(QListView):
|
|||||||
return CoverTuple(book_id=book_id, has_cover=has_cover, cache_valid=cache_valid,
|
return CoverTuple(book_id=book_id, has_cover=has_cover, cache_valid=cache_valid,
|
||||||
cdata=cdata, timestamp=timestamp)
|
cdata=cdata, timestamp=timestamp)
|
||||||
|
|
||||||
def render_cover(self, cover_tuple):
|
def make_thumbnail(self, cover_tuple):
|
||||||
# Render the cover image data to the current size and correct image format.
|
# Render the cover image data to the thumbnail size and correct format.
|
||||||
# This method must be called on the GUI thread
|
# Rendering isn't needed if the cover came from the cache and the cache
|
||||||
if not is_gui_thread():
|
# is valid. Put a newly rendered image into the cache. Returns the
|
||||||
raise RuntimeError('render_cover not in GUI Thread')
|
# thumbnail as a PIL Image. This method is called on the cover thread.
|
||||||
dpr = self.device_pixel_ratio
|
|
||||||
page_width = int(dpr * self.delegate.cover_size.width())
|
|
||||||
page_height = int(dpr * self.delegate.cover_size.height())
|
|
||||||
cdata = cover_tuple.cdata
|
cdata = cover_tuple.cdata
|
||||||
book_id = cover_tuple.book_id
|
book_id = cover_tuple.book_id
|
||||||
|
tc = self.thumbnail_cache
|
||||||
|
# thumb = None
|
||||||
if cover_tuple.has_cover:
|
if cover_tuple.has_cover:
|
||||||
# The book has a cover. Render the cover data as needed to get the
|
# cdata contains either the resized thumbnail, the full cover.jpg,
|
||||||
# pixmap that will be cached.
|
# or None if cover.jpg isn't valid
|
||||||
p = QPixmap()
|
if cdata.getbbox() is None and cover_tuple.cache_valid:
|
||||||
p.loadFromData(cdata, CACHE_FORMAT if cover_tuple.cache_valid else 'JPEG')
|
# Something wrong with the cover data in the cache. Remove it
|
||||||
p.setDevicePixelRatio(dpr)
|
# from the cache and render it again.
|
||||||
if p.isNull() and cover_tuple.cache_valid:
|
tc.invalidate((book_id,))
|
||||||
# Something wrong with the cover data. Remove it from the cache
|
|
||||||
# and render it again.
|
|
||||||
self.thumbnail_cache.invalidate((book_id,))
|
|
||||||
self.render_queue.put(book_id)
|
self.render_queue.put(book_id)
|
||||||
return
|
return None
|
||||||
cdata = None if p.isNull() else p
|
|
||||||
if not cover_tuple.cache_valid:
|
if not cover_tuple.cache_valid:
|
||||||
# cover isn't in the cache, is stale, or isn't a valid image.
|
# The cover isn't in the cache, is stale, or isn't a valid
|
||||||
|
# image. We might have the image from cover.jpg, in which case
|
||||||
|
# make it into a thumbnail.
|
||||||
if cdata is not None:
|
if cdata is not None:
|
||||||
# Scale the cover
|
# We have a cover from the file system. Scale it by creating
|
||||||
width, height = p.width(), p.height()
|
# a thumbnail
|
||||||
scaled, nwidth, nheight = fit_image(
|
dpr = self.device_pixel_ratio
|
||||||
width, height, page_width, page_height)
|
page_width = int(dpr * self.delegate.cover_size.width())
|
||||||
|
page_height = int(dpr * self.delegate.cover_size.height())
|
||||||
|
scaled, nwidth, nheight = fit_image(cdata.width, cdata.height,
|
||||||
|
page_width, page_height)
|
||||||
if scaled:
|
if scaled:
|
||||||
|
# The image is the wrong size. Scale it.
|
||||||
if self.ignore_render_requests.is_set():
|
if self.ignore_render_requests.is_set():
|
||||||
return
|
return
|
||||||
p = p.scaled(int(nwidth), int(nheight), Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
# The PIL thumbnail operation works in-place, changing
|
||||||
p.setDevicePixelRatio(dpr)
|
# the source image.
|
||||||
cdata = p
|
cdata.thumbnail((int(nwidth), int(nheight)))
|
||||||
if cdata is None:
|
thumb = cdata
|
||||||
# The cover data isn't valid. Remove it from the cache
|
# Put the new thumbnail into the cache.
|
||||||
self.thumbnail_cache.invalidate((book_id,))
|
|
||||||
else:
|
|
||||||
# Put the newly scaled cover into the cache.
|
|
||||||
try:
|
try:
|
||||||
self.thumbnail_cache.insert(book_id, cover_tuple.timestamp,
|
with BytesIO() as buf:
|
||||||
image_to_data(cdata))
|
cdata.save(buf, format=CACHE_FORMAT)
|
||||||
except EncodeError as err:
|
# use getbuffer() instead of getvalue() to avoid a copy
|
||||||
self.thumbnail_cache.invalidate((book_id,))
|
tc.insert(book_id, cover_tuple.timestamp, buf.getbuffer())
|
||||||
prints(err)
|
thumb = cdata
|
||||||
except Exception:
|
except Exception:
|
||||||
|
tc.invalidate((book_id,))
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
# else: cached cover image used directly. Nothing to do.
|
else:
|
||||||
elif cover_tuple.cache_valid is not None:
|
# The cover data isn't valid. Remove it from the cache
|
||||||
# Cover was removed, but it exists in cache. Remove it from the cache
|
tc.invalidate((book_id,))
|
||||||
self.thumbnail_cache.invalidate((book_id,))
|
else:
|
||||||
cdata = None
|
# The data from the cover cache is valid and is already a thumb.
|
||||||
# This can put None into the cache so re-rendering doesn't try again.
|
thumb = cdata
|
||||||
self.delegate.cover_cache.set(book_id, cdata)
|
else:
|
||||||
|
# The book doesn't have a cover.
|
||||||
|
if cover_tuple.cache_valid is not None:
|
||||||
|
# Cover was removed, but it exists in cache. Remove it from the cache
|
||||||
|
tc.invalidate((book_id,))
|
||||||
|
thumb = None
|
||||||
|
# Return the thumbnail, which is either None or a PIL Image. If not None
|
||||||
|
# the image will be converted to a QPixmap on the GUI thread. Putting
|
||||||
|
# None into the CoverCache ensures re-rendering won't try again.
|
||||||
|
return thumb
|
||||||
|
|
||||||
def re_render(self, book_id):
|
def re_render(self, book_id, thumb):
|
||||||
|
# This is called on the GUI thread when a cover thumbnail is not in the
|
||||||
|
# CoverCache. The parameter "thumb" is either None if there is no cover
|
||||||
|
# or a PIL Image of the thumbnail.
|
||||||
self.delegate.cover_cache.clear_staging()
|
self.delegate.cover_cache.clear_staging()
|
||||||
|
if thumb is not None:
|
||||||
|
# Convert the image to a QPixmap
|
||||||
|
thumb = convert_PIL_image_to_pixmap(thumb)
|
||||||
|
self.delegate.cover_cache.set(book_id, thumb)
|
||||||
m = self.model()
|
m = self.model()
|
||||||
try:
|
try:
|
||||||
index = m.db.row(book_id)
|
index = m.db.row(book_id)
|
||||||
|
@ -25,7 +25,12 @@ class ThumbnailCache(TC):
|
|||||||
|
|
||||||
class CoverCache(dict):
|
class CoverCache(dict):
|
||||||
|
|
||||||
' This is a RAM cache to speed up rendering of covers by storing them as QPixmaps '
|
'''
|
||||||
|
This is a RAM cache to speed up rendering of covers by storing them as
|
||||||
|
QPixmaps. It is possible that it is called from multiple threads, thus the
|
||||||
|
locking and staging. For example, it can be called by the db layer when a
|
||||||
|
book is removed either by the GUI or the content server.
|
||||||
|
'''
|
||||||
|
|
||||||
def __init__(self, limit=100):
|
def __init__(self, limit=100):
|
||||||
self.items = OrderedDict()
|
self.items = OrderedDict()
|
||||||
@ -59,7 +64,6 @@ class CoverCache(dict):
|
|||||||
# faster
|
# faster
|
||||||
ans = QPixmap.fromImage(ans)
|
ans = QPixmap.fromImage(ans)
|
||||||
self.items[key] = ans
|
self.items[key] = ans
|
||||||
|
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def set(self, key, val):
|
def set(self, key, val):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user