%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/ebooks/comic/ |
| Current File : //usr/lib/calibre/calibre/ebooks/comic/input.py |
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Based on ideas from comiclrf created by FangornUK.
'''
import os, traceback, time
from calibre import extract, prints, walk
from calibre.constants import filesystem_encoding
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.icu import numeric_sort_key
from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob
from polyglot.queue import Empty
# If the specified screen has either dimension larger than this value, no image
# rescaling is done (we assume that it is a tablet output profile)
MAX_SCREEN_SIZE = 3000
def extract_comic(path_to_comic_file):
'''
Un-archive the comic file.
'''
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
if not isinstance(tdir, str):
# Needed in case the zip file has wrongly encoded unicode file/dir
# names
tdir = tdir.decode(filesystem_encoding)
extract(path_to_comic_file, tdir)
for x in walk(tdir):
bn = os.path.basename(x)
nbn = bn.replace('#', '_')
if nbn != bn:
os.rename(x, os.path.join(os.path.dirname(x), nbn))
return tdir
def find_pages(dir, sort_on_mtime=False, verbose=False):
'''
Find valid comic pages in a previously un-archived comic.
:param dir: Directory in which extracted comic lives
:param sort_on_mtime: If True sort pages based on their last modified time.
Otherwise, sort alphabetically.
'''
extensions = {'jpeg', 'jpg', 'gif', 'png', 'webp'}
pages = []
for datum in os.walk(dir):
for name in datum[-1]:
path = os.path.abspath(os.path.join(datum[0], name))
if '__MACOSX' in path:
continue
for ext in extensions:
if path.lower().endswith('.'+ext):
pages.append(path)
break
sep_counts = {x.replace(os.sep, '/').count('/') for x in pages}
# Use the full path to sort unless the files are in folders of different
# levels, in which case simply use the filenames.
basename = os.path.basename if len(sep_counts) > 1 else lambda x: x
if sort_on_mtime:
key = lambda x:os.stat(x).st_mtime
else:
key = lambda x:numeric_sort_key(basename(x))
pages.sort(key=key)
if verbose:
prints('Found comic pages...')
prints('\t'+'\n\t'.join([os.path.relpath(p, dir) for p in pages]))
return pages
class PageProcessor(list): # {{{
'''
Contains the actual image rendering logic. See :method:`render` and
:method:`process_pages`.
'''
def __init__(self, path_to_page, dest, opts, num):
list.__init__(self)
self.path_to_page = path_to_page
self.opts = opts
self.num = num
self.dest = dest
self.rotate = False
self.render()
def render(self):
from calibre.utils.img import image_from_data, scale_image, crop_image
with lopen(self.path_to_page, 'rb') as f:
img = image_from_data(f.read())
width, height = img.width(), img.height()
if self.num == 0: # First image so create a thumbnail from it
with lopen(os.path.join(self.dest, 'thumbnail.png'), 'wb') as f:
f.write(scale_image(img, as_png=True)[-1])
self.pages = [img]
if width > height:
if self.opts.landscape:
self.rotate = True
else:
half = width // 2
split1 = crop_image(img, 0, 0, half, height)
split2 = crop_image(img, half, 0, width - half, height)
self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages()
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 render_pages(tasks, dest, opts, notification=lambda x, y: x):
'''
Entry point for the job server.
'''
failures, pages = [], []
for num, path in tasks:
try:
pages.extend(PageProcessor(path, dest, opts, num))
msg = _('Rendered %s')%path
except:
failures.append(path)
msg = _('Failed %s')%path
if opts.verbose:
msg += '\n' + traceback.format_exc()
prints(msg)
notification(0.5, msg)
return pages, failures
class Progress:
def __init__(self, total, update):
self.total = total
self.update = update
self.done = 0
def __call__(self, percent, msg=''):
self.done += 1
# msg = msg%os.path.basename(job.args[0])
self.update(float(self.done)/self.total, msg)
def process_pages(pages, opts, update, tdir):
'''
Render all identified comic pages.
'''
progress = Progress(len(pages), update)
server = Server()
jobs = []
tasks = [(p, os.path.join(tdir, os.path.basename(p))) for p in pages]
tasks = server.split(pages)
for task in tasks:
jobs.append(ParallelJob('render_pages', '', progress,
args=[task, tdir, opts]))
server.add_job(jobs[-1])
while True:
time.sleep(1)
running = False
for job in jobs:
while True:
try:
x = job.notifications.get_nowait()
progress(*x)
except Empty:
break
job.update()
if not job.is_finished:
running = True
if not running:
break
server.close()
ans, failures = [], []
for job in jobs:
if job.failed or job.result is None:
raise Exception(_('Failed to process comic: \n\n%s')%
job.log_file.read())
pages, failures_ = job.result
ans += pages
failures += failures_
return ans, failures