mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
b5f80d5855
106
Changelog.yaml
106
Changelog.yaml
@ -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"
|
||||
|
BIN
resources/images/mimetypes/snb.png
Normal file
BIN
resources/images/mimetypes/snb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
@ -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()
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 += [
|
||||
|
@ -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`.
|
||||
|
@ -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()))
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)])
|
||||
|
||||
|
@ -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
|
||||
|
47
src/calibre/ebooks/metadata/snb.py
Executable file
47
src/calibre/ebooks/metadata/snb.py
Executable 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
|
@ -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})
|
||||
|
@ -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))
|
||||
|
9
src/calibre/ebooks/snb/__init__.py
Normal file
9
src/calibre/ebooks/snb/__init__.py
Normal 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
103
src/calibre/ebooks/snb/input.py
Executable 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'&').replace(u'<', u'<').replace(u'>', u'>').replace(u'"', u'"').replace(u"'", u''').replace(u'\n', u'<br/>').replace(u' ', u' ')
|
||||
|
||||
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
|
||||
|
264
src/calibre/ebooks/snb/output.py
Normal file
264
src/calibre/ebooks/snb/output.py
Normal 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);
|
319
src/calibre/ebooks/snb/snbfile.py
Normal file
319
src/calibre/ebooks/snb/snbfile.py
Normal 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())
|
263
src/calibre/ebooks/snb/snbml.py
Normal file
263
src/calibre/ebooks/snb/snbml.py
Normal 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
|
@ -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']),
|
||||
]
|
||||
|
35
src/calibre/gui2/convert/snb_output.py
Normal file
35
src/calibre/gui2/convert/snb_output.py
Normal 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)
|
74
src/calibre/gui2/convert/snb_output.ui
Normal file
74
src/calibre/gui2/convert/snb_output.ui
Normal 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>&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>&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>&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>
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
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
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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', '')
|
||||
})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user