Merge from trunk

This commit is contained in:
Charles Haley 2010-10-18 11:10:43 +01:00
commit b5f80d5855
59 changed files with 43284 additions and 19454 deletions

View File

@ -4,6 +4,111 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.24
date: 2010-10-17
new features:
- title: "Content server: New interface that allows browsing via categories, similar to the Tag Browser in the calibre interface."
description: >
"You can access the new interface by going to /browse. So if your calibre content server is available at http://192.168.1.2, use
http://192.168.1.2/browse. The new interface requires a fairly modern browser, so no Internet Explorer 6,7."
type: major
- title: "Support for the SNB e-book format, used by the Bambook e-book reader"
type: major
- title: "Driver for the Wifi Kobo"
- title: "Edit metadata dialog: If metadata is downloaded successfully, set focus to download cover button"
- title: "News download system: Allow recipes with optional subscriptions"
tickets: [7199]
- title: "Templates: Improve the smarten function"
- title: "Linux device mounting: Use udisks, if it is available, to mount devices, so that I no longer have to hear bug reports from users using distro packages that have crippled calibre-mount-helper. You can turn off udisks by setting the environment variable CALIBRE_DISABLE_UDISKS=1"
- title: "Implement Drag'n'drop to tags in user categories"
tickets: [7172]
- title: "Ebook viewer: Add command line option to start in full screen mode"
- title: "Set completion mode on search boxes to popup completion"
- title: "Update version of jQuery used in content server and viewer. Required a little hackery in the viewer, hopefully nothing broke"
bug fixes:
- title: "Linux device drivers: Ignore read only partition exported by the device"
- title: "E-book viewer: Fix scrolling down with mouse wheel not always reaching bottom in windows"
- title: "Smarten punctuation: Fix bug in handling of comments and <style> tags"
- title: "EPUB Input: Handle EPUB files with components encoded in an encoding other than UTF-8 correctly, though why anyone would do that is a mystery."
tickets: [7196]
- title: "OS X commandline tools: Decode non-ascii command line arguments correctly"
tickets: [6964]
- title: "MOBI Output: Fix bug that broke conversion of <svg> elements in the input document when the <svg> element was followed by non-whitespace text."
tickets: [7083]
- title: "CHM Input: Fix handling of relative file paths in <img> tags."
tickets: [7159]
- title: "EPUB Output: Fix incorrect format for xml:lang when specifying a sub language"
tickets: [7198]
- title: "EPUB Input: Make parsing of toc.ncx more robust."
tickets: [7170]
- title: "Content server: Fix searching with non-ascii characters on windows"
tickets: [5249]
- title: "Fix average rating calculation for rating datatype in Tag Browser incorrect"
- title: "Comic Input: Fix image borders becoming yellow on some windows installs"
- title: "Email sending: Fix sending of email with non ascii chars"
tickets: [7137]
- title: "SONY driver: Fix collections created from series not in order with manual metadata management, if all books in the series are not sent at once"
- title: "Content server: Apply the search restriction when generating category lists as well"
- title: "RTF Input: Fix regression in conversion of WMF images on linux at least, maybe on other platforms as wel"
- title: "Fix isbndb.com metadata downloading sometimes yield a title of Unknown"
tickets: [7114]
- title: "Fix edit metadata dialog causing the hour:minute:seconds of the date column being lost, even when date is not changed"
tickets: [7125]
new recipes:
- title: "Revista El Cultural"
author: "Jefferson Frantz"
- title: "Novaya Gazeta"
author: "muwa"
- title: "frazpc.pl"
author: "Tomasz Dlugosz"
- title: "Orsai and Financial Times UK"
author: "Darko Miletic"
- title: "Malayasian Mirror and Rolling Stones"
author: "Tony Stegall"
improved recipes:
- Globe and Mail
- Business Standard
- Miami Herald
- El Mercurio
- volkskrant.nl
- GoComics.com
- The New Yorker
- version: 0.7.23
date: 2010-10-08
@ -51,6 +156,7 @@
- title: "CHM input: handle another class of broken CHM files"
tickets: [7058]
- title: "Make calibre worker processes use the same temp directory as the calibre GUI"
new recipes:
- title: "Communications of the Association for Computing Machinery"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -38,13 +38,19 @@ class Push(Command):
description = 'Push code to another host'
def run(self, opts):
from threading import Thread
threads = []
for host in (
r'Owner@winxp:/cygdrive/c/Documents\ and\ Settings/Owner/calibre',
'kovid@ox:calibre'
):
rcmd = BASE_RSYNC + EXCLUDES + ['.', host]
print '\n\nPushing to:', host, '\n'
threads.append(Thread(target=subprocess.check_call, args=(rcmd,)))
threads[-1].start()
subprocess.check_call(rcmd)
for thread in threads:
thread.join()

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.23'
__version__ = '0.7.24'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -292,6 +292,17 @@ class RTFMetadataReader(MetadataReaderPlugin):
def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.rtf import get_metadata
return get_metadata(stream)
class SNBMetadataReader(MetadataReaderPlugin):
name = 'Read SNB metadata'
file_types = set(['snb'])
description = _('Read metadata from %s files') % 'SNB'
author = 'Li Fanxi'
def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.snb import get_metadata
return get_metadata(stream)
class TOPAZMetadataReader(MetadataReaderPlugin):
@ -420,6 +431,7 @@ from calibre.ebooks.tcr.input import TCRInput
from calibre.ebooks.txt.input import TXTInput
from calibre.ebooks.lrf.input import LRFInput
from calibre.ebooks.chm.input import CHMInput
from calibre.ebooks.snb.input import SNBInput
from calibre.ebooks.epub.output import EPUBOutput
from calibre.ebooks.fb2.output import FB2Output
@ -434,6 +446,7 @@ from calibre.ebooks.rb.output import RBOutput
from calibre.ebooks.rtf.output import RTFOutput
from calibre.ebooks.tcr.output import TCROutput
from calibre.ebooks.txt.output import TXTOutput
from calibre.ebooks.snb.output import SNBOutput
from calibre.customize.profiles import input_profiles, output_profiles
@ -495,6 +508,7 @@ plugins += [
TXTInput,
LRFInput,
CHMInput,
SNBInput,
]
plugins += [
EPUBOutput,
@ -510,6 +524,7 @@ plugins += [
RTFOutput,
TCROutput,
TXTOutput,
SNBOutput,
]
# Order here matters. The first matched device is the one used.
plugins += [

View File

@ -120,6 +120,11 @@ class InputFormatPlugin(Plugin):
#: to make its output suitable for viewing
for_viewer = False
#: The encoding that this input plugin creates files in. A value of
#: None means that the encoding is undefined and must be
#: detected individually
output_encoding = 'utf-8'
#: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`.

View File

@ -647,11 +647,25 @@ class NookOutput(OutputProfile):
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
class BambookOutput(OutputProfile):
name = 'Sanda Bambook'
short_name = 'bambook'
description = _('This profile is intended for the Sanda Bambook.')
# Screen size is a best guess
screen_size = (800, 600)
comic_screen_size = (700, 540)
dpi = 168.451
fbase = 12
fsizes = [10, 12, 14, 16]
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
iPadOutput, KoboReaderOutput,
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,]
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
BambookOutput, ]
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))

View File

@ -36,8 +36,8 @@ class KOBO(USBMS):
PRODUCT_ID = [0x4161]
BCD = [0x0110]
VENDOR_NAME = 'KOBO_INC'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '.KOBOEREADER'
VENDOR_NAME = ['KOBO_INC', 'KOBO']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['.KOBOEREADER', 'EREADER']
EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True

View File

@ -25,7 +25,7 @@ class DRMError(ValueError):
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan']
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'mbp', 'tan', 'snb']
class HTMLRenderer(object):

View File

@ -93,6 +93,7 @@ class CHMReader(CHMFile):
return data
def ExtractFiles(self, output_dir=os.getcwdu()):
html_files = set([])
for path in self.Contents():
lpath = os.path.join(output_dir, path)
self._ensure_dir(lpath)
@ -106,14 +107,27 @@ class CHMReader(CHMFile):
lpath = lpath.split(';')[0]
try:
with open(lpath, 'wb') as f:
if guess_mimetype(path)[0] == ('text/html'):
data = self._reformat(data)
f.write(data)
try:
if 'html' in guess_mimetype(path)[0]:
html_files.add(lpath)
except:
pass
except:
if iswindows and len(lpath) > 250:
self.log.warn('%r filename too long, skipping'%path)
continue
raise
for lpath in html_files:
with open(lpath, 'r+b') as f:
data = f.read()
data = self._reformat(data, lpath)
if isinstance(data, unicode):
data = data.encode('utf-8')
f.seek(0)
f.truncate()
f.write(data)
self._extracted = True
files = [x for x in os.listdir(output_dir) if
os.path.isfile(os.path.join(output_dir, x))]
@ -125,7 +139,7 @@ class CHMReader(CHMFile):
if self.hhc_path not in files and files:
self.hhc_path = files[0]
def _reformat(self, data):
def _reformat(self, data, htmlpath):
try:
data = xml_to_unicode(data, strip_encoding_pats=True)[0]
soup = BeautifulSoup(data)
@ -169,15 +183,19 @@ class CHMReader(CHMFile):
br[0].extract()
# some images seem to be broken in some chm's :/
for img in soup('img'):
try:
# some are supposedly "relative"... lies.
while img['src'].startswith('../'): img['src'] = img['src'][3:]
# some have ";<junk>" at the end.
img['src'] = img['src'].split(';')[0]
except KeyError:
# and some don't even have a src= ?!
pass
base = os.path.dirname(htmlpath)
for img in soup('img', src=True):
src = img['src']
ipath = os.path.join(base, *src.split('/'))
if os.path.exists(ipath):
continue
src = src.split(';')[0]
if not src: continue
ipath = os.path.join(base, *src.split('/'))
if not os.path.exists(ipath):
while src.startswith('../'):
src = src[3:]
img['src'] = src
try:
# if there is only a single table with a single element
# in the body, replace it by the contents of this single element

View File

@ -838,7 +838,8 @@ OptionRecommendation(name='timestamp',
self.opts_to_mi(self.user_metadata)
if not hasattr(self.oeb, 'manifest'):
self.oeb = create_oebbook(self.log, self.oeb, self.opts,
self.input_plugin)
self.input_plugin,
encoding=self.input_plugin.output_encoding)
self.input_plugin.postprocess_book(self.oeb, self.opts, self.log)
self.opts.is_image_collection = self.input_plugin.is_image_collection
pr = CompositeProgressReporter(0.34, 0.67, self.ui_reporter)

View File

@ -543,6 +543,13 @@ class HTMLPreProcessor(object):
def smarten_punctuation(self, html):
from calibre.utils.smartypants import smartyPants
from calibre.ebooks.chardet import substitute_entites
from uuid import uuid4
start = 'calibre-smartypants-'+str(uuid4())
stop = 'calibre-smartypants-'+str(uuid4())
html = html.replace('<!--', start)
html = html.replace('-->', stop)
html = smartyPants(html)
html = html.replace(start, '<!--')
html = html.replace(stop, '-->')
return substitute_entites(html)

View File

@ -16,6 +16,7 @@ class EPUBInput(InputFormatPlugin):
author = 'Kovid Goyal'
description = 'Convert EPUB files (.epub) to HTML'
file_types = set(['epub'])
output_encoding = None
recommendations = set([('page_breaks_before', '/', OptionRecommendation.MED)])

View File

@ -15,7 +15,7 @@ _METADATA_PRIORITIES = [
'html', 'htm', 'xhtml', 'xhtm',
'rtf', 'fb2', 'pdf', 'prc', 'odt',
'epub', 'lit', 'lrx', 'lrf', 'mobi',
'rb', 'imp', 'azw'
'rb', 'imp', 'azw', 'snb'
]
# The priorities for loading metadata from different file types

View File

@ -0,0 +1,47 @@
'''Read meta information from SNB files'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
import os
from StringIO import StringIO
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.snb.snbfile import SNBFile
from lxml import etree
def get_metadata(stream, extract_cover=True):
""" Return metadata as a L{MetaInfo} object """
mi = MetaInformation(_('Unknown'), [_('Unknown')])
snbFile = SNBFile()
try:
if not hasattr(stream, 'write'):
snbFile.Parse(StringIO(stream), True)
else:
stream.seek(0)
snbFile.Parse(stream, True)
meta = snbFile.GetFileStream('snbf/book.snbf')
if meta != None:
meta = etree.fromstring(meta)
mi.title = meta.find('.//head/name').text
mi.authors = [meta.find('.//head/author').text]
mi.language = meta.find('.//head/language').text.lower().replace('_', '-')
mi.publisher = meta.find('.//head/publisher').text
if extract_cover:
cover = meta.find('.//head/cover')
if cover != None and cover.text != None:
root, ext = os.path.splitext(cover.text)
if ext == '.jpeg':
ext = '.jpg'
mi.cover_data = (ext[-3:], snbFile.GetFileStream('snbc/images/' + cover.text))
except Exception:
import traceback
traceback.print_exc()
return mi

View File

@ -282,9 +282,9 @@ def XPath(expr):
def xpath(elem, expr):
return elem.xpath(expr, namespaces=XPNSMAP)
def xml2str(root, pretty_print=False, strip_comments=False):
def xml2str(root, pretty_print=False, strip_comments=False, with_tail=True):
ans = etree.tostring(root, encoding='utf-8', xml_declaration=True,
pretty_print=pretty_print)
pretty_print=pretty_print, with_tail=with_tail)
if strip_comments:
ans = re.compile(r'<!--.*?-->', re.DOTALL).sub('', ans)
@ -1908,6 +1908,7 @@ class OEBBook(object):
def _to_ncx(self):
lang = unicode(self.metadata.language[0])
lang = lang.replace('_', '-')
ncx = etree.Element(NCX('ncx'),
attrib={'version': '2005-1', XML('lang'): lang},
nsmap={None: NCX_NS})

View File

@ -55,7 +55,7 @@ class SVGRasterizer(object):
self.rasterize_cover()
def rasterize_svg(self, elem, width=0, height=0, format='PNG'):
data = QByteArray(xml2str(elem))
data = QByteArray(xml2str(elem, with_tail=False))
svg = QSvgRenderer(data)
size = svg.defaultSize()
view_box = elem.get('viewBox', elem.get('viewbox', None))

View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
'''
Used for snb output
'''

103
src/calibre/ebooks/snb/input.py Executable file
View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import os, uuid
from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.oeb.base import DirContainer
from calibre.ebooks.snb.snbfile import SNBFile
from calibre.ptempfile import TemporaryDirectory
from calibre.utils.filenames import ascii_filename
from lxml import etree
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
def html_encode(s):
return s.replace(u'&', u'&amp;').replace(u'<', u'&lt;').replace(u'>', u'&gt;').replace(u'"', u'&quot;').replace(u"'", u'&apos;').replace(u'\n', u'<br/>').replace(u' ', u'&nbsp;')
class SNBInput(InputFormatPlugin):
name = 'SNB Input'
author = 'Li Fanxi'
description = 'Convert SNB files to OEB'
file_types = set(['snb'])
options = set([
])
def convert(self, stream, options, file_ext, log,
accelerators):
log.debug("Parsing SNB file...")
snbFile = SNBFile()
try:
snbFile.Parse(stream)
except:
raise ValueError("Invalid SNB file")
if not snbFile.IsValid():
log.debug("Invaild SNB file")
raise ValueError("Invalid SNB file")
log.debug("Handle meta data ...")
from calibre.ebooks.conversion.plumber import create_oebbook
oeb = create_oebbook(log, None, options, self,
encoding=options.input_encoding, populate=False)
meta = snbFile.GetFileStream('snbf/book.snbf')
if meta != None:
meta = etree.fromstring(meta)
oeb.metadata.add('title', meta.find('.//head/name').text)
oeb.metadata.add('creator', meta.find('.//head/author').text, attrib={'role':'aut'})
oeb.metadata.add('language', meta.find('.//head/language').text.lower().replace('_', '-'))
oeb.metadata.add('creator', meta.find('.//head/generator').text)
oeb.metadata.add('publisher', meta.find('.//head/publisher').text)
cover = meta.find('.//head/cover')
if cover != None and cover.text != None:
oeb.guide.add('cover', 'Cover', cover.text)
bookid = str(uuid.uuid4())
oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')
for ident in oeb.metadata.identifier:
if 'id' in ident.attrib:
oeb.uid = oeb.metadata.identifier[0]
break
with TemporaryDirectory('_chm2oeb', keep=True) as tdir:
log.debug('Process TOC ...')
toc = snbFile.GetFileStream('snbf/toc.snbf')
oeb.container = DirContainer(tdir, log)
if toc != None:
toc = etree.fromstring(toc)
i = 1
for ch in toc.find('.//body'):
chapterName = ch.text
chapterSrc = ch.get('src')
fname = 'ch_%d.htm' % i
data = snbFile.GetFileStream('snbc/' + chapterSrc)
if data != None:
snbc = etree.fromstring(data)
outputFile = open(os.path.join(tdir, fname), 'wb')
lines = []
for line in snbc.find('.//body'):
if line.tag == 'text':
lines.append(u'<p>%s</p>' % html_encode(line.text))
elif line.tag == 'img':
lines.append(u'<p><img src="%s" /></p>' % html_encode(line.text))
outputFile.write((HTML_TEMPLATE % (chapterName, u'\n'.join(lines))).encode('utf-8', 'replace'))
outputFile.close()
oeb.toc.add(ch.text, fname)
id, href = oeb.manifest.generate(id='html',
href=ascii_filename(fname))
item = oeb.manifest.add(id, href, 'text/html')
item.html_input_href = fname
oeb.spine.add(item, True)
i = i + 1
imageFiles = snbFile.OutputImageFiles(tdir)
for f, m in imageFiles:
id, href = oeb.manifest.generate(id='image',
href=ascii_filename(f))
item = oeb.manifest.add(id, href, m)
item.html_input_href = f
return oeb

View File

@ -0,0 +1,264 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import os, string
from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation
from calibre.ptempfile import TemporaryDirectory
from calibre.constants import __appname__, __version__
from calibre.ebooks.snb.snbfile import SNBFile
from calibre.ebooks.snb.snbml import SNBMLizer, ProcessFileName
class SNBOutput(OutputFormatPlugin):
name = 'SNB Output'
author = 'Li Fanxi'
file_type = 'snb'
options = set([
# OptionRecommendation(name='newline', recommended_value='system',
# level=OptionRecommendation.LOW,
# short_switch='n', choices=TxtNewlines.NEWLINE_TYPES.keys(),
# help=_('Type of newline to use. Options are %s. Default is \'system\'. '
# 'Use \'old_mac\' for compatibility with Mac OS 9 and earlier. '
# 'For Mac OS X use \'unix\'. \'system\' will default to the newline '
# 'type used by this OS.') % sorted(TxtNewlines.NEWLINE_TYPES.keys())),
OptionRecommendation(name='snb_output_encoding', recommended_value='utf-8',
level=OptionRecommendation.LOW,
help=_('Specify the character encoding of the output document. ' \
'The default is utf-8.')),
# OptionRecommendation(name='inline_toc',
# recommended_value=False, level=OptionRecommendation.LOW,
# help=_('Add Table of Contents to beginning of the book.')),
OptionRecommendation(name='snb_max_line_length',
recommended_value=0, level=OptionRecommendation.LOW,
help=_('The maximum number of characters per line. This splits on '
'the first space before the specified value. If no space is found '
'the line will be broken at the space after and will exceed the '
'specified value. Also, there is a minimum of 25 characters. '
'Use 0 to disable line splitting.')),
# OptionRecommendation(name='force_max_line_length',
# recommended_value=False, level=OptionRecommendation.LOW,
# help=_('Force splitting on the max-line-length value when no space '
# 'is present. Also allows max-line-length to be below the minimum')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
self.opts = opts
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
try:
rasterizer = SVGRasterizer()
rasterizer(oeb_book, opts)
except Unavailable:
log.warn('SVG rasterizer unavailable, SVG will not be converted')
# Create temp dir
with TemporaryDirectory('_snb_output') as tdir:
# Create stub directories
snbfDir = os.path.join(tdir, 'snbf')
snbcDir = os.path.join(tdir, 'snbc')
snbiDir = os.path.join(tdir, 'snbc/images')
os.mkdir(snbfDir)
os.mkdir(snbcDir)
os.mkdir(snbiDir)
# Process Meta data
meta = oeb_book.metadata
if meta.title:
title = unicode(meta.title[0])
else:
title = ''
authors = [unicode(x) for x in meta.creator if x.role == 'aut']
if meta.publisher:
publishers = unicode(meta.publisher[0])
else:
publishers = ''
if meta.language:
lang = unicode(meta.language[0]).upper()
else:
lang = ''
if meta.description:
abstract = unicode(meta.description[0])
else:
abstract = ''
# Process Cover
g, m, s = oeb_book.guide, oeb_book.manifest, oeb_book.spine
href = None
if 'titlepage' not in g:
if 'cover' in g:
href = g['cover'].href
# Output book info file
bookInfoTree = etree.Element("book-snbf", version="1.0")
headTree = etree.SubElement(bookInfoTree, "head")
etree.SubElement(headTree, "name").text = title
etree.SubElement(headTree, "author").text = ' '.join(authors)
etree.SubElement(headTree, "language").text = lang
etree.SubElement(headTree, "rights")
etree.SubElement(headTree, "publisher").text = publishers
etree.SubElement(headTree, "generator").text = __appname__ + ' ' + __version__
etree.SubElement(headTree, "created")
etree.SubElement(headTree, "abstract").text = abstract
if href != None:
etree.SubElement(headTree, "cover").text = ProcessFileName(href)
else:
etree.SubElement(headTree, "cover")
bookInfoFile = open(os.path.join(snbfDir, 'book.snbf'), 'wb')
bookInfoFile.write(etree.tostring(bookInfoTree, pretty_print=True, encoding='utf-8'))
bookInfoFile.close()
# Output TOC
tocInfoTree = etree.Element("toc-snbf")
tocHead = etree.SubElement(tocInfoTree, "head")
tocBody = etree.SubElement(tocInfoTree, "body")
outputFiles = { }
if oeb_book.toc.count() == 0:
log.warn('This SNB file has no Table of Contents. '
'Creating a default TOC')
first = iter(oeb_book.spine).next()
oeb_book.toc.add(_('Start Page'), first.href)
else:
first = iter(oeb_book.spine).next()
if oeb_book.toc[0].href != first.href:
# The pages before the fist item in toc will be stored as
# "Cover Pages".
# oeb_book.toc does not support "insert", so we generate
# the tocInfoTree directly instead of modifying the toc
ch = etree.SubElement(tocBody, "chapter")
ch.set("src", ProcessFileName(first.href) + ".snbc")
ch.text = _('Cover Pages')
outputFiles[first.href] = []
outputFiles[first.href].append(("", _("Cover Pages")))
for tocitem in oeb_book.toc:
if tocitem.href.find('#') != -1:
item = string.split(tocitem.href, '#')
if len(item) != 2:
log.error('Error in TOC item: %s' % tocitem)
else:
if item[0] in outputFiles:
outputFiles[item[0]].append((item[1], tocitem.title))
else:
outputFiles[item[0]] = []
if not "" in outputFiles[item[0]]:
outputFiles[item[0]].append(("", tocitem.title + _(" (Preface)")))
ch = etree.SubElement(tocBody, "chapter")
ch.set("src", ProcessFileName(item[0]) + ".snbc")
ch.text = tocitem.title + _(" (Preface)")
outputFiles[item[0]].append((item[1], tocitem.title))
else:
if tocitem.href in outputFiles:
outputFiles[tocitem.href].append(("", tocitem.title))
else:
outputFiles[tocitem.href] = []
outputFiles[tocitem.href].append(("", tocitem.title))
ch = etree.SubElement(tocBody, "chapter")
ch.set("src", ProcessFileName(tocitem.href) + ".snbc")
ch.text = tocitem.title
etree.SubElement(tocHead, "chapters").text = '%d' % len(tocBody)
tocInfoFile = open(os.path.join(snbfDir, 'toc.snbf'), 'wb')
tocInfoFile.write(etree.tostring(tocInfoTree, pretty_print=True, encoding='utf-8'))
tocInfoFile.close()
# Output Files
oldTree = None
mergeLast = False
lastName = None
for item in s:
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_IMAGES
if m.hrefs[item.href].media_type in OEB_DOCS:
if not item.href in outputFiles:
log.debug('File %s is unused in TOC. Continue in last chapter' % item.href)
mergeLast = True
else:
if oldTree != None and mergeLast:
log.debug('Output the modified chapter again: %s' % lastName)
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
outputFile.close()
mergeLast = False
log.debug('Converting %s to snbc...' % item.href)
snbwriter = SNBMLizer(log)
snbcTrees = None
if not mergeLast:
snbcTrees = snbwriter.extract_content(oeb_book, item, outputFiles[item.href], opts)
for subName in snbcTrees:
postfix = ''
if subName != '':
postfix = '_' + subName
lastName = ProcessFileName(item.href + postfix + ".snbc")
oldTree = snbcTrees[subName]
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
outputFile.close()
else:
log.debug('Merge %s with last TOC item...' % item.href)
snbwriter.merge_content(oldTree, oeb_book, item, [('', _("Start"))], opts)
# Output the last one if needed
log.debug('Output the last modified chapter again: %s' % lastName)
if oldTree != None and mergeLast:
outputFile = open(os.path.join(snbcDir, lastName), 'wb')
outputFile.write(etree.tostring(oldTree, pretty_print=True, encoding='utf-8'))
outputFile.close()
mergeLast = False
for item in m:
if m.hrefs[item.href].media_type in OEB_IMAGES:
log.debug('Converting image: %s ...' % item.href)
content = m.hrefs[item.href].data
# Convert & Resize image
self.HandleImage(content, os.path.join(snbiDir, ProcessFileName(item.href)))
# Package as SNB File
snbFile = SNBFile()
snbFile.FromDir(tdir)
snbFile.Output(output_path)
def HandleImage(self, imageData, imagePath):
from calibre.utils.magick import Image
img = Image()
img.load(imageData)
(x,y) = img.size
if self.opts:
SCREEN_Y, SCREEN_X = 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.size = (x / scale, y / scale)
img.save(imagePath)
if __name__ == '__main__':
from calibre.ebooks.oeb.reader import OEBReader
from calibre.ebooks.oeb.base import OEBBook
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
from calibre.customize.profiles import HanlinV3Output
class OptionValues(object):
pass
opts = OptionValues()
opts.output_profile = HanlinV3Output(None)
html_preprocessor = HTMLPreProcessor(None, None, opts)
from calibre.utils.logging import default_log
oeb = OEBBook(default_log, html_preprocessor)
reader = OEBReader
reader()(oeb, '/tmp/bbb/processed/')
SNBOutput(None).convert(oeb, '/tmp/test.snb', None, None, default_log);

View File

@ -0,0 +1,319 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import sys, struct, zlib, bz2, os
from mimetypes import types_map
class FileStream:
def IsBinary(self):
return self.attr & 0x41000000 != 0x41000000
def compareFileStream(file1, file2):
return cmp(file1.fileName, file2.fileName)
class BlockData:
pass
class SNBFile:
MAGIC = 'SNBP000B'
REV80 = 0x00008000
REVA3 = 0x00A3A3A3
REVZ1 = 0x00000000
REVZ2 = 0x00000000
def __init__(self, inputFile = None):
self.files = []
self.blocks = []
if inputFile != None:
self.Open(inputFile)
def Open(self, inputFile):
self.fileName = inputFile
snbFile = open(self.fileName, "rb")
snbFile.seek(0)
self.Parse(snbFile)
snbFile.close()
def Parse(self, snbFile, metaOnly = False):
# Read header
vmbr = snbFile.read(44)
(self.magic, self.rev80, self.revA3, self.revZ1,
self.fileCount, self.vfatSize, self.vfatCompressed,
self.binStreamSize, self.plainStreamSizeUncompressed,
self.revZ2) = struct.unpack('>8siiiiiiiii', vmbr)
# Read FAT
self.vfat = zlib.decompress(snbFile.read(self.vfatCompressed))
self.ParseFile(self.vfat, self.fileCount)
# Read tail
snbFile.seek(-16, os.SEEK_END)
#plainStreamEnd = snbFile.tell()
tailblock = snbFile.read(16)
(self.tailSize, self.tailOffset, self.tailMagic) = struct.unpack('>ii8s', tailblock)
snbFile.seek(self.tailOffset)
self.vTailUncompressed = zlib.decompress(snbFile.read(self.tailSize))
self.tailSizeUncompressed = len(self.vTailUncompressed)
self.ParseTail(self.vTailUncompressed, self.fileCount)
# Uncompress file data
# Read files
binPos = 0
plainPos = 0
uncompressedData = None
for f in self.files:
if f.attr & 0x41000000 == 0x41000000:
# Compressed Files
if uncompressedData == None:
uncompressedData = ""
for i in range(self.plainBlock):
bzdc = bz2.BZ2Decompressor()
if (i < self.plainBlock - 1):
bSize = self.blocks[self.binBlock + i + 1].Offset - self.blocks[self.binBlock + i].Offset;
else:
bSize = self.tailOffset - self.blocks[self.binBlock + i].Offset;
snbFile.seek(self.blocks[self.binBlock + i].Offset);
try:
data = snbFile.read(bSize)
uncompressedData += bzdc.decompress(data)
except Exception, e:
print e
f.fileBody = uncompressedData[plainPos:plainPos+f.fileSize]
plainPos += f.fileSize
elif f.attr & 0x01000000 == 0x01000000:
# Binary Files
snbFile.seek(44 + self.vfatCompressed + binPos)
f.fileBody = snbFile.read(f.fileSize)
binPos += f.fileSize
else:
print f.attr, f.fileName
raise Exception("Invalid file")
def ParseFile(self, vfat, fileCount):
fileNames = vfat[fileCount*12:].split('\0');
for i in range(fileCount):
f = FileStream()
(f.attr, f.fileNameOffset, f.fileSize) = struct.unpack('>iii', vfat[i * 12 : (i+1)*12])
f.fileName = fileNames[i]
self.files.append(f)
def ParseTail(self, vtail, fileCount):
self.binBlock = (self.binStreamSize + 0x8000 - 1) / 0x8000;
self.plainBlock = (self.plainStreamSizeUncompressed + 0x8000 - 1) / 0x8000;
for i in range(self.binBlock + self.plainBlock):
block = BlockData()
(block.Offset,) = struct.unpack('>i', vtail[i * 4 : (i+1) * 4])
self.blocks.append(block)
for i in range(fileCount):
(self.files[i].blockIndex, self.files[i].contentOffset) = struct.unpack('>ii', vtail[(self.binBlock + self.plainBlock) * 4 + i * 8 : (self.binBlock + self.plainBlock) * 4 + (i+1) * 8])
def IsValid(self):
if self.magic != SNBFile.MAGIC:
return False
if self.rev80 != SNBFile.REV80:
return False
if self.revA3 != SNBFile.REVA3:
return False
if self.revZ1 != SNBFile.REVZ1:
return False
if self.revZ2 != SNBFile.REVZ2:
return False
if self.vfatSize != len(self.vfat):
return False
if self.fileCount != len(self.files):
return False
if (self.binBlock + self.plainBlock) * 4 + self.fileCount * 8 != self.tailSizeUncompressed:
return False
if self.tailMagic != SNBFile.MAGIC:
print self.tailMagic
return False
return True
def FromDir(self, tdir):
for root, dirs, files in os.walk(tdir):
for name in files:
p, ext = os.path.splitext(name)
if ext in [ ".snbf", ".snbc" ]:
self.AppendPlain(os.path.relpath(os.path.join(root, name), tdir), tdir)
else:
self.AppendBinary(os.path.relpath(os.path.join(root, name), tdir), tdir)
def AppendPlain(self, fileName, tdir):
f = FileStream()
f.attr = 0x41000000
f.fileSize = os.path.getsize(os.path.join(tdir,fileName))
f.fileBody = open(os.path.join(tdir,fileName), 'rb').read()
f.fileName = fileName.replace(os.sep, '/')
self.files.append(f)
def AppendBinary(self, fileName, tdir):
f = FileStream()
f.attr = 0x01000000
f.fileSize = os.path.getsize(os.path.join(tdir,fileName))
f.fileBody = open(os.path.join(tdir,fileName), 'rb').read()
f.fileName = fileName.replace(os.sep, '/')
self.files.append(f)
def GetFileStream(self, fileName):
for file in self.files:
if file.fileName == fileName:
return file.fileBody
return None
def OutputImageFiles(self, path):
fileNames = []
for f in self.files:
fname = os.path.basename(f.fileName)
root, ext = os.path.splitext(fname)
if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]:
file = open(os.path.join(path, fname), 'wb')
file.write(f.fileBody)
file.close()
fileNames.append((fname, types_map[ext]))
return fileNames
def Output(self, outputFile):
# Sort the files in file buffer,
# requried by the SNB file format
self.files.sort(compareFileStream)
outputFile = open(outputFile, 'wb')
# File header part 1
vmbrp1 = struct.pack('>8siiii', SNBFile.MAGIC, SNBFile.REV80, SNBFile.REVA3, SNBFile.REVZ1, len(self.files))
# Create VFAT & file stream
vfat = ''
fileNameTable = ''
plainStream = ''
binStream = ''
for f in self.files:
vfat += struct.pack('>iii', f.attr, len(fileNameTable), f.fileSize);
fileNameTable += (f.fileName + '\0')
if f.attr & 0x41000000 == 0x41000000:
# Plain Files
f.contentOffset = len(plainStream)
plainStream += f.fileBody
elif f.attr & 0x01000000 == 0x01000000:
# Binary Files
f.contentOffset = len(binStream)
binStream += f.fileBody
else:
print f.attr, f.fileName
raise Exception("Unknown file type")
vfatCompressed = zlib.compress(vfat+fileNameTable)
# File header part 2
vmbrp2 = struct.pack('>iiiii', len(vfat+fileNameTable), len(vfatCompressed), len(binStream), len(plainStream), SNBFile.REVZ2)
# Write header
outputFile.write(vmbrp1 + vmbrp2)
# Write vfat
outputFile.write(vfatCompressed)
# Generate block information
binBlockOffset = 0x2C + len(vfatCompressed)
plainBlockOffset = binBlockOffset + len(binStream)
binBlock = (len(binStream) + 0x8000 - 1) / 0x8000
#plainBlock = (len(plainStream) + 0x8000 - 1) / 0x8000
offset = 0
tailBlock = ''
for i in range(binBlock):
tailBlock += struct.pack('>i', binBlockOffset + offset)
offset += 0x8000;
tailRec = ''
for f in self.files:
t = 0
if f.IsBinary():
t = 0
else:
t = binBlock
tailRec += struct.pack('>ii', f.contentOffset / 0x8000 + t, f.contentOffset % 0x8000);
# Write binary stream
outputFile.write(binStream)
# Write plain stream
pos = 0
offset = 0
while pos < len(plainStream):
tailBlock += struct.pack('>i', plainBlockOffset + offset);
block = plainStream[pos:pos+0x8000];
compressed = bz2.compress(block)
outputFile.write(compressed)
offset += len(compressed)
pos += 0x8000
# Write tail block
compressedTail = zlib.compress(tailBlock + tailRec)
outputFile.write(compressedTail)
# Write tail pointer
veom = struct.pack('>ii', len(compressedTail), plainBlockOffset + offset)
outputFile.write(veom)
# Write file end mark
outputFile.write(SNBFile.MAGIC);
# Close
outputFile.close()
return
def Dump(self):
if self.fileName:
print "File Name:\t", self.fileName
print "File Count:\t", self.fileCount
print "VFAT Size(Compressed):\t%d(%d)" % (self.vfatSize, self.vfatCompressed)
print "Binary Stream Size:\t", self.binStreamSize
print "Plain Stream Uncompressed Size:\t", self.plainStreamSizeUncompressed
print "Binary Block Count:\t", self.binBlock
print "Plain Block Count:\t", self.plainBlock
for i in range(self.fileCount):
print "File ", i
f = self.files[i]
print "File Name: ", f.fileName
print "File Attr: ", f.attr
print "File Size: ", f.fileSize
print "Block Index: ", f.blockIndex
print "Content Offset: ", f.contentOffset
tempFile = open("/tmp/" + f.fileName, 'wb')
tempFile.write(f.fileBody)
tempFile.close()
def usage():
print "This unit test is for INTERNAL usage only!"
print "This unit test accept two parameters."
print "python snbfile.py <INPUTFILE> <DESTFILE>"
print "The input file will be extracted and write to dest file. "
print "Meta data of the file will be shown during this process."
def main():
if len(sys.argv) != 3:
usage()
sys.exit(0)
inputFile = sys.argv[1]
outputFile = sys.argv[2]
print "Input file: ", inputFile
print "Output file: ", outputFile
snbFile = SNBFile(inputFile)
if snbFile.IsValid():
snbFile.Dump()
snbFile.Output(outputFile)
else:
print "The input file is invalid."
return 1
return 0
if __name__ == "__main__":
"""SNB file unit test"""
sys.exit(main())

View File

@ -0,0 +1,263 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
'''
Transform OEB content into SNB format
'''
import os
import re
from lxml import etree
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace
from calibre.ebooks.oeb.stylizer import Stylizer
def ProcessFileName(fileName):
# Flat the path
fileName = fileName.replace("/", "_").replace(os.sep, "_")
# Handle bookmark for HTML file
fileName = fileName.replace("#", "_")
# Make it lower case
fileName = fileName.lower()
# Change all images to jpg
root, ext = os.path.splitext(fileName)
if ext in [ '.jpeg', '.jpg', '.gif', '.svg', '.png' ]:
fileName = root + '.jpg'
return fileName
BLOCK_TAGS = [
'div',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'li',
'tr',
]
BLOCK_STYLES = [
'block',
]
SPACE_TAGS = [
'td',
]
CALIBRE_SNB_IMG_TAG = "<$$calibre_snb_temp_img$$>"
CALIBRE_SNB_BM_TAG = "<$$calibre_snb_bm_tag$$>"
CALIBRE_SNB_PRE_TAG = "<$$calibre_snb_pre_tag$$>"
class SNBMLizer(object):
curSubItem = ""
# curText = [ ]
def __init__(self, log):
self.log = log
def extract_content(self, oeb_book, item, subitems, opts):
self.log.info('Converting XHTML to SNBC...')
self.oeb_book = oeb_book
self.opts = opts
self.item = item
self.subitems = subitems
return self.mlize();
def merge_content(self, old_tree, oeb_book, item, subitems, opts):
newTrees = self.extract_content(oeb_book, item, subitems, opts)
body = old_tree.find(".//body")
if body != None:
for subName in newTrees:
newbody = newTrees[subName].find(".//body")
for entity in newbody:
body.append(entity)
def mlize(self):
output = [ u'' ]
stylizer = Stylizer(self.item.data, self.item.href, self.oeb_book, self.opts, self.opts.output_profile)
content = unicode(etree.tostring(self.item.data.find(XHTML('body')), encoding=unicode))
# content = self.remove_newlines(content)
trees = { }
for subitem, subtitle in self.subitems:
snbcTree = etree.Element("snbc")
etree.SubElement(etree.SubElement(snbcTree, "head"), "title").text = subtitle
etree.SubElement(snbcTree, "body")
trees[subitem] = snbcTree
output.append(u'%s%s\n\n' % (CALIBRE_SNB_BM_TAG, ""))
output += self.dump_text(self.subitems, etree.fromstring(content), stylizer)[0]
output = self.cleanup_text(u''.join(output))
subitem = ''
for line in output.splitlines():
if not line.find(CALIBRE_SNB_PRE_TAG) == 0:
line = line.strip(u' \t\n\r\u3000')
else:
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
etree.CDATA(line[len(CALIBRE_SNB_PRE_TAG):])
continue
if len(line) != 0:
if line.find(CALIBRE_SNB_IMG_TAG) == 0:
prefix = ProcessFileName(os.path.dirname(self.item.href))
if prefix != '':
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
prefix + '_' + line[len(CALIBRE_SNB_IMG_TAG):]
else:
etree.SubElement(trees[subitem].find(".//body"), "img").text = \
line[len(CALIBRE_SNB_IMG_TAG):]
elif line.find(CALIBRE_SNB_BM_TAG) == 0:
subitem = line[len(CALIBRE_SNB_BM_TAG):]
else:
etree.SubElement(trees[subitem].find(".//body"), "text").text = \
etree.CDATA(unicode(u'\u3000\u3000' + line))
return trees
def remove_newlines(self, text):
self.log.debug('\tRemove newlines for processing...')
text = text.replace('\r\n', ' ')
text = text.replace('\n', ' ')
text = text.replace('\r', ' ')
return text
def cleanup_text(self, text):
self.log.debug('\tClean up text...')
# Replace bad characters.
text = text.replace(u'\xc2', '')
text = text.replace(u'\xa0', ' ')
text = text.replace(u'\xa9', '(C)')
# Replace tabs, vertical tags and form feeds with single space.
text = text.replace('\t+', ' ')
text = text.replace('\v+', ' ')
text = text.replace('\f+', ' ')
# Single line paragraph.
text = re.sub('(?<=.)%s(?=.)' % os.linesep, ' ', text)
# Remove multiple spaces.
#text = re.sub('[ ]{2,}', ' ', text)
# Remove excessive newlines.
text = re.sub('\n[ ]+\n', '\n\n', text)
if self.opts.remove_paragraph_spacing:
text = re.sub('\n{2,}', '\n', text)
text = re.sub('(?imu)^(?=.)', '\t', text)
else:
text = re.sub('\n{3,}', '\n\n', text)
# Replace spaces at the beginning and end of lines
text = re.sub('(?imu)^[ ]+', '', text)
text = re.sub('(?imu)[ ]+$', '', text)
if self.opts.snb_max_line_length:
max_length = self.opts.snb_max_line_length
if self.opts.max_line_length < 25:# and not self.opts.force_max_line_length:
max_length = 25
short_lines = []
lines = text.splitlines()
for line in lines:
while len(line) > max_length:
space = line.rfind(' ', 0, max_length)
if space != -1:
# Space was found.
short_lines.append(line[:space])
line = line[space + 1:]
else:
# Space was not found.
if False and self.opts.force_max_line_length:
# Force breaking at max_lenght.
short_lines.append(line[:max_length])
line = line[max_length:]
else:
# Look for the first space after max_length.
space = line.find(' ', max_length, len(line))
if space != -1:
# Space was found.
short_lines.append(line[:space])
line = line[space + 1:]
else:
# No space was found cannot break line.
short_lines.append(line)
line = ''
# Add the text that was less than max_lengh to the list
short_lines.append(line)
text = '\n'.join(short_lines)
return text
def dump_text(self, subitems, elem, stylizer, end='', pre=False, li = ''):
if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS:
return ['']
text = ['']
style = stylizer.style(elem)
if elem.attrib.get('id') != None and elem.attrib['id'] in [ href for href, title in subitems ]:
if self.curSubItem != None and self.curSubItem != elem.attrib['id']:
self.curSubItem = elem.attrib['id']
text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_BM_TAG, self.curSubItem))
if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \
or style['visibility'] == 'hidden':
return ['']
tag = barename(elem.tag)
in_block = False
# Are we in a paragraph block?
if tag in BLOCK_TAGS or style['display'] in BLOCK_STYLES:
in_block = True
if not end.endswith(u'\n\n') and hasattr(elem, 'text') and elem.text:
text.append(u'\n\n')
if tag in SPACE_TAGS:
if not end.endswith('u ') and hasattr(elem, 'text') and elem.text:
text.append(u' ')
if tag == 'img':
text.append(u'\n\n%s%s\n\n' % (CALIBRE_SNB_IMG_TAG, ProcessFileName(elem.attrib['src'])))
if tag == 'br':
text.append(u'\n\n')
if tag == 'li':
li = '- '
pre = (tag == 'pre' or pre)
# Process tags that contain text.
if hasattr(elem, 'text') and elem.text:
if pre:
text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join((li + elem.text).splitlines()))
else:
text.append(li + elem.text)
li = ''
for item in elem:
en = u''
if len(text) >= 2:
en = text[-1][-2:]
t = self.dump_text(subitems, item, stylizer, en, pre, li)[0]
text += t
if in_block:
text.append(u'\n\n')
if hasattr(elem, 'tail') and elem.tail:
if pre:
text.append((u'\n\n%s' % CALIBRE_SNB_PRE_TAG ).join(elem.tail.splitlines()))
else:
text.append(li + elem.tail)
li = ''
return text, li

View File

@ -166,6 +166,7 @@ class AddAction(InterfaceAction):
(_('Topaz books'), ['tpz','azw1']),
(_('Text books'), ['txt', 'rtf']),
(_('PDF Books'), ['pdf']),
(_('SNB Books'), ['snb']),
(_('Comics'), ['cbz', 'cbr', 'cbc']),
(_('Archives'), ['zip', 'rar']),
]

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.convert.snb_output_ui import Ui_Form
from calibre.gui2.convert import Widget
newline_model = None
class PluginWidget(Widget, Ui_Form):
TITLE = _('SNB Output')
HELP = _('Options specific to')+' SNB '+_('output')
COMMIT_NAME = 'snb_output'
ICON = I('mimetypes/snb.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
[])
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)
# default = self.opt_newline.currentText()
# global newline_model
# if newline_model is None:
# newline_model = BasicComboModel(TxtNewlines.NEWLINE_TYPES.keys())
# self.newline_model = newline_model
# self.opt_newline.setModel(self.newline_model)
# default_index = self.opt_newline.findText(default)
# system_index = self.opt_newline.findText('system')
# self.opt_newline.setCurrentIndex(default_index if default_index != -1 else system_index if system_index != -1 else 0)

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<!-- <item row="0" column="0"> -->
<!-- <widget class="QLabel" name="label"> -->
<!-- <property name="text"> -->
<!-- <string>&amp;Line ending style:</string> -->
<!-- </property> -->
<!-- <property name="buddy"> -->
<!-- <cstring>opt_newline</cstring> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
<!-- <item row="0" column="1"> -->
<!-- <widget class="QComboBox" name="opt_newline"/> -->
<!-- </item> -->
<!-- <item row="4" column="0"> -->
<!-- <spacer name="verticalSpacer"> -->
<!-- <property name="orientation"> -->
<!-- <enum>Qt::Vertical</enum> -->
<!-- </property> -->
<!-- <property name="sizeHint" stdset="0"> -->
<!-- <size> -->
<!-- <width>20</width> -->
<!-- <height>246</height> -->
<!-- </size> -->
<!-- </property> -->
<!-- </spacer> -->
<!-- </item> -->
<!-- <item row="3" column="0" colspan="2"> -->
<!-- <widget class="QCheckBox" name="opt_inline_toc"> -->
<!-- <property name="text"> -->
<!-- <string>&amp;Inline TOC</string> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
<!-- <item row="1" column="1"> -->
<!-- <widget class="QSpinBox" name="opt_max_line_length"/> -->
<!-- </item> -->
<!-- <item row="1" column="0"> -->
<!-- <widget class="QLabel" name="label_2"> -->
<!-- <property name="text"> -->
<!-- <string>&amp;Maximum line length:</string> -->
<!-- </property> -->
<!-- <property name="buddy"> -->
<!-- <cstring>opt_max_line_length</cstring> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
<!-- <item row="2" column="0" colspan="2"> -->
<!-- <widget class="QCheckBox" name="opt_force_max_line_length"> -->
<!-- <property name="text"> -->
<!-- <string>Force maximum line length</string> -->
<!-- </property> -->
<!-- </widget> -->
<!-- </item> -->
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>752</width>
<height>715</height>
<height>633</height>
</rect>
</property>
<property name="windowTitle">
@ -660,8 +660,8 @@ nothing should be put between the original text and the inserted text</string>
<rect>
<x>0</x>
<y>0</y>
<width>122</width>
<height>34</height>
<width>726</width>
<height>334</height>
</rect>
</property>
<layout class="QGridLayout" name="testgrid">
@ -682,19 +682,6 @@ nothing should be put between the original text and the inserted text</string>
</widget>
</widget>
</item>
<item row="20" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>

View File

@ -729,10 +729,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.series.setText(book.series)
if book.series_index is not None:
self.series_index.setValue(book.series_index)
# Needed because of Qt focus bug on OS X
self.fetch_cover_button.setFocus(Qt.OtherFocusReason)
else:
error_dialog(self, _('Cannot fetch metadata'),
_('You must specify at least one of ISBN, Title, '
'Authors or Publisher'), show=True)
self.title.setFocus(Qt.OtherFocusReason)
def enable_series_index(self, *args):
self.series_index.setEnabled(True)

View File

@ -120,12 +120,15 @@ class SchedulerDialog(QDialog, Ui_Dialog):
if self.account.isVisible():
un, pw = map(unicode, (self.username.text(), self.password.text()))
un, pw = un.strip(), pw.strip()
if not un and not pw and self.schedule.isChecked():
error_dialog(self, _('Need username and password'),
_('You must provide a username and/or password to '
'use this news source.'), show=True)
return False
self.recipe_model.set_account_info(urn, un.strip(), pw.strip())
if not getattr(self, 'subscription_optional', False):
error_dialog(self, _('Need username and password'),
_('You must provide a username and/or password to '
'use this news source.'), show=True)
return False
if un or pw:
self.recipe_model.set_account_info(urn, un, pw)
if self.schedule.isChecked():
schedule_type = 'interval' if self.interval_button.isChecked() else 'day/time'
@ -157,7 +160,13 @@ class SchedulerDialog(QDialog, Ui_Dialog):
account_info = self.recipe_model.account_info_from_urn(urn)
customize_info = self.recipe_model.get_customize_info(urn)
self.account.setVisible(recipe.get('needs_subscription', '') == 'yes')
ns = recipe.get('needs_subscription', '')
self.account.setVisible(ns in ('yes', 'optional'))
self.subscription_optional = ns == 'optional'
act = _('Account')
act2 = _('(optional)') if self.subscription_optional else \
_('(required)')
self.account.setTitle(act+' '+act2)
un = pw = ''
if account_info is not None:
un, pw = account_info[:2]

View File

@ -353,6 +353,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.pending_bookmark = bm
if spine_index < 0 or spine_index >= len(self.iterator.spine):
spine_index = 0
self.pending_bookmark = None
self.load_path(self.iterator.spine[spine_index])
def toc_clicked(self, index):

View File

@ -20,9 +20,9 @@ What formats does |app| support conversion to/from?
|app| supports the conversion of many input formats to many output formats.
It can convert every input format in the following list, to every output format.
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, TCR, TXT
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, SNB, TCR, TXT
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PML, RB, PDF, TCR, TXT
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PML, RB, PDF, SNB, TCR, TXT
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers

View File

@ -16,7 +16,7 @@ __builtin__.__dict__['_'] = lambda s: s
# immediately translated to the environment language
__builtin__.__dict__['__'] = lambda s: s
from calibre.constants import iswindows, preferred_encoding, plugins
from calibre.constants import iswindows, preferred_encoding, plugins, isosx
_run_once = False
winutil = winutilerror = None
@ -35,9 +35,17 @@ if not _run_once:
################################################################################
# Convert command line arguments to unicode
enc = preferred_encoding
if isosx:
# Newer versions of OS X seem to use UTF-8
try:
[x.decode('utf-8') for x in sys.argv[1:]]
enc = 'utf-8'
except:
pass
for i in range(1, len(sys.argv)):
if not isinstance(sys.argv[i], unicode):
sys.argv[i] = sys.argv[i].decode(preferred_encoding, 'replace')
sys.argv[i] = sys.argv[i].decode(enc, 'replace')
################################################################################
# Setup resources

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

11559
src/calibre/translations/ur.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -376,7 +376,8 @@ default_smartypants_attr = "1"
import re
tags_to_skip_regex = re.compile(r"<(/)?(pre|code|kbd|script|math)[^>]*>", re.I)
# style added by Kovid
tags_to_skip_regex = re.compile(r"<(/)?(style|pre|code|kbd|script|math)[^>]*>", re.I)
def verify_installation(request):

View File

@ -110,9 +110,11 @@ class BasicNewsRecipe(Recipe):
#: If True the GUI will ask the user for a username and password
#: to use while downloading
#: @type: boolean
#: If set to "optional" the use of a username and password becomes optional
needs_subscription = False
#:
#: If True the navigation bar is center aligned, otherwise it is left aligned
center_navbar = True
@ -609,7 +611,8 @@ class BasicNewsRecipe(Recipe):
if self.needs_subscription and (\
self.username is None or self.password is None or \
(not self.username and not self.password)):
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)
if self.needs_subscription != 'optional':
raise ValueError(_('The "%s" recipe needs a username and password.')%self.title)
self.browser = self.get_browser()
self.image_map, self.image_counter = {}, 1

View File

@ -45,12 +45,17 @@ def serialize_recipe(urn, recipe_class):
return ans
default_author = _('You') if urn.startswith('custom:') else _('Unknown')
ns = attr('needs_subscription', False)
if not ns:
ns = 'no'
if ns is True:
ns = 'yes'
return E.recipe({
'id' : str(urn),
'title' : attr('title', _('Unknown')),
'author' : attr('__author__', default_author),
'language' : attr('language', 'und'),
'needs_subscription' : 'yes' if attr('needs_subscription', False) else 'no',
'needs_subscription' : ns,
'description' : attr('description', '')
})