Fix #951 ([comic2lrf] on windows multi-threading isn't working)

This commit is contained in:
Kovid Goyal 2008-08-27 11:57:48 -07:00
parent c3cced43ec
commit 606f3a6dd1
8 changed files with 91 additions and 49 deletions

View File

@ -161,12 +161,12 @@ def main(args=sys.argv):
'win32process', 'win32api', 'msvcrt', 'win32process', 'win32api', 'msvcrt',
'win32event', 'calibre.ebooks.lrf.any.*', 'win32event', 'calibre.ebooks.lrf.any.*',
'calibre.ebooks.lrf.feeds.*', 'calibre.ebooks.lrf.feeds.*',
'lxml', 'lxml._elementpath', 'genshi', 'genshi',
'path', 'pydoc', 'IPython.Extensions.*', 'path', 'pydoc', 'IPython.Extensions.*',
'calibre.web.feeds.recipes.*', 'calibre.web.feeds.recipes.*',
'PyQt4.QtWebKit', 'PyQt4.QtNetwork', 'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
], ],
'packages' : ['PIL'], 'packages' : ['PIL', 'lxml'],
'excludes' : ["Tkconstants", "Tkinter", "tcl", 'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk" "_imagingtk", "ImageTk", "FixTk"
], ],

View File

@ -25,9 +25,7 @@ Run an embedded python interpreter.
def update_zipfile(zipfile, mod, path): def update_zipfile(zipfile, mod, path):
if 'win32' in sys.platform: if 'win32' in sys.platform:
print 'WARNING: On Windows Vista you must run this from a console that has been started in Administrator mode.' print 'WARNING: On Windows Vista using this option may cause windows to put library.zip into the Virtual Store (typically located in c:\Users\username\AppData\Local\VirtualStore). If it does this you must delete it from there after you\'re done debugging).'
print 'Press Enter to continue if this is an Administrator console or Ctrl-C to Cancel'
raw_input()
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*') pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
name = mod.replace('.', '/') + os.path.splitext(path)[-1] name = mod.replace('.', '/') + os.path.splitext(path)[-1]
update(zipfile, [pat], [path], [name]) update(zipfile, [pat], [path], [name])

View File

@ -8,8 +8,28 @@ Conversion to EPUB.
''' '''
import sys import sys
from calibre.utils.config import Config, StringConfig from calibre.utils.config import Config, StringConfig
from calibre.utils.zipfile import ZipFile, ZIP_DEFLATED
from calibre.ebooks.html import config as common_config from calibre.ebooks.html import config as common_config
def initialize_container(path_to_container, opf_name='metadata.opf'):
'''
Create an empty EPUB document, with a default skeleton.
'''
CONTAINER='''\
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="%s" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
'''%opf_name
zf = ZipFile(path_to_container, 'w')
zf.writestr('mimetype', 'application/epub+zip', compression=ZIP_DEFLATED)
zf.writestr('META-INF/', '', 0700)
zf.writestr('META-INF/container.xml', CONTAINER)
return zf
def config(defaults=None): def config(defaults=None):
desc = _('Options to control the conversion to EPUB') desc = _('Options to control the conversion to EPUB')
if defaults is None: if defaults is None:

View File

@ -36,7 +36,7 @@ class HTMLProcessor(Parser):
for text in get_text(self.body if self.body is not None else self.root): for text in get_text(self.body if self.body is not None else self.root):
length, parent = len(re.sub(r'\s+', '', text)), text.getparent() length, parent = len(re.sub(r'\s+', '', text)), text.getparent()
#TODO: Use cssutils on self.raw_css to figure out the font size #TODO: Use cssutils on self.raw_css to figure out the font size
# of this piece text and update statistics accordingly # of this piece of text and update statistics accordingly
def split(self): def split(self):
''' Split into individual flows to accommodate Adobe's incompetence ''' ''' Split into individual flows to accommodate Adobe's incompetence '''
@ -74,13 +74,17 @@ def convert(htmlfile, opts, notification=None):
mi = merge_metadata(htmlfile, opf, opts) mi = merge_metadata(htmlfile, opf, opts)
opts.chapter = XPath(opts.chapter, opts.chapter = XPath(opts.chapter,
namespaces={'re':'http://exslt.org/regular-expressions'}) namespaces={'re':'http://exslt.org/regular-expressions'})
resource_map = parse_content(filelist, opts) resource_map = parse_content(filelist, opts)
resources = [os.path.join(opts.output, 'content', f) for f in resource_map.values()] resources = [os.path.join(opts.output, 'content', f) for f in resource_map.values()]
if opf.cover and os.access(opf.cover, os.R_OK): if opf.cover and os.access(opf.cover, os.R_OK):
shutil.copyfile(opf.cover, os.path.join(opts.output, 'content', 'resources', '_cover_'+os.path.splitext(opf.cover))) shutil.copyfile(opf.cover, os.path.join(opts.output, 'content', 'resources', '_cover_'+os.path.splitext(opf.cover)))
cpath = os.path.join(opts.output, 'content', 'resources', '_cover_'+os.path.splitext(opf.cover)) cpath = os.path.join(opts.output, 'content', 'resources', '_cover_'+os.path.splitext(opf.cover))
shutil.copyfile(opf.cover, cpath) shutil.copyfile(opf.cover, cpath)
resources.append(cpath) resources.append(cpath)
mi.cover = cpath
def main(args=sys.argv): def main(args=sys.argv):
parser = option_parser() parser = option_parser()

View File

@ -7,14 +7,13 @@ __docformat__ = 'restructuredtext en'
Based on ideas from comiclrf created by FangornUK. Based on ideas from comiclrf created by FangornUK.
''' '''
import os, sys, traceback, shutil import os, sys, traceback, shutil, threading
from uuid import uuid4 from uuid import uuid4
from calibre import extract, detect_ncpus, terminal_controller, \ from calibre import extract, terminal_controller, __appname__, __version__
__appname__, __version__
from calibre.utils.config import Config, StringConfig from calibre.utils.config import Config, StringConfig
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.threadpool import ThreadPool, WorkRequest from calibre.parallel import Server, ParallelJob
from calibre.utils.terminfo import ProgressBar from calibre.utils.terminfo import ProgressBar
from calibre.ebooks.lrf.pylrs.pylrs import Book, BookSetting, ImageStream, ImageBlock from calibre.ebooks.lrf.pylrs.pylrs import Book, BookSetting, ImageStream, ImageBlock
try: try:
@ -84,12 +83,13 @@ class PageProcessor(list):
''' '''
def __init__(self, path_to_page, dest, opts, num): def __init__(self, path_to_page, dest, opts, num):
self.path_to_page = path_to_page
self.opts = opts
self.num = num
self.dest = dest
self.rotate = False
list.__init__(self) list.__init__(self)
self.path_to_page = path_to_page
self.opts = opts
self.num = num
self.dest = dest
self.rotate = False
def __call__(self): def __call__(self):
try: try:
@ -100,7 +100,6 @@ class PageProcessor(list):
raise IOError('Failed to read image from: %'%self.path_to_page) raise IOError('Failed to read image from: %'%self.path_to_page)
width = MagickGetImageWidth(img) width = MagickGetImageWidth(img)
height = MagickGetImageHeight(img) height = MagickGetImageHeight(img)
if self.num == 0: # First image so create a thumbnail from it if self.num == 0: # First image so create a thumbnail from it
thumb = CloneMagickWand(img) thumb = CloneMagickWand(img)
if thumb < 0: if thumb < 0:
@ -122,7 +121,6 @@ class PageProcessor(list):
MagickCropImage(split1, (width/2)-1, height, 0, 0) MagickCropImage(split1, (width/2)-1, height, 0, 0)
MagickCropImage(split2, (width/2)-1, height, width/2, 0 ) MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
self.pages = [split2, split1] if self.opts.right2left else [split1, split2] self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages() self.process_pages()
except Exception, err: except Exception, err:
print 'Failed to process page: %s'%os.path.basename(self.path_to_page) print 'Failed to process page: %s'%os.path.basename(self.path_to_page)
@ -138,23 +136,20 @@ class PageProcessor(list):
PixelSetColor(pw, 'white') PixelSetColor(pw, 'white')
MagickSetImageBorderColor(wand, pw) MagickSetImageBorderColor(wand, pw)
if self.rotate: if self.rotate:
MagickRotateImage(wand, pw, -90) MagickRotateImage(wand, pw, -90)
# 25 percent fuzzy trim? # 25 percent fuzzy trim?
MagickTrimImage(wand, 25*65535/100) MagickTrimImage(wand, 25*65535/100)
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage" MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent # Do the Photoshop "Auto Levels" equivalent
if not self.opts.dont_normalize: if not self.opts.dont_normalize:
MagickNormalizeImage(wand) MagickNormalizeImage(wand)
sizex = MagickGetImageWidth(wand) sizex = MagickGetImageWidth(wand)
sizey = MagickGetImageHeight(wand) sizey = MagickGetImageHeight(wand)
SCRWIDTH, SCRHEIGHT = PROFILES[self.opts.profile] SCRWIDTH, SCRHEIGHT = PROFILES[self.opts.profile]
print 77777, threading.currentThread()
if self.opts.keep_aspect_ratio: if self.opts.keep_aspect_ratio:
# Preserve the aspect ratio by adding border # Preserve the aspect ratio by adding border
aspect = float(sizex) / float(sizey) aspect = float(sizex) / float(sizey)
@ -168,7 +163,6 @@ class PageProcessor(list):
newsizey = int(newsizex / aspect) newsizey = int(newsizex / aspect)
deltax = 0 deltax = 0
deltay = (SCRHEIGHT - newsizey) / 2 deltay = (SCRHEIGHT - newsizey) / 2
MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0) MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0)
MagickSetImageBorderColor(wand, pw) MagickSetImageBorderColor(wand, pw)
MagickBorderImage(wand, pw, deltax, deltay) MagickBorderImage(wand, pw, deltax, deltay)
@ -193,6 +187,12 @@ class PageProcessor(list):
DestroyPixelWand(pw) DestroyPixelWand(pw)
wand = DestroyMagickWand(wand) wand = DestroyMagickWand(wand)
def process_page(path, dest, opts, num):
pp = PageProcessor(path, dest, opts, num)
with ImageMagick():
pp()
return list(pp)
class Progress(object): class Progress(object):
def __init__(self, total, update): def __init__(self, total, update):
@ -200,10 +200,11 @@ class Progress(object):
self.update = update self.update = update
self.done = 0 self.done = 0
def __call__(self, req, res): def __call__(self, job):
self.done += 1 self.done += 1
self.update(float(self.done)/self.total, msg = _('Rendered %s') if job.result else _('Failed %s')
_('Rendered %s')%os.path.basename(req.callable.path_to_page)) msg = msg%os.path.basename(job.args[0])
self.update(float(self.done)/self.total, msg)
def process_pages(pages, opts, update): def process_pages(pages, opts, update):
''' '''
@ -211,23 +212,25 @@ def process_pages(pages, opts, update):
''' '''
if not _imagemagick_loaded: if not _imagemagick_loaded:
raise RuntimeError('Failed to load ImageMagick') raise RuntimeError('Failed to load ImageMagick')
with ImageMagick():
tdir = PersistentTemporaryDirectory('_comic2lrf_pp') tdir = PersistentTemporaryDirectory('_comic2lrf_pp')
processed_pages = [PageProcessor(path, tdir, opts, i) for i, path in enumerate(pages)] notify = Progress(len(pages), update)
tp = ThreadPool(detect_ncpus()) server = Server()
update(0, '') jobs = []
notify = Progress(len(pages), update) for i, path in enumerate(pages):
for pp in processed_pages: jobs.append(ParallelJob('render_page', notify, args=[path, tdir, opts, i]))
tp.putRequest(WorkRequest(pp, callback=notify)) server.add_job(jobs[-1])
tp.wait() server.wait()
ans, failures = [], [] server.killall()
server.close()
ans, failures = [], []
for pp in processed_pages: for job in jobs:
if len(pp) == 0: if not job.result:
failures.append(os.path.basename(pp.path_to_page)) failures.append(os.path.basename(job.args[0]))
else: else:
ans += pp ans += job.result
return ans, failures, tdir return ans, failures, tdir
def config(defaults=None): def config(defaults=None):
desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks') desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks')

View File

@ -36,19 +36,22 @@ DEBUG = False
#: A mapping from job names to functions that perform the jobs #: A mapping from job names to functions that perform the jobs
PARALLEL_FUNCS = { PARALLEL_FUNCS = {
'any2lrf' : 'any2lrf' :
('calibre.ebooks.lrf.any.convert_from', 'main', dict(gui_mode=True), None), ('calibre.ebooks.lrf.any.convert_from', 'main', dict(gui_mode=True), None),
'lrfviewer' : 'lrfviewer' :
('calibre.gui2.lrf_renderer.main', 'main', {}, None), ('calibre.gui2.lrf_renderer.main', 'main', {}, None),
'feeds2lrf' : 'feeds2lrf' :
('calibre.ebooks.lrf.feeds.convert_from', 'main', {}, 'notification'), ('calibre.ebooks.lrf.feeds.convert_from', 'main', {}, 'notification'),
'render_table' : 'render_table' :
('calibre.ebooks.lrf.html.table_as_image', 'do_render', {}, None), ('calibre.ebooks.lrf.html.table_as_image', 'do_render', {}, None),
'render_page' :
('calibre.ebooks.lrf.comic.convert_from', 'process_page', {}, None),
'comic2lrf' : 'comic2lrf' :
('calibre.ebooks.lrf.comic.convert_from', 'do_convert', {}, 'notification'), ('calibre.ebooks.lrf.comic.convert_from', 'do_convert', {}, 'notification'),
} }
@ -657,6 +660,21 @@ class Server(Thread):
if job.job_manager is not None: if job.job_manager is not None:
job.job_manager.add_job(job) job.job_manager.add_job(job)
def poll(self):
'''
Return True if the server has either working or queued jobs
'''
with self.job_lock:
with self.working_lock:
return len(self.jobs) + len(self.working) > 0
def wait(self, sleep=1):
'''
Wait until job queue is empty
'''
while self.poll():
time.sleep(sleep)
def run(self): def run(self):
while True: while True:
job = None job = None

View File

@ -46,7 +46,6 @@ __date__ = "$Date: 2006/06/23 12:32:25 $"
__license__ = 'Python license' __license__ = 'Python license'
# standard library modules # standard library modules
import sys
import threading import threading
import Queue import Queue

View File

@ -1136,7 +1136,7 @@ class ZipFile:
self.filelist.append(zinfo) self.filelist.append(zinfo)
self.NameToInfo[zinfo.filename] = zinfo self.NameToInfo[zinfo.filename] = zinfo
def writestr(self, zinfo_or_arcname, bytes, permissions=0600): def writestr(self, zinfo_or_arcname, bytes, permissions=0600, compression=ZIP_DEFLATED):
"""Write a file into the archive. The contents is the string """Write a file into the archive. The contents is the string
'bytes'. 'zinfo_or_arcname' is either a ZipInfo instance or 'bytes'. 'zinfo_or_arcname' is either a ZipInfo instance or
the name of the file in the archive.""" the name of the file in the archive."""
@ -1145,7 +1145,7 @@ class ZipFile:
zinfo_or_arcname = zinfo_or_arcname.encode('utf-8') zinfo_or_arcname = zinfo_or_arcname.encode('utf-8')
zinfo = ZipInfo(filename=zinfo_or_arcname, zinfo = ZipInfo(filename=zinfo_or_arcname,
date_time=time.localtime(time.time())[:6]) date_time=time.localtime(time.time())[:6])
zinfo.compress_type = self.compression zinfo.compress_type = compression
zinfo.external_attr = permissions << 16 zinfo.external_attr = permissions << 16
else: else:
zinfo = zinfo_or_arcname zinfo = zinfo_or_arcname