def paintEvent(self, event): QWidget.paintEvent(self, event) pmap = self._pixmap if pmap.isNull(): return w, h = pmap.width(), pmap.height() ow, oh = w, h cw, ch = self.rect().width(), self.rect().height() scaled, nw, nh = fit_image(w, h, cw, ch) if scaled: pmap = pmap.scaled(int(nw*pmap.devicePixelRatio()), int(nh*pmap.devicePixelRatio()), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) w, h = int(pmap.width()/pmap.devicePixelRatio()), int(pmap.height()/pmap.devicePixelRatio()) x = int(abs(cw - w)/2.) y = int(abs(ch - h)/2.) target = QRect(x, y, w, h) p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap(target, pmap) if self.draw_border: pen = QPen() pen.setWidth(self.BORDER_WIDTH) p.setPen(pen) p.drawRect(target) if self.show_size: draw_size(p, target, ow, oh) p.end()
def paintEvent(self, event): QWidget.paintEvent(self, event) pmap = self._pixmap if pmap.isNull(): return w, h = pmap.width(), pmap.height() ow, oh = w, h cw, ch = self.rect().width(), self.rect().height() scaled, nw, nh = fit_image(w, h, cw, ch) if scaled: pmap = pmap.scaled(nw, nh, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) w, h = pmap.width(), pmap.height() x = int(abs(cw - w) / 2.0) y = int(abs(ch - h) / 2.0) target = QRect(x, y, w, h) p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap(target, pmap) if self.draw_border: pen = QPen() pen.setWidth(self.BORDER_WIDTH) p.setPen(pen) p.drawRect(target) if self.show_size: sztgt = target.adjusted(0, 0, 0, -4) f = p.font() f.setBold(True) p.setFont(f) sz = u"\u00a0%d x %d\u00a0" % (ow, oh) flags = Qt.AlignBottom | Qt.AlignRight | Qt.TextSingleLine szrect = p.boundingRect(sztgt, flags, sz) p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) p.setPen(QPen(QColor(255, 255, 255))) p.drawText(sztgt, flags, sz) p.end()
def get_lines_for_image(self, img, view): if img.isNull(): return 0, 0 w, h = img.width(), img.height() scaled, w, h = fit_image(w, h, view.available_width() - 3, int(0.9 * view.height())) line_height = view.blockBoundingRect(view.document().begin()).height() return int(ceil(h / line_height)) + 1, w
def create_cover(report, icons=(), cols=5, size=60, padding=8): icons = icons or tuple(default_cover_icons(cols)) rows = int(math.ceil(len(icons) / cols)) canvas = create_canvas(cols * (size + padding), rows * (size + padding), '#eeeeee') y = -size - padding // 2 x = 0 for i, icon in enumerate(icons): if i % cols == 0: y += padding + size x = padding // 2 else: x += size + padding if report and icon in report.name_map: ipath = os.path.join(report.path, report.name_map[icon]) else: ipath = I(icon, allow_user_override=False) img = Image() with open(ipath, 'rb') as f: img.load(f.read()) scaled, nwidth, nheight = fit_image(img.size[0], img.size[1], size, size) img.size = nwidth, nheight dx = (size - nwidth) // 2 canvas.compose(img, x + dx, y) return canvas.export('JPEG')
def browse_icon(self, name="blank.png"): cherrypy.response.headers["Content-Type"] = "image/png" cherrypy.response.headers["Last-Modified"] = self.last_modified(self.build_time) if not hasattr(self, "__browse_icon_cache__"): self.__browse_icon_cache__ = {} if name not in self.__browse_icon_cache__: if name.startswith("_"): name = sanitize_file_name2(name[1:]) try: with open(os.path.join(config_dir, "tb_icons", name), "rb") as f: data = f.read() except: raise cherrypy.HTTPError(404, "no icon named: %r" % name) else: try: data = I(name, data=True) except: raise cherrypy.HTTPError(404, "no icon named: %r" % name) img = Image() img.load(data) width, height = img.size scaled, width, height = fit_image(width, height, 48, 48) if scaled: img.size = (width, height) self.__browse_icon_cache__[name] = img.export("png") return self.__browse_icon_cache__[name]
def browse_icon(self, name='blank.png'): cherrypy.response.headers['Content-Type'] = 'image/png' cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time) if not hasattr(self, '__browse_icon_cache__'): self.__browse_icon_cache__ = {} if name not in self.__browse_icon_cache__: if name.startswith('_'): name = sanitize_file_name2(name[1:]) try: with open(os.path.join(config_dir, 'tb_icons', name), 'rb') as f: data = f.read() except: raise cherrypy.HTTPError(404, 'no icon named: %r'%name) else: try: data = I(name, data=True) except: raise cherrypy.HTTPError(404, 'no icon named: %r'%name) img = Image() img.load(data) width, height = img.size scaled, width, height = fit_image(width, height, 48, 48) if scaled: img.size = (width, height) self.__browse_icon_cache__[name] = img.export('png') return self.__browse_icon_cache__[name]
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. if isinstance(data, QImage): img = data else: img = QImage() if not img.loadFromData(data): raise ValueError('Could not load image for thumbnail generation') 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) if not as_png and img.hasAlphaChannel(): nimg = QImage(img.size(), QImage.Format_RGB32) nimg.fill(Qt.white) p = QPainter(nimg) p.drawImage(0, 0, img) p.end() img = nimg ba = QByteArray() buf = QBuffer(ba) buf.open(QBuffer.WriteOnly) fmt = 'PNG' if as_png else 'JPEG' if not img.save(buf, fmt, quality=compression_quality): raise ValueError('Failed to export thumbnail image to: ' + fmt) return img.width(), img.height(), ba.data()
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80): try: cherrypy.response.headers['Content-Type'] = 'image/jpeg' cherrypy.response.timeout = 3600 cover = self.db.cover(id, index_is_id=True) if cover is None: cover = self.default_cover updated = self.build_time else: updated = self.db.cover_last_modified(id, index_is_id=True) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) if thumbnail: return generate_thumbnail(cover, width=thumb_width, height=thumb_height)[-1] img = Image() img.load(cover) width, height = img.size scaled, width, height = fit_image(width, height, thumb_width if thumbnail else self.max_cover_width, thumb_height if thumbnail else self.max_cover_height) if not scaled: return cover return save_cover_data_to(img, 'img.jpg', return_data=True, resize_to=(width, height)) except Exception as err: import traceback cherrypy.log.error('Failed to generate cover:') cherrypy.log.error(traceback.print_exc()) raise cherrypy.HTTPError(404, 'Failed to generate cover: %r'%err)
def rescale(self): from PIL import Image from io import BytesIO is_image_collection = getattr(self.opts, 'is_image_collection', False) if is_image_collection: page_width, page_height = self.opts.dest.comic_screen_size else: page_width, page_height = self.opts.dest.width, self.opts.dest.height page_width -= (self.opts.margin_left + self.opts.margin_right) * self.opts.dest.dpi/72. page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi/72. for item in self.oeb.manifest: if item.media_type.startswith('image'): ext = item.media_type.split('/')[-1].upper() if ext == 'JPG': ext = 'JPEG' if ext not in ('PNG', 'JPEG', 'GIF'): ext = 'JPEG' raw = item.data if hasattr(raw, 'xpath') or not raw: # Probably an svg image continue try: img = Image.open(BytesIO(raw)) except Exception: continue width, height = img.size try: if self.check_colorspaces and img.mode == 'CMYK': self.log.warn( 'The image %s is in the CMYK colorspace, converting it ' 'to RGB as Adobe Digital Editions cannot display CMYK' % item.href) img = img.convert('RGB') except Exception: self.log.exception('Failed to convert image %s from CMYK to RGB' % item.href) scaled, new_width, new_height = fit_image(width, height, page_width, page_height) if scaled: new_width = max(1, new_width) new_height = max(1, new_height) self.log('Rescaling image from %dx%d to %dx%d'%( width, height, new_width, new_height), item.href) try: img = img.resize((new_width, new_height)) except Exception: self.log.exception('Failed to rescale image: %s' % item.href) continue buf = BytesIO() try: img.save(buf, ext) except Exception: self.log.exception('Failed to rescale image: %s' % item.href) else: item.data = buf.getvalue() item.unload_data_from_memory()
def _get_goodreader_thumb(self, remote_path): ''' remote_path is relative to /Documents GoodReader caches small thumbs of book covers. If we didn't send the book, fetch the cached copy from the iDevice. These thumbs will be scaled up to the size we use when sending from calibre for consistency. ''' from PIL import Image as PILImage from calibre import fit_image def _build_local_path(): ''' GoodReader stores individual dbs for each book, matching the folder and name structure in the Documents folder. Make a local version, renamed to .db ''' path = remote_db_path.split('/')[-1] if iswindows: from calibre.utils.filenames import shorten_components_to plen = len(self.temp_dir) path = ''.join(shorten_components_to(245-plen, [path])) full_path = os.path.join(self.temp_dir, path) base = os.path.splitext(full_path)[0] full_path = base + ".db" return os.path.normpath(full_path) self._log_location(remote_path) remote_db_path = '/'.join(['/Library','Application Support', 'com.goodiware.GoodReader.ASRoot', 'Previews', '0', remote_path]) thumb_data = None db_stats = self.ios.stat(remote_db_path) if db_stats: full_path = _build_local_path() with open(full_path, 'wb') as out: self.ios.copy_from_idevice(remote_db_path, out) local_db_path = out.name con = sqlite3.connect(local_db_path) with con: con.row_factory = sqlite3.Row cur = con.cursor() cur.execute('''SELECT thumb FROM Pages WHERE pageNum = "1" ''') row = cur.fetchone() if row: img_data = cStringIO.StringIO(row[b'thumb']) im = PILImage.open(img_data) scaled, width, height = fit_image(im.size[0], im.size[1], self.COVER_WIDTH, self.COVER_HEIGHT) im = im.resize((self.COVER_WIDTH, self.COVER_HEIGHT), PILImage.NEAREST) thumb = cStringIO.StringIO() im.convert('RGB').save(thumb, 'JPEG') thumb_data = thumb.getvalue() img_data.close() thumb.close() return thumb_data
def rescale(self, qt=True): from calibre.utils.magick.draw import Image is_image_collection = getattr(self.opts, 'is_image_collection', False) if is_image_collection: page_width, page_height = self.opts.dest.comic_screen_size else: page_width, page_height = self.opts.dest.width, self.opts.dest.height page_width -= (self.opts.margin_left + self.opts.margin_right) * self.opts.dest.dpi/72. page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi/72. for item in self.oeb.manifest: if item.media_type.startswith('image'): ext = item.media_type.split('/')[-1].upper() if ext == 'JPG': ext = 'JPEG' if ext not in ('PNG', 'JPEG', 'GIF'): ext = 'JPEG' raw = item.data if hasattr(raw, 'xpath') or not raw: # Probably an svg image continue try: img = Image() img.load(raw) except: continue width, height = img.size try: if self.check_colorspaces and img.colorspace == 'CMYKColorspace': # We cannot do an automatic conversion of CMYK to RGB as # ImageMagick inverts colors if you just set the colorspace # to rgb. See for example: https://bugs.launchpad.net/bugs/1246710 self.log.warn( 'The image %s is in the CMYK colorspace, you should convert' ' it to sRGB as Adobe Digital Editions cannot render CMYK' % item.href) except Exception: pass scaled, new_width, new_height = fit_image(width, height, page_width, page_height) if scaled: new_width = max(1, new_width) new_height = max(1, new_height) self.log('Rescaling image from %dx%d to %dx%d'%( width, height, new_width, new_height), item.href) try: img.size = (new_width, new_height) data = img.export(ext.lower()) except KeyboardInterrupt: raise except: self.log.exception('Failed to rescale image') else: item.data = data item.unload_data_from_memory()
def create_image_markup(self, html_img, stylizer, href, as_block=False): # TODO: img inside a link (clickable image) style = stylizer.style(html_img) floating = style['float'] if floating not in {'left', 'right'}: floating = None if as_block: ml, mr = style._get('margin-left'), style._get('margin-right') if ml == 'auto': floating = 'center' if mr == 'auto' else 'right' if mr == 'auto': floating = 'center' if ml == 'auto' else 'right' else: parent = html_img.getparent() if len(parent) == 1 and not (parent.text or '').strip() and not (html_img.tail or '').strip(): # We have an inline image alone inside a block pstyle = stylizer.style(parent) if pstyle['text-align'] in ('center', 'right') and 'block' in pstyle['display']: floating = pstyle['text-align'] fake_margins = floating is None self.count += 1 img = self.images[href] name = urlunquote(posixpath.basename(href)) width, height = style.img_size(img.width, img.height) scaled, width, height = fit_image(width, height, self.page_width, self.page_height) width, height = map(pt_to_emu, (width, height)) makeelement, namespaces = self.document_relationships.namespace.makeelement, self.document_relationships.namespace.namespaces root = etree.Element('root', nsmap=namespaces) ans = makeelement(root, 'w:drawing', append=False) if floating is None: parent = makeelement(ans, 'wp:inline') else: parent = makeelement(ans, 'wp:anchor', **get_image_margins(style)) # The next three lines are boilerplate that Word requires, even # though the DOCX specs define defaults for all of them parent.set('simplePos', '0'), parent.set('relativeHeight', '1'), parent.set('behindDoc',"0"), parent.set('locked', "0") parent.set('layoutInCell', "1"), parent.set('allowOverlap', '1') makeelement(parent, 'wp:simplePos', x='0', y='0') makeelement(makeelement(parent, 'wp:positionH', relativeFrom='margin'), 'wp:align').text = floating makeelement(makeelement(parent, 'wp:positionV', relativeFrom='line'), 'wp:align').text = 'top' makeelement(parent, 'wp:extent', cx=str(width), cy=str(height)) if fake_margins: # DOCX does not support setting margins for inline images, so we # fake it by using effect extents to simulate margins makeelement(parent, 'wp:effectExtent', **{k[-1].lower():v for k, v in get_image_margins(style).iteritems()}) else: makeelement(parent, 'wp:effectExtent', l='0', r='0', t='0', b='0') if floating is not None: # The idiotic Word requires this to be after the extent settings if as_block: makeelement(parent, 'wp:wrapTopAndBottom') else: makeelement(parent, 'wp:wrapSquare', wrapText='bothSides') self.create_docx_image_markup(parent, name, html_img.get('alt') or name, img.rid, width, height) return ans
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 extension. Removes any transparency. If there is no transparency and no resize and the input and output image formats are the same, no changes are made. :param data: Image data as bytestring :param path: If None img data is returned, in JPEG format :param data_fmt: The fmt to return data in when path is None. Defaults to JPEG :param compression_quality: The quality of the image after compression. Number between 1 and 100. 1 means highest compression, 100 means no compression (lossless). :param bgcolor: The color for transparent pixels. Must be specified in hex. :param resize_to: A tuple (width, height) or None for no resizing :param minify_to: A tuple (width, height) to specify maximum target size. The image will be resized to fit into this target size. If None the value from the tweak is used. """ fmt = normalize_format_name(data_fmt if path is None else os.path.splitext(path)[1][1:]) if isinstance(data, QImage): img = data changed = True else: img, orig_fmt = image_and_format_from_data(data) orig_fmt = normalize_format_name(orig_fmt) changed = fmt != orig_fmt if resize_to is not None: changed = True img = img.scaled(resize_to[0], resize_to[1], Qt.IgnoreAspectRatio, Qt.SmoothTransformation) owidth, oheight = img.width(), img.height() nwidth, nheight = tweaks["maximum_cover_size"] if minify_to is None else minify_to scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight) if scaled: changed = True img = img.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) if img.hasAlphaChannel(): changed = True img = blend_image(img, bgcolor) if grayscale: if not img.allGray(): changed = True img = grayscale_image(img) if path is None: return image_to_data(img, compression_quality, fmt) if changed else data with lopen(path, "wb") as f: f.write(image_to_data(img, compression_quality, fmt) if changed else data)
def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model().db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id,)) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, page_width, page_height) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id,)) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id,)) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id,)) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id)
def resize_image(self, raw, base, max_width, max_height): img = Image() img.load(raw) resized, nwidth, nheight = fit_image(img.size[0], img.size[1], max_width, max_height) if resized: img.size = (nwidth, nheight) base, ext = os.path.splitext(base) base = base + '-%dx%d%s' % (max_width, max_height, ext) raw = img.export(ext[1:]) return raw, base, resized
def blend_on_canvas(img, width, height, bgcolor="#ffffff"): w, h = img.width(), img.height() scaled, nw, nh = fit_image(w, h, width, height) if scaled: img = img.scaled(nw, nh, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) w, h = nw, nh canvas = QImage(width, height, QImage.Format_RGB32) canvas.fill(QColor(bgcolor)) overlay(img, canvas, (width - w) // 2, (height - h) // 2) return canvas
def paintEvent(self, event): w = self.viewport().rect().width() painter = QPainter(self.viewport()) painter.setClipRect(event.rect()) floor = event.rect().bottom() ceiling = event.rect().top() fv = self.firstVisibleBlock().blockNumber() origin = self.contentOffset() doc = self.document() lines = [] for num, text in self.headers: top, bot = num, num + 3 if bot < fv: continue y_top = self.blockBoundingGeometry(doc.findBlockByNumber(top)).translated(origin).y() y_bot = self.blockBoundingGeometry(doc.findBlockByNumber(bot)).translated(origin).y() if max(y_top, y_bot) < ceiling: continue if min(y_top, y_bot) > floor: break painter.setFont(self.heading_font) br = painter.drawText(3, y_top, w, y_bot - y_top - 5, Qt.TextSingleLine, text) painter.setPen(QPen(self.palette().text(), 2)) painter.drawLine(0, br.bottom()+3, w, br.bottom()+3) for top, bot, kind in self.changes: if bot < fv: continue y_top = self.blockBoundingGeometry(doc.findBlockByNumber(top)).translated(origin).y() y_bot = self.blockBoundingGeometry(doc.findBlockByNumber(bot)).translated(origin).y() if max(y_top, y_bot) < ceiling: continue if min(y_top, y_bot) > floor: break if y_top != y_bot: painter.fillRect(0, y_top, w, y_bot - y_top, self.diff_backgrounds[kind]) lines.append((y_top, y_bot, kind)) if top in self.images: img, maxw = self.images[top][:2] if bot > top + 1 and not img.isNull(): y_top = self.blockBoundingGeometry(doc.findBlockByNumber(top+1)).translated(origin).y() + 3 y_bot -= 3 scaled, imgw, imgh = fit_image(img.width(), img.height(), w - 3, y_bot - y_top) painter.setRenderHint(QPainter.SmoothPixmapTransform, True) painter.drawPixmap(QRect(3, y_top, imgw, imgh), img) painter.end() PlainTextEdit.paintEvent(self, event) painter = QPainter(self.viewport()) painter.setClipRect(event.rect()) for top, bottom, kind in sorted(lines, key=lambda (t, b, k):{'replace':0}.get(k, 1)): painter.setPen(QPen(self.diff_foregrounds[kind], 1)) painter.drawLine(0, top, w, top) painter.drawLine(0, bottom - 1, w, bottom - 1)
def icon(ctx, rd, which): sz = rd.query.get("sz") if sz != "full": try: sz = int(rd.query.get("sz", 48)) except Exception: sz = 48 if which in {"", "_"}: raise HTTPNotFound() if which.startswith("_"): base = os.path.join(config_dir, "tb_icons") path = os.path.abspath(os.path.join(base, *which[1:].split("/"))) if not path.startswith(base) or ":" in which: raise HTTPNotFound("Naughty, naughty!") else: base = P("images", allow_user_override=False) path = os.path.abspath(os.path.join(base, *which.split("/"))) if not path.startswith(base) or ":" in which: raise HTTPNotFound("Naughty, naughty!") path = os.path.relpath(path, base).replace(os.sep, "/") path = P("images/" + path) if sz == "full": try: return share_open(path, "rb") except EnvironmentError: raise HTTPNotFound() with lock: tdir = os.path.join(rd.tdir, "icons") cached = os.path.join(tdir, "%d-%s.png" % (sz, which)) try: return share_open(cached, "rb") except EnvironmentError: pass try: src = share_open(path, "rb") except EnvironmentError: raise HTTPNotFound() with src: img = Image() img.load(src.read()) width, height = img.size scaled, width, height = fit_image(width, height, sz, sz) if scaled: img.size = (width, height) try: ans = share_open(cached, "w+b") except EnvironmentError: try: os.mkdir(tdir) except EnvironmentError: pass ans = share_open(cached, "w+b") ans.write(img.export("png")) ans.seek(0) return ans
def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return pixmap = self.pixmap pwidth, pheight = pixmap.width(), pixmap.height() try: self.pwidth, self.pheight = fit_image(pwidth, pheight, self.rect().width(), self.rect().height())[1:] except: self.pwidth, self.pheight = self.rect().width() - 1, self.rect().height() - 1 self.current_pixmap_size = QSize(self.pwidth, self.pheight) self.animation.setEndValue(self.current_pixmap_size)
def icon(ctx, rd, which): sz = rd.query.get('sz') if sz != 'full': try: sz = int(rd.query.get('sz', 48)) except Exception: sz = 48 if which in {'', '_'}: raise HTTPNotFound() if which.startswith('_'): base = os.path.join(config_dir, 'tb_icons') path = os.path.abspath(os.path.join(base, *which[1:].split('/'))) if not path.startswith(base) or ':' in which: raise HTTPNotFound('Naughty, naughty!') else: base = P('images', allow_user_override=False) path = os.path.abspath(os.path.join(base, *which.split('/'))) if not path.startswith(base) or ':' in which: raise HTTPNotFound('Naughty, naughty!') path = os.path.relpath(path, base).replace(os.sep, '/') path = P('images/' + path) if sz == 'full': try: return lopen(path, 'rb') except EnvironmentError: raise HTTPNotFound() tdir = os.path.join(rd.tdir, 'icons') cached = os.path.join(tdir, '%d-%s.png' % (sz, which)) try: return lopen(cached, 'rb') except EnvironmentError: pass try: src = lopen(path, 'rb') except EnvironmentError: raise HTTPNotFound() with src: img = Image() img.load(src.read()) width, height = img.size scaled, width, height = fit_image(width, height, sz, sz) if scaled: img.size = (width, height) try: ans = lopen(cached, 'w+b') except EnvironmentError: try: os.mkdir(tdir) except EnvironmentError: pass ans = lopen(cached, 'w+b') ans.write(img.export('png')) ans.seek(0) return ans
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() scaled, nw, nh = fit_image(w, h, width, height) if scaled: img = img.scaled(nw, nh, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) w, h = nw, nh canvas = QImage(width, height, QImage.Format_RGB32) canvas.fill(QColor(bgcolor)) overlay_image(img, canvas, (width - w)//2, (height - h)//2) return canvas
def icon(ctx, rd, which): sz = rd.query.get('sz') if sz != 'full': try: sz = int(rd.query.get('sz', 48)) except Exception: sz = 48 if which in {'', '_'}: raise HTTPNotFound() if which.startswith('_'): base = os.path.join(config_dir, 'tb_icons') path = os.path.abspath(os.path.join(base, *which[1:].split('/'))) if not path.startswith(base) or ':' in which: raise HTTPNotFound('Naughty, naughty!') else: base = P('images', allow_user_override=False) path = os.path.abspath(os.path.join(base, *which.split('/'))) if not path.startswith(base) or ':' in which: raise HTTPNotFound('Naughty, naughty!') path = os.path.relpath(path, base).replace(os.sep, '/') path = P('images/' + path) if sz == 'full': try: return share_open(path, 'rb') except EnvironmentError: raise HTTPNotFound() with lock: tdir = os.path.join(rd.tdir, 'icons') cached = os.path.join(tdir, '%d-%s.png' % (sz, which)) try: return share_open(cached, 'rb') except EnvironmentError: pass try: src = share_open(path, 'rb') except EnvironmentError: raise HTTPNotFound() with src: idata = src.read() img = image_from_data(idata) scaled, width, height = fit_image(img.width(), img.height(), sz, sz) if scaled: idata = scale_image(img, width, height, as_png=True)[-1] try: ans = share_open(cached, 'w+b') except EnvironmentError: try: os.mkdir(tdir) except EnvironmentError: pass ans = share_open(cached, 'w+b') ans.write(idata) ans.seek(0) return ans
def resize_cover(self): if self.cover_pixmap is None: return pixmap = self.cover_pixmap if self.fit_cover.isChecked(): scaled, new_width, new_height = fit_image(pixmap.width(), pixmap.height(), self.cover.size().width()-10, self.cover.size().height()-10) if scaled: pixmap = pixmap.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.cover.set_pixmap(pixmap) self.update_cover_tooltip()
def create_cover_page( top_lines, logo_path, width=590, height=750, bgcolor="#ffffff", output_format="jpg", texture_data=None, texture_opacity=1.0, ): """ Create the standard calibre cover page and return it as a byte string in the specified output_format. """ canvas = create_canvas(width, height, bgcolor) if texture_data and hasattr(canvas, "texture"): texture = Image() texture.load(texture_data) texture.set_opacity(texture_opacity) canvas.texture(texture) bottom = 10 for line in top_lines: twand = create_text_wand(line.font_size, font_path=line.font_path) bottom = draw_centered_text(canvas, twand, line.text, bottom) bottom += line.bottom_margin bottom -= top_lines[-1].bottom_margin foot_font = P("fonts/liberation/LiberationMono-Regular.ttf") vanity = create_text_arc(__appname__ + " " + __version__, 24, font=foot_font, bgcolor="#00000000") lwidth, lheight = vanity.size left = int(max(0, (width - lwidth) / 2.0)) top = height - lheight - 10 canvas.compose(vanity, left, top) available = (width, int(top - bottom) - 20) if available[1] > 40: logo = Image() logo.open(logo_path) lwidth, lheight = logo.size scaled, lwidth, lheight = fit_image(lwidth, lheight, *available) if scaled: logo.size = (lwidth, lheight) left = int(max(0, (width - lwidth) / 2.0)) top = bottom + 10 extra = int((available[1] - lheight) / 2.0) if extra > 0: top += extra canvas.compose(logo, left, top) return canvas.export(output_format)
def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block): top = title_block.position.y + 10 extra_spacing = subtitle_block.line_spacing // 2 if subtitle_block.line_spacing else title_block.line_spacing // 3 height = title_block.height + subtitle_block.height + extra_spacing + title_block.leading top += height + 25 bottom = footer_block.position.y - 50 logo = QImage(logo_path or I('library.png')) pwidth, pheight = rect.width(), bottom - top scaled, width, height = fit_image(logo.width(), logo.height(), pwidth, pheight) x, y = (pwidth - width) // 2, (pheight - height) // 2 rect = QRect(x, top + y, width, height) painter.setRenderHint(QPainter.SmoothPixmapTransform) painter.drawImage(rect, logo) return self.ccolor1, self.ccolor1, self.ccolor1
def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True): if preserve_aspect_ratio: aspect_ratio = float(p.width()) / p.height() nw, nh = page_rect.width(), page_rect.height() if aspect_ratio > 1: nh = int(page_rect.width() / aspect_ratio) else: # Width is smaller than height nw = page_rect.height() * aspect_ratio __, nnw, nnh = fit_image(nw, nh, page_rect.width(), page_rect.height()) dx = int((page_rect.width() - nnw) / 2.0) dy = int((page_rect.height() - nnh) / 2.0) page_rect.translate(dx, dy) page_rect.setHeight(nnh) page_rect.setWidth(nnw) painter.drawPixmap(page_rect, p, p.rect())
def rescale(self, qt=True): from calibre.utils.magick.draw import Image is_image_collection = getattr(self.opts, 'is_image_collection', False) if is_image_collection: page_width, page_height = self.opts.dest.comic_screen_size else: page_width, page_height = self.opts.dest.width, self.opts.dest.height page_width -= (self.opts.margin_left + self.opts.margin_right) * self.opts.dest.dpi/72. page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi/72. for item in self.oeb.manifest: if item.media_type.startswith('image'): ext = item.media_type.split('/')[-1].upper() if ext == 'JPG': ext = 'JPEG' if ext not in ('PNG', 'JPEG', 'GIF'): ext = 'JPEG' raw = item.data if hasattr(raw, 'xpath') or not raw: # Probably an svg image continue try: img = Image() img.load(raw) except: continue width, height = img.size scaled, new_width, new_height = fit_image(width, height, page_width, page_height) if scaled: new_width = max(1, new_width) new_height = max(1, new_height) self.log('Rescaling image from %dx%d to %dx%d'%( width, height, new_width, new_height), item.href) try: img.size = (new_width, new_height) data = img.export(ext.lower()) except KeyboardInterrupt: raise except: self.log.exception('Failed to rescale image') else: item.data = data item.unload_data_from_memory()
def load_pixmap(self): canvas_size = self.rect().width(), self.rect().height() if self.last_canvas_size != canvas_size: if self.last_canvas_size is not None and self.selection_state.rect is not None: self.selection_state.reset() # TODO: Migrate the selection rect self.last_canvas_size = canvas_size self.current_scaled_pixmap = None if self.current_scaled_pixmap is None: pwidth, pheight = self.last_canvas_size i = self.current_image width, height = i.width(), i.height() scaled, width, height = fit_image(width, height, pwidth, pheight) if scaled: i = self.current_image.scaled(width, height, transformMode=Qt.SmoothTransformation) self.current_scaled_pixmap = QPixmap.fromImage(i)
def resize_cover(self): if self.cover_pixmap is None: return self.setWindowIcon(QIcon(self.cover_pixmap)) pixmap = self.cover_pixmap if self.fit_cover.isChecked(): scaled, new_width, new_height = fit_image(pixmap.width(), pixmap.height(), self.cover.size().width()-10, self.cover.size().height()-10) if scaled: pixmap = pixmap.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) self.cover.set_pixmap(pixmap) sz = pixmap.size() self.cover.setToolTip(_('Cover size: %(width)d x %(height)d')%dict( width=sz.width(), height=sz.height()))
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 paintEvent(self, event): QWidget.paintEvent(self, event) pmap = self._pixmap if pmap.isNull(): return w, h = pmap.width(), pmap.height() ow, oh = w, h cw, ch = self.rect().width(), self.rect().height() scaled, nw, nh = fit_image(w, h, cw, ch) if scaled: pmap = pmap.scaled(int(nw * pmap.devicePixelRatio()), int(nh * pmap.devicePixelRatio()), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) w, h = int(pmap.width() / pmap.devicePixelRatio()), int( pmap.height() / pmap.devicePixelRatio()) x = int(abs(cw - w) / 2.) y = int(abs(ch - h) / 2.) target = QRect(x, y, w, h) p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap(target, pmap) if self.draw_border: pen = QPen() pen.setWidth(self.BORDER_WIDTH) p.setPen(pen) p.drawRect(target) if self.show_size: sztgt = target.adjusted(0, 0, 0, -4) f = p.font() f.setBold(True) p.setFont(f) sz = u'\u00a0%d x %d\u00a0' % (ow, oh) flags = Qt.AlignBottom | Qt.AlignRight | Qt.TextSingleLine szrect = p.boundingRect(sztgt, flags, sz) p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) p.setPen(QPen(QColor(255, 255, 255))) p.drawText(sztgt, flags, sz) p.end()
def create_cover(report, icons=(), cols=5, size=120, padding=16): icons = icons or tuple(default_cover_icons(cols)) rows = int(math.ceil(len(icons) / cols)) with Canvas(cols * (size + padding), rows * (size + padding), bgcolor='#eee') as canvas: y = -size - padding // 2 x = 0 for i, icon in enumerate(icons): if i % cols == 0: y += padding + size x = padding // 2 else: x += size + padding if report and icon in report.name_map: ipath = os.path.join(report.path, report.name_map[icon]) else: ipath = I(icon, allow_user_override=False) with lopen(ipath, 'rb') as f: img = image_from_data(f.read()) scaled, nwidth, nheight = fit_image(img.width(), img.height(), size, size) img = img.scaled(nwidth, nheight, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) dx = (size - nwidth) // 2 canvas.compose(img, x + dx, y) return canvas.export()
def resize_cover(self): if self.cover_pixmap is None: self.cover.set_marked(self.marked) return pixmap = self.cover_pixmap if self.fit_cover.isChecked(): scaled, new_width, new_height = fit_image( pixmap.width(), pixmap.height(), self.cover.size().width() - 10, self.cover.size().height() - 10) if scaled: try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() pixmap = pixmap.scaled( int(dpr * new_width), int(dpr * new_height), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) pixmap.setDevicePixelRatio(dpr) self.cover.set_pixmap(pixmap) self.cover.set_marked(self.marked) self.update_cover_tooltip()
def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg', preserve_aspect_ratio=True, compression_quality=70): img = Image() img.load(data) owidth, oheight = img.size if not preserve_aspect_ratio: scaled = owidth > width or oheight > height nwidth = width nheight = height else: scaled, nwidth, nheight = fit_image(owidth, oheight, width, height) if scaled: img.size = (nwidth, nheight) canvas = create_canvas(img.size[0], img.size[1], bgcolor) canvas.compose(img) if fmt == 'jpg': canvas.set_compression_quality(compression_quality) return (canvas.size[0], canvas.size[1], canvas.export(fmt))
def paint(self, painter, option, index): name = index.data(Qt.ItemDataRole.DisplayRole) sz = human_readable(index.data(Qt.ItemDataRole.UserRole)) pmap = index.data(Qt.ItemDataRole.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.TransformationMode.SmoothTransformation) index.model().setData(index, pmap, Qt.ItemDataRole.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.StateFlag.State_Selected: painter.setPen(QPen(option.palette.color(QPalette.ColorRole.HighlightedText))) painter.drawText(trect, Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, name + '\n' + sz) painter.restore()
def load_pixmap(self): canvas_size = self.rect().width(), self.rect().height() if self.last_canvas_size != canvas_size: if self.last_canvas_size is not None and self.selection_state.rect is not None: self.selection_state.reset() # TODO: Migrate the selection rect self.last_canvas_size = canvas_size self.current_scaled_pixmap = None if self.current_scaled_pixmap is None: pwidth, pheight = self.last_canvas_size i = self.current_image width, height = i.width(), i.height() scaled, width, height = fit_image(width, height, pwidth, pheight) try: dpr = self.devicePixelRatioF() except AttributeError: dpr = self.devicePixelRatio() if scaled: i = self.current_image.scaled( int(dpr * width), int(dpr * height), transformMode=Qt.TransformationMode.SmoothTransformation) self.current_scaled_pixmap = QPixmap.fromImage(i) self.current_scaled_pixmap.setDevicePixelRatio(dpr)
def paint(self, painter, option, index): QStyledItemDelegate.paint(self, painter, option, QModelIndex()) # draw the hover and selection highlights name = unicode(index.data(Qt.DisplayRole) or '') cover = self.cover_cache.get(name, None) if cover is None: cover = self.cover_cache[name] = QPixmap() try: raw = current_container().raw_data(name, decode=False) except: pass else: cover.loadFromData(raw) if not cover.isNull(): scaled, width, height = fit_image(cover.width(), cover.height(), self.cover_size.width(), self.cover_size.height()) if scaled: cover = self.cover_cache[name] = cover.scaled(width, height, transformMode=Qt.SmoothTransformation) painter.save() try: rect = option.rect rect.adjust(self.MARGIN, self.MARGIN, -self.MARGIN, -self.MARGIN) trect = QRect(rect) rect.setBottom(rect.bottom() - self.title_height) if not cover.isNull(): dx = max(0, int((rect.width() - cover.width())/2.0)) dy = max(0, rect.height() - cover.height()) rect.adjust(dx, dy, -dx, 0) painter.drawPixmap(rect, cover) rect = trect rect.setTop(rect.bottom() - self.title_height + 5) painter.setRenderHint(QPainter.TextAntialiasing, True) metrics = painter.fontMetrics() painter.drawText(rect, Qt.AlignCenter|Qt.TextSingleLine, metrics.elidedText(name, Qt.ElideLeft, rect.width())) finally: painter.restore()
def rescale_image(self, data): orig_w, orig_h, ifmt = identify_data(data) orig_data = data # save it in case compression fails if self.scale_news_images is not None: wmax, hmax = self.scale_news_images scale, new_w, new_h = fit_image(orig_w, orig_h, wmax, hmax) if scale: data = thumbnail(data, new_w, new_h, compression_quality=95)[-1] orig_w = new_w orig_h = new_h if self.compress_news_images_max_size is None: if self.compress_news_images_auto_size is None: # not compressing return data else: maxsizeb = (orig_w * orig_h)/self.compress_news_images_auto_size else: maxsizeb = self.compress_news_images_max_size * 1024 scaled_data = data # save it in case compression fails if len(scaled_data) <= maxsizeb: # no compression required return scaled_data img = Image() quality = 95 img.load(data) while len(data) >= maxsizeb and quality >= 5: quality -= 5 img.set_compression_quality(quality) data = img.export('jpg') if len(data) >= len(scaled_data): # compression failed return orig_data if len(orig_data) <= len(scaled_data) else scaled_data if len(data) >= len(orig_data): # no improvement return orig_data return data
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80): try: cherrypy.response.headers['Content-Type'] = 'image/jpeg' cherrypy.response.timeout = 3600 cover = self.db.cover(id, index_is_id=True) if cover is None: cover = self.default_cover updated = self.build_time else: updated = self.db.cover_last_modified(id, index_is_id=True) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) if thumbnail: quality = tweaks['content_server_thumbnail_compression_quality'] if quality < 50: quality = 50 elif quality > 99: quality = 99 return generate_thumbnail(cover, width=thumb_width, height=thumb_height, compression_quality=quality)[-1] img = Image() img.load(cover) width, height = img.size scaled, width, height = fit_image(width, height, thumb_width if thumbnail else self.max_cover_width, thumb_height if thumbnail else self.max_cover_height) if not scaled: return cover return save_cover_data_to(img, 'img.jpg', return_data=True, resize_to=(width, height)) except Exception as err: import traceback cherrypy.log.error('Failed to generate cover:') cherrypy.log.error(traceback.print_exc()) raise cherrypy.HTTPError(404, 'Failed to generate cover: %r'%err)
def rescale(self): from PIL import Image from io import BytesIO is_image_collection = getattr(self.opts, 'is_image_collection', False) if is_image_collection: page_width, page_height = self.opts.dest.comic_screen_size else: page_width, page_height = self.opts.dest.width, self.opts.dest.height page_width -= (self.opts.margin_left + self.opts.margin_right) * self.opts.dest.dpi / 72. page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi / 72. for item in self.oeb.manifest: if item.media_type.startswith('image'): ext = item.media_type.split('/')[-1].upper() if ext == 'JPG': ext = 'JPEG' if ext not in ('PNG', 'JPEG', 'GIF'): ext = 'JPEG' raw = item.data if hasattr(raw, 'xpath') or not raw: # Probably an svg image continue try: img = Image.open(BytesIO(raw)) except Exception: continue width, height = img.size try: if self.check_colorspaces and img.mode == 'CMYK': self.log.warn( 'The image %s is in the CMYK colorspace, converting it ' 'to RGB as Adobe Digital Editions cannot display CMYK' % item.href) img = img.convert('RGB') except Exception: self.log.exception( 'Failed to convert image %s from CMYK to RGB' % item.href) scaled, new_width, new_height = fit_image( width, height, page_width, page_height) if scaled: new_width = max(1, new_width) new_height = max(1, new_height) self.log( 'Rescaling image from %dx%d to %dx%d' % (width, height, new_width, new_height), item.href) img.resize((new_width, new_height)) buf = BytesIO() try: img.save(buf, ext) except Exception: self.log.exception('Failed to rescale image') else: item.data = buf.getvalue() item.unload_data_from_memory()
def rescale(self, qt=True): from calibre.utils.magick.draw import Image is_image_collection = getattr(self.opts, 'is_image_collection', False) if is_image_collection: page_width, page_height = self.opts.dest.comic_screen_size else: page_width, page_height = self.opts.dest.width, self.opts.dest.height page_width -= (self.opts.margin_left + self.opts.margin_right) * self.opts.dest.dpi/72. page_height -= (self.opts.margin_top + self.opts.margin_bottom) * self.opts.dest.dpi/72. for item in self.oeb.manifest: if item.media_type.startswith('image'): ext = item.media_type.split('/')[-1].upper() if ext == 'JPG': ext = 'JPEG' if ext not in ('PNG', 'JPEG', 'GIF'): ext = 'JPEG' raw = item.data if hasattr(raw, 'xpath') or not raw: # Probably an svg image continue try: img = Image() img.load(raw) except: continue width, height = img.size try: if self.check_colorspaces and img.colorspace == 'CMYKColorspace': # We cannot do an imagemagick conversion of CMYK to RGB as # ImageMagick inverts colors if you just set the colorspace # to rgb. See for example: https://bugs.launchpad.net/bugs/1246710 from PyQt4.Qt import QImage from calibre.gui2 import pixmap_to_data qimg = QImage() qimg.loadFromData(raw) if not qimg.isNull(): raw = item.data = pixmap_to_data(qimg, format=ext, quality=95) img = Image() img.load(raw) self.log.warn( 'The image %s is in the CMYK colorspace, converting it ' 'to RGB as Adobe Digital Editions cannot display CMYK' % item.href) else: self.log.warn( 'The image %s is in the CMYK colorspace, you should convert' ' it to sRGB as Adobe Digital Editions cannot render CMYK' % item.href) except Exception: pass scaled, new_width, new_height = fit_image(width, height, page_width, page_height) if scaled: new_width = max(1, new_width) new_height = max(1, new_height) self.log('Rescaling image from %dx%d to %dx%d'%( width, height, new_width, new_height), item.href) try: img.size = (new_width, new_height) data = img.export(ext.lower()) except KeyboardInterrupt: raise except: self.log.exception('Failed to rescale image') else: item.data = data item.unload_data_from_memory()
def render_cover(self, book_id): if self.ignore_render_requests.is_set(): return dpr = self.device_pixel_ratio page_width = int(dpr * self.delegate.cover_size.width()) page_height = int(dpr * self.delegate.cover_size.height()) tcdata, timestamp = self.thumbnail_cache[book_id] use_cache = False if timestamp is None: # Not in cache has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, 0) else: has_cover, cdata, timestamp = self.model( ).db.new_api.cover_or_cache(book_id, timestamp) if has_cover and cdata is None: # The cached cover is fresh cdata = tcdata use_cache = True if has_cover: p = QImage() p.loadFromData(cdata, CACHE_FORMAT if cdata is tcdata else 'JPEG') p.setDevicePixelRatio(dpr) if p.isNull() and cdata is tcdata: # Invalid image in cache self.thumbnail_cache.invalidate((book_id, )) self.update_item.emit(book_id) return cdata = None if p.isNull() else p if not use_cache: # cache is stale if cdata is not None: width, height = p.width(), p.height() scaled, nwidth, nheight = fit_image( width, height, page_width, page_height) if scaled: if self.ignore_render_requests.is_set(): return p = p.scaled( nwidth, nheight, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) p.setDevicePixelRatio(dpr) cdata = p # update cache if cdata is None: self.thumbnail_cache.invalidate((book_id, )) else: try: self.thumbnail_cache.insert(book_id, timestamp, image_to_data(cdata)) except EncodeError as err: self.thumbnail_cache.invalidate((book_id, )) prints(err) except Exception: import traceback traceback.print_exc() elif tcdata is not None: # Cover was removed, but it exists in cache, remove from cache self.thumbnail_cache.invalidate((book_id, )) self.delegate.cover_cache.set(book_id, cdata) self.update_item.emit(book_id)
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 create_image_markup(self, html_img, stylizer, href, as_block=False): # TODO: img inside a link (clickable image) style = stylizer.style(html_img) floating = style['float'] if floating not in {'left', 'right'}: floating = None if as_block: ml, mr = style._get('margin-left'), style._get('margin-right') if ml == 'auto': floating = 'center' if mr == 'auto' else 'right' if mr == 'auto': floating = 'center' if ml == 'auto' else 'right' else: parent = html_img.getparent() if len(parent) == 1 and not (parent.text or '').strip() and not ( html_img.tail or '').strip(): pstyle = stylizer.style(parent) if 'block' in pstyle['display']: # We have an inline image alone inside a block as_block = True floating = pstyle['float'] if floating not in {'left', 'right'}: floating = None if pstyle['text-align'] in ('center', 'right'): floating = pstyle['text-align'] floating = floating or 'left' fake_margins = floating is None self.count += 1 img = self.images[href] name = urlunquote(posixpath.basename(href)) width, height = style.img_size(img.width, img.height) scaled, width, height = fit_image(width, height, self.page_width, self.page_height) width, height = map(pt_to_emu, (width, height)) makeelement, namespaces = self.document_relationships.namespace.makeelement, self.document_relationships.namespace.namespaces root = etree.Element('root', nsmap=namespaces) ans = makeelement(root, 'w:drawing', append=False) if floating is None: parent = makeelement(ans, 'wp:inline') else: parent = makeelement(ans, 'wp:anchor', **get_image_margins(style)) # The next three lines are boilerplate that Word requires, even # though the DOCX specs define defaults for all of them parent.set('simplePos', '0'), parent.set('relativeHeight', '1'), parent.set( 'behindDoc', "0"), parent.set('locked', "0") parent.set('layoutInCell', "1"), parent.set('allowOverlap', '1') makeelement(parent, 'wp:simplePos', x='0', y='0') makeelement( makeelement(parent, 'wp:positionH', relativeFrom='margin'), 'wp:align').text = floating makeelement( makeelement(parent, 'wp:positionV', relativeFrom='line'), 'wp:align').text = 'top' makeelement(parent, 'wp:extent', cx=str(width), cy=str(height)) if fake_margins: # DOCX does not support setting margins for inline images, so we # fake it by using effect extents to simulate margins makeelement( parent, 'wp:effectExtent', **{ k[-1].lower(): v for k, v in get_image_margins(style).iteritems() }) else: makeelement(parent, 'wp:effectExtent', l='0', r='0', t='0', b='0') if floating is not None: # The idiotic Word requires this to be after the extent settings if as_block: makeelement(parent, 'wp:wrapTopAndBottom') else: makeelement(parent, 'wp:wrapSquare', wrapText='bothSides') self.create_docx_image_markup(parent, name, html_img.get('alt') or name, img.rid, width, height) return ans
def paintEvent(self, event): w = self.viewport().rect().width() painter = QPainter(self.viewport()) painter.setClipRect(event.rect()) floor = event.rect().bottom() ceiling = event.rect().top() fv = self.firstVisibleBlock().blockNumber() origin = self.contentOffset() doc = self.document() lines = [] for num, text in self.headers: top, bot = num, num + 3 if bot < fv: continue y_top = self.blockBoundingGeometry( doc.findBlockByNumber(top)).translated(origin).y() y_bot = self.blockBoundingGeometry( doc.findBlockByNumber(bot)).translated(origin).y() if max(y_top, y_bot) < ceiling: continue if min(y_top, y_bot) > floor: break painter.setFont(self.heading_font) br = painter.drawText(3, y_top, w, y_bot - y_top - 5, Qt.TextSingleLine, text) painter.setPen(QPen(self.palette().text(), 2)) painter.drawLine(0, br.bottom() + 3, w, br.bottom() + 3) for top, bot, kind in self.changes: if bot < fv: continue y_top = self.blockBoundingGeometry( doc.findBlockByNumber(top)).translated(origin).y() y_bot = self.blockBoundingGeometry( doc.findBlockByNumber(bot)).translated(origin).y() if max(y_top, y_bot) < ceiling: continue if min(y_top, y_bot) > floor: break if y_top != y_bot: painter.fillRect(0, y_top, w, y_bot - y_top, self.diff_backgrounds[kind]) lines.append((y_top, y_bot, kind)) if top in self.images: img, maxw = self.images[top][:2] if bot > top + 1 and not img.isNull(): y_top = self.blockBoundingGeometry( doc.findBlockByNumber(top + 1)).translated(origin).y() + 3 y_bot -= 3 scaled, imgw, imgh = fit_image(img.width(), img.height(), w - 3, y_bot - y_top) painter.setRenderHint(QPainter.SmoothPixmapTransform, True) painter.drawPixmap(QRect(3, y_top, imgw, imgh), img) painter.end() PlainTextEdit.paintEvent(self, event) painter = QPainter(self.viewport()) painter.setClipRect(event.rect()) for top, bottom, kind in sorted(lines, key=lambda (t, b, k): {'replace': 0}.get(k, 1)): painter.setPen(QPen(self.diff_foregrounds[kind], 1)) painter.drawLine(0, top, w, top) painter.drawLine(0, bottom - 1, w, bottom - 1)
def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, return_data=False, compression_quality=90, minify_to=None, grayscale=False): ''' Saves image in data to path, in the format specified by the path extension. Removes any transparency. If there is no transparency and no resize and the input and output image formats are the same, no changes are made. :param data: Image data as bytestring or Image object :param compression_quality: The quality of the image after compression. Number between 1 and 100. 1 means highest compression, 100 means no compression (lossless). :param bgcolor: The color for transparent pixels. Must be specified in hex. :param resize_to: A tuple (width, height) or None for no resizing :param minify_to: A tuple (width, height) to specify maximum target size. :param grayscale: If True, the image is grayscaled will be resized to fit into this target size. If None the value from the tweak is used. ''' changed = False img = _data_to_image(data) orig_fmt = normalize_format_name(img.format) fmt = os.path.splitext(path)[1] fmt = normalize_format_name(fmt[1:]) if grayscale: img.type = "GrayscaleType" changed = True if resize_to is not None: img.size = (resize_to[0], resize_to[1]) changed = True owidth, oheight = img.size nwidth, nheight = tweaks[ 'maximum_cover_size'] if minify_to is None else minify_to scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight) if scaled: img.size = (nwidth, nheight) changed = True if img.has_transparent_pixels(): canvas = create_canvas(img.size[0], img.size[1], bgcolor) canvas.compose(img) img = canvas changed = True if not changed: changed = fmt != orig_fmt ret = None if return_data: ret = data if changed or isinstance(ret, Image): if hasattr(img, 'set_compression_quality') and fmt == 'jpg': img.set_compression_quality(compression_quality) ret = img.export(fmt) else: if changed or isinstance(ret, Image): if hasattr(img, 'set_compression_quality') and fmt == 'jpg': img.set_compression_quality(compression_quality) img.save(path) else: with lopen(path, 'wb') as f: f.write(data) return ret
def _generate_thumbnail(self, book): ''' Fetch the cover image, generate a thumbnail, cache Extracts covers from zipped epubs ''' self._log_location(book.title) #self._log("book_path: %s" % book.path) #self._log("book: '%s' by %s uuid: %s" % (book.title, book.author, book.uuid)) # Parse the cover from the connected device, model Fetch_Annotations:_get_epub_toc() thumb_data = None thumb_path = book.path.rpartition('.')[0] + '.jpg' # Try getting the cover from the cache try: zfr = ZipFile(self.archive_path) thumb_data = zfr.read(thumb_path) if thumb_data == 'None': self._log("returning None from cover cache") zfr.close() return None except: self._log("opening cover cache for appending") zfw = ZipFile(self.archive_path, mode='a') else: self._log("returning thumb from cover cache") return thumb_data # Get the cover from the book try: stream = cStringIO.StringIO(self.ios.read(book.path, mode='rb')) mi = get_metadata(stream) if mi.cover_data is not None: img_data = cStringIO.StringIO(mi.cover_data[1]) except: if self.verbose: self._log("ERROR: unable to get cover from '%s'" % book.title) import traceback #traceback.print_exc() exc_type, exc_value, exc_traceback = sys.exc_info() self._log(traceback.format_exception_only(exc_type, exc_value)[0].strip()) return thumb_data # Generate a thumb try: im = PILImage.open(img_data) scaled, width, height = fit_image(im.size[0], im.size[1], 60, 80) im = im.resize((int(width), int(height)), PILImage.ANTIALIAS) thumb = cStringIO.StringIO() im.convert('RGB').save(thumb, 'JPEG') thumb_data = thumb.getvalue() thumb.close() self._log("SUCCESS: generated thumb for '%s', caching" % book.title) # Cache the tagged thumb zfw.writestr(thumb_path, thumb_data) except: if self.verbose: self._log("ERROR generating thumb for '%s', caching empty marker" % book.title) import traceback exc_type, exc_value, exc_traceback = sys.exc_info() self._log(traceback.format_exception_only(exc_type, exc_value)[0].strip()) # Cache the empty cover zfw.writestr(thumb_path, 'None') finally: img_data.close() zfw.close() return thumb_data
def pixmap(self, thumbnail_height, entry): pmap = QPixmap(current_container().name_to_abspath(entry.name)) if entry.width > 0 and entry.height > 0 else QPixmap() scaled, width, height = fit_image(entry.width, entry.height, thumbnail_height, thumbnail_height) if scaled and not pmap.isNull(): pmap = pmap.scaled(width, height, transformMode=Qt.SmoothTransformation) return pmap
def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compression_quality=90, minify_to=None, grayscale=False, eink=False, letterbox=False, letterbox_color='#000000', data_fmt='jpeg'): ''' Saves image in data to path, in the format specified by the path extension. Removes any transparency. If there is no transparency and no resize and the input and output image formats are the same, no changes are made. :param data: Image data as bytestring :param path: If None img data is returned, in JPEG format :param data_fmt: The fmt to return data in when path is None. Defaults to JPEG :param compression_quality: The quality of the image after compression. Number between 1 and 100. 1 means highest compression, 100 means no compression (lossless). When generating PNG this number is divided by 10 for the png_compression_level. :param bgcolor: The color for transparent pixels. Must be specified in hex. :param resize_to: A tuple (width, height) or None for no resizing :param minify_to: A tuple (width, height) to specify maximum target size. The image will be resized to fit into this target size. If None the value from the tweak is used. :param grayscale: If True, the image is converted to grayscale, if that's not already the case. :param eink: If True, the image is dithered down to the 16 specific shades of gray of the eInk palette. Works best with formats that actually support color indexing (i.e., PNG) :param letterbox: If True, in addition to fit resize_to inside minify_to, the image will be letterboxed (i.e., centered on a black background). :param letterbox_color: If letterboxing is used, this is the background color used. The default is black. ''' fmt = normalize_format_name( data_fmt if path is None else os.path.splitext(path)[1][1:]) if isinstance(data, QImage): img = data changed = True else: img, orig_fmt = image_and_format_from_data(data) orig_fmt = normalize_format_name(orig_fmt) changed = fmt != orig_fmt if resize_to is not None: changed = True img = img.scaled(resize_to[0], resize_to[1], Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) owidth, oheight = img.width(), img.height() nwidth, nheight = tweaks[ 'maximum_cover_size'] if minify_to is None else minify_to if letterbox: img = blend_on_canvas(img, nwidth, nheight, bgcolor=letterbox_color) # Check if we were minified if oheight != nheight or owidth != nwidth: changed = True else: scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight) if scaled: changed = True img = img.scaled(nwidth, nheight, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation) if img.hasAlphaChannel(): changed = True img = blend_image(img, bgcolor) if grayscale and not eink: if not img.allGray(): changed = True img = grayscale_image(img) if eink: # NOTE: Keep in mind that JPG does NOT actually support indexed colors, so the JPG algorithm will then smush everything back into a 256c mess... # Thankfully, Nickel handles PNG just fine, and we potentially generate smaller files to boot, because they can be properly color indexed ;). img = eink_dither_image(img) changed = True if path is None: return image_to_data(img, compression_quality, fmt, compression_quality // 10) if changed else data with lopen(path, 'wb') as f: f.write( image_to_data(img, compression_quality, fmt, compression_quality // 10) if changed else data)
def _get_goodreader_thumb(self, remote_path): ''' remote_path is relative to /Documents GoodReader caches small thumbs of book covers. If we didn't send the book, fetch the cached copy from the iDevice. These thumbs will be scaled up to the size we use when sending from calibre for consistency. ''' from PIL import Image as PILImage from calibre import fit_image def _build_local_path(): ''' GoodReader stores individual dbs for each book, matching the folder and name structure in the Documents folder. Make a local version, renamed to .db ''' path = remote_db_path.split('/')[-1] if iswindows: from calibre.utils.filenames import shorten_components_to plen = len(self.temp_dir) path = ''.join(shorten_components_to(245 - plen, [path])) full_path = os.path.join(self.temp_dir, path) base = os.path.splitext(full_path)[0] full_path = base + ".db" return os.path.normpath(full_path) self._log_location(remote_path) remote_db_path = '/'.join([ '/Library', 'Application Support', 'com.goodiware.GoodReader.ASRoot', 'Previews', '0', remote_path ]) thumb_data = None db_stats = self.ios.stat(remote_db_path) if db_stats: full_path = _build_local_path() with open(full_path, 'wb') as out: self.ios.copy_from_idevice(remote_db_path, out) local_db_path = out.name con = sqlite3.connect(local_db_path) with con: con.row_factory = sqlite3.Row cur = con.cursor() cur.execute('''SELECT thumb FROM Pages WHERE pageNum = "1" ''') row = cur.fetchone() if row: img_data = cStringIO.StringIO(row[b'thumb']) im = PILImage.open(img_data) scaled, width, height = fit_image(im.size[0], im.size[1], self.COVER_WIDTH, self.COVER_HEIGHT) im = im.resize((self.COVER_WIDTH, self.COVER_HEIGHT), PILImage.NEAREST) thumb = cStringIO.StringIO() im.convert('RGB').save(thumb, 'JPEG') thumb_data = thumb.getvalue() img_data.close() thumb.close() return thumb_data
def _generate_thumbnail(self, book, cover_path): ''' Fetch the cover image, generate a thumbnail, cache Specific implementation for iBooks ''' self._log_location(book.title) self._log_diagnostic(" book_path: %s" % book.path) self._log_diagnostic("cover_path: %s" % repr(cover_path)) thumb_data = None thumb_path = book.path.rpartition('.')[0] + '.jpg' # Try getting the cover from the cache try: zfr = ZipFile(self.archive_path) thumb_data = zfr.read(thumb_path) if thumb_data == 'None': self._log_diagnostic("returning None from cover cache") zfr.close() return None except: self._log_diagnostic("opening cover cache for appending") zfw = ZipFile(self.archive_path, mode='a') else: self._log_diagnostic("returning thumb from cover cache") return thumb_data ''' # Is book.path a directory (iBooks) or an epub? stats = self.ios.stat(book.path) if stats['st_ifmt'] == 'S_IFDIR': # *** This needs to fetch the cover data from the directory *** self._log_diagnostic("returning None, can't read iBooks covers yet") return thumb_data # Get the cover from the book try: stream = cStringIO.StringIO(self.ios.read(book.path, mode='rb')) mi = get_metadata(stream) if mi.cover_data is not None: img_data = cStringIO.StringIO(mi.cover_data[1]) except: if self.verbose: self._log_diagnostic("ERROR: unable to get cover from '%s'" % book.title) import traceback #traceback.print_exc() exc_type, exc_value, exc_traceback = sys.exc_info() self._log_diagnostic(traceback.format_exception_only(exc_type, exc_value)[0].strip()) return thumb_data ''' try: img_data = cStringIO.StringIO(self.ios.read(cover_path, mode='rb')) except: if self.verbose: self._log_diagnostic("ERROR fetching cover data for '%s', caching empty marker" % book.title) import traceback exc_type, exc_value, exc_traceback = sys.exc_info() self._log_diagnostic(traceback.format_exception_only(exc_type, exc_value)[0].strip()) # Cache the empty cover zfw.writestr(thumb_path, 'None') return thumb_data # Generate a thumb try: im = PILImage.open(img_data) scaled, width, height = fit_image(im.size[0], im.size[1], 60, 80) im = im.resize((int(width), int(height)), PILImage.ANTIALIAS) thumb = cStringIO.StringIO() im.convert('RGB').save(thumb, 'JPEG') thumb_data = thumb.getvalue() thumb.close() self._log_diagnostic("SUCCESS: generated thumb for '%s', caching" % book.title) # Cache the tagged thumb zfw.writestr(thumb_path, thumb_data) except: if self.verbose: self._log_diagnostic("ERROR generating thumb for '%s', caching empty marker" % book.title) import traceback exc_type, exc_value, exc_traceback = sys.exc_info() self._log_diagnostic(traceback.format_exception_only(exc_type, exc_value)[0].strip()) # Cache the empty cover zfw.writestr(thumb_path, 'None') finally: #img_data.close() zfw.close() return thumb_data
def create_cover_page(top_lines, bottom_lines, display_image, options, image_path, output_format='jpg'): from calibre.gui2 import ensure_app ensure_app() (width, height) = options.get(cfg.KEY_SIZE, (590, 750)) margins = options.get(cfg.KEY_MARGINS) (top_mgn, bottom_mgn, left_mgn, right_mgn, image_mgn) = ( margins['top'], margins['bottom'], margins['left'], margins['right'], margins['image']) left_mgn = min([left_mgn, (width / 2) - 10]) left_text_margin = left_mgn if left_mgn > 0 else 10 right_mgn = min([right_mgn, (width / 2) - 10]) right_text_margin = right_mgn if right_mgn > 0 else 10 colors = options[cfg.KEY_COLORS] bgcolor, border_color, fill_color, stroke_color = ( colors['background'], colors['border'], colors['fill'], colors['stroke']) if not options.get(cfg.KEY_COLOR_APPLY_STROKE, False): stroke_color = None auto_reduce_font = options.get(cfg.KEY_FONTS_AUTOREDUCED, False) borders = options[cfg.KEY_BORDERS] (cover_border_width, image_border_width) = ( borders['coverBorder'], borders['imageBorder']) is_background_image = options.get(cfg.KEY_BACKGROUND_IMAGE, False) if image_path: if not os.path.exists(image_path) or os.path.getsize(image_path) == 0: display_image = is_background_image = False canvas = create_canvas(width - cover_border_width * 2, height - cover_border_width * 2, bgcolor) if cover_border_width > 0: canvas = add_border(canvas, cover_border_width, border_color, bgcolor) if is_background_image: logo = Image() logo.open(image_path) outer_margin = 0 if cover_border_width == 0 else cover_border_width logo.size = (width - outer_margin * 2, height - outer_margin * 2) left = top = outer_margin canvas.compose(logo, int(left), int(top)) top = top_mgn if len(top_lines) > 0: for line in top_lines: twand = create_colored_text_wand(line, fill_color, stroke_color) top = draw_sized_text( canvas, twand, line, top, left_text_margin, right_text_margin, auto_reduce_font) top += line.bottom_margin top -= top_lines[-1].bottom_margin if len(bottom_lines) > 0: # Draw this on a fake canvas so can determine the space required fake_canvas = create_canvas(width, height, bgcolor) footer_height = 0 for line in bottom_lines: line.twand = create_colored_text_wand( line, fill_color, stroke_color) footer_height = draw_sized_text( fake_canvas, line.twand, line, footer_height, left_text_margin, right_text_margin, auto_reduce_font) footer_height += line.bottom_margin footer_height -= bottom_lines[-1].bottom_margin footer_top = height - footer_height - bottom_mgn bottom = footer_top # Re-use the text wand from previously which we will have adjusted the # font size on for line in bottom_lines: bottom = draw_sized_text( canvas, line.twand, line, bottom, left_text_margin, right_text_margin, auto_reduce_font=False) bottom += line.bottom_margin available = (width - (left_mgn + right_mgn), int(footer_top - top) - (image_mgn * 2)) else: available = (width - (left_mgn + right_mgn), int(height - top) - bottom_mgn - (image_mgn * 2)) if not is_background_image and display_image and available[1] > 40: logo = Image() logo.open(image_path) lwidth, lheight = logo.size available = (available[0] - image_border_width * 2, available[1] - image_border_width * 2) scaled, lwidth, lheight = fit_image(lwidth, lheight, *available) if not scaled and options.get(cfg.KEY_RESIZE_IMAGE_TO_FIT, False): scaled, lwidth, lheight = scaleup_image( lwidth, lheight, *available) if scaled: logo.size = (lwidth, lheight) if image_border_width > 0: logo = add_border(logo, image_border_width, border_color, bgcolor) left = int(max(0, (width - lwidth) / 2.)) top = top + image_mgn + ((available[1] - lheight) / 2.) canvas.compose(logo, int(left), int(top)) return canvas.export(output_format)