Sync to trunk

This commit is contained in:
John Schember 2009-01-29 18:25:52 -05:00
commit 4dc03e2c16
24 changed files with 296 additions and 177 deletions

View File

@ -83,6 +83,23 @@ def debug_device_driver():
s = DeviceScanner() s = DeviceScanner()
s.scan() s.scan()
print 'USB devices on system:', repr(s.devices) print 'USB devices on system:', repr(s.devices)
if iswindows:
wmi = __import__('wmi', globals(), locals(), [], -1)
drives = []
print 'Drives detected:'
print '\t', '(ID, Partitions, Drive letter)'
for drive in wmi.WMI().Win32_DiskDrive():
if drive.Partitions == 0:
continue
try:
partition = drive.associators("Win32_DiskDriveToDiskPartition")[0]
logical_disk = partition.associators('Win32_LogicalDiskToPartition')[0]
prefix = logical_disk.DeviceID+os.sep
drives.append((str(drive.PNPDeviceID), drive.Index, prefix))
except IndexError:
drives.append(str(drive.PNPDeviceID))
for drive in drives:
print '\t', drive
from calibre.devices import devices from calibre.devices import devices
for dev in devices(): for dev in devices():
print 'Looking for', dev.__name__ print 'Looking for', dev.__name__

View File

@ -142,6 +142,8 @@ to auto-generate a Table of Contents.
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level one. If this is specified, it takes precedence over other forms of auto-detection.')) help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level one. If this is specified, it takes precedence over other forms of auto-detection.'))
toc('level2_toc', ['--level2-toc'], default=None, toc('level2_toc', ['--level2-toc'], default=None,
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level two. Each entry is added under the previous level one entry.')) help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level two. Each entry is added under the previous level one entry.'))
toc('level3_toc', ['--level3-toc'], default=None,
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level three. Each entry is added under the previous level two entry.'))
toc('from_ncx', ['--from-ncx'], default=None, toc('from_ncx', ['--from-ncx'], default=None,
help=_('Path to a .ncx file that contains the table of contents to use for this ebook. The NCX file should contain links relative to the directory it is placed in. See http://www.niso.org/workrooms/daisy/Z39-86-2005.html#NCX for an overview of the NCX format.')) help=_('Path to a .ncx file that contains the table of contents to use for this ebook. The NCX file should contain links relative to the directory it is placed in. See http://www.niso.org/workrooms/daisy/Z39-86-2005.html#NCX for an overview of the NCX format.'))
toc('use_auto_toc', ['--use-auto-toc'], default=False, toc('use_auto_toc', ['--use-auto-toc'], default=False,

View File

@ -377,16 +377,13 @@ def convert(htmlfile, opts, notification=None, create_epub=True,
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'})
if opts.level1_toc: for x in (1, 2, 3):
opts.level1_toc = XPath(opts.level1_toc, attr = 'level%d_toc'%x
namespaces={'re':'http://exslt.org/regular-expressions'}) if getattr(opts, attr):
else: setattr(opts, attr, XPath(getattr(opts, attr),
opts.level1_toc = None namespaces={'re':'http://exslt.org/regular-expressions'}))
if opts.level2_toc: else:
opts.level2_toc = XPath(opts.level2_toc, setattr(opts, attr, None)
namespaces={'re':'http://exslt.org/regular-expressions'})
else:
opts.level2_toc = None
with TemporaryDirectory(suffix='_html2epub', keep=opts.keep_intermediate) as tdir: with TemporaryDirectory(suffix='_html2epub', keep=opts.keep_intermediate) as tdir:
if opts.keep_intermediate: if opts.keep_intermediate:

View File

@ -307,7 +307,11 @@ class Splitter(LoggingInterface):
Search order is: Search order is:
* Heading tags * Heading tags
* <div> tags * <div> tags
* <pre> tags
* <hr> tags
* <p> tags * <p> tags
* <br> tags
* <li> tags
We try to split in the "middle" of the file (as defined by tag counts. We try to split in the "middle" of the file (as defined by tag counts.
''' '''
@ -327,6 +331,7 @@ class Splitter(LoggingInterface):
'//hr', '//hr',
'//p', '//p',
'//br', '//br',
'//li',
): ):
elems = root.xpath(path, namespaces={'re':'http://exslt.org/regular-expressions'}) elems = root.xpath(path, namespaces={'re':'http://exslt.org/regular-expressions'})
elem = pick_elem(elems) elem = pick_elem(elems)

View File

@ -558,30 +558,21 @@ class Processor(Parser):
def detect_chapters(self): def detect_chapters(self):
self.detected_chapters = self.opts.chapter(self.root) self.detected_chapters = self.opts.chapter(self.root)
chapter_mark = self.opts.chapter_mark
page_break_before = 'display: block; page-break-before: always'
page_break_after = 'display: block; page-break-after: always'
for elem in self.detected_chapters: for elem in self.detected_chapters:
text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')]) text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')])
self.log_info('\tDetected chapter: %s', text[:50]) self.log_info('\tDetected chapter: %s', text[:50])
if self.opts.chapter_mark != 'none': if chapter_mark == 'none':
hr = etree.Element('hr') continue
if elem.getprevious() is None: elif chapter_mark == 'rule':
elem.getparent()[:0] = [hr] mark = etree.Element('hr')
elif elem.getparent() is not None: elif chapter_mark == 'pagebreak':
insert = None mark = etree.Element('div', style=page_break_after)
for i, c in enumerate(elem.getparent()): else: # chapter_mark == 'both':
if c is elem: mark = etree.Element('hr', style=page_break_before)
insert = i elem.addprevious(mark)
break
elem.getparent()[insert:insert] = [hr]
if self.opts.chapter_mark != 'rule':
hr.set('style', 'width:0pt;page-break-before:always')
if self.opts.chapter_mark == 'both':
hr2 = etree.Element('hr')
hr2.tail = u'\u00a0'
p = hr.getparent()
i = p.index(hr)
p[i:i] = [hr2]
def save(self): def save(self):
style_path = os.path.splitext(os.path.basename(self.save_path()))[0] style_path = os.path.splitext(os.path.basename(self.save_path()))[0]
@ -647,6 +638,7 @@ class Processor(Parser):
added[elem] = add_item(_href, frag, text, toc, type='chapter') added[elem] = add_item(_href, frag, text, toc, type='chapter')
add_item(_href, frag, 'Top', added[elem], type='chapter') add_item(_href, frag, 'Top', added[elem], type='chapter')
if self.opts.level2_toc is not None: if self.opts.level2_toc is not None:
added2 = {}
level2 = list(self.opts.level2_toc(self.root)) level2 = list(self.opts.level2_toc(self.root))
for elem in level2: for elem in level2:
level1 = None level1 = None
@ -657,7 +649,21 @@ class Processor(Parser):
text, _href, frag = elem_to_link(elem, href, counter) text, _href, frag = elem_to_link(elem, href, counter)
counter += 1 counter += 1
if text: if text:
added2[elem] = \
add_item(_href, frag, text, level1, type='chapter') add_item(_href, frag, text, level1, type='chapter')
if self.opts.level3_toc is not None:
level3 = list(self.opts.level3_toc(self.root))
for elem in level3:
level2 = None
for item in self.root.iterdescendants():
if item in added2.keys():
level2 = added2[item]
elif item == elem and level2 is not None:
text, _href, frag = elem_to_link(elem, href, counter)
counter += 1
if text:
add_item(_href, frag, text, level2, type='chapter')
if len(toc) > 0: if len(toc) > 0:
return return

View File

@ -106,9 +106,11 @@ class CoverRenderer(QObject):
WIDTH = 600 WIDTH = 600
HEIGHT = 800 HEIGHT = 800
def __init__(self, url, size, loop): def __init__(self, path):
if QApplication.instance() is None:
QApplication([])
QObject.__init__(self) QObject.__init__(self)
self.loop = loop self.loop = QEventLoop()
self.page = QWebPage() self.page = QWebPage()
pal = self.page.palette() pal = self.page.palette()
pal.setBrush(QPalette.Background, Qt.white) pal.setBrush(QPalette.Background, Qt.white)
@ -117,32 +119,42 @@ class CoverRenderer(QObject):
self.page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
self.page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
QObject.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html) QObject.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html)
self.image_data = None self._image_data = None
self.rendered = False self.rendered = False
url = QUrl.fromLocalFile(os.path.normpath(path))
self.page.mainFrame().load(url) self.page.mainFrame().load(url)
def render_html(self, ok): def render_html(self, ok):
self.rendered = True
try: try:
if not ok: if not ok:
self.rendered = True
return return
#size = self.page.mainFrame().contentsSize()
#width, height = fit_image(size.width(), size.height(), self.WIDTH, self.HEIGHT)[1:]
#self.page.setViewportSize(QSize(width, height))
image = QImage(self.page.viewportSize(), QImage.Format_ARGB32) image = QImage(self.page.viewportSize(), QImage.Format_ARGB32)
image.setDotsPerMeterX(96*(100/2.54)) image.setDotsPerMeterX(96*(100/2.54))
image.setDotsPerMeterY(96*(100/2.54)) image.setDotsPerMeterY(96*(100/2.54))
painter = QPainter(image) painter = QPainter(image)
self.page.mainFrame().render(painter) self.page.mainFrame().render(painter)
painter.end() painter.end()
ba = QByteArray() ba = QByteArray()
buf = QBuffer(ba) buf = QBuffer(ba)
buf.open(QBuffer.WriteOnly) buf.open(QBuffer.WriteOnly)
image.save(buf, 'JPEG') image.save(buf, 'JPEG')
self.image_data = str(ba.data()) self._image_data = str(ba.data())
finally: finally:
self.loop.exit(0) self.loop.exit(0)
self.rendered = True
def image_data():
def fget(self):
if not self.rendered:
self.loop.exec_()
count = 0
while count < 50 and not self.rendered:
time.sleep(0.1)
count += 1
return self._image_data
return property(fget=fget)
image_data = image_data()
def get_cover(opf, opf_path, stream): def get_cover(opf, opf_path, stream):
@ -155,20 +167,11 @@ def get_cover(opf, opf_path, stream):
stream.seek(0) stream.seek(0)
ZipFile(stream).extractall() ZipFile(stream).extractall()
opf_path = opf_path.replace('/', os.sep) opf_path = opf_path.replace('/', os.sep)
cpage = os.path.join(tdir, os.path.dirname(opf_path), *cpage.split('/')) cpage = os.path.join(tdir, os.path.dirname(opf_path), cpage)
if not os.path.exists(cpage): if not os.path.exists(cpage):
return return
if QApplication.instance() is None: cr = CoverRenderer(cpage)
QApplication([]) return cr.image_data
url = QUrl.fromLocalFile(cpage)
loop = QEventLoop()
cr = CoverRenderer(url, os.stat(cpage).st_size, loop)
loop.exec_()
count = 0
while count < 50 and not cr.rendered:
time.sleep(0.1)
count += 1
return cr.image_data
def get_metadata(stream, extract_cover=True): def get_metadata(stream, extract_cover=True):
""" Return metadata as a :class:`MetaInformation` object """ """ Return metadata as a :class:`MetaInformation` object """

View File

@ -148,10 +148,6 @@ class MobiMLizer(object):
if bstate.pbreak: if bstate.pbreak:
etree.SubElement(body, MBP('pagebreak')) etree.SubElement(body, MBP('pagebreak'))
bstate.pbreak = False bstate.pbreak = False
if istate.ids:
for id in istate.ids:
etree.SubElement(body, XHTML('a'), attrib={'id': id})
istate.ids.clear()
bstate.istate = None bstate.istate = None
bstate.anchor = None bstate.anchor = None
parent = bstate.nested[-1] if bstate.nested else bstate.body parent = bstate.nested[-1] if bstate.nested else bstate.body
@ -186,14 +182,17 @@ class MobiMLizer(object):
wrapper.attrib['height'] = self.mobimlize_measure(vspace) wrapper.attrib['height'] = self.mobimlize_measure(vspace)
para.attrib['width'] = self.mobimlize_measure(indent) para.attrib['width'] = self.mobimlize_measure(indent)
elif tag == 'table' and vspace > 0: elif tag == 'table' and vspace > 0:
body = bstate.body
vspace = int(round(vspace / self.profile.fbase)) vspace = int(round(vspace / self.profile.fbase))
index = max((0, len(body) - 1))
while vspace > 0: while vspace > 0:
body.insert(index, etree.Element(XHTML('br'))) wrapper.addprevious(etree.Element(XHTML('br')))
vspace -= 1 vspace -= 1
if istate.halign != 'auto': if istate.halign != 'auto':
para.attrib['align'] = istate.halign para.attrib['align'] = istate.halign
if istate.ids:
last = bstate.body[-1]
for id in istate.ids:
last.addprevious(etree.Element(XHTML('a'), attrib={'id': id}))
istate.ids.clear()
pstate = bstate.istate pstate = bstate.istate
if tag in CONTENT_TAGS: if tag in CONTENT_TAGS:
bstate.inline = para bstate.inline = para

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Read data from .mobi files Read data from .mobi files
''' '''
import sys, struct, os, cStringIO, re, atexit, shutil, tempfile import sys, struct, os, cStringIO, re
try: try:
from PIL import Image as PILImage from PIL import Image as PILImage
@ -14,7 +14,7 @@ except ImportError:
from lxml import html, etree from lxml import html, etree
from calibre import __appname__, entity_to_unicode from calibre import entity_to_unicode
from calibre.ebooks import DRMError from calibre.ebooks import DRMError
from calibre.ebooks.chardet import ENCODING_PATS from calibre.ebooks.chardet import ENCODING_PATS
from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi import MobiError
@ -28,7 +28,7 @@ from calibre import sanitize_file_name
class EXTHHeader(object): class EXTHHeader(object):
def __init__(self, raw, codec): def __init__(self, raw, codec, title):
self.doctype = raw[:4] self.doctype = raw[:4]
self.length, self.num_items = struct.unpack('>LL', raw[4:12]) self.length, self.num_items = struct.unpack('>LL', raw[4:12])
raw = raw[12:] raw = raw[12:]
@ -45,21 +45,15 @@ class EXTHHeader(object):
elif id == 203: elif id == 203:
self.has_fake_cover = bool(struct.unpack('>L', content)[0]) self.has_fake_cover = bool(struct.unpack('>L', content)[0])
elif id == 201: elif id == 201:
self.cover_offset, = struct.unpack('>L', content) co, = struct.unpack('>L', content)
if co < 1e7:
self.cover_offset = co
elif id == 202: elif id == 202:
self.thumbnail_offset, = struct.unpack('>L', content) self.thumbnail_offset, = struct.unpack('>L', content)
#else: #else:
# print 'unknown record', id, repr(content) # print 'unknown record', id, repr(content)
title = re.search(r'\0+([^\0]+)\0+', raw[pos:])
if title: if title:
title = title.group(1).decode(codec, 'replace') self.mi.title = title
if len(title) > 2:
self.mi.title = title
else:
title = re.search(r'\0+([^\0]+)\0+', ''.join(reversed(raw[pos:])))
if title:
self.mi.title = ''.join(reversed(title.group(1).decode(codec, 'replace')))
def process_metadata(self, id, content, codec): def process_metadata(self, id, content, codec):
if id == 100: if id == 100:
@ -119,6 +113,9 @@ class BookHeader(object):
if self.compression_type == 'DH': if self.compression_type == 'DH':
self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78]) self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78])
toff, tlen = struct.unpack('>II', raw[0x54:0x5c])
tend = toff + tlen
self.title = raw[toff:tend] if tend < len(raw) else _('Unknown')
langcode = struct.unpack('!L', raw[0x5C:0x60])[0] langcode = struct.unpack('!L', raw[0x5C:0x60])[0]
langid = langcode & 0xFF langid = langcode & 0xFF
sublangid = (langcode >> 10) & 0xFF sublangid = (langcode >> 10) & 0xFF
@ -129,7 +126,7 @@ class BookHeader(object):
self.exth_flag, = struct.unpack('>L', raw[0x80:0x84]) self.exth_flag, = struct.unpack('>L', raw[0x80:0x84])
self.exth = None self.exth = None
if self.exth_flag & 0x40: if self.exth_flag & 0x40:
self.exth = EXTHHeader(raw[16+self.length:], self.codec) self.exth = EXTHHeader(raw[16+self.length:], self.codec, self.title)
self.exth.mi.uid = self.unique_id self.exth.mi.uid = self.unique_id
self.exth.mi.language = self.language self.exth.mi.language = self.language
@ -480,7 +477,7 @@ def get_metadata(stream):
try: try:
if hasattr(mr.book_header.exth, 'cover_offset'): if hasattr(mr.book_header.exth, 'cover_offset'):
cover_index = mr.book_header.first_image_index + mr.book_header.exth.cover_offset cover_index = mr.book_header.first_image_index + mr.book_header.exth.cover_offset
data = mr.sections[cover_index][0] data = mr.sections[int(cover_index)][0]
else: else:
data = mr.sections[mr.book_header.first_image_index][0] data = mr.sections[mr.book_header.first_image_index][0]
buf = cStringIO.StringIO(data) buf = cStringIO.StringIO(data)

View File

@ -23,6 +23,7 @@ from PIL import Image
from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS, \ from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS, \
OEB_RASTER_IMAGES OEB_RASTER_IMAGES
from calibre.ebooks.oeb.base import xpath, barename, namespace, prefixname from calibre.ebooks.oeb.base import xpath, barename, namespace, prefixname
from calibre.ebooks.oeb.base import urlnormalize
from calibre.ebooks.oeb.base import Logger, OEBBook from calibre.ebooks.oeb.base import Logger, OEBBook
from calibre.ebooks.oeb.profile import Context from calibre.ebooks.oeb.profile import Context
from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener
@ -178,7 +179,7 @@ class Serializer(object):
def serialize_href(self, href, base=None): def serialize_href(self, href, base=None):
hrefs = self.oeb.manifest.hrefs hrefs = self.oeb.manifest.hrefs
path, frag = urldefrag(href) path, frag = urldefrag(urlnormalize(href))
if path and base: if path and base:
path = base.abshref(path) path = base.abshref(path)
if path and path not in hrefs: if path and path not in hrefs:
@ -196,6 +197,7 @@ class Serializer(object):
def serialize_body(self): def serialize_body(self):
buffer = self.buffer buffer = self.buffer
self.anchor_offset = buffer.tell()
buffer.write('<body>') buffer.write('<body>')
# CybookG3 'Start Reading' link # CybookG3 'Start Reading' link
if 'text' in self.oeb.guide: if 'text' in self.oeb.guide:
@ -224,14 +226,17 @@ class Serializer(object):
or namespace(elem.tag) not in nsrmap: or namespace(elem.tag) not in nsrmap:
return return
tag = prefixname(elem.tag, nsrmap) tag = prefixname(elem.tag, nsrmap)
for attr in ('name', 'id'): # Previous layers take care of @name
if attr in elem.attrib: id = elem.attrib.pop('id', None)
href = '#'.join((item.href, elem.attrib[attr])) if id is not None:
self.id_offsets[href] = buffer.tell() href = '#'.join((item.href, id))
del elem.attrib[attr] offset = self.anchor_offset or buffer.tell()
if tag == 'a' and not elem.attrib \ self.id_offsets[href] = offset
and not len(elem) and not elem.text: if self.anchor_offset is not None and \
tag == 'a' and not elem.attrib and \
not len(elem) and not elem.text:
return return
self.anchor_offset = buffer.tell()
buffer.write('<') buffer.write('<')
buffer.write(tag) buffer.write(tag)
if elem.attrib: if elem.attrib:
@ -256,10 +261,12 @@ class Serializer(object):
if elem.text or len(elem) > 0: if elem.text or len(elem) > 0:
buffer.write('>') buffer.write('>')
if elem.text: if elem.text:
self.anchor_offset = None
self.serialize_text(elem.text) self.serialize_text(elem.text)
for child in elem: for child in elem:
self.serialize_elem(child, item) self.serialize_elem(child, item)
if child.tail: if child.tail:
self.anchor_offset = None
self.serialize_text(child.tail) self.serialize_text(child.tail)
buffer.write('</%s>' % tag) buffer.write('</%s>' % tag)
else: else:

View File

@ -23,6 +23,8 @@ from calibre import LoggingInterface
from calibre.translations.dynamic import translate from calibre.translations.dynamic import translate
from calibre.startup import get_lang from calibre.startup import get_lang
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
from calibre.ebooks.metadata.epub import CoverRenderer
from calibre.ptempfile import TemporaryDirectory
XML_NS = 'http://www.w3.org/XML/1998/namespace' XML_NS = 'http://www.w3.org/XML/1998/namespace'
XHTML_NS = 'http://www.w3.org/1999/xhtml' XHTML_NS = 'http://www.w3.org/1999/xhtml'
@ -351,9 +353,13 @@ class Manifest(object):
try: try:
data = etree.fromstring(data) data = etree.fromstring(data)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
# TODO: Factor out HTML->XML coercion
self.oeb.logger.warn('Parsing file %r as HTML' % self.href) self.oeb.logger.warn('Parsing file %r as HTML' % self.href)
data = html.fromstring(data) data = html.fromstring(data)
data.attrib.pop('xmlns', None) data.attrib.pop('xmlns', None)
for elem in data.iter(tag=etree.Comment):
if elem.text:
elem.text = elem.text.strip('-')
data = etree.tostring(data, encoding=unicode) data = etree.tostring(data, encoding=unicode)
data = etree.fromstring(data) data = etree.fromstring(data)
# Force into the XHTML namespace # Force into the XHTML namespace
@ -447,7 +453,7 @@ class Manifest(object):
return cmp(skey, okey) return cmp(skey, okey)
def relhref(self, href): def relhref(self, href):
if '/' not in self.href: if '/' not in self.href or ':' in href:
return href return href
base = os.path.dirname(self.href).split('/') base = os.path.dirname(self.href).split('/')
target, frag = urldefrag(href) target, frag = urldefrag(href)
@ -463,7 +469,7 @@ class Manifest(object):
return relhref return relhref
def abshref(self, href): def abshref(self, href):
if '/' not in self.href: if '/' not in self.href or ':' in href:
return href return href
dirname = os.path.dirname(self.href) dirname = os.path.dirname(self.href)
href = os.path.join(dirname, href) href = os.path.join(dirname, href)
@ -546,7 +552,7 @@ class Manifest(object):
elif media_type in OEB_STYLES: elif media_type in OEB_STYLES:
media_type = CSS_MIME media_type = CSS_MIME
attrib = {'id': item.id, 'href': item.href, attrib = {'id': item.id, 'href': item.href,
'media-type': item.media_type} 'media-type': media_type}
if item.fallback: if item.fallback:
attrib['fallback'] = item.fallback attrib['fallback'] = item.fallback
element(elem, OPF('item'), attrib=attrib) element(elem, OPF('item'), attrib=attrib)
@ -796,6 +802,9 @@ class TOC(object):
class OEBBook(object): class OEBBook(object):
COVER_SVG_XP = XPath('h:body//svg:svg[position() = 1]')
COVER_OBJECT_XP = XPath('h:body//h:object[@data][position() = 1]')
def __init__(self, opfpath=None, container=None, encoding=None, def __init__(self, opfpath=None, container=None, encoding=None,
logger=FauxLogger()): logger=FauxLogger()):
if opfpath and not container: if opfpath and not container:
@ -928,7 +937,7 @@ class OEBBook(object):
spine.add(item, elem.get('linear')) spine.add(item, elem.get('linear'))
extras = [] extras = []
for item in self.manifest.values(): for item in self.manifest.values():
if item.media_type == XHTML_MIME \ if item.media_type in OEB_DOCS \
and item not in spine: and item not in spine:
extras.append(item) extras.append(item)
extras.sort() extras.sort()
@ -971,7 +980,7 @@ class OEBBook(object):
ncx = item.data ncx = item.data
self.manifest.remove(item) self.manifest.remove(item)
title = xpath(ncx, 'ncx:docTitle/ncx:text/text()') title = xpath(ncx, 'ncx:docTitle/ncx:text/text()')
title = title[0].strip() if title else unicode(self.metadata.title) title = title[0].strip() if title else unicode(self.metadata.title[0])
self.toc = toc = TOC(title) self.toc = toc = TOC(title)
navmaps = xpath(ncx, 'ncx:navMap') navmaps = xpath(ncx, 'ncx:navMap')
for navmap in navmaps: for navmap in navmaps:
@ -1051,41 +1060,58 @@ class OEBBook(object):
if self._toc_from_html(opf): return if self._toc_from_html(opf): return
self._toc_from_spine(opf) self._toc_from_spine(opf)
def _ensure_cover_image(self): def _cover_from_html(self, hcover):
cover = None with TemporaryDirectory('_html_cover') as tdir:
writer = DirWriter()
writer.dump(self, tdir)
path = os.path.join(tdir, hcover.href)
renderer = CoverRenderer(path)
data = renderer.image_data
id, href = self.manifest.generate('cover', 'cover.jpeg')
item = self.manifest.add(id, href, JPEG_MIME, data=data)
return item
def _locate_cover_image(self):
if self.metadata.cover:
id = str(self.metadata.cover[0])
item = self.manifest.ids.get(id, None)
if item is not None:
return item
hcover = self.spine[0] hcover = self.spine[0]
if 'cover' in self.guide: if 'cover' in self.guide:
href = self.guide['cover'].href href = self.guide['cover'].href
item = self.manifest.hrefs[href] item = self.manifest.hrefs[href]
media_type = item.media_type media_type = item.media_type
if media_type in OEB_RASTER_IMAGES: if media_type in OEB_IMAGES:
cover = item return item
elif media_type in OEB_DOCS: elif media_type in OEB_DOCS:
hcover = item hcover = item
html = hcover.data html = hcover.data
if cover is not None: if MS_COVER_TYPE in self.guide:
pass
elif self.metadata.cover:
id = str(self.metadata.cover[0])
cover = self.manifest.ids[id]
elif MS_COVER_TYPE in self.guide:
href = self.guide[MS_COVER_TYPE].href href = self.guide[MS_COVER_TYPE].href
cover = self.manifest.hrefs[href] item = self.manifest.hrefs.get(href, None)
elif xpath(html, '//h:img[position()=1]'): if item is not None and item.media_type in OEB_IMAGES:
img = xpath(html, '//h:img[position()=1]')[0] return item
href = hcover.abshref(img.get('src')) if self.COVER_SVG_XP(html):
cover = self.manifest.hrefs[href] svg = copy.deepcopy(self.COVER_SVG_XP(html)[0])
elif xpath(html, '//h:object[position()=1]'):
object = xpath(html, '//h:object[position()=1]')[0]
href = hcover.abshref(object.get('data'))
cover = self.manifest.hrefs[href]
elif xpath(html, '//svg:svg[position()=1]'):
svg = copy.deepcopy(xpath(html, '//svg:svg[position()=1]')[0])
href = os.path.splitext(hcover.href)[0] + '.svg' href = os.path.splitext(hcover.href)[0] + '.svg'
id, href = self.manifest.generate(hcover.id, href) id, href = self.manifest.generate(hcover.id, href)
cover = self.manifest.add(id, href, SVG_MIME, data=svg) item = self.manifest.add(id, href, SVG_MIME, data=svg)
if cover and not self.metadata.cover: return item
self.metadata.add('cover', cover.id) if self.COVER_OBJECT_XP(html):
object = self.COVER_OBJECT_XP(html)[0]
href = hcover.abshref(object.get('data'))
item = self.manifest.hrefs.get(href, None)
if item is not None and item.media_type in OEB_IMAGES:
return item
return self._cover_from_html(hcover)
def _ensure_cover_image(self):
cover = self._locate_cover_image()
if self.metadata.cover:
self.metadata.cover[0].value = cover.id
return
self.metadata.add('cover', cover.id)
def _all_from_opf(self, opf): def _all_from_opf(self, opf):
self._metadata_from_opf(opf) self._metadata_from_opf(opf)

View File

@ -265,6 +265,8 @@ class Stylizer(object):
class Style(object): class Style(object):
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|px|mm|cm|in|pt|pc)$')
def __init__(self, element, stylizer): def __init__(self, element, stylizer):
self._element = element self._element = element
self._profile = stylizer.profile self._profile = stylizer.profile
@ -319,13 +321,11 @@ class Style(object):
if isinstance(value, (int, long, float)): if isinstance(value, (int, long, float)):
return value return value
try: try:
if float(value) == 0: return float(value) * 72.0 / self._profile.dpi
return 0.0
except: except:
pass pass
result = value result = value
m = re.search( m = self.UNIT_RE.match(value)
r"^(-*[0-9]*\.?[0-9]*)\s*(%|em|px|mm|cm|in|pt|pc)$", value)
if m is not None and m.group(1): if m is not None and m.group(1):
value = float(m.group(1)) value = float(m.group(1))
unit = m.group(2) unit = m.group(2)

View File

@ -23,6 +23,12 @@ from calibre.ebooks.oeb.stylizer import Stylizer
COLLAPSE = re.compile(r'[ \t\r\n\v]+') COLLAPSE = re.compile(r'[ \t\r\n\v]+')
STRIPNUM = re.compile(r'[-0-9]+$') STRIPNUM = re.compile(r'[-0-9]+$')
def asfloat(value, default):
if not isinstance(value, (int, long, float)):
value = default
return float(value)
class KeyMapper(object): class KeyMapper(object):
def __init__(self, sbase, dbase, dkey): def __init__(self, sbase, dbase, dkey):
self.sbase = float(sbase) self.sbase = float(sbase)
@ -179,12 +185,13 @@ class CSSFlattener(object):
if cssdict: if cssdict:
if self.lineh and self.fbase and tag != 'body': if self.lineh and self.fbase and tag != 'body':
self.clean_edges(cssdict, style, psize) self.clean_edges(cssdict, style, psize)
margin = style['margin-left'] margin = asfloat(style['margin-left'], 0)
left += margin if isinstance(margin, float) else 0 indent = asfloat(style['text-indent'], 0)
if (left + style['text-indent']) < 0: left += margin
percent = (margin - style['text-indent']) / style['width'] if (left + indent) < 0:
percent = (margin - indent) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100) cssdict['margin-left'] = "%d%%" % (percent * 100)
left -= style['text-indent'] left -= indent
if 'display' in cssdict and cssdict['display'] == 'in-line': if 'display' in cssdict and cssdict['display'] == 'in-line':
cssdict['display'] = 'inline' cssdict['display'] = 'inline'
if self.unfloat and 'float' in cssdict \ if self.unfloat and 'float' in cssdict \

View File

@ -23,6 +23,7 @@ from PyQt4.QtGui import QApplication
from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK
from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME, JPEG_MIME from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME, JPEG_MIME
from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename
from calibre.ebooks.oeb.base import urlnormalize
from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks.oeb.stylizer import Stylizer
IMAGE_TAGS = set([XHTML('img'), XHTML('object')]) IMAGE_TAGS = set([XHTML('img'), XHTML('object')])
@ -78,7 +79,7 @@ class SVGRasterizer(object):
svg = item.data svg = item.data
hrefs = self.oeb.manifest.hrefs hrefs = self.oeb.manifest.hrefs
for elem in xpath(svg, '//svg:*[@xl:href]'): for elem in xpath(svg, '//svg:*[@xl:href]'):
href = elem.attrib[XLINK('href')] href = urlnormalize(elem.attrib[XLINK('href')])
path, frag = urldefrag(href) path, frag = urldefrag(href)
if not path: if not path:
continue continue
@ -100,15 +101,15 @@ class SVGRasterizer(object):
def rasterize_item(self, item, stylizer): def rasterize_item(self, item, stylizer):
html = item.data html = item.data
hrefs = self.oeb.manifest.hrefs hrefs = self.oeb.manifest.hrefs
for elem in xpath(html, '//h:img'): for elem in xpath(html, '//h:img[@src]'):
src = elem.get('src', None) src = urlnormalize(elem.attrib['src'])
image = hrefs.get(item.abshref(src), None) if src else None image = hrefs.get(item.abshref(src), None)
if image and image.media_type == SVG_MIME: if image and image.media_type == SVG_MIME:
style = stylizer.style(elem) style = stylizer.style(elem)
self.rasterize_external(elem, style, item, image) self.rasterize_external(elem, style, item, image)
for elem in xpath(html, '//h:object[@type="%s"]' % SVG_MIME): for elem in xpath(html, '//h:object[@type="%s" and @data]' % SVG_MIME):
data = elem.get('data', None) data = urlnormalize(elem.attrib['data'])
image = hrefs.get(item.abshref(data), None) if data else None image = hrefs.get(item.abshref(data), None)
if image and image.media_type == SVG_MIME: if image and image.media_type == SVG_MIME:
style = stylizer.style(elem) style = stylizer.style(elem)
self.rasterize_external(elem, style, item, image) self.rasterize_external(elem, style, item, image)

View File

@ -54,7 +54,7 @@ class ManifestTrimmer(object):
new.add(found) new.add(found)
elif item.media_type == CSS_MIME: elif item.media_type == CSS_MIME:
def replacer(uri): def replacer(uri):
absuri = item.abshref(uri) absuri = item.abshref(urlnormalize(uri))
if absuri in oeb.manifest.hrefs: if absuri in oeb.manifest.hrefs:
found = oeb.manifest.hrefs[href] found = oeb.manifest.hrefs[href]
if found not in used: if found not in used:

View File

@ -252,7 +252,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.source_format = d.format() self.source_format = d.format()
def accept(self): def accept(self):
for opt in ('chapter', 'level1_toc', 'level2_toc'): for opt in ('chapter', 'level1_toc', 'level2_toc', 'level3_toc'):
text = unicode(getattr(self, 'opt_'+opt).text()) text = unicode(getattr(self, 'opt_'+opt).text())
if text: if text:
try: try:

View File

@ -93,7 +93,7 @@
<item> <item>
<widget class="QStackedWidget" name="stack" > <widget class="QStackedWidget" name="stack" >
<property name="currentIndex" > <property name="currentIndex" >
<number>1</number> <number>0</number>
</property> </property>
<widget class="QWidget" name="metadata_page" > <widget class="QWidget" name="metadata_page" >
<layout class="QGridLayout" name="gridLayout_4" > <layout class="QGridLayout" name="gridLayout_4" >
@ -105,36 +105,6 @@
<string>Book Cover</string> <string>Book Cover</string>
</property> </property>
<layout class="QGridLayout" name="_2" > <layout class="QGridLayout" name="_2" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" > <item row="1" column="0" >
<layout class="QVBoxLayout" name="_4" > <layout class="QVBoxLayout" name="_4" >
<property name="spacing" > <property name="spacing" >
@ -186,6 +156,36 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
<zorder>opt_prefer_metadata_cover</zorder> <zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder> <zorder></zorder>
@ -777,10 +777,10 @@ p, li { white-space: pre-wrap; }
<item row="5" column="1" > <item row="5" column="1" >
<widget class="QLineEdit" name="opt_level2_toc" /> <widget class="QLineEdit" name="opt_level2_toc" />
</item> </item>
<item row="6" column="1" > <item row="7" column="1" >
<widget class="QLineEdit" name="opt_toc_title" /> <widget class="QLineEdit" name="opt_toc_title" />
</item> </item>
<item row="6" column="0" > <item row="7" column="0" >
<widget class="QLabel" name="toc_title_label" > <widget class="QLabel" name="toc_title_label" >
<property name="text" > <property name="text" >
<string>&amp;Title for generated TOC</string> <string>&amp;Title for generated TOC</string>
@ -790,6 +790,19 @@ p, li { white-space: pre-wrap; }
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1" >
<widget class="QLineEdit" name="opt_level3_toc" />
</item>
<item row="6" column="0" >
<widget class="QLabel" name="label_11" >
<property name="text" >
<string>Level &amp;3 TOC</string>
</property>
<property name="buddy" >
<cstring>opt_level3_toc</cstring>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -638,6 +638,31 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>title</tabstop>
<tabstop>swap_button</tabstop>
<tabstop>authors</tabstop>
<tabstop>author_sort</tabstop>
<tabstop>auto_author_sort</tabstop>
<tabstop>rating</tabstop>
<tabstop>publisher</tabstop>
<tabstop>tags</tabstop>
<tabstop>series</tabstop>
<tabstop>tag_editor_button</tabstop>
<tabstop>remove_series_button</tabstop>
<tabstop>series_index</tabstop>
<tabstop>isbn</tabstop>
<tabstop>comments</tabstop>
<tabstop>fetch_metadata_button</tabstop>
<tabstop>fetch_cover_button</tabstop>
<tabstop>password_button</tabstop>
<tabstop>formats</tabstop>
<tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop>
<tabstop>button_set_cover</tabstop>
<tabstop>cover_path</tabstop>
<tabstop>cover_button</tabstop>
<tabstop>reset_cover</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>button_box</tabstop> <tabstop>button_box</tabstop>
</tabstops> </tabstops>
<resources> <resources>

View File

@ -8,7 +8,7 @@ Scheduler for automated recipe downloads
''' '''
import sys, copy, time import sys, copy, time
from datetime import datetime, timedelta from datetime import datetime, timedelta, date
from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \ from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \
QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \ QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \
QFile, QObject, QTimer, QMutex, QMenu, QAction, QTime QFile, QObject, QTimer, QMutex, QMenu, QAction, QTime
@ -289,7 +289,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
recipe.last_downloaded = datetime.fromordinal(1) recipe.last_downloaded = datetime.fromordinal(1)
recipes.append(recipe) recipes.append(recipe)
if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]: if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]:
error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_() error_dialog(self, _('Must set account information'),
_('This recipe requires a username and password')).exec_()
self.schedule.setCheckState(Qt.Unchecked) self.schedule.setCheckState(Qt.Unchecked)
return return
if self.interval_button.isChecked(): if self.interval_button.isChecked():
@ -350,9 +351,11 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.username.blockSignals(False) self.username.blockSignals(False)
self.password.blockSignals(False) self.password.blockSignals(False)
d = datetime.utcnow() - recipe.last_downloaded d = datetime.utcnow() - recipe.last_downloaded
ld = '%.2f'%(d.days + d.seconds/(24.*3600)) def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60
hours, minutes = hm(d.seconds)
tm = _('%d days, %d hours and %d minutes ago')%(d.days, hours, minutes)
if d < timedelta(days=366): if d < timedelta(days=366):
self.last_downloaded.setText(_('Last downloaded: %s days ago')%ld) self.last_downloaded.setText(_('Last downloaded')+': '+tm)
else: else:
self.last_downloaded.setText(_('Last downloaded: never')) self.last_downloaded.setText(_('Last downloaded: never'))
@ -431,7 +434,7 @@ class Scheduler(QObject):
day_matches = day > 6 or day == now.tm_wday day_matches = day > 6 or day == now.tm_wday
tnow = now.tm_hour*60 + now.tm_min tnow = now.tm_hour*60 + now.tm_min
matches = day_matches and (hour*60+minute) < tnow matches = day_matches and (hour*60+minute) < tnow
if matches and delta >= timedelta(days=1): if matches and nowt.toordinal() < date.today().toordinal():
needs_downloading.add(recipe) needs_downloading.add(recipe)
self.debug('Needs downloading:', needs_downloading) self.debug('Needs downloading:', needs_downloading)

View File

@ -5,7 +5,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>726</width> <width>738</width>
<height>575</height> <height>575</height>
</rect> </rect>
</property> </property>
@ -194,6 +194,9 @@
<property name="text" > <property name="text" >
<string/> <string/>
</property> </property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>

View File

@ -21,3 +21,4 @@ class BookView(QGraphicsView):
def resize_for(self, width, height): def resize_for(self, width, height):
self.preferred_size = QSize(width, height) self.preferred_size = QSize(width, height)

View File

@ -80,8 +80,8 @@ class Main(MainWindow, Ui_MainWindow):
QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find) QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find)
self.action_next_page.setShortcuts(QKeySequence.MoveToNextPage) self.action_next_page.setShortcuts([QKeySequence.MoveToNextPage, QKeySequence(Qt.Key_Space)])
self.action_previous_page.setShortcuts(QKeySequence.MoveToPreviousPage) self.action_previous_page.setShortcuts([QKeySequence.MoveToPreviousPage, QKeySequence(Qt.Key_Backspace)])
self.action_next_match.setShortcuts(QKeySequence.FindNext) self.action_next_match.setShortcuts(QKeySequence.FindNext)
self.addAction(self.action_next_match) self.addAction(self.action_next_match)
QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next) QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next)
@ -191,6 +191,7 @@ class Main(MainWindow, Ui_MainWindow):
self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,)) self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,))
self.spin_box.updateGeometry() self.spin_box.updateGeometry()
self.stack.setCurrentIndex(0) self.stack.setCurrentIndex(0)
self.graphics_view.setFocus(Qt.OtherFocusReason)
elif self.renderer.exception is not None: elif self.renderer.exception is not None:
exception = self.renderer.exception exception = self.renderer.exception
print >>sys.stderr, 'Error rendering document' print >>sys.stderr, 'Error rendering document'

View File

@ -312,7 +312,8 @@ class LibraryServer(object):
book, books = MarkupTemplate(self.BOOK), [] book, books = MarkupTemplate(self.BOOK), []
for record in items[start:start+num]: for record in items[start:start+num]:
authors = '|'.join([i.replace('|', ',') for i in record[2].split(',')]) aus = record[2] if record[2] else _('Unknown')
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
updated = self.db.last_modified() updated = self.db.last_modified()

View File

@ -299,10 +299,10 @@ To learn more about writing advanced recipes using some of the facilities, avail
:ref:`API Documentation <news_recipe>` :ref:`API Documentation <news_recipe>`
Documentation of the ``BasicNewsRecipe`` class and all its important methods and fields. Documentation of the ``BasicNewsRecipe`` class and all its important methods and fields.
`BasicNewsRecipe <http://bazaar.launchpad.net/~kovid/calibre/trunk/annotate/kovid%40kovidgoyal.net-20080509231359-le3xf7ynwc6eew90?file_id=1245%40b0dd1a5d-880a-0410-ada5-a57097536bc1%3Alibprs500%252Ftrunk%3Asrc%252Flibprs500%252Fweb%252Ffeeds%252Fnews.py>`_ `BasicNewsRecipe <http://bazaar.launchpad.net/~kovid/calibre/trunk/annotate/head:/src/calibre/web/feeds/news.py>`_
The source code of ``BasicNewsRecipe`` The source code of ``BasicNewsRecipe``
`Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/kovid%40kovidgoyal.net-20080509231359-le3xf7ynwc6eew90?file_id=1298%40b0dd1a5d-880a-0410-ada5-a57097536bc1%3Alibprs500%252Ftrunk%3Asrc%252Flibprs500%252Fweb%252Ffeeds%252Frecipes>`_ `Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/head:/src/calibre/web/feeds/recipes/>`_
The source code for the built-in recipes that come with |app| The source code for the built-in recipes that come with |app|
Migrating old style profiles to recipes Migrating old style profiles to recipes

View File

@ -32,3 +32,8 @@ class LondonReviewOfBooks(BasicNewsRecipe):
def print_version(self, url): def print_version(self, url):
main, split, rest = url.rpartition('/') main, split, rest = url.rpartition('/')
return main + '/print/' + rest return main + '/print/' + rest
def postprocess_html(self, soup, first_fetch):
for t in soup.findAll(['table', 'tr', 'td']):
t.name = 'div'
return soup