mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
R-organiz img modul
This commit is contained in:
parent
d8227f7f1a
commit
fb8f12cce9
@ -13,11 +13,19 @@ from calibre import fit_image, force_unicode
|
|||||||
from calibre.constants import iswindows, plugins
|
from calibre.constants import iswindows, plugins
|
||||||
from calibre.utils.config_base import tweaks
|
from calibre.utils.config_base import tweaks
|
||||||
from calibre.utils.filenames import atomic_rename
|
from calibre.utils.filenames import atomic_rename
|
||||||
|
|
||||||
|
# Utilitis {{{
|
||||||
imageops, imageops_err = plugins['imageops']
|
imageops, imageops_err = plugins['imageops']
|
||||||
|
|
||||||
class NotImage(ValueError):
|
class NotImage(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def normalize_format_name(fmt):
|
||||||
|
fmt = fmt.lower()
|
||||||
|
if fmt == 'jpg':
|
||||||
|
fmt = 'jpeg'
|
||||||
|
return fmt
|
||||||
|
|
||||||
def get_exe_path(name):
|
def get_exe_path(name):
|
||||||
from calibre.ebooks.pdf.pdftohtml import PDFTOHTML
|
from calibre.ebooks.pdf.pdftohtml import PDFTOHTML
|
||||||
base = os.path.dirname(PDFTOHTML)
|
base = os.path.dirname(PDFTOHTML)
|
||||||
@ -26,11 +34,16 @@ def get_exe_path(name):
|
|||||||
if not base:
|
if not base:
|
||||||
return name
|
return name
|
||||||
return os.path.join(base, name)
|
return os.path.join(base, name)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Loading images {{{
|
||||||
|
|
||||||
def null_image():
|
def null_image():
|
||||||
|
' Create an invalid image. For internal use. '
|
||||||
return QImage()
|
return QImage()
|
||||||
|
|
||||||
def image_from_data(data):
|
def image_from_data(data):
|
||||||
|
' Create an image object from data, which should be a bytestring. '
|
||||||
if isinstance(data, QImage):
|
if isinstance(data, QImage):
|
||||||
return data
|
return data
|
||||||
i = QImage()
|
i = QImage()
|
||||||
@ -39,10 +52,12 @@ def image_from_data(data):
|
|||||||
return i
|
return i
|
||||||
|
|
||||||
def image_from_path(path):
|
def image_from_path(path):
|
||||||
|
' Load an image from the specified path. '
|
||||||
with lopen(path, 'rb') as f:
|
with lopen(path, 'rb') as f:
|
||||||
return image_from_data(f.read())
|
return image_from_data(f.read())
|
||||||
|
|
||||||
def image_from_x(x):
|
def image_from_x(x):
|
||||||
|
' Create an image from a bytestring or a path or a file like object. '
|
||||||
if isinstance(x, type('')):
|
if isinstance(x, type('')):
|
||||||
return image_from_path(x)
|
return image_from_path(x)
|
||||||
if hasattr(x, 'read'):
|
if hasattr(x, 'read'):
|
||||||
@ -54,45 +69,16 @@ def image_from_x(x):
|
|||||||
raise TypeError('Unknown image src type: %s' % type(x))
|
raise TypeError('Unknown image src type: %s' % type(x))
|
||||||
|
|
||||||
def image_and_format_from_data(data):
|
def image_and_format_from_data(data):
|
||||||
|
' Create an image object from the specified data which should be a bytsestring and also return the format of the image '
|
||||||
ba = QByteArray(data)
|
ba = QByteArray(data)
|
||||||
buf = QBuffer(ba)
|
buf = QBuffer(ba)
|
||||||
buf.open(QBuffer.ReadOnly)
|
buf.open(QBuffer.ReadOnly)
|
||||||
r = QImageReader(buf)
|
r = QImageReader(buf)
|
||||||
fmt = bytes(r.format()).decode('utf-8')
|
fmt = bytes(r.format()).decode('utf-8')
|
||||||
return r.read(), fmt
|
return r.read(), fmt
|
||||||
|
# }}}
|
||||||
|
|
||||||
def add_borders_to_image(img, left=0, top=0, right=0, bottom=0, border_color='#ffffff'):
|
# Saving images {{{
|
||||||
img = image_from_data(img)
|
|
||||||
if not (left > 0 or right > 0 or top > 0 or bottom > 0):
|
|
||||||
return img
|
|
||||||
canvas = QImage(img.width() + left + right, img.height() + top + bottom, QImage.Format_RGB32)
|
|
||||||
canvas.fill(QColor(border_color))
|
|
||||||
overlay_image(img, canvas, left, top)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
def overlay_image(img, canvas=None, left=0, top=0):
|
|
||||||
if canvas is None:
|
|
||||||
canvas = QImage(img.size(), QImage.Format_RGB32)
|
|
||||||
canvas.fill(Qt.white)
|
|
||||||
left, top = int(left), int(top)
|
|
||||||
if imageops is None:
|
|
||||||
# This is for people running from source who have not updated the
|
|
||||||
# binary and so do not have the imageops module
|
|
||||||
from PyQt5.Qt import QPainter
|
|
||||||
from calibre.gui2 import ensure_app
|
|
||||||
ensure_app()
|
|
||||||
p = QPainter(canvas)
|
|
||||||
p.drawImage(left, top, img)
|
|
||||||
p.end()
|
|
||||||
else:
|
|
||||||
imageops.overlay(img, canvas, left, top)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
def blend_image(img, bgcolor='#ffffff'):
|
|
||||||
canvas = QImage(img.size(), QImage.Format_RGB32)
|
|
||||||
canvas.fill(QColor(bgcolor))
|
|
||||||
overlay_image(img, canvas)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
def image_to_data(img, compression_quality=95, fmt='JPEG', png_compression_level=9, jpeg_optimized=True, jpeg_progressive=False):
|
def image_to_data(img, compression_quality=95, fmt='JPEG', png_compression_level=9, jpeg_optimized=True, jpeg_progressive=False):
|
||||||
'''
|
'''
|
||||||
@ -126,77 +112,14 @@ def image_to_data(img, compression_quality=95, fmt='JPEG', png_compression_level
|
|||||||
return ba.data()
|
return ba.data()
|
||||||
|
|
||||||
def save_image(img, path, **kw):
|
def save_image(img, path, **kw):
|
||||||
|
''' Save image to the specified path. Image format is taken from the file
|
||||||
|
extension. You can pass the same keyword arguments as for the
|
||||||
|
`image_to_data()` function. '''
|
||||||
fmt = path.rpartition('.')[-1]
|
fmt = path.rpartition('.')[-1]
|
||||||
kw['fmt'] = kw.get('fmt', fmt)
|
kw['fmt'] = kw.get('fmt', fmt)
|
||||||
with lopen(path, 'wb') as f:
|
with lopen(path, 'wb') as f:
|
||||||
f.write(image_to_data(image_from_data(img), **kw))
|
f.write(image_to_data(image_from_data(img), **kw))
|
||||||
|
|
||||||
def resize_image(img, width, height):
|
|
||||||
return img.scaled(int(width), int(height), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
||||||
|
|
||||||
def resize_to_fit(img, width, height):
|
|
||||||
img = image_from_data(img)
|
|
||||||
resize_needed, nw, nh = fit_image(img.width(), img.height(), width, height)
|
|
||||||
if resize_needed:
|
|
||||||
resize_image(img, nw, nh)
|
|
||||||
return resize_needed, img
|
|
||||||
|
|
||||||
def scale_image(data, width=60, height=80, compression_quality=70, as_png=False, preserve_aspect_ratio=True):
|
|
||||||
''' Scale an image, returning it as either JPEG or PNG data (bytestring).
|
|
||||||
Transparency is alpha blended with white when converting to JPEG. Is thread
|
|
||||||
safe and does not require a QApplication. '''
|
|
||||||
# We use Qt instead of ImageMagick here because ImageMagick seems to use
|
|
||||||
# some kind of memory pool, causing memory consumption to sky rocket.
|
|
||||||
img = image_from_data(data)
|
|
||||||
if preserve_aspect_ratio:
|
|
||||||
scaled, nwidth, nheight = fit_image(img.width(), img.height(), width, height)
|
|
||||||
if scaled:
|
|
||||||
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
||||||
else:
|
|
||||||
if img.width() != width or img.height() != height:
|
|
||||||
img = img.scaled(width, height, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
|
||||||
fmt = 'PNG' if as_png else 'JPEG'
|
|
||||||
w, h = img.width(), img.height()
|
|
||||||
return w, h, image_to_data(img, compression_quality=compression_quality, fmt=fmt)
|
|
||||||
|
|
||||||
def crop_image(img, x, y, width, height):
|
|
||||||
'''
|
|
||||||
Return the specified section of the image.
|
|
||||||
|
|
||||||
:param x, y: The top left corner of the crop box
|
|
||||||
:param width, height: The width and height of the crop box. Note that if
|
|
||||||
the crop box exceeds the source images dimensions, width and height will be
|
|
||||||
auto-truncated.
|
|
||||||
'''
|
|
||||||
img = image_from_data(img)
|
|
||||||
width = min(width, img.width() - x)
|
|
||||||
height = min(height, img.height() - y)
|
|
||||||
return img.copy(x, y, width, height)
|
|
||||||
|
|
||||||
def clone_image(img):
|
|
||||||
''' Returns a shallow copy of the image. However, the underlying data buffer
|
|
||||||
will be automatically copied-on-write '''
|
|
||||||
return QImage(img)
|
|
||||||
|
|
||||||
def normalize_format_name(fmt):
|
|
||||||
fmt = fmt.lower()
|
|
||||||
if fmt == 'jpg':
|
|
||||||
fmt = 'jpeg'
|
|
||||||
return fmt
|
|
||||||
|
|
||||||
def grayscale_image(img):
|
|
||||||
if imageops is not None:
|
|
||||||
return imageops.grayscale(image_from_data(img))
|
|
||||||
return img
|
|
||||||
|
|
||||||
def set_image_opacity(img, alpha=0.5):
|
|
||||||
''' Change the opacity of `img`. Note that the alpha value is multiplied to
|
|
||||||
any existing alpha values, so you cannot use this function to convert a
|
|
||||||
semi-transparent image to an opaque one. For that use `blend_image()`. '''
|
|
||||||
if imageops is None:
|
|
||||||
raise RuntimeError(imageops_err)
|
|
||||||
return imageops.set_opacity(image_from_data(img), alpha)
|
|
||||||
|
|
||||||
def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compression_quality=90, minify_to=None, grayscale=False, data_fmt='jpeg'):
|
def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compression_quality=90, minify_to=None, grayscale=False, data_fmt='jpeg'):
|
||||||
'''
|
'''
|
||||||
Saves image in data to path, in the format specified by the path
|
Saves image in data to path, in the format specified by the path
|
||||||
@ -240,8 +163,12 @@ def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compr
|
|||||||
return image_to_data(img, compression_quality, fmt) if changed else data
|
return image_to_data(img, compression_quality, fmt) if changed else data
|
||||||
with lopen(path, 'wb') as f:
|
with lopen(path, 'wb') as f:
|
||||||
f.write(image_to_data(img, compression_quality, fmt) if changed else data)
|
f.write(image_to_data(img, compression_quality, fmt) if changed else data)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Overlaying images {{{
|
||||||
|
|
||||||
def blend_on_canvas(img, width, height, bgcolor='#ffffff'):
|
def blend_on_canvas(img, width, height, bgcolor='#ffffff'):
|
||||||
|
' Blend the `img` onto a canvas with the specified background color and size '
|
||||||
w, h = img.width(), img.height()
|
w, h = img.width(), img.height()
|
||||||
scaled, nw, nh = fit_image(w, h, width, height)
|
scaled, nw, nh = fit_image(w, h, width, height)
|
||||||
if scaled:
|
if scaled:
|
||||||
@ -272,10 +199,136 @@ class Canvas(object):
|
|||||||
return image_to_data(self.img, compression_quality=compression_quality, fmt=fmt)
|
return image_to_data(self.img, compression_quality=compression_quality, fmt=fmt)
|
||||||
|
|
||||||
def create_canvas(width, height, bgcolor='#ffffff'):
|
def create_canvas(width, height, bgcolor='#ffffff'):
|
||||||
|
'Create a blank canvas of the specified size and color '
|
||||||
img = QImage(width, height, QImage.Format_RGB32)
|
img = QImage(width, height, QImage.Format_RGB32)
|
||||||
img.fill(QColor(bgcolor))
|
img.fill(QColor(bgcolor))
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def overlay_image(img, canvas=None, left=0, top=0):
|
||||||
|
' Overlay the `img` onto the canvas at the specified position '
|
||||||
|
if canvas is None:
|
||||||
|
canvas = QImage(img.size(), QImage.Format_RGB32)
|
||||||
|
canvas.fill(Qt.white)
|
||||||
|
left, top = int(left), int(top)
|
||||||
|
if imageops is None:
|
||||||
|
# This is for people running from source who have not updated the
|
||||||
|
# binary and so do not have the imageops module
|
||||||
|
from PyQt5.Qt import QPainter
|
||||||
|
from calibre.gui2 import ensure_app
|
||||||
|
ensure_app()
|
||||||
|
p = QPainter(canvas)
|
||||||
|
p.drawImage(left, top, img)
|
||||||
|
p.end()
|
||||||
|
else:
|
||||||
|
imageops.overlay(img, canvas, left, top)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
def texture_image(canvas, texture):
|
||||||
|
' Repeatedly tile the image `texture` across and down the image `canvas` '
|
||||||
|
if imageops is None:
|
||||||
|
raise RuntimeError(imageops_err)
|
||||||
|
if canvas.hasAlphaChannel():
|
||||||
|
canvas = blend_image(canvas)
|
||||||
|
return imageops.texture_image(canvas, texture)
|
||||||
|
|
||||||
|
def blend_image(img, bgcolor='#ffffff'):
|
||||||
|
' Used to convert images that have semi-transparent pixels to opaque by blending with the specified color '
|
||||||
|
canvas = QImage(img.size(), QImage.Format_RGB32)
|
||||||
|
canvas.fill(QColor(bgcolor))
|
||||||
|
overlay_image(img, canvas)
|
||||||
|
return canvas
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Image borders {{{
|
||||||
|
|
||||||
|
def add_borders_to_image(img, left=0, top=0, right=0, bottom=0, border_color='#ffffff'):
|
||||||
|
img = image_from_data(img)
|
||||||
|
if not (left > 0 or right > 0 or top > 0 or bottom > 0):
|
||||||
|
return img
|
||||||
|
canvas = QImage(img.width() + left + right, img.height() + top + bottom, QImage.Format_RGB32)
|
||||||
|
canvas.fill(QColor(border_color))
|
||||||
|
overlay_image(img, canvas, left, top)
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
def remove_borders_from_image(img, fuzz=None):
|
||||||
|
''' Try to auto-detect and remove any borders from the image. Returns
|
||||||
|
the image itself if no borders could be removed. `fuzz` is a measure of
|
||||||
|
what colors are considered identical (must be a number between 0 and 255 in
|
||||||
|
absolute intensity units). Default is from a tweak whose default value is 10. '''
|
||||||
|
if imageops is None:
|
||||||
|
raise RuntimeError(imageops_err)
|
||||||
|
fuzz = tweaks['cover_trim_fuzz_value'] if fuzz is None else fuzz
|
||||||
|
ans = imageops.remove_borders(image_from_data(img), max(0, fuzz))
|
||||||
|
return ans if ans.size() != img.size() else img
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Cropping/scaling of images {{{
|
||||||
|
|
||||||
|
def resize_image(img, width, height):
|
||||||
|
return img.scaled(int(width), int(height), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||||
|
|
||||||
|
def resize_to_fit(img, width, height):
|
||||||
|
img = image_from_data(img)
|
||||||
|
resize_needed, nw, nh = fit_image(img.width(), img.height(), width, height)
|
||||||
|
if resize_needed:
|
||||||
|
resize_image(img, nw, nh)
|
||||||
|
return resize_needed, img
|
||||||
|
|
||||||
|
def clone_image(img):
|
||||||
|
''' Returns a shallow copy of the image. However, the underlying data buffer
|
||||||
|
will be automatically copied-on-write '''
|
||||||
|
return QImage(img)
|
||||||
|
|
||||||
|
def scale_image(data, width=60, height=80, compression_quality=70, as_png=False, preserve_aspect_ratio=True):
|
||||||
|
''' Scale an image, returning it as either JPEG or PNG data (bytestring).
|
||||||
|
Transparency is alpha blended with white when converting to JPEG. Is thread
|
||||||
|
safe and does not require a QApplication. '''
|
||||||
|
# We use Qt instead of ImageMagick here because ImageMagick seems to use
|
||||||
|
# some kind of memory pool, causing memory consumption to sky rocket.
|
||||||
|
img = image_from_data(data)
|
||||||
|
if preserve_aspect_ratio:
|
||||||
|
scaled, nwidth, nheight = fit_image(img.width(), img.height(), width, height)
|
||||||
|
if scaled:
|
||||||
|
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
|
else:
|
||||||
|
if img.width() != width or img.height() != height:
|
||||||
|
img = img.scaled(width, height, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||||
|
fmt = 'PNG' if as_png else 'JPEG'
|
||||||
|
w, h = img.width(), img.height()
|
||||||
|
return w, h, image_to_data(img, compression_quality=compression_quality, fmt=fmt)
|
||||||
|
|
||||||
|
def crop_image(img, x, y, width, height):
|
||||||
|
'''
|
||||||
|
Return the specified section of the image.
|
||||||
|
|
||||||
|
:param x, y: The top left corner of the crop box
|
||||||
|
:param width, height: The width and height of the crop box. Note that if
|
||||||
|
the crop box exceeds the source images dimensions, width and height will be
|
||||||
|
auto-truncated.
|
||||||
|
'''
|
||||||
|
img = image_from_data(img)
|
||||||
|
width = min(width, img.width() - x)
|
||||||
|
height = min(height, img.height() - y)
|
||||||
|
return img.copy(x, y, width, height)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Image transformations {{{
|
||||||
|
|
||||||
|
def grayscale_image(img):
|
||||||
|
if imageops is not None:
|
||||||
|
return imageops.grayscale(image_from_data(img))
|
||||||
|
return img
|
||||||
|
|
||||||
|
def set_image_opacity(img, alpha=0.5):
|
||||||
|
''' Change the opacity of `img`. Note that the alpha value is multiplied to
|
||||||
|
any existing alpha values, so you cannot use this function to convert a
|
||||||
|
semi-transparent image to an opaque one. For that use `blend_image()`. '''
|
||||||
|
if imageops is None:
|
||||||
|
raise RuntimeError(imageops_err)
|
||||||
|
return imageops.set_opacity(image_from_data(img), alpha)
|
||||||
|
|
||||||
def flip_image(img, horizontal=False, vertical=False):
|
def flip_image(img, horizontal=False, vertical=False):
|
||||||
return image_from_data(img).mirrored(horizontal, vertical)
|
return image_from_data(img).mirrored(horizontal, vertical)
|
||||||
|
|
||||||
@ -293,17 +346,6 @@ def rotate_image(img, degrees):
|
|||||||
t.rotate(degrees)
|
t.rotate(degrees)
|
||||||
return image_from_data(img).transformed(t)
|
return image_from_data(img).transformed(t)
|
||||||
|
|
||||||
def remove_borders_from_image(img, fuzz=None):
|
|
||||||
''' Try to auto-detect and remove any borders from the image. Returns
|
|
||||||
the image itself if no borders could be removed. `fuzz` is a measure of
|
|
||||||
what colors are considered identical (must be a number between 0 and 255 in
|
|
||||||
absolute intensity units). Default is from a tweak whose default value is 10. '''
|
|
||||||
if imageops is None:
|
|
||||||
raise RuntimeError(imageops_err)
|
|
||||||
fuzz = tweaks['cover_trim_fuzz_value'] if fuzz is None else fuzz
|
|
||||||
ans = imageops.remove_borders(image_from_data(img), max(0, fuzz))
|
|
||||||
return ans if ans.size() != img.size() else img
|
|
||||||
|
|
||||||
def gaussian_sharpen_image(img, radius=0, sigma=3, high_quality=True):
|
def gaussian_sharpen_image(img, radius=0, sigma=3, high_quality=True):
|
||||||
if imageops is None:
|
if imageops is None:
|
||||||
raise RuntimeError(imageops_err)
|
raise RuntimeError(imageops_err)
|
||||||
@ -349,13 +391,7 @@ def quantize_image(img, max_colors=256, dither=True, palette=''):
|
|||||||
palette = palette.split()
|
palette = palette.split()
|
||||||
return imageops.quantize(img, max_colors, dither, [QColor(x).rgb() for x in palette])
|
return imageops.quantize(img, max_colors, dither, [QColor(x).rgb() for x in palette])
|
||||||
|
|
||||||
def texture_image(canvas, texture):
|
# }}}
|
||||||
' Repeatedly tile the image `texture` across and down the image `canvas` '
|
|
||||||
if imageops is None:
|
|
||||||
raise RuntimeError(imageops_err)
|
|
||||||
if canvas.hasAlphaChannel():
|
|
||||||
canvas = blend_image(canvas)
|
|
||||||
return imageops.texture_image(canvas, texture)
|
|
||||||
|
|
||||||
# Optimization of images {{{
|
# Optimization of images {{{
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user