def export(self, fmt='JPEG'): if fmt.lower() == 'gif': data = image_to_data(self.img, fmt='PNG', png_compression_level=0) from PIL import Image i = Image.open(BytesIO(data)) buf = BytesIO() i.save(buf, 'gif') return buf.getvalue() return image_to_data(self.img, fmt=fmt)
def flip_image(img, flip): from calibre.utils.img import flip_image, image_and_format_from_data, image_to_data with lopen(img, 'r+b') as f: img, fmt = image_and_format_from_data(f.read()) img = flip_image(img, horizontal='x' in flip, vertical='y' in flip) f.seek(0), f.truncate() f.write(image_to_data(img, fmt=fmt))
def resize_image(self, raw, base, max_width, max_height): resized, img = resize_to_fit(raw, max_width, max_height) if resized: base, ext = os.path.splitext(base) base = base + '-%dx%d%s' % (max_width, max_height, ext) raw = image_to_data(img, fmt=ext[1:]) return raw, base, resized
def rescale_image(data, scale_news_images, compress_news_images_max_size, compress_news_images_auto_size): orig_data = data # save it in case compression fails img = image_from_data(data) orig_w, orig_h = img.width(), img.height() if scale_news_images is not None: wmax, hmax = scale_news_images if wmax < orig_w or hmax < orig_h: orig_w, orig_h, data = scale_image(img, wmax, hmax, compression_quality=95) if compress_news_images_max_size is None: if compress_news_images_auto_size is None: # not compressing return data maxsizeb = (orig_w * orig_h)/compress_news_images_auto_size else: maxsizeb = compress_news_images_max_size * 1024 if len(data) <= maxsizeb: # no compression required return data scaled_data = data # save it in case compression fails quality = 90 while len(data) >= maxsizeb and quality >= 5: data = image_to_data(image_from_data(scaled_data), compression_quality=quality) quality -= 5 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 convert_webp(self, item): from calibre.utils.img import image_and_format_from_data, image_to_data img, fmt = image_and_format_from_data(item.data) if fmt == 'webp' and not img.isNull(): self.log.info(f'Converting WebP image {item.href} to PNG') item.data = image_to_data(img, fmt='PNG') item.media_type = 'image/png'
def test_qt(self): from PyQt5.Qt import QImageReader, QNetworkAccessManager, QFontDatabase from calibre.utils.img import image_from_data, image_to_data, test # Ensure that images can be read before QApplication is constructed. # Note that this requires QCoreApplication.libraryPaths() to return the # path to the Qt plugins which it always does in the frozen build, # because the QT_PLUGIN_PATH env var is set. On non-frozen builds, # it should just work because the hard-coded paths of the Qt # installation should work. If they do not, then it is a distro # problem. fmts = set(map(unicode, QImageReader.supportedImageFormats())) testf = {'jpg', 'png', 'svg', 'ico', 'gif'} self.assertEqual(testf.intersection(fmts), testf, "Qt doesn't seem to be able to load some of its image plugins. Available plugins: %s" % fmts) data = I('blank.png', allow_user_override=False, data=True) img = image_from_data(data) image_from_data(P('catalog/mastheadImage.gif', allow_user_override=False, data=True)) for fmt in 'png bmp jpeg'.split(): d = image_to_data(img, fmt=fmt) image_from_data(d) # Run the imaging tests test() from calibre.gui2 import Application os.environ.pop('DISPLAY', None) app = Application([], headless=islinux) self.assertGreaterEqual(len(QFontDatabase().families()), 5, 'The QPA headless plugin is not able to locate enough system fonts via fontconfig') na = QNetworkAccessManager() self.assertTrue(hasattr(na, 'sslErrors'), 'Qt not compiled with openssl') from PyQt5.QtWebKitWidgets import QWebView QWebView() del QWebView del na del app
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 width is None: width = owidth if height is None: height = oheight 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) data = image_to_data(canvas.img, compression_quality=compression_quality) return (canvas.size[0], canvas.size[1], data)
def convert_jxr_to_tiff(jxr_data, resource_name): if calibre_numeric_version is not None and calibre_numeric_version >= ( 3, 9, 0): try: from calibre.utils.img import (load_jxr_data, image_to_data) img = load_jxr_data(jxr_data) tiff_data = image_to_data(img, fmt="TIFF") if tiff_data: return tiff_data except Exception as e: log.warning("Conversion of JPEG-XR resource failed: %s" % repr(e)) log.info("Using fallback JPEG-XR conversion for %s" % resource_name) start_time = time.time() im = JXRContainer(jxr_data).unpack_image() duration = time.time() - start_time if duration >= 0.5: log.info("JPEG-XR to TIFF conversion took %0.1f sec" % duration) outfile = io.BytesIO() with disable_debug_log(): im.save(outfile, "TIFF") im.close() return outfile.getvalue()
def insertFromMimeData(self, md): files = tuple(self.get_droppable_files(md)) base = self.highlighter.doc_name or None def get_name(name): return get_recommended_folders(current_container(), (name, ))[name] + '/' + name def get_href(name): return current_container().name_to_href(name, base) def insert_text(text): c = self.textCursor() c.insertText(text) self.setTextCursor(c) self.ensureCursorVisible() def add_file(name, data, mt=None): from calibre.gui2.tweak_book.boss import get_boss name = current_container().add_file(name, data, media_type=mt, modify_name_if_needed=True) get_boss().refresh_file_list() return name if files: for path, mt, is_name in files: if is_name: name = path else: name = get_name(os.path.basename(path)) with lopen(path, 'rb') as f: name = add_file(name, f.read(), mt) href = get_href(name) if mt.startswith('image/'): self.insert_image(href) elif mt in OEB_STYLES: insert_text( '<link href="{}" rel="stylesheet" type="text/css"/>'. format(href)) elif mt in OEB_DOCS: self.insert_hyperlink(href, name) self.ensureCursorVisible() return if md.hasImage(): img = md.imageData() if img is not None and img.isValid(): data = image_to_data(img, fmt='PNG') name = add_file(get_name('dropped_image.png', data)) self.insert_image(get_href(name)) self.ensureCursorVisible() return if md.hasText(): return insert_text(md.text()) if md.hasHtml(): insert_text(md.html()) return
def calibre_cover(title, author_string, series_string=None, output_format='jpg', title_size=46, author_size=36, logo_path=None): title = normalize(title) author_string = normalize(author_string) series_string = normalize(series_string) from calibre.ebooks.covers import calibre_cover2 from calibre.utils.img import image_to_data ans = calibre_cover2(title, author_string or '', series_string or '', logo_path=logo_path, as_qimage=True) return image_to_data(ans, fmt=output_format)
def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None): ''' Convert image setting all transparent pixels to white and changing format to JPEG. Ensure the resultant image has a byte size less than maxsizeb. If dimen is not None, generate a thumbnail of width=dimen, height=dimen or width, height = dimen (depending on the type of dimen) Returns the image as a bytestring ''' if dimen is not None: if hasattr(dimen, '__len__'): width, height = dimen else: width = height = dimen data = scale_image(data, width=width, height=height, compression_quality=90)[-1] else: # Replace transparent pixels with white pixels and convert to JPEG data = save_cover_data_to(data) if len(data) <= maxsizeb: return data orig_data = data # save it in case compression fails quality = 90 while len(data) > maxsizeb and quality >= 5: data = image_to_data(image_from_data(orig_data), compression_quality=quality) quality -= 5 if len(data) <= maxsizeb: return data orig_data = data scale = 0.9 while len(data) > maxsizeb and scale >= 0.05: img = image_from_data(data) w, h = img.width(), img.height() img = resize_image(img, int(scale * w), int(scale * h)) data = image_to_data(img, compression_quality=quality) scale -= 0.05 return data
def insertFromMimeData(self, md): files = tuple(self.get_droppable_files(md)) base = self.highlighter.doc_name or None def get_name(name): folder = get_recommended_folders(current_container(), (name,))[name] or '' if folder: folder += '/' return folder + name def get_href(name): return current_container().name_to_href(name, base) def insert_text(text): c = self.textCursor() c.insertText(text) self.setTextCursor(c) self.ensureCursorVisible() def add_file(name, data, mt=None): from calibre.gui2.tweak_book.boss import get_boss name = current_container().add_file(name, data, media_type=mt, modify_name_if_needed=True) get_boss().refresh_file_list() return name if files: for path, mt, is_name in files: if is_name: name = path else: name = get_name(os.path.basename(path)) with lopen(path, 'rb') as f: name = add_file(name, f.read(), mt) href = get_href(name) if mt.startswith('image/'): self.insert_image(href) elif mt in OEB_STYLES: insert_text('<link href="{}" rel="stylesheet" type="text/css"/>'.format(href)) elif mt in OEB_DOCS: self.insert_hyperlink(href, name) self.ensureCursorVisible() return if md.hasImage(): img = md.imageData() if img is not None and not img.isNull(): data = image_to_data(img, fmt='PNG') name = add_file(get_name('dropped_image.png'), data) self.insert_image(get_href(name)) self.ensureCursorVisible() return if md.hasText(): return insert_text(md.text()) if md.hasHtml(): insert_text(md.html()) return
def test_image_formats(): # Must be run before QApplication is constructed # Test that the image formats are available without a QApplication being # constructed from calibre.utils.img import image_from_data, image_to_data data = I('blank.png', allow_user_override=False, data=True) img = image_from_data(data) image_from_data(P('catalog/mastheadImage.gif', allow_user_override=False, data=True)) for fmt in 'png bmp jpeg'.split(): d = image_to_data(img, fmt=fmt) image_from_data(d)
def test_qt(self): from PyQt5.QtGui import QImageReader, QFontDatabase from PyQt5.QtNetwork import QNetworkAccessManager from calibre.utils.img import image_from_data, image_to_data, test # Ensure that images can be read before QApplication is constructed. # Note that this requires QCoreApplication.libraryPaths() to return the # path to the Qt plugins which it always does in the frozen build, # because the QT_PLUGIN_PATH env var is set. On non-frozen builds, # it should just work because the hard-coded paths of the Qt # installation should work. If they do not, then it is a distro # problem. fmts = set( map(lambda x: x.data().decode('utf-8'), QImageReader.supportedImageFormats())) # no2to3 testf = {'jpg', 'png', 'svg', 'ico', 'gif'} self.assertEqual( testf.intersection(fmts), testf, "Qt doesn't seem to be able to load some of its image plugins. Available plugins: %s" % fmts) data = P('images/blank.png', allow_user_override=False, data=True) img = image_from_data(data) image_from_data( P('catalog/mastheadImage.gif', allow_user_override=False, data=True)) for fmt in 'png bmp jpeg'.split(): d = image_to_data(img, fmt=fmt) image_from_data(d) # Run the imaging tests test() from calibre.gui2 import Application os.environ.pop('DISPLAY', None) has_headless = isosx or islinux app = Application([], headless=has_headless) self.assertGreaterEqual( len(QFontDatabase().families()), 5, 'The QPA headless plugin is not able to locate enough system fonts via fontconfig' ) if has_headless: from calibre.ebooks.covers import create_cover create_cover('xxx', ['yyy']) na = QNetworkAccessManager() self.assertTrue(hasattr(na, 'sslErrors'), 'Qt not compiled with openssl') from PyQt5.QtWebKitWidgets import QWebView if iswindows: from PyQt5.Qt import QtWin QtWin QWebView() del QWebView del na del app
def manual_trim_cover(self): book_id = self.data.get('id') if not book_id: return from calibre.gui2.dialogs.trim_image import TrimImage from calibre.utils.img import image_to_data cdata = image_to_data(image_from_x(self.pixmap), fmt='PNG', png_compression_level=1) d = TrimImage(cdata, parent=self) if d.exec() == QDialog.DialogCode.Accepted and d.image_data is not None: self.last_trim_id = book_id self.last_trim_pixmap = self.pixmap self.update_cover(cdata=d.image_data)
def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None): ''' Convert image setting all transparent pixels to white and changing format to JPEG. Ensure the resultant image has a byte size less than maxsizeb. If dimen is not None, generate a thumbnail of width=dimen, height=dimen or width, height = dimen (depending on the type of dimen) Returns the image as a bytestring ''' if dimen is not None: if hasattr(dimen, '__len__'): width, height = dimen else: width = height = dimen data = scale_image(data, width=width, height=height, compression_quality=90)[-1] else: # Replace transparent pixels with white pixels and convert to JPEG data = save_cover_data_to(data) if len(data) <= maxsizeb: return data orig_data = data # save it in case compression fails quality = 90 while len(data) > maxsizeb and quality >= 5: data = image_to_data(image_from_data(orig_data), compression_quality=quality) quality -= 5 if len(data) <= maxsizeb: return data orig_data = data scale = 0.9 while len(data) > maxsizeb and scale >= 0.05: img = image_from_data(data) w, h = img.width(), img.height() img = resize_image(img, int(scale*w), int(scale*h)) data = image_to_data(img, compression_quality=quality) scale -= 0.05 return data
def test_image_formats(): # Must be run before QApplication is constructed # Test that the image formats are available without a QApplication being # constructed from calibre.utils.img import image_from_data, image_to_data, test data = I('blank.png', allow_user_override=False, data=True) img = image_from_data(data) image_from_data( P('catalog/mastheadImage.gif', allow_user_override=False, data=True)) for fmt in 'png bmp jpeg'.split(): d = image_to_data(img, fmt=fmt) image_from_data(d) # Run the imaging tests test()
def add_borders_to_image(img_data, left=0, top=0, right=0, bottom=0, border_color='#ffffff', fmt='jpg'): img = abti(img_data, left=left, top=top, right=right, bottom=bottom, border_color=border_color) return image_to_data(img, fmt=fmt)
def get_image_data(self, quality=90): if not self.is_modified: return self.original_image_data fmt = self.original_image_format or 'JPEG' if fmt.lower() not in set(map(lambda x:bytes(x).decode('ascii'), QImageWriter.supportedImageFormats())): if fmt.lower() == 'gif': data = image_to_data(self.current_image, fmt='PNG', png_compression_level=0) from PIL import Image i = Image.open(BytesIO(data)) buf = BytesIO() i.save(buf, 'gif') return buf.getvalue() else: raise ValueError('Cannot save %s format images' % fmt) return pixmap_to_data(self.current_image, format=fmt, quality=90)
def process_result(log, result): plugin, data = result try: if getattr(plugin, 'auto_trim_covers', False): img = image_from_data(data) nimg = remove_borders_from_image(img) if nimg is not img: data = image_to_data(nimg) fmt, width, height = identify(data) if width < 0 or height < 0: raise ValueError('Could not read cover image dimensions') if width < 50 or height < 50: raise ValueError('Image too small') data = save_cover_data_to(data) except Exception: log.exception('Invalid cover from', plugin.name) return None return (plugin, width, height, fmt, data)
def HandleImage(self, imageData, imagePath): from calibre.utils.img import image_from_data, resize_image, image_to_data img = image_from_data(imageData) x, y = img.width(), img.height() if self.opts: if self.opts.snb_full_screen: SCREEN_X, SCREEN_Y = self.opts.output_profile.screen_size else: SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size else: SCREEN_X = 540 SCREEN_Y = 700 # Handle big image only if x > SCREEN_X or y > SCREEN_Y: xScale = float(x) / SCREEN_X yScale = float(y) / SCREEN_Y scale = max(xScale, yScale) # TODO : intelligent image rotation # img = img.rotate(90) # x,y = y,x img = resize_image(img, x // scale, y // scale) with lopen(imagePath, 'wb') as f: f.write(image_to_data(img, fmt=imagePath.rpartition('.')[-1]))
def HandleImage(self, imageData, imagePath): from calibre.utils.img import image_from_data, resize_image, image_to_data img = image_from_data(imageData) x, y = img.width(), img.height() if self.opts: if self.opts.snb_full_screen: SCREEN_X, SCREEN_Y = self.opts.output_profile.screen_size else: SCREEN_X, SCREEN_Y = self.opts.output_profile.comic_screen_size else: SCREEN_X = 540 SCREEN_Y = 700 # Handle big image only if x > SCREEN_X or y > SCREEN_Y: xScale = float(x) / SCREEN_X yScale = float(y) / SCREEN_Y scale = max(xScale, yScale) # TODO : intelligent image rotation # img = img.rotate(90) # x,y = y,x img = resize_image(img, x / scale, y / scale) with lopen(imagePath, 'wb') as f: f.write(image_to_data(img, fmt=imagePath.rpartition('.')[-1]))
def process_images(self, soup, baseurl): diskpath = unicode_path(os.path.join(self.current_dir, 'images')) if not os.path.exists(diskpath): os.mkdir(diskpath) c = 0 for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')): iurl = tag['src'] if iurl.startswith('data:image/'): try: data = b64decode(iurl.partition(',')[-1]) except: self.log.exception('Failed to decode embedded image') continue else: if callable(self.image_url_processor): iurl = self.image_url_processor(baseurl, iurl) if not urlparse.urlsplit(iurl).scheme: iurl = urlparse.urljoin(baseurl, iurl, False) with self.imagemap_lock: if self.imagemap.has_key(iurl): tag['src'] = self.imagemap[iurl] continue try: data = self.fetch_url(iurl) if data == 'GIF89a\x01': # Skip empty GIF files as PIL errors on them anyway continue except Exception: self.log.exception('Could not fetch image ', iurl) continue c += 1 fname = ascii_filename('img'+str(c)) if isinstance(fname, unicode): fname = fname.encode('ascii', 'replace') itype = what(None, data) if itype is None and b'<svg' in data[:1024]: # SVG image imgpath = os.path.join(diskpath, fname+'.svg') with self.imagemap_lock: self.imagemap[iurl] = imgpath with open(imgpath, 'wb') as x: x.write(data) tag['src'] = imgpath else: try: # Ensure image is valid img = image_from_data(data) if itype not in {'png', 'jpg', 'jpeg'}: itype = 'png' if itype == 'gif' else 'jpeg' data = image_to_data(img, fmt=itype) if self.compress_news_images and itype in {'jpg','jpeg'}: try: data = self.rescale_image(data) except Exception: self.log.exception('failed to compress image '+iurl) # Moon+ apparently cannot handle .jpeg files if itype == 'jpeg': itype = 'jpg' imgpath = os.path.join(diskpath, fname+'.'+itype) with self.imagemap_lock: self.imagemap[iurl] = imgpath with open(imgpath, 'wb') as x: x.write(data) tag['src'] = imgpath except Exception: traceback.print_exc() continue
def process_pages(self): from calibre.utils.img import (image_to_data, rotate_image, remove_borders_from_image, normalize_image, add_borders_to_image, resize_image, gaussian_sharpen_image, grayscale_image, despeckle_image, quantize_image) for i, img in enumerate(self.pages): if self.rotate: img = rotate_image(img, -90) if not self.opts.disable_trim: img = remove_borders_from_image(img) # Do the Photoshop "Auto Levels" equivalent if not self.opts.dont_normalize: img = normalize_image(img) sizex, sizey = img.width(), img.height() SCRWIDTH, SCRHEIGHT = self.opts.output_profile.comic_screen_size try: if self.opts.comic_image_size: SCRWIDTH, SCRHEIGHT = map(int, [ x.strip() for x in self.opts.comic_image_size.split('x') ]) except: pass # Ignore if self.opts.keep_aspect_ratio: # Preserve the aspect ratio by adding border aspect = float(sizex) / float(sizey) if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)): newsizey = SCRHEIGHT newsizex = int(newsizey * aspect) deltax = (SCRWIDTH - newsizex) // 2 deltay = 0 else: newsizex = SCRWIDTH newsizey = int(newsizex // aspect) deltax = 0 deltay = (SCRHEIGHT - newsizey) // 2 if newsizex < MAX_SCREEN_SIZE and newsizey < MAX_SCREEN_SIZE: # Too large and resizing fails, so better # to leave it as original size img = resize_image(img, newsizex, newsizey) img = add_borders_to_image(img, left=deltax, right=deltax, top=deltay, bottom=deltay) elif self.opts.wide: # Keep aspect and Use device height as scaled image width so landscape mode is clean aspect = float(sizex) / float(sizey) screen_aspect = float(SCRWIDTH) / float(SCRHEIGHT) # Get dimensions of the landscape mode screen # Add 25px back to height for the battery bar. wscreenx = SCRHEIGHT + 25 wscreeny = int(wscreenx // screen_aspect) if aspect <= screen_aspect: newsizey = wscreeny newsizex = int(newsizey * aspect) deltax = (wscreenx - newsizex) // 2 deltay = 0 else: newsizex = wscreenx newsizey = int(newsizex // aspect) deltax = 0 deltay = (wscreeny - newsizey) // 2 if newsizex < MAX_SCREEN_SIZE and newsizey < MAX_SCREEN_SIZE: # Too large and resizing fails, so better # to leave it as original size img = resize_image(img, newsizex, newsizey) img = add_borders_to_image(img, left=deltax, right=deltax, top=deltay, bottom=deltay) else: if SCRWIDTH < MAX_SCREEN_SIZE and SCRHEIGHT < MAX_SCREEN_SIZE: img = resize_image(img, SCRWIDTH, SCRHEIGHT) if not self.opts.dont_sharpen: img = gaussian_sharpen_image(img, 0.0, 1.0) if not self.opts.dont_grayscale: img = grayscale_image(img) if self.opts.despeckle: img = despeckle_image(img) if self.opts.output_format.lower() == 'png' and self.opts.colors: img = quantize_image(img, max_colors=min(256, self.opts.colors)) dest = '%d_%d.%s' % (self.num, i, self.opts.output_format) dest = os.path.join(self.dest, dest) with lopen(dest, 'wb') as f: f.write(image_to_data(img, fmt=self.opts.output_format)) self.append(dest)
def prepare_masthead_image(path_to_image, out_path, mi_width, mi_height): with lopen(path_to_image, 'rb') as f: img = image_from_data(f.read()) img = blend_on_canvas(img, mi_width, mi_height) with lopen(out_path, 'wb') as f: f.write(image_to_data(img))
def process_images(self, soup, baseurl): diskpath = unicode_path(os.path.join(self.current_dir, 'images')) if not os.path.exists(diskpath): os.mkdir(diskpath) c = 0 for tag in soup.findAll(lambda tag: tag.name.lower() == 'img' and tag. has_key('src')): # noqa iurl = tag['src'] if iurl.startswith('data:image/'): try: data = b64decode(iurl.partition(',')[-1]) except: self.log.exception('Failed to decode embedded image') continue else: if callable(self.image_url_processor): iurl = self.image_url_processor(baseurl, iurl) if not urlsplit(iurl).scheme: iurl = urljoin(baseurl, iurl, False) with self.imagemap_lock: if self.imagemap.has_key(iurl): # noqa tag['src'] = self.imagemap[iurl] continue try: data = self.fetch_url(iurl) if data == 'GIF89a\x01': # Skip empty GIF files as PIL errors on them anyway continue except Exception: self.log.exception('Could not fetch image ', iurl) continue c += 1 fname = ascii_filename('img' + str(c)) if isinstance(fname, unicode_type): fname = fname.encode('ascii', 'replace') data = self.preprocess_image_ext( data, iurl) if self.preprocess_image_ext is not None else data if data is None: continue itype = what(None, data) if itype == 'svg' or (itype is None and b'<svg' in data[:1024]): # SVG image imgpath = os.path.join(diskpath, fname + '.svg') with self.imagemap_lock: self.imagemap[iurl] = imgpath with open(imgpath, 'wb') as x: x.write(data) tag['src'] = imgpath else: try: # Ensure image is valid img = image_from_data(data) if itype not in {'png', 'jpg', 'jpeg'}: itype = 'png' if itype == 'gif' else 'jpeg' data = image_to_data(img, fmt=itype) if self.compress_news_images and itype in {'jpg', 'jpeg'}: try: data = self.rescale_image(data) except Exception: self.log.exception('failed to compress image ' + iurl) # Moon+ apparently cannot handle .jpeg files if itype == 'jpeg': itype = 'jpg' imgpath = os.path.join(diskpath, fname + '.' + itype) with self.imagemap_lock: self.imagemap[iurl] = imgpath with open(imgpath, 'wb') as x: x.write(data) tag['src'] = imgpath except Exception: traceback.print_exc() continue
def process_pages(self): from calibre.utils.img import ( image_to_data, rotate_image, remove_borders_from_image, normalize_image, add_borders_to_image, resize_image, gaussian_sharpen_image, grayscale_image, despeckle_image, quantize_image ) for i, img in enumerate(self.pages): if self.rotate: img = rotate_image(img, -90) if not self.opts.disable_trim: img = remove_borders_from_image(img) # Do the Photoshop "Auto Levels" equivalent if not self.opts.dont_normalize: img = normalize_image(img) sizex, sizey = img.width(), img.height() SCRWIDTH, SCRHEIGHT = self.opts.output_profile.comic_screen_size try: if self.opts.comic_image_size: SCRWIDTH, SCRHEIGHT = map(int, [x.strip() for x in self.opts.comic_image_size.split('x')]) except: pass # Ignore if self.opts.keep_aspect_ratio: # Preserve the aspect ratio by adding border aspect = float(sizex) / float(sizey) if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)): newsizey = SCRHEIGHT newsizex = int(newsizey * aspect) deltax = (SCRWIDTH - newsizex) / 2 deltay = 0 else: newsizex = SCRWIDTH newsizey = int(newsizex / aspect) deltax = 0 deltay = (SCRHEIGHT - newsizey) / 2 if newsizex < MAX_SCREEN_SIZE and newsizey < MAX_SCREEN_SIZE: # Too large and resizing fails, so better # to leave it as original size img = resize_image(img, newsizex, newsizey) img = add_borders_to_image(img, left=deltax, right=deltax, top=deltay, bottom=deltay) elif self.opts.wide: # Keep aspect and Use device height as scaled image width so landscape mode is clean aspect = float(sizex) / float(sizey) screen_aspect = float(SCRWIDTH) / float(SCRHEIGHT) # Get dimensions of the landscape mode screen # Add 25px back to height for the battery bar. wscreenx = SCRHEIGHT + 25 wscreeny = int(wscreenx / screen_aspect) if aspect <= screen_aspect: newsizey = wscreeny newsizex = int(newsizey * aspect) deltax = (wscreenx - newsizex) / 2 deltay = 0 else: newsizex = wscreenx newsizey = int(newsizex / aspect) deltax = 0 deltay = (wscreeny - newsizey) / 2 if newsizex < MAX_SCREEN_SIZE and newsizey < MAX_SCREEN_SIZE: # Too large and resizing fails, so better # to leave it as original size img = resize_image(img, newsizex, newsizey) img = add_borders_to_image(img, left=deltax, right=deltax, top=deltay, bottom=deltay) else: if SCRWIDTH < MAX_SCREEN_SIZE and SCRHEIGHT < MAX_SCREEN_SIZE: img = resize_image(img, SCRWIDTH, SCRHEIGHT) if not self.opts.dont_sharpen: img = gaussian_sharpen_image(img, 0.0, 1.0) if not self.opts.dont_grayscale: img = grayscale_image(img) if self.opts.despeckle: img = despeckle_image(img) if self.opts.output_format.lower() == 'png' and self.opts.colors: img = quantize_image(img, max_colors=min(256, self.opts.colors)) dest = '%d_%d.%s'%(self.num, i, self.opts.output_format) dest = os.path.join(self.dest, dest) with lopen(dest, 'wb') as f: f.write(image_to_data(img, fmt=self.opts.output_format)) self.append(dest)
def test_qt(self): from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import QApplication from PyQt5.QtWebEngineWidgets import QWebEnginePage from PyQt5.QtGui import QImageReader, QFontDatabase from PyQt5.QtNetwork import QNetworkAccessManager from calibre.utils.img import image_from_data, image_to_data, test # Ensure that images can be read before QApplication is constructed. # Note that this requires QCoreApplication.libraryPaths() to return the # path to the Qt plugins which it always does in the frozen build, # because Qt is patched to know the layout of the calibre application # package. On non-frozen builds, it should just work because the # hard-coded paths of the Qt installation should work. If they do not, # then it is a distro problem. fmts = set(map(lambda x: x.data().decode('utf-8'), QImageReader.supportedImageFormats())) # no2to3 testf = {'jpg', 'png', 'svg', 'ico', 'gif'} self.assertEqual(testf.intersection(fmts), testf, "Qt doesn't seem to be able to load some of its image plugins. Available plugins: %s" % fmts) data = P('images/blank.png', allow_user_override=False, data=True) img = image_from_data(data) image_from_data(P('catalog/mastheadImage.gif', allow_user_override=False, data=True)) for fmt in 'png bmp jpeg'.split(): d = image_to_data(img, fmt=fmt) image_from_data(d) # Run the imaging tests test() from calibre.gui2 import ensure_app, destroy_app display_env_var = os.environ.pop('DISPLAY', None) try: ensure_app() self.assertGreaterEqual(len(QFontDatabase().families()), 5, 'The QPA headless plugin is not able to locate enough system fonts via fontconfig') from calibre.ebooks.covers import create_cover create_cover('xxx', ['yyy']) na = QNetworkAccessManager() self.assertTrue(hasattr(na, 'sslErrors'), 'Qt not compiled with openssl') if iswindows: from PyQt5.Qt import QtWin QtWin p = QWebEnginePage() def callback(result): callback.result = result if hasattr(print_callback, 'result'): QApplication.instance().quit() def print_callback(result): print_callback.result = result if hasattr(callback, 'result'): QApplication.instance().quit() p.runJavaScript('1 + 1', callback) p.printToPdf(print_callback) QTimer.singleShot(5000, lambda: QApplication.instance().quit()) QApplication.instance().exec_() test_flaky = ismacos and not is_ci if not test_flaky: self.assertEqual(callback.result, 2, 'Simple JS computation failed') self.assertIn(b'Skia/PDF', bytes(print_callback.result), 'Print to PDF failed') del p del na destroy_app() del QWebEnginePage finally: if display_env_var is not None: os.environ['DISPLAY'] = display_env_var
def process_images(self, soup, baseurl): diskpath = unicode_path(os.path.join(self.current_dir, 'images')) if not os.path.exists(diskpath): os.mkdir(diskpath) c = 0 for tag in soup.findAll('img', src=True): iurl = tag['src'] if iurl.startswith('data:'): try: data = urlopen(iurl).read() except Exception: self.log.exception('Failed to decode embedded image') continue else: if callable(self.image_url_processor): iurl = self.image_url_processor(baseurl, iurl) if not urlsplit(iurl).scheme: iurl = urljoin(baseurl, iurl, False) found_in_cache = False with self.imagemap_lock: if iurl in self.imagemap: tag['src'] = self.imagemap[iurl] found_in_cache = True if found_in_cache: continue try: data = self.fetch_url(iurl) if data == b'GIF89a\x01': # Skip empty GIF files as PIL errors on them anyway continue except Exception: self.log.exception('Could not fetch image ', iurl) continue c += 1 fname = ascii_filename('img' + str(c)) data = self.preprocess_image_ext( data, iurl) if self.preprocess_image_ext is not None else data if data is None: continue itype = what(None, data) if itype == 'svg' or (itype is None and b'<svg' in data[:1024]): # SVG image imgpath = os.path.join(diskpath, fname + '.svg') with self.imagemap_lock: self.imagemap[iurl] = imgpath with open(imgpath, 'wb') as x: x.write(data) tag['src'] = imgpath else: from calibre.utils.img import image_from_data, image_to_data try: # Ensure image is valid img = image_from_data(data) if itype not in {'png', 'jpg', 'jpeg'}: itype = 'png' if itype == 'gif' else 'jpeg' data = image_to_data(img, fmt=itype) if self.compress_news_images and itype in {'jpg', 'jpeg'}: try: data = self.rescale_image(data) except Exception: self.log.exception('failed to compress image ' + iurl) # Moon+ apparently cannot handle .jpeg files if itype == 'jpeg': itype = 'jpg' imgpath = os.path.join(diskpath, fname + '.' + itype) with self.imagemap_lock: self.imagemap[iurl] = imgpath with open(imgpath, 'wb') as x: x.write(data) tag['src'] = imgpath except Exception: traceback.print_exc() continue
def do_all(self): cache = self.db.new_api args = self.args # Title and authors if args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) authors_map = cache.all_field_for('authors', self.ids) def new_title(authors): ans = authors_to_string(authors) return titlecase(ans) if args.do_title_case else ans new_title_map = {bid:new_title(authors) for bid, authors in authors_map.iteritems()} new_authors_map = {bid:string_to_authors(title) for bid, title in title_map.iteritems()} cache.set_field('authors', new_authors_map) cache.set_field('title', new_title_map) if args.do_title_case and not args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) cache.set_field('title', {bid:titlecase(title) for bid, title in title_map.iteritems()}) if args.do_title_sort: lang_map = cache.all_field_for('languages', self.ids) title_map = cache.all_field_for('title', self.ids) def get_sort(book_id): if args.languages: lang = args.languages[0] else: try: lang = lang_map[book_id][0] except (KeyError, IndexError, TypeError, AttributeError): lang = 'eng' return title_sort(title_map[book_id], lang=lang) cache.set_field('sort', {bid:get_sort(bid) for bid in self.ids}) if args.au: authors = string_to_authors(args.au) cache.set_field('authors', {bid:authors for bid in self.ids}) if args.do_auto_author: aus_map = cache.author_sort_strings_for_books(self.ids) cache.set_field('author_sort', {book_id:' & '.join(aus_map[book_id]) for book_id in aus_map}) if args.aus and args.do_aus: cache.set_field('author_sort', {bid:args.aus for bid in self.ids}) # Covers if args.cover_action == 'remove': cache.set_cover({bid:None for bid in self.ids}) elif args.cover_action == 'generate': from calibre.ebooks.covers import generate_cover for book_id in self.ids: mi = self.db.get_metadata(book_id, index_is_id=True) cdata = generate_cover(mi, prefs=args.generate_cover_settings) cache.set_cover({book_id:cdata}) elif args.cover_action == 'fromfmt': for book_id in self.ids: fmts = cache.formats(book_id, verify_formats=False) if fmts: covers = [] for fmt in fmts: fmtf = cache.format(book_id, fmt, as_file=True) if fmtf is None: continue cdata, area = get_cover_data(fmtf, fmt) if cdata: covers.append((cdata, area)) covers.sort(key=lambda x: x[1]) if covers: cache.set_cover({book_id:covers[-1][0]}) elif args.cover_action == 'trim': from calibre.utils.img import remove_borders, image_to_data, image_from_data for book_id in self.ids: cdata = cache.cover(book_id) if cdata: img = image_from_data(cdata) nimg = remove_borders(img) if nimg is not img: cdata = image_to_data(nimg) cache.set_cover({book_id:cdata}) elif args.cover_action == 'clone': cdata = None for book_id in self.ids: cdata = cache.cover(book_id) if cdata: break if cdata: cache.set_cover({bid:cdata for bid in self.ids if bid != book_id}) # Formats if args.do_remove_format: cache.remove_formats({bid:(args.remove_format,) for bid in self.ids}) if args.restore_original: for book_id in self.ids: formats = cache.formats(book_id) originals = tuple(x.upper() for x in formats if x.upper().startswith('ORIGINAL_')) for ofmt in originals: cache.restore_original_format(book_id, ofmt) # Various fields if args.rating != -1: cache.set_field('rating', {bid:args.rating*2 for bid in self.ids}) if args.clear_pub: cache.set_field('publisher', {bid:'' for bid in self.ids}) if args.pub: cache.set_field('publisher', {bid:args.pub for bid in self.ids}) if args.clear_series: cache.set_field('series', {bid:'' for bid in self.ids}) if args.pubdate is not None: cache.set_field('pubdate', {bid:args.pubdate for bid in self.ids}) if args.adddate is not None: cache.set_field('timestamp', {bid:args.adddate for bid in self.ids}) if args.do_series: sval = args.series_start_value if args.do_series_restart else cache.get_next_series_num_for(args.series, current_indices=True) cache.set_field('series', {bid:args.series for bid in self.ids}) if not args.series: cache.set_field('series_index', {bid:1.0 for bid in self.ids}) else: def next_series_num(bid, i): if args.do_series_restart: return sval + (i * args.series_increment) next_num = _get_next_series_num_for_list(sorted(sval.itervalues()), unwrap=False) sval[bid] = next_num return next_num smap = {bid:next_series_num(bid, i) for i, bid in enumerate(self.ids)} if args.do_autonumber: cache.set_field('series_index', smap) elif tweaks['series_index_auto_increment'] != 'no_change': cache.set_field('series_index', {bid:1.0 for bid in self.ids}) if args.comments is not null: cache.set_field('comments', {bid:args.comments for bid in self.ids}) if args.do_remove_conv: cache.delete_conversion_options(self.ids) if args.clear_languages: cache.set_field('languages', {bid:() for bid in self.ids}) elif args.languages: cache.set_field('languages', {bid:args.languages for bid in self.ids}) if args.remove_all: cache.set_field('tags', {bid:() for bid in self.ids}) if args.add or args.remove: self.db.bulk_modify_tags(self.ids, add=args.add, remove=args.remove) if self.do_sr: for book_id in self.ids: self.s_r_func(book_id) if self.sr_calls: for field, book_id_val_map in self.sr_calls.iteritems(): self.refresh_books.update(self.db.new_api.set_field(field, book_id_val_map))