Merged upstream changes

This commit is contained in:
Marshall T. Vandegrift 2008-10-07 17:07:38 -04:00
commit bb523550df
243 changed files with 78826 additions and 15715 deletions

View File

@ -23,3 +23,9 @@ installer/windows/calibre/build.log
src/calibre/translations/.errors
src/calibre/plugins/*
src/calibre/gui2/pictureflow/.build
src/cssutils/.svn/
src/cssutils/_todo/
src/cssutils/scripts/
src/cssutils/css/.svn/
src/cssutils/stylesheets/.svn/
src/odf/.svn

View File

@ -22,6 +22,9 @@ LIBZ = '/lib/libz.so.1'
LIBBZ2 = '/lib/libbz2.so.1'
LIBUSB = '/usr/lib/libusb.so'
LIBPOPPLER = '/usr/lib/libpoppler.so.3'
LIBXML2 = '/usr/lib/libxml2.so.2'
LIBXSLT = '/usr/lib/libxslt.so.1'
LIBEXSLT = '/usr/lib/libexslt.so.0'
CALIBRESRC = os.path.join(CALIBREPREFIX, 'src')
@ -121,7 +124,7 @@ binaries += [('pdftohtml', PDFTOHTML, 'BINARY'),
print 'Adding external libraries...'
binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS,
LIBMNG, LIBZ, LIBBZ2, LIBUSB, LIBPOPPLER)]
LIBMNG, LIBZ, LIBBZ2, LIBUSB, LIBPOPPLER, LIBXML2, LIBXSLT, LIBEXSLT)]
qt = []

View File

@ -150,9 +150,9 @@ _check_symlinks_prescript()
if not match:
print dep
raise Exception('Unknown Qt dependency')
module = match.group(1)
module = match.group(1)
newpath = fp + '%s.framework/Versions/Current/%s'%(module, module)
cmd = ' '.join(['/usr/bin/install_name_tool', '-change', dep, newpath, path])
cmd = ' '.join(['/usr/bin/install_name_tool', '-change', dep, newpath, path])
subprocess.check_call(cmd, shell=True)
@ -220,7 +220,7 @@ _check_symlinks_prescript()
def run(self):
py2app.run(self)
resource_dir = os.path.join(self.dist_dir,
resource_dir = os.path.join(self.dist_dir,
APPNAME + '.app', 'Contents', 'Resources')
frameworks_dir = os.path.join(os.path.dirname(resource_dir), 'Frameworks')
all_scripts = scripts['console'] + scripts['gui']
@ -235,7 +235,7 @@ _check_symlinks_prescript()
path = os.path.join(loader_path, name)
print 'Creating loader:', path
f = open(path, 'w')
f.write(BuildAPP.LOADER_TEMPLATE % dict(module=module,
f.write(BuildAPP.LOADER_TEMPLATE % dict(module=module,
function=function))
f.close()
os.chmod(path, stat.S_IXUSR|stat.S_IXGRP|stat.S_IXOTH|stat.S_IREAD\
@ -301,6 +301,10 @@ sys.frameworks_dir = os.path.join(os.path.dirname(os.environ['RESOURCEPATH']), '
def main():
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
sys.argv[1:2] = ['py2app']
d = os.path.dirname
icon = os.path.abspath('icons/library.icns')
if not os.access(icon, os.R_OK):
raise Exception('No icon at '+icon)
setup(
name = APPNAME,
app = [scripts['gui'][0]],
@ -310,15 +314,16 @@ def main():
'optimize' : 2,
'dist_dir' : 'build/py2app',
'argv_emulation' : True,
'iconfile' : 'icons/library.icns',
'iconfile' : icon,
'frameworks': ['libusb.dylib', 'libunrar.dylib'],
'includes' : ['sip', 'pkg_resources', 'PyQt4.QtXml',
'PyQt4.QtSvg', 'PyQt4.QtWebKit',
'PyQt4.QtSvg', 'PyQt4.QtWebKit', 'commands',
'mechanize', 'ClientForm', 'usbobserver',
'genshi', 'calibre.web.feeds.recipes.*',
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
'keyword', 'codeop', 'pydoc', 'readline'],
'packages' : ['PIL', 'Authorization', 'rtf2xml', 'lxml'],
'keyword', 'codeop', 'pydoc', 'readline',
'BeautifulSoup'],
'packages' : ['PIL', 'Authorization', 'lxml'],
'excludes' : ['IPython'],
'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.'''
''' Visit http://calibre.kovidgoyal.net for details.''',

View File

@ -5,9 +5,11 @@ __docformat__ = 'restructuredtext en'
'''
'''
import sys, time, subprocess, os
import sys, time, subprocess, os, re
from calibre import __appname__, __version__
sv = re.sub(r'[a-z]\d+', '', __version__)
cmdline = [
'/usr/local/installjammer/installjammer',
'--build-dir', '/tmp/calibre-installjammer',
@ -18,10 +20,10 @@ cmdline = [
'-DPackageDescription', '%s is an e-book library manager. It can view, convert and catalog e-books in most of the major e-book formats. It can also talk to a few e-book reader devices. It can go out to the internet and fetch metadata for your books. It can download newspapers and convert them into e-books for convenient reading.'%__appname__,
'-DPackageSummary', '%s: E-book library management'%__appname__,
'-DVersion', __version__,
'-DInstallVersion', __version__ + '.0',
'-DInstallVersion', sv + '.0',
'-DLicense', open(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'LICENSE')).read().replace('\n', '\r\n'),
'--output-dir', os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'dist'),
'--platform', 'Windows',
'--platform', 'Windows',
]
def run_install_jammer(installer_name='<%AppName%>-<%Version%><%Ext%>', build_for_release=True):
@ -43,4 +45,4 @@ def main(args=sys.argv):
return 0
if __name__ == '__main__':
sys.exit(main())
sys.exit(main())

View File

@ -207,289 +207,7 @@ test
}
FileGroup ::BEF8D398-58BA-1F66-39D6-D4A63D5BEEF9 -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows TarArchive ZipArchive} -name {Program Files} -parent FileGroups
File ::6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3 -filemethod {Always overwrite files} -type dir -directory <%InstallDir%> -name /home/kovid/work/calibre/build/py2exe -parent BEF8D398-58BA-1F66-39D6-D4A63D5BEEF9
File ::B444456B-BA8D-A058-8C9B-AAC2BBB1560D -type dir -name bin -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D9A3AF75-5939-CB51-9F33-5A048911103E -type dir -name etc -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A628E495-239B-DAF4-D858-BCE36CB41E6E -type dir -name imageformats -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0A533DB2-D494-A9ED-1334-DECC357BD426 -type dir -name codecs -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0F47A44E-E347-1CD4-E89F-37B447C4A270 -type dir -name driver -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A146565C-D163-7F68-7C70-A6A336B32526 -type dir -name iconengines -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3245B06C-1C22-1A8A-5710-6D36651AAA70 -name etree.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B49A5610-13F6-FB5D-0673-DB47C6BB385D -name rtf-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CE4F2A21-12CC-2B9A-6D48-6A0FEA7C9D13 -name _compiled_base.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D6C46340-8335-7FC4-A027-D701DF1B70AB -name pdf2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::53F2B07D-8F92-2328-C55E-5F7F0E63D5DB -name opf-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::63A13E6F-CF99-4D85-2F82-5DFBBB7D8180 -name sqlite3.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B69EBBDA-04FA-A67F-E3ED-3C2E7D761B92 -name _sqlite3.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::24CC4E53-95EB-A527-21C3-B8166080D181 -name lrs2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::99B750DB-EC00-5521-E93E-7FABEA416B0C -name lit2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D8FC08E6-B361-307C-8F62-953A739B2F40 -name txt2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3FCD0C96-DC3A-EC73-7E3A-46A02CE631B0 -name web2disk.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::71F6F80A-0F8E-A96E-CA1B-974F928A0D9B -name calibre-parallel.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EBC815EE-7438-6588-97F9-FBFC39715044 -name html2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AB47ED69-BD17-198D-DE41-0AF44257480F -name LICENSE -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::10537419-4DBF-EE7D-6561-6B0DD6875C47 -name lapack_lite.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8CF0C9F4-3813-7EDE-139F-D78C6FC38694 -name feeds2disk.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::95B4ACBF-CCB7-421E-8956-3C96D6D85DE4 -name calibre.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4B78188A-11BC-E7DA-8583-C690270FDD8B -name web2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8CA63124-E326-1CC2-9B86-C118157ED034 -name QtNetwork4.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::431AD2DB-AD8C-4AEB-709C-E1CB8382F8FA -name win32ui.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B145A772-C20C-A6F2-E872-ACC229FFE1C7 -name fb2-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::168973D9-DEE6-7307-9A2E-746EB9456311 -name txt2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BD39D6F0-5125-F3A3-043F-E8FD1C87A823 -name isbndb.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6C294A11-52D4-C567-64F7-1DCF21AB06D7 -name epub2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A9BB9531-5BC4-92C1-F614-B84B93F8698F -name pywintypes25.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AA725BD8-5FFA-B426-733F-5A1264A30DA2 -name Qt.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B48D9BFD-326D-1C92-4D94-C17F9ACD9207 -name epub2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9104E2C3-8E0D-8275-59EB-6B5E57C7F7C1 -name isbndb.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EAE07E71-7B94-DE2F-4A30-4DAF1ADDB591 -name any2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4140B65A-A7F4-0CD2-4613-BED2E87DA01B -name library.zip -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F59AC283-F006-F856-3EA4-694FE4DD4A88 -name unicodedata.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::479B1590-634A-1847-54E9-EF29C239E8E7 -name win32api.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9E7341CF-0A6A-8F36-9B87-BF50C7C825AA -name libfontconfig-1.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::223E6E1F-F83C-1144-9952-98ED4E80F38C -name QtSvg.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A0C13AC8-184A-2F9A-FCD5-D6DA18A7200D -name win32pdh.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::153509C0-E955-D07B-4A7E-0AD74877F530 -name lrs2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::2C03CD3B-6A0E-D31D-FB58-91E48B96E3B1 -name fftpack_lite.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CFA05433-447F-F504-F050-D6B6966D6A74 -name win32clipboard.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::931DA8AF-4252-E09A-BBCD-9591BBACC7FA -name pdfreflow.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C42D8FFA-561F-552E-78DD-A3E441F630CA -name sip.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E1C0697F-BA9B-17AA-3581-079553DCDCAF -name pdfreflow.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9E2D6AAA-3606-82C2-9074-F51017624305 -name pdf-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A665DBDD-BE19-0818-3F30-FA4924AC811F -name rtf-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::2AABAB06-38EF-6F8C-3675-575D8B044E0C -name calibredb.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A7EA318A-0542-FBB8-5C93-A39B364A51D9 -name epub-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4CD878C5-1487-1FFD-99A1-EDFC90892F0A -name win32security.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ADFDF202-EED8-0319-0D69-DF1C44AE465B -name librarything.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0E83E956-FF8D-27AB-9BE9-A24C57F8886A -name pythoncom25.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F8DAAE4E-9BF0-F75A-78BD-81E0F4BD1F99 -name epub-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E2982BD3-D0BB-70B4-0BF8-18B2B68C9918 -name QtWebKit4.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::73F6E879-EF13-E4D2-D66D-C4A50D5B7A68 -name freetype6.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::47B3352F-C480-8704-0D8D-7CF0FE1D1E8F -name QtCore.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DDAB10F5-6B92-D2E0-F192-441694C26EB0 -name fb22lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8995CD27-3E9E-8689-5B49-E965E7E52D9A -name pyexpat.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E081A898-F163-D9F4-11E2-C691636BA702 -name calibre-debug.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::801BE53F-D9E4-CA5A-0702-DAF30B96CC04 -name select.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::215B915B-F0FE-C029-C022-24CE35D4A5C1 -name pdf-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D13FB9B3-0A36-B7CA-CA0B-3EF7376A2943 -name feeds2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5404250A-4A38-AE68-7EE1-96947457D92D -name _imagingmath.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::82E7B1F5-0866-8B48-453B-0F7C0A978141 -name QtGui4.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B1711D23-A1F1-5660-F7E7-0F30539B7513 -name markdown-calibre.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AEA3C386-69EF-7BF9-44AC-86CA6B303366 -name lrf-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F4B61311-9341-EEB0-92D6-1756B1923358 -name _hashlib.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AFD6F6A1-BD1D-2755-0F72-F5558E5C4C3F -name feeds2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C68EDFE9-EF12-E041-8FF4-240CDC7E23D0 -name libexpat.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C22C6509-C1A2-E883-A675-A97E063FBE73 -name calibre-fontconfig.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::33A4D1AC-78C9-AD8F-BE8E-8136EF0AA299 -name fb22lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E108A8AD-174E-0A51-2C2B-F5A5BB07E0F5 -name unrar.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BA5B6556-608D-2844-4151-25BCA476153B -name mobi2oeb.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::53A8A015-111B-957E-6702-3E3A44E61521 -name pdf2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8B8A372C-1473-880E-BBD9-D82DE09F9CCC -name QtGui.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0DF5DAF8-1B14-9D00-30A6-882DAFB107EE -name any2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D75E742F-EF44-769A-EBEF-1EC81B1CCCA7 -name markdown-calibre.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4B02260C-E90F-7B9E-5831-A5F881483BCB -name lit2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::942A727E-E6EE-FE2A-6720-FDB19B8029AA -name calibre-debug.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A61FDA7B-009F-C397-D7F5-E39DC30F14F2 -name prs500.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5324C64E-8774-E051-91EE-CD10289346C7 -name QtXml4.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C75049A3-5ADA-664D-8708-133B879C4D3E -name rtf2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A95D56FB-74E0-5A49-ACAE-29F99B4848C0 -name pdftohtml.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1F00E5E9-7298-E356-0294-CCE7EEE92D22 -name opf-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E5E3FC49-D699-001C-5C97-724CD74FA013 -name web2disk.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0B4D16CB-03A7-36A8-F507-FA6E09DFC303 -name rtf2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F2FEFC47-D830-1A6E-473E-156B3C7C7707 -name QtWebKit.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CDD6643D-5510-EEE5-95F2-D610F8B17801 -name lrfviewer.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9BF1A1FD-61FA-B173-135B-F8CC6AF85ADC -name lrfviewer.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0D47D92A-B91F-76AF-CF1F-81FF655A3E6E -name mobi2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EFB240F8-2D21-655E-6086-764B1ADBD3DB -name lit-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::27D31189-89C1-8B44-70A7-5DB3C2693761 -name umath.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::768C94EF-7C5E-88C9-9513-0ACB7DF63B54 -name _ssl.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D22ADBFF-B91B-4668-400C-F5FCC8ECD23E -name multiarray.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DD83F9CC-D1CA-F525-64A4-75F5788292B5 -name w9xpopen.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7ED462B5-9B9B-5476-5D91-16AA65FD08C2 -name _sort.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::2132131C-549D-8E7E-AD2D-DEEBECEA8441 -name QtNetwork.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EBAFE980-DF41-5535-5934-1CEA5F4489E8 -name prs500.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E1C90FD6-DE05-2BFC-3B69-586DC4F169F3 -name win32wnet.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AA00495C-C441-F486-B0C0-A3BACB4A2551 -name mtrand.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::31CEC572-AA14-134B-1A4A-FCD9A74DF2FB -name lrf2lrs.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C30F4910-D164-FEA3-4D53-8A559250C071 -name feeds2disk.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E34D773E-A263-F166-3771-59D87FBBF2B8 -name mobi2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A4261177-EB5E-5DD7-2818-4A70C10C5FB7 -name lrf-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B3B72A4E-8B11-2352-A591-0C43E52A2329 -name fb2-meta.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::99EE1D50-3EA6-C2C0-130A-0B830763BC99 -name calibre-fontconfig.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::068F9CD2-4429-9BDC-33E2-417F7A89DF46 -name win32file.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0B892D2B-69B9-A0EF-4CDE-DC13F50EE47A -name lrf2html.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::56F70DCF-A20E-985F-B754-498323515A88 -name lit-meta.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::642F88FB-A6AF-65BA-2D3F-6DFFC51E962F -name MSVCR71.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8D888F32-B960-6FAE-16D1-207449570FD9 -name web2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::086AC033-B68D-0CF2-4317-0EA0D6A13202 -name win32process.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B1DDA977-5BAF-FA72-5A94-886F142D6AE8 -name librarything.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::00220302-54D5-8D81-4DD1-D5F05F36F5C5 -name lrf2html.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C8FC6F5A-7B18-6760-2843-600214FD9C00 -name calibredb.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9AE97139-D6D6-CA2C-70DF-91F97F6C8E66 -name _win32sysloader.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::74B69813-5F2F-4096-6940-EED8EAB7E5C8 -name pdftohtml.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::15FE9B66-6A50-987C-6D0A-FAC4FB65D085 -name QtCore4.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::91513702-1C71-C2CA-D686-B66501E3E150 -name lrf2lrs.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0F6E5A2A-8D26-31C5-7075-8621CEE7472F -name python25.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::50892EC8-2ED5-92E0-5136-28FE9E6F9501 -name _ctypes.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::40C5FFA9-C3FD-2E0A-E25D-F2AA4C2CD672 -name QtSvg4.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9ECEF6F4-E95D-3F1A-6400-8726EBE14A6B -name win32event.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6C60B310-E979-5950-4316-B19333F266D8 -name _imagingft.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::23FC9D7B-EC4D-1FB5-1E4D-8AA052A4790B -name html2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B0DBCC9A-8FE2-D917-4FE0-B6D05971F8E8 -name calibre-parallel.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6A287FDE-AB84-041E-886A-923F1D4EADA7 -name _dotblas.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7CDDA16F-629B-5F03-EA19-2508B9C00942 -name win32pipe.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::24A8B830-B46D-207C-6A91-B5E6856E99A4 -name _imaging.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::95782943-BCB9-9A8B-4DB8-1186D35F84E7 -name mingwm10.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::058A6811-F130-999C-B46A-FA86915CDA4E -name calibre.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BEAEEC0C-8321-922D-A113-A2E03C94DC7C -name scalarmath.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ACE1537B-B234-3C90-759A-8947A7AADC77 -name mobi2oeb.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::92701E8F-1D91-A796-C899-2A266029F61D -name _socket.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::45BD27B5-B910-7633-C827-37E82E89C27C -name w9xpopen.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::45C27909-D761-787F-84B2-66596E5C4E99 -name bz2.pyd -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2 -type dir -name plugins -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::71930E14-A27B-C23C-8D94-C7E97ADB8723 -name pictureflow.pyd -parent 36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2
File ::293E6ABE-17C9-5E53-1B44-C27029C8C061 -name winutil.pyd -parent 36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2
File ::A5737158-18DF-7F20-2BDF-2DF615663891 -name lzx.pyd -parent 36E8EEAC-F54D-5DE9-02D8-ECDFEBB4B5E2
File ::CA9E098C-2931-9781-1303-213C242F9A5E -name lit2oeb.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::16B5A447-066C-C93E-F63D-8BC0D57CA544 -name lit2oeb.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ABF342D2-82A9-2A20-BA97-54AD5BAF1A2A -name IM_MOD_RL_sfw_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3877E295-C7EB-DF63-DA91-B9E2F9D8035A -name comic2lrf.exe.local -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F47345A3-6CE0-4F5B-9CAE-3F4A18D4AA5A -name IM_MOD_RL_rle_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3A211C93-1B8B-A8AA-E240-A3287974DAB4 -name IM_MOD_RL_tiff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F24F3123-05A3-B452-D12B-CE6C126501B1 -name CORE_RL_libxml_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D7034E85-2733-DB61-DB49-C34B767B4B45 -name IM_MOD_RL_sun_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::21C1F4D3-487E-5FFF-C8CE-8E5FE779A786 -name IM_MOD_RL_msl_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::01EC7979-C9CB-696C-E8B3-F5945E1115BB -name IM_MOD_RL_jp2_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0F86B693-D83A-DB03-8641-219FE766D980 -name CORE_RL_ttf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DDCAEB76-7AC4-CA1A-0742-34B556592D2A -name IM_MOD_RL_exr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4D274537-6B6D-63F2-2615-E0CD279880A3 -name CORE_RL_Magick++_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A87CE00E-9F87-DBE3-DDA5-FC68D6D0731E -name IM_MOD_RL_dib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9114D530-B73B-CC7C-F6A6-655B7271AB35 -name IM_MOD_RL_preview_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::31EE0880-F1C8-94D6-4EDC-B09A576E0908 -name CORE_RL_magick_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BEA2B769-1A54-4398-E8B4-5BE15637B705 -name IM_MOD_RL_svg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7E300AD4-7C03-5835-0DD6-E9FA8737585A -name IM_MOD_RL_mpeg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5E9901D8-BBB7-A17C-5A04-837C0ADF8CAE -name IM_MOD_RL_clipboard_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F13497D2-87C0-243D-916A-0A160F1A1896 -name IM_MOD_RL_png_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DBD1CF95-1B01-9F5C-66D9-C7B4E1B44CC7 -name CORE_RL_bzlib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::149C038D-9CD6-20C5-49C3-FC6948D0709D -name IM_MOD_RL_wmf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F77F5E54-1D54-F7D3-9520-BB1811C11AA6 -name IM_MOD_RL_txt_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3545B38D-1BDF-B355-F779-4D83F292E2B6 -name IM_MOD_RL_viff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6E33F2FD-17BB-F096-4551-0E3B22924A4D -name IM_MOD_RL_ps2_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F4C810FF-4291-4491-0FA2-CFAD0BA690A9 -name type.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::02A3CD7D-743C-FAA8-9C20-3E8E59B8C2C2 -name IM_MOD_RL_ps3_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::697020D6-C5DA-A7DC-9454-1F9523D7748D -name IM_MOD_RL_dot_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E9D0609E-2D12-A8C0-9B47-D09CACB4A3AF -name IM_MOD_RL_xwd_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F237A6C8-4037-B9E5-8D65-29A5A69CADFE -name IM_MOD_RL_fits_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BDB45C50-E57A-357D-1D5A-392036227E6B -name IM_MOD_RL_histogram_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5A6DBEB5-CD8A-4109-A04C-EF0436BC1CDC -name mfc71.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FCC2A44D-D2F9-74DC-0C27-86F094E2C3E9 -name IM_MOD_RL_pnm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8E71473E-34AE-B7A3-B506-8A6AA622DAD7 -name IM_MOD_RL_ipl_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B680E84A-BA1C-5EA2-902E-095DD22A48F2 -name msvcp71.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6AF09D1D-8889-8A87-9FD4-1471DBB1354C -name IM_MOD_RL_rgb_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::941B86E2-428A-3F4A-EB34-CBDBDDAD648C -name IM_MOD_RL_xbm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1613269D-8A63-C843-E862-9B80CC17E60F -name IM_MOD_RL_otb_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::334E3925-703D-DDCA-A079-C53DB06AA069 -name IM_MOD_RL_avi_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::23549B03-F856-3B90-C9C5-3B64A5910C7B -name CORE_RL_lcms_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::0E3F5727-D99A-44CD-35E0-4FDFBB95FCBC -name IM_MOD_RL_xpm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::955B5799-4DB3-F422-589A-CDC20A82B6CB -name IM_MOD_RL_xcf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D3AB494C-3218-0137-4399-3FB1662C05D3 -name IM_MOD_RL_emf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1DEF5AF0-2376-539B-2A61-35B6ADC2F4BA -name log.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D472B449-3644-C538-30EF-EC42E3B84C43 -name IM_MOD_RL_mtv_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8B4E61F1-8FC2-7E65-4B94-3F19100DF58B -name CORE_RL_tiff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B747FE2A-0054-6815-40D0-74F89FC8C757 -name IM_MOD_RL_dps_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8B1660CC-7A97-96A2-1280-34554028CB9F -name IM_MOD_RL_dcm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::21B7EBEC-30C8-F2E8-9D73-E4E6965EA856 -name locale.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::85087BFC-42D6-C583-586E-19CAD45E6A61 -name CORE_RL_wand_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BE24EB64-E7BB-0E63-256E-DEDC2BBF1C2B -name IM_MOD_RL_ttf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7693E752-1A81-F6F3-C55D-9E8D94D6E4DC -name IM_MOD_RL_dpx_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A1451D28-A06B-3F03-4DCA-884729C5A030 -name IM_MOD_RL_jpeg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3F0D8F7A-906F-8CAE-84D7-E3480A09D39D -name IM_MOD_RL_fax_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CA8F1852-F5C1-86E8-31B9-8B1EFE837ADB -name IM_MOD_RL_avs_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::72370AAC-67CF-F570-2AA2-658E4C81C859 -name IM_MOD_RL_mvg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A8265A1E-E9B5-A38F-9ACF-99669CAE1E9F -name IM_MOD_RL_tga_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A8A9A383-0364-515F-C1D8-F82C274D652B -name configure.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A2167661-AF2B-E15E-60DA-715F47E5AA30 -name IM_MOD_RL_uil_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7608E2FF-1BF6-E18A-A884-244794BDA01B -name IM_MOD_RL_cut_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::772CA344-5EFD-78A0-3542-777F12356C8D -name Xext.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::37F8D85E-4EE9-80E5-A4A2-8F30444AD5CC -name IM_MOD_RL_scr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::973011B9-D193-6D64-D4EB-D82B0C730379 -name IM_MOD_RL_map_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9B9F088C-A20A-0C19-EF7D-52908A020D36 -name thresholds.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::831AAF1C-7CBE-CAD3-79A8-7430E8DE484E -name IM_MOD_RL_thumbnail_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::85236603-D71F-359C-B235-98C77809DDF1 -name IM_MOD_RL_mpc_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::94300805-117F-8337-A9BF-41E10D8AB437 -name IM_MOD_RL_cmyk_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::618A299D-4A28-E37A-D4BF-9209B594FAAF -name IM_MOD_RL_pcd_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9F6572D8-6BE6-290B-D4A7-A0D4E4DBAC23 -name IM_MOD_RL_sct_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::942E63AC-F579-0D17-FF56-E2C8CC5DECA3 -name IM_MOD_RL_pict_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F68DE3F9-742C-D8EE-B2FC-FF9B37EED8F3 -name IM_MOD_RL_gradient_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C460E29B-38EE-6FC0-757B-69563EFC3225 -name IM_MOD_RL_icon_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AE00BD3D-734C-78F6-9078-C04749F4652A -name X11.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B70ED455-A480-56E3-3BDE-E06CDDB62C04 -name IM_MOD_RL_jbig_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CFFC9A5D-2902-FD37-DBD1-6800C7C0C1AE -name magic.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::32DA3775-410C-0391-7ADB-B58028CC04E2 -name IM_MOD_RL_mat_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AB64F079-1F8D-BE3A-731B-4B20ABD20289 -name IM_MOD_RL_meta_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5A7F49E9-119A-FD9B-8186-0BE6B9DCF210 -name IM_MOD_RL_gray_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D92B4157-F307-64A4-9AA5-C5AA1F138E1B -name IM_MOD_RL_pwp_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BA631BF0-CB17-D0EC-FAA9-D7B426457DD3 -name IM_MOD_RL_fpx_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9EA95108-72D5-13B5-2BD4-87CECED9B367 -name IM_MOD_RL_pcl_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::3111AC7E-2387-AD7D-253F-979195AC4EA1 -name english.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::14C1E910-6F5D-9540-7430-6B0B92311EB2 -name IM_MOD_RL_wpg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5D7050F4-177A-03A2-3DD1-A7DFC968E4ED -name IM_MOD_RL_pdb_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5BACE29D-FAFD-E673-16A9-D22DCE6E0655 -name IM_MOD_RL_label_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::25F84452-26F7-4305-B405-B1D0C7D072D2 -name IM_MOD_RL_clip_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::807E6FF7-2D61-F308-BA2A-BD07A213078A -name IM_MOD_RL_pix_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1A951976-DBCC-9FAE-190C-B24BBA38A97A -name colors.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8608BB2C-6CDE-BBE7-39C6-DF83625D5BFB -name IM_MOD_RL_cin_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::CBE1DFDA-7E32-759F-346E-DD469B1CE1F0 -name IM_MOD_RL_bmp_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7AD432A3-5146-4966-8C8E-85ACDCC8CA7A -name IM_MOD_RL_raw_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9DC22033-0F40-26CC-9E09-959738F62855 -name IM_MOD_RL_cip_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A8C777EC-AEAA-6B3F-22A6-CEC28A2E5058 -name IM_MOD_RL_pdf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ACA1A829-27AE-EFE7-4EDD-01D050A2E0A6 -name analyze.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::C883300E-0C2A-EAF6-D72E-81E8B99535E1 -name IM_MOD_RL_mpr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A223D40F-EFC5-31E3-8E33-B90984080A3E -name CORE_RL_jpeg_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::DE197248-9758-A368-6058-B72C5169E0DD -name IM_MOD_RL_wbmp_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FD15A9C0-5C14-11CA-AA27-D66D638E58FC -name IM_MOD_RL_stegano_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::6668D58B-E040-328B-4AF4-14C738C172BA -name CORE_RL_jp2_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::871E464B-4566-1FC2-55CB-B65AEB416413 -name IM_MOD_RL_yuv_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::7D7A8325-4C69-B9D3-C832-803BCF999B5C -name coder.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B0A3651D-19B1-09F1-8197-1E58ED2CC704 -name IM_MOD_RL_null_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AB877243-6DAE-BF0C-70C2-F2D702B16231 -name IM_MOD_RL_pattern_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F571F366-1737-7E65-5441-DEBD166DE247 -name IM_MOD_RL_plasma_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::93F18CDE-B871-B2D4-3C0F-7C1B933E1ACB -name IM_MOD_RL_pcx_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D1567C76-29D0-C200-9FC7-F7E1399D3011 -name CORE_RL_xlib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BEF1E1AC-9564-EA49-2B8F-1AAC9F6A7669 -name IM_MOD_RL_caption_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::2D14864E-6A39-FE03-4EA8-CCE7AC94487D -name comic2lrf.exe -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::27CD65A5-D5F9-C982-5096-65298417EE61 -name IM_MOD_RL_url_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A273E901-0B63-390B-D44A-7240491C6F59 -name IM_MOD_RL_info_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B39B27EC-325A-D222-01FC-F6B3BC92E99A -name IM_MOD_RL_hdf_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::BA634D39-716B-C895-73DD-2E5FA3CA2F9C -name CORE_RL_png_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::35560FB0-A7BD-54C7-C799-3EB2922BED2C -name delegates.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FCE51670-E4AE-B813-6CFC-A7A9B627F72C -name IM_MOD_RL_matte_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::D10DB719-887D-4898-DAA8-8F1C6A4203B2 -name IM_MOD_RL_mono_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::78A66F97-ECEC-BFEC-75F2-2FA2087927C2 -name CORE_RL_jbig_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::FB74C41B-3F08-A9E8-B38D-C7C2FDFE9560 -name IM_MOD_RL_xtrn_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::ABC0A7AF-B14B-09BE-4756-76C8FE771517 -name IM_MOD_RL_vicar_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::F69B9AAD-EC2B-5EC7-5ED8-1395033DE0F5 -name IM_MOD_RL_psd_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::66CB1D9D-9995-F71C-155D-F1F4AA3B6D40 -name IM_MOD_RL_uyvy_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::4DB66BC3-4C48-C763-9BCA-9E831CA1FF0B -name IM_MOD_RL_art_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EB24F574-4226-6404-B069-7B46C04988E0 -name IM_MOD_RL_ycbcr_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A9990A18-D6A1-AA14-1EDF-FC43D8AE0C7E -name type-ghostscript.xml -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::5B72558A-192B-76EB-1BA8-C4CBA43C6C05 -name IM_MOD_RL_x_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::A3937F85-1D17-D3DD-2DF5-FB9FE4A99ADB -name IM_MOD_RL_dng_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B2C41CC1-EB2D-F7E7-B22E-0C154C4C96C1 -name IM_MOD_RL_html_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::68A62902-7F48-6E7A-E5D3-1F58C895B409 -name IM_MOD_RL_tim_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::E5C83E45-56B1-9BD7-7676-07CABD98E0BF -name IM_MOD_RL_tile_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::084206D9-98DB-DE2A-19BC-FD17A191096D -name IM_MOD_RL_xc_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::43BE7C18-6369-E035-8390-2E13C8CBB33C -name CORE_RL_zlib_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::EFC4D6E5-4FC9-25D5-B308-8CC8C13EF3A1 -name IM_MOD_RL_gif_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::05A8646B-F100-4803-5916-4CBAC154BFE9 -name IM_MOD_RL_sgi_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::1B354F22-4795-739A-A47D-8F2D99DFB58A -name IM_MOD_RL_ept_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::B6725A29-1F09-2982-6BE1-29062A90F684 -name IM_MOD_RL_palm_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::445BD28F-0E70-B452-15B3-9E0C353CE345 -name IM_MOD_RL_ps_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::183A1789-2ED2-D555-AE4B-B7EBC97EB1D5 -name IM_MOD_RL_miff_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::69178142-77D3-D7C5-74C7-6F1597474123 -name IM_MOD_RL_vid_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::9D84C810-6DEC-5831-CFC6-AD0543D95881 -name IM_MOD_RL_rla_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::514DCC61-0BE9-6C5C-A970-170219D3A87E -name IM_MOD_RL_magick_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::AAF94AED-250D-DE8D-14C5-FA8BC05AAE74 -name IM_MOD_RL_djvu_.dll -parent 6CCF3F71-74BB-ED69-D0E6-9F12348ABDD3
File ::8E5D85A4-7608-47A1-CF7C-309060D5FF40 -filemethod {Always overwrite files} -type dir -directory <%InstallDir%> -name /home/kovid/work/calibre/build/py2exe -parent BEF8D398-58BA-1F66-39D6-D4A63D5BEEF9
Component ::F6829AB7-9F66-4CEE-CA0E-21F54C6D3609 -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Main -parent Components
SetupType ::D9ADE41C-B744-690C-2CED-CF826BF03D2E -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Typical -parent SetupTypes
@ -508,9 +226,6 @@ InstallComponent 28FDA3F4-B799-901F-8A27-AA04F0C022AB -setup Install -type pane
InstallComponent A75C97CC-01AC-C12A-D663-A54E3257F11B -setup Install -type action -title {Disable Buttons} -component ModifyWidget -active Yes -parent 28FDA3F4-B799-901F-8A27-AA04F0C022AB
InstallComponent B6D03F99-8B73-BE6E-1050-721B286D3D60 -setup Install -type action -title {Execute Action} -component ExecuteAction -active Yes -parent 28FDA3F4-B799-901F-8A27-AA04F0C022AB
InstallComponent 91AB3DE5-D61C-522D-5B3B-F2953E1DE771 -setup Install -type action -title {Move Forward} -component MoveForward -active Yes -parent 28FDA3F4-B799-901F-8A27-AA04F0C022AB
InstallComponent D6631BA5-577E-B30C-A73D-2B12B826811A -setup Install -type pane -conditions C6DE83DD-5C2A-AC5B-6ABA-84D73AE38655 -title {Install USB Driver} -component CustomBlankPane1 -command insert -active Yes -parent StandardInstall
Condition C6DE83DD-5C2A-AC5B-6ABA-84D73AE38655 -active Yes -parent D6631BA5-577E-B30C-A73D-2B12B826811A -title {Platform Condition} -component PlatformCondition -TreeObject::id C6DE83DD-5C2A-AC5B-6ABA-84D73AE38655
InstallComponent 1B9E77A3-10D6-9EDD-160B-64B5EBB31981 -setup Install -type action -title {Add Widget} -component AddWidget -command reorder -active Yes -parent D6631BA5-577E-B30C-A73D-2B12B826811A
InstallComponent 8A7FD0C2-F053-8764-F204-4BAE71E05708 -setup Install -type pane -title {Setup Complete} -component SetupComplete -active Yes -parent StandardInstall
InstallComponent 710F2507-2557-652D-EA55-440D710EFDFA -setup Install -type action -conditions {69188956-D764-5B26-B048-46A4239C3733 08195201-0797-932C-4B51-E5EF9D1D41BD 2E18F4AE-F1BB-5C62-2900-73A576A49261} -title {Install USB Driver} -component ExecuteExternalProgram -command insert -alias {Install USB Driver} -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
Condition 69188956-D764-5B26-B048-46A4239C3733 -active Yes -parent 710F2507-2557-652D-EA55-440D710EFDFA -title {Platform Condition} -component PlatformCondition -TreeObject::id 69188956-D764-5B26-B048-46A4239C3733
@ -667,30 +382,6 @@ false
1ADA4DE6-31A7-E816-7719-4C8558F5378D,String
<%UpgradeInstall%>
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,Checked
No
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,Conditions
{0 conditions}
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,ExecuteAction
{Before Pane is Displayed}
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,Text,subst
1
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,Type
checkbutton
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,VirtualText
InstallUSBDriver
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,X
185
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,Y
220
1CA13495-CB19-27A9-56E5-9BCF91958249,Comment
{Ask the user if they want to proceed with the uninstall.}
@ -871,6 +562,9 @@ false
48596410-DF5A-1E56-D59C-1B1E2F094FCA,Conditions
{1 condition}
48596410-DF5A-1E56-D59C-1B1E2F094FCA,IconPath
{<%InstallDir%>\library.ico}
48596410-DF5A-1E56-D59C-1B1E2F094FCA,ShortcutName
<%AppName%>
@ -1094,7 +788,7 @@ DevconStatus
{}
77D1144B-1013-79B5-034B-5D6BDA6B2FD2,IconPath
{<%InstallDir%>\lrfviewer<%Ext%>}
{<%InstallDir%>\viewer.ico}
77D1144B-1013-79B5-034B-5D6BDA6B2FD2,InstallForAllUsers
Yes
@ -1492,6 +1186,9 @@ system
BEF8D398-58BA-1F66-39D6-D4A63D5BEEF9,Destination
<%InstallDir%>
BEF8D398-58BA-1F66-39D6-D4A63D5BEEF9,FileUpdateMethod
{Always overwrite files}
BEF8D398-58BA-1F66-39D6-D4A63D5BEEF9,Name
{Program Files}
@ -1525,9 +1222,6 @@ C5416030-8AB4-3466-F341-9A0BBFEA55EF,String
C5AD5B0C-26BE-16E5-899A-9A9436C8F688,ExitType
Finish
C6DE83DD-5C2A-AC5B-6ABA-84D73AE38655,Platform
Windows
C7D6444E-DB8B-EE24-3E12-A2AF8A392C5D,Background
white
@ -1567,33 +1261,15 @@ false
CFBE4459-450B-1FAB-3422-609544334AA2,String
<%InstallStopped%>
D6631BA5-577E-B30C-A73D-2B12B826811A,Active
Yes
D6631BA5-577E-B30C-A73D-2B12B826811A,BackButton,subst
1
D6631BA5-577E-B30C-A73D-2B12B826811A,CancelButton,subst
1
D6631BA5-577E-B30C-A73D-2B12B826811A,Caption,subst
1
D6631BA5-577E-B30C-A73D-2B12B826811A,Conditions
{1 condition}
D6631BA5-577E-B30C-A73D-2B12B826811A,Message,subst
1
D6631BA5-577E-B30C-A73D-2B12B826811A,NextButton,subst
1
D79DC0D2-38BC-9D9F-2DF4-3C76D89BF933,ExitType
Finish
D86BBA5C-4903-33BA-59F8-4266A3D45896,Conditions
{2 conditions}
D86BBA5C-4903-33BA-59F8-4266A3D45896,IconPath
{<%InstallDir%>\library.ico}
D86BBA5C-4903-33BA-59F8-4266A3D45896,ShortcutDirectory
<%QUICK_LAUNCH%>
@ -1621,6 +1297,9 @@ Typical
E32519F3-A540-C8F3-957F-4C1DB5B25DFE,Conditions
{2 conditions}
E32519F3-A540-C8F3-957F-4C1DB5B25DFE,IconPath
{<%InstallDir%>\library.ico}
E32519F3-A540-C8F3-957F-4C1DB5B25DFE,ShortcutName
<%AppName%>
@ -2205,9 +1884,6 @@ E611105F-DC85-9E20-4F7B-E63C54E5DF06,Message
1356216E-90D2-8324-0EEB-975A64F23EB8,Message
<%InstallingApplicationText%>
1B9E77A3-10D6-9EDD-160B-64B5EBB31981,Text
{Install USB driver for the SONY PRS500}
21B897C4-24BE-70D1-58EA-DE78EFA60719,Message
{USB Driver installation failed with return code <%DevconStatus%> and console output \n\n<%DevconResult%>}
@ -2273,12 +1949,6 @@ C3E9E5D9-58C8-C2C5-DF75-21D908A64782,Message
C7D6444E-DB8B-EE24-3E12-A2AF8A392C5D,Text
<%CreateDesktopShortcutText%>
D6631BA5-577E-B30C-A73D-2B12B826811A,Caption
{Install USB Driver}
D6631BA5-577E-B30C-A73D-2B12B826811A,Message
{If you intend to use <%AppName%> to manage your SONY PRS500, you have to install the USB driver. Note that you cannot use the PRS500 in both <%AppName%> and the SONY E-library software.}
D9ADE41C-B744-690C-2CED-CF826BF03D2E,Description
<%TypicalInstallDescription%>

View File

@ -6,8 +6,7 @@ __docformat__ = 'restructuredtext en'
'''
Freeze app into executable using py2exe.
'''
QT_DIR = 'C:\\Qt\\4.4.0'
DEVCON = 'C:\\devcon\\i386\\devcon.exe'
QT_DIR = 'C:\\Qt\\4.4.1'
LIBUSB_DIR = 'C:\\libusb'
LIBUNRAR = 'C:\\Program Files\\UnrarDLL\\unrar.dll'
PDFTOHTML = 'C:\\pdftohtml\\pdftohtml.exe'
@ -15,7 +14,7 @@ IMAGEMAGICK_DIR = 'C:\\ImageMagick'
FONTCONFIG_DIR = 'C:\\fontconfig'
import sys, os, py2exe, shutil, zipfile, glob, subprocess
import sys, os, py2exe, shutil, zipfile, glob, subprocess, re
from distutils.core import setup
from distutils.filelist import FileList
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
@ -23,6 +22,13 @@ sys.path.insert(0, BASE_DIR)
from setup import VERSION, APPNAME, entry_points, scripts, basenames
sys.path.remove(BASE_DIR)
ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')]
for icon in ICONS:
if not os.access(icon, os.R_OK):
raise Exception('No icon at '+icon)
VERSION = re.sub('[a-z]\d+', '', VERSION)
PY2EXE_DIR = os.path.join(BASE_DIR, 'build','py2exe')
class BuildEXE(py2exe.build_exe.py2exe):
@ -59,29 +65,18 @@ class BuildEXE(py2exe.build_exe.py2exe):
shutil.copyfile(f, os.path.join(self.dist_dir, os.path.basename(f)))
for f in glob.glob(os.path.join(BASE_DIR, 'src', 'calibre', 'plugins', '*.pyd')):
shutil.copyfile(f, os.path.join(tgt, os.path.basename(f)))
qtsvgdll = None
for other in self.other_depends:
if 'qtsvg4.dll' in other.lower():
qtsvgdll = other
break
shutil.copyfile('LICENSE', os.path.join(self.dist_dir, 'LICENSE'))
print
if qtsvgdll:
print 'Adding', qtsvgdll
shutil.copyfile(qtsvgdll, os.path.join(self.dist_dir, os.path.basename(qtsvgdll)))
qtxmldll = os.path.join(os.path.dirname(qtsvgdll), 'QtXml4.dll')
print 'Adding', qtxmldll
shutil.copyfile(qtxmldll,
os.path.join(self.dist_dir, os.path.basename(qtxmldll)))
print 'Adding QtXml4.dll'
shutil.copyfile(os.path.join(QT_DIR, 'bin', 'QtXml4.dll'),
os.path.join(self.dist_dir, 'QtXml4.dll'))
print 'Adding Qt plugins...',
qt_prefix = QT_DIR
if qtsvgdll:
qt_prefix = os.path.dirname(os.path.dirname(qtsvgdll))
plugdir = os.path.join(qt_prefix, 'plugins')
for d in ('imageformats', 'codecs', 'iconengines'):
print d,
imfd = os.path.join(plugdir, d)
tg = os.path.join(self.dist_dir, d)
tg = os.path.join(self.dist_dir, d)
if os.path.exists(tg):
shutil.rmtree(tg)
shutil.copytree(imfd, tg)
@ -93,6 +88,11 @@ class BuildEXE(py2exe.build_exe.py2exe):
f.write(i, i.partition('\\')[-1])
f.close()
print
print 'Copying icons'
for icon in ICONS:
shutil.copyfile(icon, os.path.join(PY2EXE_DIR, os.path.basename(icon)))
print
print 'Adding third party dependencies'
print '\tAdding devcon'
@ -101,7 +101,6 @@ class BuildEXE(py2exe.build_exe.py2exe):
for pat in ('*.dll', '*.sys', '*.cat', '*.inf'):
for f in glob.glob(os.path.join(LIBUSB_DIR, pat)):
shutil.copyfile(f, os.path.join(tdir, os.path.basename(f)))
shutil.copyfile(DEVCON, os.path.join(tdir, os.path.basename(DEVCON)))
print '\tAdding unrar'
shutil.copyfile(LIBUNRAR, os.path.join(PY2EXE_DIR, os.path.basename(LIBUNRAR)))
print '\tAdding pdftohtml'
@ -126,8 +125,8 @@ class BuildEXE(py2exe.build_exe.py2exe):
@classmethod
def manifest(cls, prog):
cls.manifest_resource_id += 1
return (24, cls.manifest_resource_id,
cls.MANIFEST_TEMPLATE % dict(prog=prog, version=VERSION+'.0'))
return (24, cls.manifest_resource_id,
cls.MANIFEST_TEMPLATE % dict(prog=prog, version=(VERSION+'.0')))
def main(args=sys.argv):
@ -137,18 +136,17 @@ def main(args=sys.argv):
console = [dict(dest_base=basenames['console'][i], script=scripts['console'][i])
for i in range(len(scripts['console']))]
setup(
cmdclass = {'py2exe': BuildEXE},
windows = [
{'script' : scripts['gui'][0],
'dest_base' : APPNAME,
'icon_resources' : [(1, os.path.join(BASE_DIR, 'icons', 'library.ico'))],
'icon_resources' : [(1, ICONS[0])],
'other_resources' : [BuildEXE.manifest(APPNAME)],
},
{'script' : scripts['gui'][1],
'dest_base' : 'lrfviewer',
'icon_resources' : [(1, os.path.join(BASE_DIR, 'icons', 'viewer.ico'))],
'icon_resources' : [(1, ICONS[1])],
'other_resources' : [BuildEXE.manifest('lrfviewer')],
},
],
@ -163,12 +161,12 @@ def main(args=sys.argv):
'win32process', 'win32api', 'msvcrt',
'win32event', 'calibre.ebooks.lrf.any.*',
'calibre.ebooks.lrf.feeds.*',
'lxml', 'lxml._elementpath', 'genshi',
'genshi', 'BeautifulSoup',
'path', 'pydoc', 'IPython.Extensions.*',
'calibre.web.feeds.recipes.*',
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
],
'packages' : ['PIL'],
'packages' : ['PIL', 'lxml'],
'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk"
],
@ -180,4 +178,4 @@ def main(args=sys.argv):
return 0
if __name__ == '__main__':
sys.exit(main())
sys.exit(main())

View File

@ -10,7 +10,7 @@ from distutils.core import Extension
from distutils.command.build_ext import build_ext as _build_ext
from distutils.dep_util import newer_group
from distutils import log
import sipconfig, os, sys, string, glob, shutil
from PyQt4 import pyqtconfig
iswindows = 'win32' in sys.platform
@ -22,7 +22,7 @@ def replace_suffix(path, new_suffix):
return os.path.splitext(path)[0] + new_suffix
class PyQtExtension(Extension):
def __init__(self, name, sources, sip_sources, **kw):
'''
:param sources: Qt .cpp and .h files needed for this extension
@ -32,16 +32,16 @@ class PyQtExtension(Extension):
self.module_makefile = pyqtconfig.QtGuiModuleMakefile
self.sip_sources = map(lambda x: x.replace('/', os.sep), sip_sources)
Extension.__init__(self, name, sources, **kw)
class build_ext(_build_ext):
def make(self, makefile):
make = 'make'
if iswindows:
make = 'mingw32-make'
self.spawn([make, '-f', makefile])
def build_qt_objects(self, ext, bdir):
if not iswindows:
bdir = os.path.join(bdir, 'qt')
@ -53,7 +53,7 @@ class build_ext(_build_ext):
try:
headers = set([f for f in sources if f.endswith('.h')])
sources = set(sources) - headers
name = ext.name.rpartition('.')[-1]
name = ext.name.rpartition('.')[-1]
pro = '''\
TARGET = %s
TEMPLATE = lib
@ -69,7 +69,7 @@ CONFIG += x86 ppc
return map(os.path.abspath, glob.glob(pat))
finally:
os.chdir(cwd)
def build_sbf(self, sip, sbf, bdir):
sip_bin = self.sipcfg.sip_bin
self.spawn([sip_bin,
@ -78,10 +78,10 @@ CONFIG += x86 ppc
'-I', self.pyqtcfg.pyqt_sip_dir,
] + self.pyqtcfg.pyqt_sip_flags.split()+
[sip])
def build_pyqt(self, bdir, sbf, ext, qtobjs, headers):
makefile = ext.module_makefile(configuration=self.pyqtcfg,
build_file=sbf, dir=bdir,
build_file=sbf, dir=bdir,
makefile='Makefile.pyqt',
universal=OSX_SDK, qt=1)
if 'win32' in sys.platform:
@ -95,14 +95,14 @@ CONFIG += x86 ppc
self.make('Makefile.pyqt')
finally:
os.chdir(cwd)
def build_extension(self, ext):
self.inplace = True # Causes extensions to be built in the source tree
if not isinstance(ext, PyQtExtension):
return _build_ext.build_extension(self, ext)
fullname = self.get_ext_fullname(ext.name)
if self.inplace:
# ignore build-lib -- put the compiled extension into
@ -122,20 +122,20 @@ CONFIG += x86 ppc
bdir = os.path.abspath(os.path.join(self.build_temp, fullname))
if not os.path.exists(bdir):
os.makedirs(bdir)
ext.sources = map(os.path.abspath, ext.sources)
ext.sources2 = map(os.path.abspath, ext.sources)
qt_dir = 'qt\\release' if iswindows else 'qt'
objects = set(map(lambda x: os.path.join(bdir, qt_dir, replace_suffix(os.path.basename(x), '.o')),
[s for s in ext.sources if not s.endswith('.h')]))
[s for s in ext.sources2 if not s.endswith('.h')]))
newer = False
for object in objects:
if newer_group(ext.sources, object, missing='newer'):
if newer_group(ext.sources2, object, missing='newer'):
newer = True
break
headers = [f for f in ext.sources if f.endswith('.h')]
headers = [f for f in ext.sources2 if f.endswith('.h')]
if self.force or newer:
log.info('building \'%s\' extension', ext.name)
objects = self.build_qt_objects(ext, bdir)
self.sipcfg = sipconfig.Configuration()
self.pyqtcfg = pyqtconfig.Configuration()
sbf_sources = []
@ -148,19 +148,19 @@ CONFIG += x86 ppc
generated_sources = []
for sbf in sbf_sources:
generated_sources += self.get_sip_output_list(sbf, bdir)
depends = generated_sources + list(objects)
mod = os.path.join(bdir, os.path.basename(ext_filename))
if self.force or newer_group(depends, mod, 'newer'):
self.build_pyqt(bdir, sbf_sources[0], ext, list(objects), headers)
if self.force or newer_group([mod], ext_filename, 'newer'):
if os.path.exists(ext_filename):
os.unlink(ext_filename)
shutil.copyfile(mod, ext_filename)
shutil.copymode(mod, ext_filename)
def get_sip_output_list(self, sbf, bdir):
"""
Parse the sbf file specified to extract the name of the generated source
@ -175,7 +175,7 @@ CONFIG += x86 ppc
return out
raise RuntimeError, "cannot parse SIP-generated '%s'" % sbf
def run_sip(self, sip_files):
sip_bin = self.sipcfg.sip_bin
sip_sources = [i[0] for i in sip_files]
@ -191,6 +191,5 @@ CONFIG += x86 ppc
] + self.pyqtcfg.pyqt_sip_flags.split()+
[sip])
generated_sources += self.get_sip_output_list(sbf)
return generated_sources
return generated_sources

View File

@ -1,46 +0,0 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Compile resource files.
'''
import os, sys, glob
sys.path.insert(1, os.path.join(os.getcwd(), 'src'))
from calibre import __appname__
RESOURCES = dict(
opf_template = '%p/ebooks/metadata/opf.xml',
ncx_template = '%p/ebooks/metadata/ncx.xml',
fb2_xsl = '%p/ebooks/lrf/fb2/fb2.xsl',
metadata_sqlite = '%p/library/metadata_sqlite.sql',
)
def main(args=sys.argv):
data = ''
for key, value in RESOURCES.items():
path = value.replace('%p', 'src'+os.sep+__appname__)
bytes = repr(open(path, 'rb').read())
data += key + ' = ' + bytes + '\n\n'
translations_found = False
for TPATH in ('/usr/share/qt4/translations', '/usr/lib/qt4/translations'):
if os.path.exists(TPATH):
files = glob.glob(TPATH + '/qt_??.qm')
for f in files:
key = os.path.basename(f).partition('.')[0]
bytes = repr(open(f, 'rb').read())
data += key + ' = ' + bytes + '\n\n'
translations_found = True
break
if not translations_found:
print 'WARNING: Could not find Qt transations'
dest = os.path.abspath(os.path.join('src', __appname__, 'resources.py'))
print 'Writing resources to', dest
open(dest, 'wb').write(data)
return 0
if __name__ == '__main__':
sys.exit(main())

374
setup.py
View File

@ -1,8 +1,8 @@
#!/usr/bin/env python
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, re, os, shutil
import sys, re, os, shutil, cStringIO, tempfile, subprocess
sys.path.append('src')
iswindows = re.search('win(32|64)', sys.platform)
isosx = 'darwin' in sys.platform
@ -48,66 +48,294 @@ main_functions = {
if __name__ == '__main__':
from setuptools import setup, find_packages, Extension
from distutils.command.build import build as _build
from distutils.core import Command
from distutils.core import Command as _Command
from pyqtdistutils import PyQtExtension, build_ext
import subprocess, glob
class pot(Command):
user_options = []
def initialize_options(self): pass
def finalize_options(self): pass
def run(self):
from calibre.translations import create_pot
create_pot()
def build_manual():
cwd = os.path.abspath(os.getcwd())
os.chdir(os.path.join('src', 'calibre', 'manual'))
try:
for d in ('.build', 'cli'):
if os.path.exists(d):
shutil.rmtree(d)
os.makedirs(d)
if not os.path.exists('.build'+os.sep+'html'):
os.makedirs('.build'+os.sep+'html')
subprocess.check_call(['sphinx-build', '-b', 'custom', '-d',
'.build/doctrees', '.', '.build/html'])
finally:
os.chdir(cwd)
def newer(targets, sources):
'''
Return True is sources is newer that targets or if targets
does not exist.
'''
for f in targets:
if not os.path.exists(f):
return True
ttimes = map(lambda x: os.stat(x).st_mtime, targets)
stimes = map(lambda x: os.stat(x).st_mtime, sources)
newest_source, oldest_target = max(stimes), min(ttimes)
return newest_source > oldest_target
class manual(Command):
class Command(_Command):
user_options = []
def initialize_options(self): pass
def finalize_options(self): pass
class sdist(Command):
description = "create a source distribution using bzr"
def run(self):
build_manual()
name = 'dist/calibre-%s.tar.gz'%VERSION
subprocess.check_call(('bzr export '+name).split())
self.distribution.dist_files.append(('sdist', '', name))
class pot(Command):
description = '''Create the .pot template for all translatable strings'''
PATH = os.path.join('src', APPNAME, 'translations')
def source_files(self):
ans = []
for root, dirs, files in os.walk(os.path.dirname(self.PATH)):
for name in files:
if name.endswith('.py'):
ans.append(os.path.abspath(os.path.join(root, name)))
return ans
def run(self):
sys.path.insert(0, os.path.abspath(self.PATH))
try:
from pygettext import main as pygettext
files = self.source_files()
buf = cStringIO.StringIO()
print 'Creating translations template'
tempdir = tempfile.mkdtemp()
pygettext(buf, ['-p', tempdir]+files)
src = buf.getvalue()
pot = os.path.join(tempdir, 'calibre.pot')
f = open(pot, 'wb')
f.write(src)
f.close()
print 'Translations template:', pot
return pot
finally:
sys.path.remove(os.path.abspath(self.PATH))
class manual(Command):
description='''Build the User Manual '''
def run(self):
cwd = os.path.abspath(os.getcwd())
os.chdir(os.path.join('src', 'calibre', 'manual'))
try:
for d in ('.build', 'cli'):
if os.path.exists(d):
shutil.rmtree(d)
os.makedirs(d)
if not os.path.exists('.build'+os.sep+'html'):
os.makedirs('.build'+os.sep+'html')
subprocess.check_call(['sphinx-build', '-b', 'custom', '-d',
'.build/doctrees', '.', '.build/html'])
finally:
os.chdir(cwd)
@classmethod
def clean(cls):
path = os.path.join('src', 'calibre', 'manual', '.build')
if os.path.exists(path):
shutil.rmtree(path)
class resources(Command):
description='''Compile various resource files used in calibre. '''
RESOURCES = dict(
opf_template = 'ebooks/metadata/opf.xml',
ncx_template = 'ebooks/metadata/ncx.xml',
fb2_xsl = 'ebooks/lrf/fb2/fb2.xsl',
metadata_sqlite = 'library/metadata_sqlite.sql',
)
DEST = os.path.join('src', APPNAME, 'resources.py')
def get_qt_translations(self):
data = {}
translations_found = False
for TPATH in ('/usr/share/qt4/translations', '/usr/lib/qt4/translations'):
if os.path.exists(TPATH):
files = glob.glob(TPATH + '/qt_??.qm')
for f in files:
key = os.path.basename(f).partition('.')[0]
data[key] = f
translations_found = True
break
if not translations_found:
print 'WARNING: Could not find Qt transations'
return data
def run(self):
data, dest, RESOURCES = {}, self.DEST, self.RESOURCES
for key in RESOURCES:
path = RESOURCES[key]
if not os.path.isabs(path):
RESOURCES[key] = os.path.join('src', APPNAME, path)
translations = self.get_qt_translations()
RESOURCES.update(translations)
if newer([dest], RESOURCES.values()):
print 'Compiling resources...'
with open(dest, 'wb') as f:
for key in RESOURCES:
data = open(RESOURCES[key], 'rb').read()
f.write(key + ' = ' + repr(data)+'\n\n')
else:
print 'Resources are up to date'
@classmethod
def clean(cls):
path = cls.DEST
for path in glob.glob(path+'*'):
if os.path.exists(path):
os.remove(path)
class translations(Command):
description='''Compile the translations'''
PATH = os.path.join('src', APPNAME, 'translations')
DEST = os.path.join(PATH, 'compiled.py')
def run(self):
sys.path.insert(0, os.path.abspath(self.PATH))
try:
files = glob.glob(os.path.join(self.PATH, '*.po'))
if newer([self.DEST], files):
from msgfmt import main as msgfmt
translations = {}
print 'Compiling translations...'
for po in files:
lang = os.path.basename(po).partition('.')[0]
buf = cStringIO.StringIO()
print 'Compiling', lang
msgfmt(buf, [po])
translations[lang] = buf.getvalue()
open(self.DEST, 'wb').write('translations = '+repr(translations))
else:
print 'Translations up to date'
finally:
sys.path.remove(os.path.abspath(self.PATH))
@classmethod
def clean(cls):
path = cls.DEST
if os.path.exists(path):
os.remove(path)
class gui(Command):
description='''Compile all GUI forms and images'''
PATH = os.path.join('src', APPNAME, 'gui2')
IMAGES_DEST = os.path.join(PATH, 'images_rc.py')
@classmethod
def find_forms(cls):
forms = []
for root, dirs, files in os.walk(cls.PATH):
for name in files:
if name.endswith('.ui'):
forms.append(os.path.abspath(os.path.join(root, name)))
return forms
@classmethod
def form_to_compiled_form(cls, form):
return form.rpartition('.')[0]+'_ui.py'
def run(self):
self.build_forms()
self.build_images()
def build_images(self):
cwd, images = os.getcwd(), os.path.basename(self.IMAGES_DEST)
try:
os.chdir(self.PATH)
sources, files = [], []
for root, dirs, files in os.walk('images'):
for name in files:
sources.append(os.path.join(root, name))
if newer([images], sources):
print 'Compiling images...'
for s in sources:
alias = ' alias="library"' if s.endswith('images'+os.sep+'library.png') else ''
files.append('<file%s>%s</file>'%(alias, s))
manifest = '<RCC>\n<qresource prefix="/">\n%s\n</qresource>\n</RCC>'%'\n'.join(files)
with open('images.qrc', 'wb') as f:
f.write(manifest)
subprocess.check_call(['pyrcc4', '-o', images, 'images.qrc'])
else:
print 'Images are up to date'
finally:
os.chdir(cwd)
def build_forms(self):
from PyQt4.uic import compileUi
forms = self.find_forms()
for form in forms:
compiled_form = self.form_to_compiled_form(form)
if not os.path.exists(compiled_form) or os.stat(form).st_mtime > os.stat(compiled_form).st_mtime:
print 'Compiling form', form
buf = cStringIO.StringIO()
compileUi(form, buf)
dat = buf.getvalue()
dat = dat.replace('__appname__', APPNAME)
dat = dat.replace('import images_rc', 'from calibre.gui2 import images_rc')
dat = dat.replace('from library import', 'from calibre.gui2.library import')
dat = dat.replace('from widgets import', 'from calibre.gui2.widgets import')
dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
# Workaround bug in Qt 4.4 on Windows
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
print 'Implementing Workaround for buggy pyuic in form', form
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
open(compiled_form, 'wb').write(dat)
@classmethod
def clean(cls):
forms = cls.find_forms()
for form in forms:
c = cls.form_to_compiled_form(form)
if os.path.exists(c):
os.remove(c)
images = cls.IMAGES_DEST
if os.path.exists(images):
os.remove(images)
class clean(Command):
description='''Delete all computer generated files in the source tree'''
def run(self):
print 'Cleaning...'
manual.clean()
gui.clean()
translations.clean()
resources.clean()
for f in glob.glob(os.path.join('src', 'calibre', 'plugins', '*')):
os.remove(f)
for root, dirs, files in os.walk('.'):
for name in files:
if name.endswith('~') or \
name.endswith('.pyc') or \
name.endswith('.pyo'):
os.remove(os.path.join(root, name))
for dir in 'build', 'dist':
for f in os.listdir(dir):
if os.path.isdir(dir + os.sep + f):
shutil.rmtree(dir + os.sep + f)
else:
os.remove(dir + os.sep + f)
class build(_build):
def run(self):
# Build resources
resources = __import__('resources')
resources.main([sys.executable, 'resources.py'])
from calibre.translations import main as translations
cwd = os.path.abspath(os.getcwd())
# Build translations
try:
os.chdir(os.path.join('src', 'calibre', 'translations'))
translations([sys.executable])
finally:
os.chdir(cwd)
# Build GUI
from calibre.gui2.make import main as gui2
try:
os.chdir(os.path.join('src', 'calibre', 'gui2'))
print 'Compiling GUI resources...'
gui2([sys.executable])
finally:
os.chdir(cwd)
_build.run(self)
sub_commands = \
[
('resources', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('translations', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
('gui', lambda self : 'CALIBRE_BUILDBOT' not in os.environ.keys()),
] + _build.sub_commands
entry_points['console_scripts'].append('calibre_postinstall = calibre.linux:post_install')
ext_modules = [
@ -115,10 +343,12 @@ if __name__ == '__main__':
sources=['src/calibre/utils/lzx/lzxmodule.c',
'src/calibre/utils/lzx/lzxd.c'],
include_dirs=['src/calibre/utils/lzx']),
Extension('calibre.plugins.msdes',
sources=['src/calibre/utils/msdes/msdesmodule.c',
'src/calibre/utils/msdes/des.c'],
include_dirs=['src/calibre/utils/msdes']),
PyQtExtension('calibre.plugins.pictureflow',
['src/calibre/gui2/pictureflow/pictureflow.cpp',
'src/calibre/gui2/pictureflow/pictureflow.h'],
@ -137,20 +367,20 @@ if __name__ == '__main__':
)
setup(
name=APPNAME,
packages = find_packages('src'),
package_dir = { '' : 'src' },
version=VERSION,
author='Kovid Goyal',
author_email='kovid@kovidgoyal.net',
url = 'http://%s.kovidgoyal.net'%APPNAME,
package_data = {'calibre':['plugins/*']},
include_package_data=True,
entry_points = entry_points,
zip_safe = False,
options = { 'bdist_egg' : {'exclude_source_files': True,}, },
ext_modules=ext_modules,
description =
name = APPNAME,
packages = find_packages('src'),
package_dir = { '' : 'src' },
version = VERSION,
author = 'Kovid Goyal',
author_email = 'kovid@kovidgoyal.net',
url = 'http://%s.kovidgoyal.net'%APPNAME,
package_data = {'calibre':['plugins/*']},
include_package_data = True,
entry_points = entry_points,
zip_safe = False,
options = { 'bdist_egg' : {'exclude_source_files': True,}, },
ext_modules = ext_modules,
description =
'''
E-book management application.
''',
@ -171,7 +401,7 @@ if __name__ == '__main__':
'''%(APPNAME, APPNAME, APPNAME, APPNAME, APPNAME),
license = 'GPL',
classifiers = [
classifiers = [
'Development Status :: 4 - Beta',
'Environment :: Console',
'Environment :: X11 Applications :: Qt',
@ -184,9 +414,21 @@ if __name__ == '__main__':
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Hardware :: Hardware Drivers'
],
cmdclass = {'build_ext': build_ext, 'build' : build, 'pot' : pot,
'manual' : manual},
cmdclass = {
'build_ext' : build_ext,
'build' : build,
'pot' : pot,
'manual' : manual,
'resources' : resources,
'translations' : translations,
'gui' : gui,
'clean' : clean,
'sdist' : sdist,
},
)
if 'develop' in ' '.join(sys.argv) and islinux:
subprocess.check_call('calibre_postinstall --do-not-reload-udev-hal', shell=True)
if 'install' in sys.argv and islinux:
subprocess.check_call('calibre_postinstall', shell=True)

View File

@ -2,8 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, re, logging, time, subprocess, mechanize, atexit
import sys, os, re, logging, time, subprocess, atexit
from htmlentitydefs import name2codepoint
from math import floor
from logging import Formatter
@ -15,7 +14,7 @@ from calibre.constants import iswindows, isosx, islinux, isfrozen, \
terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl
import mechanize
def unicode_path(path, abs=False):
if not isinstance(path, unicode):
@ -95,7 +94,7 @@ def filename_to_utf8(name):
def extract(path, dir):
ext = os.path.splitext(path)[1][1:].lower()
extractor = None
if ext in ['zip', 'cbz', 'epub']:
if ext in ['zip', 'cbz', 'epub', 'oebzip']:
from calibre.libunzip import extract as zipextract
extractor = zipextract
elif ext in ['cbr', 'rar']:
@ -139,9 +138,16 @@ def get_proxies():
return proxies
def browser(honor_time=False):
def browser(honor_time=True, max_time=2):
'''
Create a mechanize browser for web scraping. The browser handles cookies,
refresh requests and ignores robots.txt. Also uses proxy if avaialable.
:param honor_time: If True honors pause time in refresh requests
:param max_time: Maximum time in seconds to wait during a refresh request
'''
opener = mechanize.Browser()
opener.set_handle_refresh(True, honor_time=honor_time)
opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time)
opener.set_handle_robots(False)
opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')]
http_proxy = get_proxies().get('http', None)
@ -156,7 +162,7 @@ def fit_image(width, height, pwidth, pheight):
@param height: Height of image
@param pwidth: Width of box
@param pheight: Height of box
@return: scaled, new_width, new_height. scaled is True iff new_widdth and/or new_height is different from width or height.
@return: scaled, new_width, new_height. scaled is True iff new_width and/or new_height is different from width or height.
'''
scaled = height > pheight or width > pwidth
if height > pheight:
@ -171,6 +177,19 @@ def fit_image(width, height, pwidth, pheight):
return scaled, int(width), int(height)
class CurrentDir(object):
def __init__(self, path):
self.path = path
self.cwd = None
def __enter__(self, *args):
self.cwd = os.getcwd()
os.chdir(self.path)
return self.cwd
def __exit__(self, *args):
os.chdir(self.cwd)
def sanitize_file_name(name):
'''
@ -265,14 +284,36 @@ def english_sort(x, y):
class LoggingInterface:
def __init__(self, logger):
self.__logger = logger
self.__logger = self.logger = logger
def setup_cli_handler(self, verbosity):
for handler in self.__logger.handlers:
if isinstance(handler, logging.StreamHandler):
return
if os.environ.get('CALIBRE_WORKER', None) is not None and self.__logger.handlers:
return
stream = sys.stdout
formatter = logging.Formatter()
level = logging.INFO
if verbosity > 0:
formatter = ColoredFormatter('[%(levelname)s] %(message)s') if verbosity > 1 else \
ColoredFormatter('%(levelname)s: %(message)s')
level = logging.DEBUG
if verbosity > 1:
stream = sys.stderr
handler = logging.StreamHandler(stream)
handler.setFormatter(formatter)
handler.setLevel(level)
self.__logger.addHandler(handler)
self.__logger.setLevel(level)
def ___log(self, func, msg, args, kwargs):
args = [msg] + list(args)
for i in range(len(args)):
if isinstance(args[i], unicode):
args[i] = args[i].encode(preferred_encoding, 'replace')
func(*args, **kwargs)
def log_debug(self, msg, *args, **kwargs):
@ -296,13 +337,19 @@ class LoggingInterface:
def log_exception(self, msg, *args):
self.___log(self.__logger.exception, msg, args, {})
def walk(dir):
''' A nice interface to os.walk '''
for record in os.walk(dir):
for f in record[-1]:
yield os.path.join(record[0], f)
def strftime(fmt, t=time.localtime()):
'''
A version of strtime that returns unicode strings.
'''
result = time.strftime(fmt, t)
return unicode(result, preferred_encoding, 'replace')
''' A version of strtime that returns unicode strings. '''
if iswindows:
if isinstance(fmt, unicode):
fmt = fmt.encode('mbcs')
return plugins['winutil'][0].strftime(fmt, t)
return time.strftime(fmt, t).decode(preferred_encoding, 'replace')
def entity_to_unicode(match, exceptions=[], encoding='cp1252'):
'''

View File

@ -2,13 +2,13 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.4.83'
__version__ = '0.4.92'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
'''
Various run time constants.
'''
import sys, locale, codecs
import sys, locale, codecs, os
from calibre.utils.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout)
@ -16,7 +16,7 @@ terminal_controller = TerminalController(sys.stdout)
iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower()
isosx = 'darwin' in sys.platform.lower()
islinux = not(iswindows or isosx)
isfrozen = hasattr(sys, 'frozen')
isfrozen = hasattr(sys, 'frozen')
try:
preferred_encoding = locale.getpreferredencoding()
@ -27,4 +27,37 @@ except:
win32event = __import__('win32event') if iswindows else None
winerror = __import__('winerror') if iswindows else None
win32api = __import__('win32api') if iswindows else None
fcntl = None if iswindows else __import__('fcntl')
fcntl = None if iswindows else __import__('fcntl')
################################################################################
plugins = None
if plugins is None:
# Load plugins
def load_plugins():
plugins = {}
if isfrozen:
if iswindows:
plugin_path = os.path.join(os.path.dirname(sys.executable), 'plugins')
sys.path.insert(1, os.path.dirname(sys.executable))
elif isosx:
plugin_path = os.path.join(getattr(sys, 'frameworks_dir'), 'plugins')
elif islinux:
plugin_path = os.path.join(getattr(sys, 'frozen_path'), 'plugins')
sys.path.insert(0, plugin_path)
else:
import pkg_resources
plugin_path = getattr(pkg_resources, 'resource_filename')('calibre', 'plugins')
sys.path.insert(0, plugin_path)
for plugin in ['pictureflow', 'lzx', 'msdes'] + \
(['winutil'] if iswindows else []) + \
(['usbobserver'] if isosx else []):
try:
p, err = __import__(plugin), ''
except Exception, err:
p = None
err = str(err)
plugins[plugin] = (p, err)
return plugins
plugins = load_plugins()

View File

@ -21,13 +21,13 @@ Run an embedded python interpreter.
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
)
parser.add_option('-c', '--command', help='Run python code.', default=None)
parser.add_option('--migrate', action='store_true', default=False,
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.', default=False)
return parser
def update_zipfile(zipfile, mod, path):
if 'win32' in sys.platform:
print 'WARNING: On Windows Vista you must run this from a console that has been started in Administrator mode.'
print 'Press Enter to continue if this is an Administrator console or Ctrl-C to Cancel'
raw_input()
print 'WARNING: On Windows Vista using this option may cause windows to put library.zip into the Virtual Store (typically located in c:\Users\username\AppData\Local\VirtualStore). If it does this you must delete it from there after you\'re done debugging).'
pat = re.compile(mod.replace('.', '/')+r'\.py[co]*')
name = mod.replace('.', '/') + os.path.splitext(path)[-1]
update(zipfile, [pat], [path], [name])
@ -47,6 +47,29 @@ def update_module(mod, path):
else:
raise ValueError('Updating modules is not supported on this platform.')
def migrate(old, new):
from calibre.utils.config import prefs
from calibre.library.database import LibraryDatabase
from calibre.library.database2 import LibraryDatabase2
from calibre.utils.terminfo import ProgressBar
from calibre import terminal_controller
class Dummy(ProgressBar):
def setLabelText(self, x): pass
def setAutoReset(self, y): pass
def reset(self): pass
def setRange(self, min, max):
self.min = min
self.max = max
def setValue(self, val):
self.update(float(val)/getattr(self, 'max', 1))
db = LibraryDatabase(old)
db2 = LibraryDatabase2(new)
db2.migrate_old(db, Dummy(terminal_controller, 'Migrating database...'))
prefs['library_path'] = os.path.abspath(new)
print 'Database migrated to', os.path.abspath(new)
def main(args=sys.argv):
opts, args = option_parser().parse_args(args)
if opts.update_module:
@ -55,6 +78,11 @@ def main(args=sys.argv):
elif opts.command:
sys.argv = args[:1]
exec opts.command
elif opts.migrate:
if len(args) < 3:
print 'You must specify the path to library1.db and the path to the new library folder'
return 1
migrate(args[1], args[2])
else:
from IPython.Shell import IPShellEmbed
ipshell = IPShellEmbed()

View File

@ -258,7 +258,7 @@ class BookList(_BookList):
if book is not None:
self.remove_book(name)
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name[name.rfind(".")+1:]]
mime = MIME_MAP[name[name.rfind(".")+1:].lower()]
cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = {

View File

@ -13,7 +13,7 @@ from calibre.devices.interface import BookList as _BookList
from calibre.devices import strftime as _strftime
from calibre.devices import strptime
strftime = functools.partial(_strftime, zone=time.localtime)
strftime = functools.partial(_strftime, zone=time.gmtime)
MIME_MAP = {
"lrf" : "application/x-sony-bbeb",
@ -184,7 +184,7 @@ class BookList(_BookList):
self.remove_book(name)
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name.rpartition('.')[-1]]
mime = MIME_MAP[name.rpartition('.')[-1].lower()]
cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = {
@ -277,9 +277,12 @@ class BookList(_BookList):
def purge_empty_playlists(self):
''' Remove all playlists that have no children. Also removes any invalid playlist items.'''
for pli in self.playlist_items():
if not self.is_id_valid(pli.getAttribute('id')):
pli.parentNode.removeChild(pli)
pli.unlink()
try:
if not self.is_id_valid(pli.getAttribute('id')):
pli.parentNode.removeChild(pli)
pli.unlink()
except:
continue
for pl in self.playlists():
empty = True
for c in pl.childNodes:

View File

@ -31,7 +31,7 @@ class PRS505(Device):
PRODUCT_ID = 0x031e #: Product Id for the PRS-505
PRODUCT_NAME = 'PRS-505'
VENDOR_NAME = 'SONY'
FORMATS = ["lrf", 'epub', "rtf", "pdf", "txt"]
FORMATS = ['lrf', 'epub', "rtf", "pdf", "txt"]
MEDIA_XML = 'database/cache/media.xml'
CACHE_XML = 'Sony Reader/database/cache.xml'
@ -39,9 +39,7 @@ class PRS505(Device):
MAIN_MEMORY_VOLUME_LABEL = 'Sony Reader Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Sony Reader Storage Card'
OSX_MAIN_NAME = 'Sony PRS-505/UC Media'
OSX_SD_NAME = 'Sony PRS-505/UC:SD Media'
OSX_MS_NAME = 'Sony PRS-505/UC:MS Media'
OSX_NAME = 'Sony PRS-505'
CARD_PATH_PREFIX = __appname__
@ -101,29 +99,42 @@ class PRS505(Device):
return True
return False
@classmethod
def get_osx_mountpoints(cls, raw=None):
if raw is None:
raw = subprocess.Popen('ioreg -w 0 -S -c IOMedia'.split(),
stdout=subprocess.PIPE).stdout.read()
lines = raw.splitlines()
names = {}
for i, line in enumerate(lines):
if line.strip().endswith('<class IOMedia>') and cls.OSX_NAME in line:
loc = 'stick' if ':MS' in line else 'card' if ':SD' in line else 'main'
for line in lines[i+1:]:
line = line.strip()
if line.endswith('}'):
break
match = re.search(r'"BSD Name"\s+=\s+"(.*?)"', line)
if match is not None:
names[loc] = match.group(1)
break
if len(names.keys()) == 3:
break
return names
def open_osx(self):
mount = subprocess.Popen('mount', shell=True,
stdout=subprocess.PIPE).stdout.read()
src = subprocess.Popen('ioreg -n "%s"'%(self.OSX_MAIN_NAME,),
shell=True, stdout=subprocess.PIPE).stdout.read()
try:
devname = re.search(r'BSD Name.*=\s+"(\S+)"', src).group(1)
self._main_prefix = re.search('/dev/%s(\w*)\s+on\s+([^\(]+)\s+'%(devname,), mount).group(2) + os.sep
except:
names = self.get_osx_mountpoints()
dev_pat = r'/dev/%s(\w*)\s+on\s+([^\(]+)\s+'
if 'main' not in names.keys():
raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__)
try:
src = subprocess.Popen('ioreg -n "%s"'%(self.OSX_SD_NAME,),
shell=True, stdout=subprocess.PIPE).stdout.read()
devname = re.search(r'BSD Name.*=\s+"(\S+)"', src).group(1)
except:
try:
src = subprocess.Popen('ioreg -n "%s"'%(self.OSX_MS_NAME,),
shell=True, stdout=subprocess.PIPE).stdout.read()
devname = re.search(r'BSD Name.*=\s+"(\S+)"', src).group(1)
except:
devname = None
if devname is not None:
self._card_prefix = re.search('/dev/%s(\w*)\s+on\s+([^\(]+)\s+'%(devname,), mount).group(2) + os.sep
main_pat = dev_pat%names['main']
self._main_prefix = re.search(main_pat, mount).group(2) + os.sep
card_pat = names['stick'] if 'stick' in names.keys() else names['card'] if 'card' in names.keys() else None
if card_pat is not None:
card_pat = dev_pat%card_pat
self._card_prefix = re.search(card_pat, mount).group(2) + os.sep
def open_windows_nowmi(self):
@ -280,8 +291,15 @@ class PRS505(Device):
if prefix is None:
return 0, 0
win32file = __import__('win32file', globals(), locals(), [], -1)
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
try:
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix[:-1])
except Exception, err:
if getattr(err, 'args', [None])[0] == 21: # Disk not ready
time.sleep(3)
sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \
win32file.GetDiskFreeSpace(prefix[:-1])
else: raise
mult = sectors_per_cluster * bytes_per_sector
return total_clusters * mult, free_clusters * mult
@ -452,10 +470,14 @@ class PRS505(Device):
def sync_booklists(self, booklists, end_session=True):
fix_ids(*booklists)
if not os.path.exists(self._main_prefix):
os.makedirs(self._main_prefix)
f = open(self._main_prefix + self.__class__.MEDIA_XML, 'wb')
booklists[0].write(f)
f.close()
if self._card_prefix is not None and hasattr(booklists[1], 'write'):
if not os.path.exists(self._card_prefix):
os.makedirs(self._card_prefix)
f = open(self._card_prefix + self.__class__.CACHE_XML, 'wb')
booklists[1].write(f)
f.close()

View File

@ -15,6 +15,10 @@ class ConversionError(Exception):
class UnknownFormatError(Exception):
pass
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'epub', 'pdf', 'prc', 'mobi', 'azw',
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz']
class DRMError(ValueError):
pass
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'epub', 'pdf', 'prc', 'mobi', 'azw',
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'oebzip',
'rb', 'imp', 'odt']

View File

@ -29,7 +29,13 @@ def detect(aBuf):
return u.result
# Added by Kovid
def xml_to_unicode(raw, verbose=False):
ENCODING_PATS = [
re.compile(r'<[^<>]+encoding=[\'"](.*?)[\'"][^<>]*>', re.IGNORECASE),
re.compile(r'<meta.*?content=[\'"].*?charset=([^\s\'"]+).*?[\'"].*?>', re.IGNORECASE)
]
ENTITY_PATTERN = re.compile(r'&(\S+?);')
def xml_to_unicode(raw, verbose=False, strip_encoding_pats=False, resolve_entities=False):
'''
Force conversion of byte string to unicode. Tries to look for XML/HTML
encoding declaration first, if not found uses the chardet library and
@ -41,11 +47,14 @@ def xml_to_unicode(raw, verbose=False):
return u'', encoding
if isinstance(raw, unicode):
return raw, encoding
match = re.compile(r'<[^<>]+encoding=[\'"](.*?)[\'"][^<>]*>', re.IGNORECASE).search(raw)
if match is None:
match = re.compile(r'<meta.*?content=[\'"].*?charset=([^\s\'"]+).*?[\'"]', re.IGNORECASE).search(raw)
if match is not None:
encoding = match.group(1)
for pat in ENCODING_PATS:
match = pat.search(raw)
if match:
encoding = match.group(1)
break
if strip_encoding_pats:
for pat in ENCODING_PATS:
raw = pat.sub('', raw)
if encoding is None:
try:
chardet = detect(raw)
@ -63,4 +72,17 @@ def xml_to_unicode(raw, verbose=False):
encoding = encoding.lower()
if CHARSET_ALIASES.has_key(encoding):
encoding = CHARSET_ALIASES[encoding]
return raw.decode(encoding, 'ignore'), encoding
if encoding == 'ascii':
encoding = 'utf-8'
try:
raw = raw.decode(encoding, 'replace')
except LookupError:
raw = raw.decode('utf-8', 'replace')
if resolve_entities:
from calibre import entity_to_unicode
from functools import partial
f = partial(entity_to_unicode, exceptions=['amp', 'apos', 'quot', 'lt', 'gt'])
raw = ENTITY_PATTERN.sub(f, raw)
return raw, encoding

View File

@ -6,3 +6,130 @@ __docformat__ = 'restructuredtext en'
'''
Conversion to EPUB.
'''
import sys, textwrap
from calibre.utils.config import Config, StringConfig
from calibre.utils.zipfile import ZipFile, ZIP_STORED
from calibre.ebooks.html import config as common_config, tostring
class DefaultProfile(object):
flow_size = sys.maxint
screen_size = None
remove_soft_hyphens = False
class PRS505(DefaultProfile):
flow_size = 300000
screen_size = (600, 775)
remove_soft_hyphens = True
PROFILES = {
'PRS505' : PRS505,
'None' : DefaultProfile,
}
def rules(stylesheets):
for s in stylesheets:
if hasattr(s, 'cssText'):
for r in s:
if r.type == r.STYLE_RULE:
yield r
def initialize_container(path_to_container, opf_name='metadata.opf'):
'''
Create an empty EPUB document, with a default skeleton.
'''
CONTAINER='''\
<?xml version="1.0"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="%s" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
'''%opf_name
zf = ZipFile(path_to_container, 'w')
zf.writestr('mimetype', 'application/epub+zip', compression=ZIP_STORED)
zf.writestr('META-INF/', '', 0700)
zf.writestr('META-INF/container.xml', CONTAINER)
return zf
def config(defaults=None):
desc = _('Options to control the conversion to EPUB')
if defaults is None:
c = Config('epub', desc)
else:
c = StringConfig(defaults, desc)
c.update(common_config())
c.remove_opt('output')
c.remove_opt('zip')
c.add_opt('output', ['-o', '--output'], default=None,
help=_('The output EPUB file. If not specified, it is derived from the input file name.'))
c.add_opt('profile', ['--profile'], default='PRS505', choices=list(PROFILES.keys()),
help=_('Profile of the target device this EPUB is meant for. Set to None to create a device independent EPUB. The profile is used for device specific restrictions on the EPUB. Choices are: ')+str(list(PROFILES.keys())))
c.add_opt('override_css', ['--override-css'], default=None,
help=_('Either the path to a CSS stylesheet or raw CSS. This CSS will override any existing CSS declarations in the source files.'))
structure = c.add_group('structure detection', _('Control auto-detection of document structure.'))
structure('chapter', ['--chapter'], default="//*[re:match(name(), 'h[1-2]') and re:test(., 'chapter|book|section|part', 'i')] | //*[@class = 'chapter']",
help=_('''\
An XPath expression to detect chapter titles. The default is to consider <h1> or
<h2> tags that contain the words "chapter","book","section" or "part" as chapter titles as
well as any tags that have class="chapter".
The expression used must evaluate to a list of elements. To disable chapter detection,
use the expression "/". See the XPath Tutorial in the calibre User Manual for further
help on using this feature.
''').replace('\n', ' '))
structure('chapter_mark', ['--chapter-mark'], choices=['pagebreak', 'rule', 'both', 'none'],
default='pagebreak', help=_('Specify how to mark detected chapters. A value of "pagebreak" will insert page breaks before chapters. A value of "rule" will insert a line before chapters. A value of "none" will disable chapter marking and a value of "both" will use both page breaks and lines to mark chapters.'))
structure('cover', ['--cover'], default=None,
help=_('Path to the cover to be used for this book'))
structure('prefer_metadata_cover', ['--prefer-metadata-cover'], default=False,
action='store_true',
help=_('Use the cover detected from the source file in preference to the specified cover.'))
toc = c.add_group('toc',
_('''\
Control the automatic generation of a Table of Contents. If an OPF file is detected
and it specifies a Table of Contents, then that will be used rather than trying
to auto-generate a Table of Contents.
''').replace('\n', ' '))
toc('max_toc_links', ['--max-toc-links'], default=50,
help=_('Maximum number of links to insert into the TOC. Set to 0 to disable. Default is: %default. Links are only added to the TOC if less than the --toc-threshold number of chapters were detected.'))
toc('no_chapters_in_toc', ['--no-chapters-in-toc'], default=False,
help=_("Don't add auto-detected chapters to the Table of Contents."))
toc('toc_threshold', ['--toc-threshold'], default=6,
help=_('If fewer than this number of chapters is detected, then links are added to the Table of Contents.'))
toc('level1_toc', ['--level1-toc'], default=None,
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level one. If this is specified, it takes precedence over other forms of auto-detection.'))
toc('level2_toc', ['--level2-toc'], default=None,
help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level two. Each entry is added under the previous level one entry.'))
toc('from_ncx', ['--from-ncx'], default=None,
help=_('Path to a .ncx file that contains the table of contents to use for this ebook. The NCX file should contain links relative to the directory it is placed in. See http://www.niso.org/workrooms/daisy/Z39-86-2005.html#NCX for an overview of the NCX format.'))
toc('use_auto_toc', ['--use-auto-toc'], default=False,
help=_('Normally, if the source file already has a Table of Contents, it is used in preference to the autodetected one. With this option, the autodetected one is always used.'))
layout = c.add_group('page layout', _('Control page layout'))
layout('margin_top', ['--margin-top'], default=5.0,
help=_('Set the top margin in pts. Default is %default'))
layout('margin_bottom', ['--margin-bottom'], default=5.0,
help=_('Set the bottom margin in pts. Default is %default'))
layout('margin_left', ['--margin-left'], default=5.0,
help=_('Set the left margin in pts. Default is %default'))
layout('margin_right', ['--margin-right'], default=5.0,
help=_('Set the right margin in pts. Default is %default'))
layout('base_font_size2', ['--base-font-size'], default=12.0,
help=_('The base font size in pts. Default is %defaultpt. Set to 0 to disable rescaling of fonts.'))
layout('remove_paragraph_spacing', ['--remove-paragraph-spacing'], default=True,
help=_('Remove spacing between paragraphs. Will not work if the source file forces inter-paragraph spacing.'))
c.add_opt('show_opf', ['--show-opf'], default=False, group='debug',
help=_('Print generated OPF file to stdout'))
c.add_opt('show_ncx', ['--show-ncx'], default=False, group='debug',
help=_('Print generated NCX file to stdout'))
c.add_opt('keep_intermediate', ['--keep-intermediate-files'], group='debug', default=False,
help=_('Keep intermediate files during processing by html2epub'))
c.add_opt('extract_to', ['--extract-to'], group='debug', default=None,
help=_('Extract the contents of the produced EPUB file to the specified directory.'))
return c

View File

@ -0,0 +1,300 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Font size rationalization. See :function:`relativize`.
'''
import logging, re, operator, functools, collections, unittest, copy, sys
from xml.dom import SyntaxErr
from lxml.cssselect import CSSSelector
from lxml import etree
from lxml.html import HtmlElement
from calibre.ebooks.html import fromstring
from calibre.ebooks.epub import rules
from cssutils import CSSParser
num = r'[-]?\d+|[-]?\d*\.\d+'
length = r'(?P<zero>0)|(?P<num>{num})(?P<unit>%|em|ex|px|in|cm|mm|pt|pc)'.replace('{num}', num)
absolute_size = r'(?P<abs>(x?x-)?(small|large)|medium)'
relative_size = r'(?P<rel>smaller|larger)'
font_size_pat = re.compile('|'.join((relative_size, absolute_size, length)), re.I)
line_height_pat = re.compile(r'({num})(px|in|cm|mm|pt|pc)'.replace('{num}', num))
PTU = {
'in' : 72.,
'cm' : 72/2.54,
'mm' : 72/25.4,
'pt' : 1.0,
'pc' : 1/12.,
}
DEFAULT_FONT_SIZE = 12
class Rationalizer(object):
@classmethod
def specificity(cls, s):
'''Map CSS specificity tuple to a single integer'''
return sum([10**(4-i) + x for i,x in enumerate(s)])
@classmethod
def compute_font_size(cls, elem):
'''
Calculate the effective font size of an element traversing its ancestors as far as
neccessary.
'''
cfs = elem.computed_font_size
if cfs is not None:
return
sfs = elem.specified_font_size
if callable(sfs):
parent = elem.getparent()
cls.compute_font_size(parent)
elem.computed_font_size = sfs(parent.computed_font_size)
else:
elem.computed_font_size = sfs
@classmethod
def calculate_font_size(cls, style):
'Return font size in pts from style object. For relative units returns a callable'
match = font_size_pat.search(style.font)
fs = ''
if match:
fs = match.group()
if style.fontSize:
fs = style.fontSize
match = font_size_pat.search(fs)
if match is None:
return None
match = match.groupdict()
unit = match.get('unit', '')
if unit: unit = unit.lower()
if unit in PTU.keys():
return PTU[unit] * float(match['num'])
if unit in ('em', 'ex'):
return functools.partial(operator.mul, float(match['num']))
if unit == '%':
return functools.partial(operator.mul, float(match['num'])/100.)
abs = match.get('abs', '')
if abs: abs = abs.lower()
if abs:
x = (1.2)**(abs.count('x') * (-1 if 'small' in abs else 1))
return 12 * x
if match.get('zero', False):
return 0.
return functools.partial(operator.mul, 1.2) if 'larger' in fs.lower() else functools.partial(operator.mul, 0.8)
@classmethod
def resolve_rules(cls, stylesheets):
for sheet in stylesheets:
if hasattr(sheet, 'fs_rules'):
continue
sheet.fs_rules = []
sheet.lh_rules = []
for r in sheet:
if r.type == r.STYLE_RULE:
font_size = cls.calculate_font_size(r.style)
if font_size is not None:
for s in r.selectorList:
sheet.fs_rules.append([CSSSelector(s.selectorText), font_size])
orig = line_height_pat.search(r.style.lineHeight)
if orig is not None:
for s in r.selectorList:
sheet.lh_rules.append([CSSSelector(s.selectorText), float(orig.group(1)) * PTU[orig.group(2).lower()]])
@classmethod
def apply_font_size_rules(cls, stylesheets, root):
'Add a ``specified_font_size`` attribute to every element that has a specified font size'
cls.resolve_rules(stylesheets)
for sheet in stylesheets:
for selector, font_size in sheet.fs_rules:
elems = selector(root)
for elem in elems:
elem.specified_font_size = font_size
@classmethod
def remove_font_size_information(cls, stylesheets):
for r in rules(stylesheets):
r.style.removeProperty('font-size')
try:
new = font_size_pat.sub('', r.style.font).strip()
if new:
r.style.font = new
else:
r.style.removeProperty('font')
except SyntaxErr:
r.style.removeProperty('font')
if line_height_pat.search(r.style.lineHeight) is not None:
r.style.removeProperty('line-height')
@classmethod
def compute_font_sizes(cls, root, stylesheets, base=12):
stylesheets = [s for s in stylesheets if hasattr(s, 'cssText')]
cls.apply_font_size_rules(stylesheets, root)
# Compute the effective font size of all tags
root.computed_font_size = DEFAULT_FONT_SIZE
for elem in root.iter(etree.Element):
cls.compute_font_size(elem)
extra_css = {}
if base > 0:
# Calculate the "base" (i.e. most common) font size
font_sizes = collections.defaultdict(lambda : 0)
body = root.xpath('//body')[0]
IGNORE = ('h1', 'h2', 'h3', 'h4', 'h5', 'h6')
for elem in body.iter(etree.Element):
if elem.tag not in IGNORE:
t = getattr(elem, 'text', '')
if t: t = t.strip()
if t:
font_sizes[elem.computed_font_size] += len(t)
t = getattr(elem, 'tail', '')
if t: t = t.strip()
if t:
parent = elem.getparent()
if parent.tag not in IGNORE:
font_sizes[parent.computed_font_size] += len(t)
try:
most_common = max(font_sizes.items(), key=operator.itemgetter(1))[0]
scale = base/most_common if most_common > 0 else 1.
except ValueError:
scale = 1.
# rescale absolute line-heights
counter = 0
for sheet in stylesheets:
for selector, lh in sheet.lh_rules:
for elem in selector(root):
elem.set('id', elem.get('id', 'cfs_%d'%counter))
counter += 1
if not extra_css.has_key(elem.get('id')):
extra_css[elem.get('id')] = []
extra_css[elem.get('id')].append('line-height:%fpt'%(lh*scale))
# Rescale all computed font sizes
for elem in body.iter(etree.Element):
if isinstance(elem, HtmlElement):
elem.computed_font_size *= scale
# Remove all font size specifications from the last stylesheet
cls.remove_font_size_information(stylesheets[-1:])
# Create the CSS to implement the rescaled font sizes
for elem in body.iter(etree.Element):
cfs, pcfs = map(operator.attrgetter('computed_font_size'), (elem, elem.getparent()))
if abs(cfs-pcfs) > 1/12. and abs(pcfs) > 1/12.:
elem.set('id', elem.get('id', 'cfs_%d'%counter))
counter += 1
if not extra_css.has_key(elem.get('id')):
extra_css[elem.get('id')] = []
extra_css[elem.get('id')].append('font-size: %f%%'%(100*(cfs/pcfs)))
css = CSSParser(loglevel=logging.ERROR).parseString('')
for id, r in extra_css.items():
css.add('#%s {%s}'%(id, ';'.join(r)))
return css
@classmethod
def rationalize(cls, stylesheets, root, opts):
logger = logging.getLogger('html2epub')
logger.info('\t\tRationalizing fonts...')
extra_css = None
if opts.base_font_size2 > 0:
try:
extra_css = cls.compute_font_sizes(root, stylesheets, base=opts.base_font_size2)
except:
logger.warning('Failed to rationalize font sizes.')
if opts.verbose > 1:
logger.exception('')
finally:
root.remove_font_size_information()
logger.debug('\t\tDone rationalizing')
return extra_css
################################################################################
############## Testing
################################################################################
class FontTest(unittest.TestCase):
def setUp(self):
from calibre.ebooks.epub import config
self.opts = config(defaults='').parse()
self.html = '''
<html>
<head>
<title>Test document</title>
</head>
<body>
<div id="div1">
<!-- A comment -->
<p id="p1">Some <b>text</b></p>
</div>
<p id="p2">Some other <span class="it">text</span>.</p>
<p id="longest">The longest piece of single font size text in this entire file. Used to test resizing.</p>
</body>
</html>
'''
self.root = fromstring(self.html)
def do_test(self, css, base=DEFAULT_FONT_SIZE, scale=1):
root1 = copy.deepcopy(self.root)
root1.computed_font_size = DEFAULT_FONT_SIZE
stylesheet = CSSParser(loglevel=logging.ERROR).parseString(css)
stylesheet2 = Rationalizer.compute_font_sizes(root1, [stylesheet], base)
root2 = copy.deepcopy(root1)
root2.remove_font_size_information()
root2.computed_font_size = DEFAULT_FONT_SIZE
Rationalizer.apply_font_size_rules([stylesheet2], root2)
for elem in root2.iter(etree.Element):
Rationalizer.compute_font_size(elem)
for e1, e2 in zip(root1.xpath('//body')[0].iter(etree.Element), root2.xpath('//body')[0].iter(etree.Element)):
self.assertAlmostEqual(e1.computed_font_size, e2.computed_font_size,
msg='Computed font sizes for %s not equal. Original: %f Processed: %f'%\
(root1.getroottree().getpath(e1), e1.computed_font_size, e2.computed_font_size))
return stylesheet2.cssText
def testStripping(self):
'Test that any original entries are removed from the CSS'
css = 'p { font: bold 10px italic smaller; font-size: x-large} \na { font-size: 0 }'
css = CSSParser(loglevel=logging.ERROR).parseString(css)
Rationalizer.compute_font_sizes(copy.deepcopy(self.root), [css])
self.assertEqual(css.cssText.replace(' ', '').replace('\n', ''),
'p{font:bolditalic}')
def testIdentity(self):
'Test that no unnecessary font size changes are made'
extra_css = self.do_test('div {font-size:12pt} \nspan {font-size:100%}')
self.assertEqual(extra_css.strip(), '')
def testRelativization(self):
'Test conversion of absolute to relative sizes'
self.do_test('#p1 {font: 24pt} b {font: 12pt} .it {font: 48pt} #p2 {font: 100%}')
def testResizing(self):
'Test resizing of fonts'
self.do_test('#longest {font: 24pt} .it {font:20pt; line-height:22pt}')
def suite():
return unittest.TestLoader().loadTestsFromTestCase(FontTest)
def test():
unittest.TextTestRunner(verbosity=2).run(suite())
if __name__ == '__main__':
sys.exit(test())

View File

@ -0,0 +1,164 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Convert any ebook format to epub.
'''
import sys, os, re
from contextlib import nested
from calibre import extract, walk
from calibre.ebooks import DRMError
from calibre.ebooks.epub import config as common_config
from calibre.ebooks.epub.from_html import convert as html2epub, find_html_index
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.utils.zipfile import ZipFile
def lit2opf(path, tdir, opts):
from calibre.ebooks.lit.reader import LitReader
print 'Exploding LIT file:', path
reader = LitReader(path)
reader.extract_content(tdir, False)
for f in walk(tdir):
if f.lower().endswith('.opf'):
return f
def mobi2opf(path, tdir, opts):
from calibre.ebooks.mobi.reader import MobiReader
print 'Exploding MOBI file:', path
reader = MobiReader(path)
reader.extract_content(tdir)
files = list(walk(tdir))
opts.dont_preserve_structure = True
opts.encoding = 'utf-8'
for f in files:
if f.lower().endswith('.opf'):
return f
html_pat = re.compile(r'\.(x){0,1}htm(l){0,1}', re.IGNORECASE)
hf = [f for f in files if html_pat.match(os.path.splitext(f)[1]) is not None]
mi = MetaInformation(os.path.splitext(os.path.basename(path))[0], [_('Unknown')])
opf = OPFCreator(tdir, mi)
opf.create_manifest([(hf[0], None)])
opf.create_spine([hf[0]])
ans = os.path.join(tdir, 'metadata.opf')
opf.render(open(ans, 'wb'))
return ans
def fb22opf(path, tdir, opts):
from calibre.ebooks.lrf.fb2.convert_from import to_html
print 'Converting FB2 to HTML...'
return to_html(path, tdir)
def rtf2opf(path, tdir, opts):
from calibre.ebooks.lrf.rtf.convert_from import generate_html
generate_html(path, tdir)
return os.path.join(tdir, 'metadata.opf')
def txt2opf(path, tdir, opts):
from calibre.ebooks.lrf.txt.convert_from import generate_html
generate_html(path, opts.encoding, tdir)
return os.path.join(tdir, 'metadata.opf')
def pdf2opf(path, tdir, opts):
from calibre.ebooks.lrf.pdf.convert_from import generate_html
generate_html(path, tdir)
return os.path.join(tdir, 'metadata.opf')
def epub2opf(path, tdir, opts):
zf = ZipFile(path)
zf.extractall(tdir)
if os.path.exists(os.path.join(tdir, 'META-INF', 'encryption.xml')):
raise DRMError(os.path.basename(path))
for f in walk(tdir):
if f.lower().endswith('.opf'):
return f
raise ValueError('%s is not a valid EPUB file'%path)
def odt2epub(path, tdir, opts):
from calibre.ebooks.odt.to_oeb import Extract
opts.encoding = 'utf-8'
return Extract()(path, tdir)
MAP = {
'lit' : lit2opf,
'mobi' : mobi2opf,
'prc' : mobi2opf,
'fb2' : fb22opf,
'rtf' : rtf2opf,
'txt' : txt2opf,
'pdf' : pdf2opf,
'epub' : epub2opf,
'odt' : odt2epub,
}
SOURCE_FORMATS = ['lit', 'mobi', 'prc', 'fb2', 'odt', 'rtf', 'txt', 'pdf', 'rar', 'zip', 'oebzip', 'htm', 'html', 'epub']
def unarchive(path, tdir):
extract(path, tdir)
files = list(walk(tdir))
for ext in ['opf'] + list(MAP.keys()):
for f in files:
if f.lower().endswith('.'+ext):
if ext in ['txt', 'rtf'] and os.stat(f).st_size < 2048:
continue
return f, ext
return find_html_index(files)
def any2epub(opts, path, notification=None):
ext = os.path.splitext(path)[1]
if not ext:
raise ValueError('Unknown file type: '+path)
ext = ext.lower()[1:]
if opts.output is None:
opts.output = os.path.splitext(os.path.basename(path))[0]+'.epub'
with nested(TemporaryDirectory('_any2epub1'), TemporaryDirectory('_any2epub2')) as (tdir1, tdir2):
if ext in ['rar', 'zip', 'oebzip']:
path, ext = unarchive(path, tdir1)
print 'Found %s file in archive'%(ext.upper())
if ext in MAP.keys():
path = MAP[ext](path, tdir2, opts)
ext = 'opf'
if re.match(r'((x){0,1}htm(l){0,1})|opf', ext) is None:
raise ValueError('Conversion from %s is not supported'%ext.upper())
print 'Creating EPUB file...'
html2epub(path, opts, notification=notification)
def config(defaults=None):
return common_config(defaults=defaults)
def formats():
return ['html', 'rar', 'zip', 'oebzip']+list(MAP.keys())
def option_parser():
return config().option_parser(usage=_('''\
%%prog [options] filename
Convert any of a large number of ebook formats to an epub file. Supported formats are: %s
''')%formats()
)
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
parser.print_help()
print 'No input file specified.'
return 1
any2epub(opts, args[1])
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,21 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'Convert a comic in CBR/CBZ format to epub'
import sys
from functools import partial
from calibre.ebooks.lrf.comic.convert_from import do_convert, option_parser, config, main as _main
convert = partial(do_convert, output_format='epub')
main = partial(_main, output_format='epub')
if __name__ == '__main__':
sys.exit(main())
if False:
option_parser
config

View File

@ -0,0 +1,69 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Convert periodical content into EPUB ebooks.
'''
import sys, glob, os
from calibre.web.feeds.main import config as feeds2disk_config, USAGE, run_recipe
from calibre.ebooks.epub.from_html import config as html2epub_config
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.epub.from_html import convert as html2epub
from calibre import strftime, sanitize_file_name
def config(defaults=None):
c = feeds2disk_config(defaults=defaults)
c.remove('lrf')
c.remove('epub')
c.remove('output_dir')
c.update(html2epub_config(defaults=defaults))
c.remove('chapter_mark')
return c
def option_parser():
c = config()
return c.option_parser(usage=USAGE)
def convert(opts, recipe_arg, notification=None):
opts.lrf = False
opts.epub = True
if opts.debug:
opts.verbose = 2
parser = option_parser()
with TemporaryDirectory('_feeds2epub') as tdir:
opts.output_dir = tdir
recipe = run_recipe(opts, recipe_arg, parser, notification=notification)
c = config()
recipe_opts = c.parse_string(recipe.html2epub_options)
c.smart_update(recipe_opts, opts)
opts = recipe_opts
opts.chapter_mark = 'none'
opf = glob.glob(os.path.join(tdir, '*.opf'))
if not opf:
raise Exception('Downloading of recipe: %s failed'%recipe_arg)
opf = opf[0]
if opts.output is None:
fname = recipe.title + strftime(recipe.timefmt) + '.epub'
opts.output = os.path.join(os.getcwd(), sanitize_file_name(fname))
print 'Generating epub...'
opts.encoding = 'utf-8'
html2epub(opf, opts, notification=notification)
def main(args=sys.argv, notification=None, handler=None):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) != 2 and opts.feeds is None:
parser.print_help()
return 1
recipe_arg = args[1] if len(args) > 1 else None
convert(opts, recipe_arg, notification=notification)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,314 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Conversion of HTML/OPF files follows several stages:
* All links in the HTML files or in the OPF manifest are
followed to build up a list of HTML files to be converted.
This stage is implemented by
:function:`calibre.ebooks.html.traverse` and
:class:`calibre.ebooks.html.HTMLFile`.
* The HTML is pre-processed to make it more semantic.
All links in the HTML files to other resources like images,
stylesheets, etc. are relativized. The resources are copied
into the `resources` sub directory. This is accomplished by
:class:`calibre.ebooks.html.PreProcessor` and
:class:`calibre.ebooks.html.Parser`.
* The HTML is processed. Various operations are performed.
All style declarations are extracted and consolidated into
a single style sheet. Chapters are auto-detected and marked.
Various font related manipulations are performed. See
:class:`HTMLProcessor`.
* The processed HTML is saved and the
:module:`calibre.ebooks.epub.split` module is used to split up
large HTML files into smaller chunks.
* The EPUB container is created.
'''
import os, sys, cStringIO, logging, re
from lxml.etree import XPath
try:
from PIL import Image as PILImage
except ImportError:
import Image as PILImage
from calibre.ebooks.html import Processor, merge_metadata, get_filelist,\
opf_traverse, create_metadata, rebase_toc
from calibre.ebooks.epub import config as common_config
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.toc import TOC
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ebooks.epub import initialize_container, PROFILES
from calibre.ebooks.epub.split import split
from calibre.ebooks.epub.fonts import Rationalizer
from calibre.constants import preferred_encoding
from calibre import walk
def find_html_index(files):
'''
Given a list of files, find the most likely root HTML file in the
list.
'''
html_pat = re.compile(r'\.(x){0,1}htm(l){0,1}$', re.IGNORECASE)
html_files = [f for f in files if html_pat.search(f) is not None]
if not html_files:
raise ValueError(_('Could not find an ebook inside the archive'))
html_files = [(f, os.stat(f).st_size) for f in html_files]
html_files.sort(cmp = lambda x, y: cmp(x[1], y[1]))
html_files = [f[0] for f in html_files]
for q in ('toc', 'index'):
for f in html_files:
if os.path.splitext(f)[0].lower() == q:
return f, os.path.splitext(f)[1].lower()[1:]
return html_files[-1], os.path.splitext(html_files[-1])[1].lower()[1:]
class HTMLProcessor(Processor, Rationalizer):
def __init__(self, htmlfile, opts, tdir, resource_map, htmlfiles, stylesheets):
Processor.__init__(self, htmlfile, opts, tdir, resource_map, htmlfiles,
name='html2epub')
if opts.verbose > 2:
self.debug_tree('parsed')
self.detect_chapters()
self.extract_css(stylesheets)
if self.opts.base_font_size2 > 0:
self.font_css = self.rationalize(self.external_stylesheets+[self.stylesheet],
self.root, self.opts)
if opts.verbose > 2:
self.debug_tree('nocss')
def save(self):
for meta in list(self.root.xpath('//meta')):
meta.getparent().remove(meta)
Processor.save(self)
def config(defaults=None):
return common_config(defaults=defaults)
def option_parser():
c = config()
return c.option_parser(usage=_('''\
%prog [options] file.html|opf
Convert a HTML file to an EPUB ebook. Recursively follows links in the HTML file.
If you specify an OPF file instead of an HTML file, the list of links is takes from
the <spine> element of the OPF file.
'''))
def parse_content(filelist, opts, tdir):
os.makedirs(os.path.join(tdir, 'content', 'resources'))
resource_map, stylesheets = {}, {}
toc = TOC(base_path=tdir, type='root')
stylesheet_map = {}
for htmlfile in filelist:
logging.getLogger('html2epub').debug('Processing %s...'%htmlfile)
hp = HTMLProcessor(htmlfile, opts, os.path.join(tdir, 'content'),
resource_map, filelist, stylesheets)
hp.populate_toc(toc)
hp.save()
stylesheet_map[os.path.basename(hp.save_path())] = \
[s for s in hp.external_stylesheets + [hp.stylesheet, hp.font_css, hp.override_css] if s is not None]
logging.getLogger('html2epub').debug('Saving stylesheets...')
if opts.base_font_size2 > 0:
Rationalizer.remove_font_size_information(stylesheets.values())
for path, css in stylesheets.items():
open(path, 'wb').write(getattr(css, 'cssText', css).encode('utf-8'))
if toc.count('chapter') > opts.toc_threshold:
toc.purge(['file', 'link', 'unknown'])
if toc.count('chapter') + toc.count('file') > opts.toc_threshold:
toc.purge(['link', 'unknown'])
toc.purge(['link'], max=opts.max_toc_links)
return resource_map, hp.htmlfile_map, toc, stylesheet_map
def resize_cover(im, opts):
width, height = im.size
dw, dh = (opts.profile.screen_size[0]-width)/float(width), (opts.profile.screen_size[1]-height)/float(height)
delta = min(dw, dh)
if delta > 0:
nwidth = int(width + delta*(width))
nheight = int(height + delta*(height))
im = im.resize((int(nwidth), int(nheight)), PILImage.ANTIALIAS).convert('RGB')
return im
def process_title_page(mi, filelist, htmlfilemap, opts, tdir):
old_title_page = None
f = lambda x : os.path.normcase(os.path.normpath(x))
if mi.cover:
if f(filelist[0].path) == f(mi.cover):
old_title_page = htmlfilemap[filelist[0].path]
#logger = logging.getLogger('html2epub')
metadata_cover = mi.cover
if metadata_cover and not os.path.exists(metadata_cover):
metadata_cover = None
if metadata_cover is not None:
with open(metadata_cover, 'rb') as src:
try:
im = PILImage.open(src)
if opts.profile.screen_size is not None:
im = resize_cover(im, opts)
metadata_cover = im
except:
metadata_cover = None
specified_cover = opts.cover
if specified_cover and not os.path.exists(specified_cover):
specified_cover = None
if specified_cover is not None:
with open(specified_cover, 'rb') as src:
try:
im = PILImage.open(src)
if opts.profile.screen_size is not None:
im = resize_cover(im, opts)
specified_cover = im
except:
specified_cover = None
cover = metadata_cover if specified_cover is None or (opts.prefer_metadata_cover and metadata_cover is not None) else specified_cover
if hasattr(cover, 'save'):
cpath = '/'.join(('resources', '_cover_.jpg'))
cover_dest = os.path.join(tdir, 'content', *cpath.split('/'))
with open(cover_dest, 'wb') as f:
im.save(f, format='jpeg')
titlepage = '''\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title>Cover</title>
<style type="text/css">@page {padding: 0pt; margin:0pt}</style>
</head>
<body style="padding: 0pt; margin: 0pt">
<div style="text-align:center">
<img style="text-align: center" src="%s" alt="cover" />
</div>
</body>
</html>
'''%cpath
tp = 'calibre_title_page.html' if old_title_page is None else old_title_page
tppath = os.path.join(tdir, 'content', tp)
with open(tppath, 'wb') as f:
f.write(titlepage)
return tp if old_title_page is None else None, True
return None, old_title_page is not None
def convert(htmlfile, opts, notification=None):
htmlfile = os.path.abspath(htmlfile)
if opts.output is None:
opts.output = os.path.splitext(os.path.basename(htmlfile))[0] + '.epub'
opts.profile = PROFILES[opts.profile]
opts.output = os.path.abspath(opts.output)
if opts.override_css is not None:
try:
opts.override_css = open(opts.override_css, 'rb').read().decode(preferred_encoding, 'replace')
except:
opts.override_css = opts.override_css.decode(preferred_encoding, 'replace')
if htmlfile.lower().endswith('.opf'):
opf = OPF(htmlfile, os.path.dirname(os.path.abspath(htmlfile)))
filelist = opf_traverse(opf, verbose=opts.verbose, encoding=opts.encoding)
if not filelist:
# Bad OPF look for a HTML file instead
htmlfile = find_html_index(walk(os.path.dirname(htmlfile)))[0]
if htmlfile is None:
raise ValueError('Could not find suitable file to convert.')
filelist = get_filelist(htmlfile, opts)[1]
mi = MetaInformation(opf)
else:
opf, filelist = get_filelist(htmlfile, opts)
mi = merge_metadata(htmlfile, opf, opts)
opts.chapter = XPath(opts.chapter,
namespaces={'re':'http://exslt.org/regular-expressions'})
if opts.level1_toc:
opts.level1_toc = XPath(opts.level1_toc,
namespaces={'re':'http://exslt.org/regular-expressions'})
else:
opts.level1_toc = None
if opts.level2_toc:
opts.level2_toc = XPath(opts.level2_toc,
namespaces={'re':'http://exslt.org/regular-expressions'})
else:
opts.level2_toc = None
with TemporaryDirectory(suffix='_html2epub', keep=opts.keep_intermediate) as tdir:
if opts.keep_intermediate:
print 'Intermediate files in', tdir
resource_map, htmlfile_map, generated_toc, stylesheet_map = \
parse_content(filelist, opts, tdir)
logger = logging.getLogger('html2epub')
resources = [os.path.join(tdir, 'content', f) for f in resource_map.values()]
title_page, has_title_page = process_title_page(mi, filelist, htmlfile_map, opts, tdir)
spine = [htmlfile_map[f.path] for f in filelist]
if title_page is not None:
spine = [title_page] + spine
mi.cover = None
mi.cover_data = (None, None)
mi = create_metadata(tdir, mi, spine, resources)
buf = cStringIO.StringIO()
if mi.toc:
rebase_toc(mi.toc, htmlfile_map, tdir)
if opts.use_auto_toc or mi.toc is None or len(list(mi.toc.flat())) < 2:
mi.toc = generated_toc
if opts.from_ncx:
toc = TOC()
toc.read_ncx_toc(opts.from_ncx)
mi.toc = toc
for item in mi.manifest:
if getattr(item, 'mime_type', None) == 'text/html':
item.mime_type = 'application/xhtml+xml'
opf_path = os.path.join(tdir, 'metadata.opf')
with open(opf_path, 'wb') as f:
mi.render(f, buf, 'toc.ncx')
toc = buf.getvalue()
if toc:
with open(os.path.join(tdir, 'toc.ncx'), 'wb') as f:
f.write(toc)
if opts.show_ncx:
print toc
split(opf_path, opts, stylesheet_map)
opf = OPF(opf_path, tdir)
opf.remove_guide()
if has_title_page:
opf.create_guide_element()
opf.add_guide_item('cover', 'Cover', 'content/'+spine[0])
with open(opf_path, 'wb') as f:
f.write(opf.render())
epub = initialize_container(opts.output)
epub.add_dir(tdir)
if opts.show_opf:
print open(os.path.join(tdir, 'metadata.opf')).read()
logger.info('Output written to %s'%opts.output)
if opts.extract_to is not None:
epub.extractall(opts.extract_to)
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
parser.print_help()
print _('You must specify an input HTML file')
return 1
convert(args[1], opts)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,405 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Split the flows in an epub file to conform to size limitations.
'''
import os, math, logging, functools, collections, re, copy
from lxml.etree import XPath as _XPath
from lxml import etree, html
from lxml.cssselect import CSSSelector
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ebooks.epub import tostring, rules
from calibre import CurrentDir, LoggingInterface
XPath = functools.partial(_XPath, namespaces={'re':'http://exslt.org/regular-expressions'})
content = functools.partial(os.path.join, 'content')
SPLIT_ATTR = 'cs'
SPLIT_POINT_ATTR = 'csp'
class SplitError(ValueError):
def __init__(self, path, root):
size = len(tostring(root))/1024.
ValueError.__init__(self, _('Could not find reasonable point at which to split: %s Sub-tree size: %d KB')%
(os.path.basename(path), size))
class Splitter(LoggingInterface):
def __init__(self, path, opts, stylesheet_map, always_remove=False):
LoggingInterface.__init__(self, logging.getLogger('htmlsplit'))
self.setup_cli_handler(opts.verbose)
self.path = path
self.always_remove = always_remove
self.base = (os.path.splitext(path)[0].replace('%', '%%') + '_split_%d.html')
self.opts = opts
self.orig_size = os.stat(content(path)).st_size
self.log_info('\tSplitting %s (%d KB)', path, self.orig_size/1024.)
root = html.fromstring(open(content(path)).read())
self.page_breaks = []
self.find_page_breaks(stylesheet_map[self.path], root)
self.trees = []
self.split_size = 0
self.split(root.getroottree())
self.commit()
self.log_info('\t\tSplit into %d parts.', len(self.trees))
if self.opts.verbose:
for f in self.files:
self.log_info('\t\t\t%s - %d KB', f, os.stat(content(f)).st_size/1024.)
self.trees = None
def split_text(self, text, root, size):
self.log_debug('\t\t\tSplitting text of length: %d'%len(text))
rest = text.replace('\r', '')
parts = re.split('\n\n', rest)
self.log_debug('\t\t\t\tFound %d parts'%len(parts))
if max(map(len, parts)) > size:
raise SplitError('Cannot split as file contains a <pre> tag with a very large paragraph', root)
ans = []
buf = ''
for part in parts:
if len(buf) + len(part) < size:
buf += '\n\n'+part
else:
ans.append(buf)
buf = part
return ans
def split(self, tree):
'''
Split ``tree`` into a *before* and *after* tree, preserving tag structure,
but not duplicating any text. All tags that have had their text and tail
removed have the attribute ``calibre_split`` set to 1.
'''
self.log_debug('\t\tSplitting...')
root = tree.getroot()
# Split large <pre> tags
for pre in list(root.xpath('//pre')):
text = u''.join(pre.xpath('./text()'))
pre.text = text
for child in list(pre.iterdescendants()):
pre.remove(child)
if len(pre.text) > self.opts.profile.flow_size*0.5:
frags = self.split_text(pre.text, root, int(0.2*self.opts.profile.flow_size))
new_pres = []
for frag in frags:
pre2 = copy.copy(pre)
pre2.text = frag
pre2.tail = u''
new_pres.append(pre2)
new_pres[-1].tail = pre.tail
p = pre.getparent()
i = p.index(pre)
p[i:i+1] = new_pres
split_point, before = self.find_split_point(root)
if split_point is None or self.split_size > 6*self.orig_size:
if not self.always_remove:
self.log_warn(_('\t\tToo much markup. Re-splitting without structure preservation. This may cause incorrect rendering.'))
raise SplitError(self.path, root)
tree2 = copy.deepcopy(tree)
root2 = tree2.getroot()
body, body2 = root.body, root2.body
path = tree.getpath(split_point)
split_point2 = root2.xpath(path)[0]
def nix_element(elem, top=True):
if self.always_remove:
parent = elem.getparent()
index = parent.index(elem)
if top:
parent.remove(elem)
else:
index = parent.index(elem)
parent[index:index+1] = list(elem.iterchildren())
else:
elem.text = u''
elem.tail = u''
elem.set(SPLIT_ATTR, '1')
if elem.tag.lower() in ['ul', 'ol', 'dl', 'table', 'hr', 'img']:
elem.set('style', 'display:none;')
def fix_split_point(sp):
sp.set('style', sp.get('style', '')+'page-break-before:avoid;page-break-after:avoid')
# Tree 1
hit_split_point = False
for elem in list(body.iterdescendants(etree.Element)):
if elem.get(SPLIT_ATTR, '0') == '1':
continue
if elem is split_point:
hit_split_point = True
if before:
nix_element(elem)
fix_split_point(elem)
continue
if hit_split_point:
nix_element(elem)
# Tree 2
hit_split_point = False
for elem in list(body2.iterdescendants(etree.Element)):
if elem.get(SPLIT_ATTR, '0') == '1':
continue
if elem is split_point2:
hit_split_point = True
if not before:
nix_element(elem, top=False)
fix_split_point(elem)
continue
if not hit_split_point:
nix_element(elem, top=False)
for t, r in [(tree, root), (tree2, root2)]:
size = len(tostring(r))
if size <= self.opts.profile.flow_size:
self.trees.append(t)
#print tostring(t.getroot(), pretty_print=True)
self.log_debug('\t\t\tCommitted sub-tree #%d (%d KB)', len(self.trees), size/1024.)
self.split_size += size
else:
self.split(t)
def find_page_breaks(self, stylesheets, root):
'''
Find all elements that have either page-break-before or page-break-after set.
'''
page_break_selectors = set([])
for rule in rules(stylesheets):
before = getattr(rule.style.getPropertyCSSValue('page-break-before'), 'cssText', '').strip().lower()
after = getattr(rule.style.getPropertyCSSValue('page-break-after'), 'cssText', '').strip().lower()
try:
if before and before != 'avoid':
page_break_selectors.add((CSSSelector(rule.selectorText), True))
except:
pass
try:
if after and after != 'avoid':
page_break_selectors.add((CSSSelector(rule.selectorText), False))
except:
pass
page_breaks = set([])
for selector, before in page_break_selectors:
for elem in selector(root):
elem.pb_before = before
page_breaks.add(elem)
for i, elem in enumerate(root.iter()):
elem.pb_order = i
page_breaks = list(page_breaks)
page_breaks.sort(cmp=lambda x,y : cmp(x.pb_order, y.pb_order))
for i, x in enumerate(page_breaks):
x.set('id', x.get('id', 'calibre_pb_%d'%i))
self.page_breaks.append((XPath('//*[@id="%s"]'%x.get('id')), x.pb_before))
def find_split_point(self, root):
'''
Find the tag at which to split the tree rooted at `root`.
Search order is:
* page breaks
* Heading tags
* <div> tags
* <p> tags
We try to split in the "middle" of the file (as defined by tag counts.
'''
def pick_elem(elems):
if elems:
elems = [i for i in elems if i.get(SPLIT_POINT_ATTR, '0') != '1'\
and i.get(SPLIT_ATTR, '0') != '1']
if elems:
i = int(math.floor(len(elems)/2.))
elems[i].set(SPLIT_POINT_ATTR, '1')
return elems[i]
page_breaks = []
for x in self.page_breaks:
pb = x[0](root)
if pb:
page_breaks.append(pb[0])
elem = pick_elem(page_breaks)
if elem is not None:
i = page_breaks.index(elem)
return elem, self.page_breaks[i][1]
for path in (
'//*[re:match(name(), "h[1-6]", "i")]',
'/html/body/div',
'//pre',
'//hr',
'//p',
'//br',
):
elems = root.xpath(path)
elem = pick_elem(elems)
if elem is not None:
try:
XPath(elem.getroottree().getpath(elem))
except:
continue
return elem, True
return None, True
def commit(self):
'''
Commit all changes caused by the split. This removes the previously
introduced ``calibre_split`` attribute and calculates an *anchor_map* for
all anchors in the original tree. Internal links are re-directed. The
original file is deleted and the split files are saved.
'''
self.anchor_map = collections.defaultdict(lambda :self.base%0)
self.files = []
for i, tree in enumerate(self.trees):
root = tree.getroot()
self.files.append(self.base%i)
for elem in root.xpath('//*[@id]'):
if elem.get(SPLIT_ATTR, '0') == '0':
self.anchor_map[elem.get('id')] = self.files[-1]
for elem in root.xpath('//*[@%s or @%s]'%(SPLIT_ATTR, SPLIT_POINT_ATTR)):
elem.attrib.pop(SPLIT_ATTR, None)
elem.attrib.pop(SPLIT_POINT_ATTR, '0')
for current, tree in zip(self.files, self.trees):
for a in tree.getroot().xpath('//a[@href]'):
href = a.get('href').strip()
if href.startswith('#'):
anchor = href[1:]
file = self.anchor_map[anchor]
if file != current:
a.set('href', file+href)
open(content(current), 'wb').\
write(tostring(tree.getroot(), pretty_print=self.opts.pretty_print))
os.remove(content(self.path))
def fix_opf(self, opf):
'''
Fix references to the split file in the OPF.
'''
items = [item for item in opf.itermanifest() if item.get('href') == 'content/'+self.path]
new_items = [('content/'+f, None) for f in self.files]
id_map = {}
for item in items:
id_map[item.get('id')] = opf.replace_manifest_item(item, new_items)
for id in id_map.keys():
opf.replace_spine_items_by_idref(id, id_map[id])
for ref in opf.iterguide():
href = ref.get('href', '')
if href.startswith('content/'+self.path):
href = href.split('#')
frag = None
if len(href) > 1:
frag = href[1]
new_file = self.anchor_map[frag]
ref.set('href', 'content/'+new_file+('' if frag is None else ('#'+frag)))
def fix_content_links(html_files, changes, opts):
split_files = [f.path for f in changes]
anchor_maps = [f.anchor_map for f in changes]
files = list(html_files)
for j, f in enumerate(split_files):
try:
i = files.index(f)
files[i:i+1] = changes[j].files
except ValueError:
continue
for htmlfile in files:
changed = False
root = html.fromstring(open(content(htmlfile), 'rb').read())
for a in root.xpath('//a[@href]'):
href = a.get('href')
if not href.startswith('#'):
href = href.split('#')
anchor = href[1] if len(href) > 1 else None
href = href[0]
if href in split_files:
newf = anchor_maps[split_files.index(href)][anchor]
frag = ('#'+anchor) if anchor else ''
a.set('href', newf+frag)
changed = True
if changed:
open(content(htmlfile), 'wb').write(tostring(root, pretty_print=opts.pretty_print))
def fix_ncx(path, changes):
split_files = [f.path for f in changes]
anchor_maps = [f.anchor_map for f in changes]
tree = etree.parse(path)
changed = False
for content in tree.getroot().xpath('//x:content[@src]', namespaces={'x':"http://www.daisy.org/z3986/2005/ncx/"}):
href = content.get('src')
if not href.startswith('#'):
href = href.split('#')
anchor = href[1] if len(href) > 1 else None
href = href[0].split('/')[-1]
if href in split_files:
newf = anchor_maps[split_files.index(href)][anchor]
frag = ('#'+anchor) if anchor else ''
content.set('src', 'content/'+newf+frag)
changed = True
if changed:
open(path, 'wb').write(etree.tostring(tree.getroot(), encoding='UTF-8', xml_declaration=True))
def split(pathtoopf, opts, stylesheet_map):
pathtoopf = os.path.abspath(pathtoopf)
with CurrentDir(os.path.dirname(pathtoopf)):
opf = OPF(open(pathtoopf, 'rb'), os.path.dirname(pathtoopf))
html_files = []
for item in opf.itermanifest():
if 'html' in item.get('media-type', '').lower():
f = item.get('href').split('/')[-1]
f2 = f.replace('&', '%26')
if not os.path.exists(content(f)) and os.path.exists(content(f2)):
f = f2
item.set('href', item.get('href').replace('&', '%26'))
html_files.append(f)
changes = []
always_remove = getattr(opts, 'dont_preserve_structure', False)
for f in html_files:
if os.stat(content(f)).st_size > opts.profile.flow_size:
try:
changes.append(Splitter(f, opts, stylesheet_map,
always_remove=(always_remove or \
os.stat(content(f)).st_size > 5*opts.profile.flow_size)))
except (SplitError, RuntimeError):
if not always_remove:
changes.append(Splitter(f, opts, stylesheet_map, always_remove=True))
else:
raise
changes[-1].fix_opf(opf)
open(pathtoopf, 'wb').write(opf.render())
fix_content_links(html_files, changes, opts)
for item in opf.itermanifest():
if item.get('media-type', '') == 'application/x-dtbncx+xml':
fix_ncx(item.get('href'), changes)
break

View File

@ -1,199 +0,0 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Recursively parse HTML files to find all linked files. See :function:`traverse`.
'''
import sys, os, re
from urlparse import urlparse
from urllib import unquote
from calibre import unicode_path
from calibre.ebooks.chardet import xml_to_unicode
class Link(object):
'''
Represents a link in a HTML file.
'''
@classmethod
def url_to_local_path(cls, url, base):
path = url.path
if os.path.isabs(path):
return path
return os.path.abspath(os.path.join(base, path))
def __init__(self, url, base):
'''
:param url: The url this link points to. Must be an unquoted unicode string.
:param base: The base directory that relative URLs are with respect to.
Must be a unicode string.
'''
assert isinstance(url, unicode) and isinstance(base, unicode)
self.url = url
self.parsed_url = urlparse(unquote(self.url))
self.is_local = self.parsed_url.scheme in ('', 'file')
self.is_internal = self.is_local and not bool(self.parsed_url.path)
self.path = None
self.fragment = self.parsed_url.fragment
if self.is_local and not self.is_internal:
self.path = self.url_to_local_path(self.parsed_url, base)
def __hash__(self):
if self.path is None:
return hash(self.url)
return hash(self.path)
def __eq__(self, other):
return self.path == getattr(other, 'path', other)
def __str__(self):
return u'Link: %s --> %s'%(self.url, self.path)
class IgnoreFile(Exception):
def __init__(self, msg, errno):
Exception.__init__(self, msg)
self.doesnt_exist = errno == 2
self.errno = errno
class HTMLFile(object):
'''
Contains basic information about an HTML file. This
includes a list of links to other files as well as
the encoding of each file. Also tries to detect if the file is not a HTML
file in which case :member:`is_binary` is set to True.
The encoding of the file is available as :member:`encoding`.
'''
HTML_PAT = re.compile(r'<\s*html', re.IGNORECASE)
LINK_PAT = re.compile(
r'<\s*a\s+.*?href\s*=\s*(?:(?:"(?P<url1>[^"]+)")|(?:\'(?P<url2>[^\']+)\')|(?P<url3>[^\s]+))',
re.DOTALL|re.IGNORECASE)
def __init__(self, path_to_html_file, level, encoding, verbose):
'''
:param level: The level of this file. Should be 0 for the root file.
:param encoding: Use `encoding` to decode HTML.
'''
self.path = unicode_path(path_to_html_file, abs=True)
self.base = os.path.dirname(self.path)
self.level = level
self.links = []
try:
with open(self.path, 'rb') as f:
src = f.read()
except IOError, err:
msg = 'Could not read from file: %s with error: %s'%(self.path, unicode(err))
if level == 0:
raise IOError(msg)
raise IgnoreFile(msg, err.errno)
self.is_binary = not bool(self.HTML_PAT.search(src[:1024]))
if not self.is_binary:
if encoding is None:
encoding = xml_to_unicode(src[:4096], verbose=verbose)[-1]
self.encoding = encoding
src = src.decode(encoding, 'replace')
self.find_links(src)
def __eq__(self, other):
return self.path == getattr(other, 'path', other)
def __str__(self):
return u'HTMLFile:%d:%s:%s'%(self.level, 'b' if self.is_binary else 'a', self.path)
def __repr__(self):
return str(self)
def find_links(self, src):
for match in self.LINK_PAT.finditer(src):
url = None
for i in ('url1', 'url2', 'url3'):
url = match.group(i)
if url:
break
link = Link(url, self.base)
if link not in self.links:
self.links.append(link)
def depth_first(root, flat, visited=set([])):
yield root
visited.add(root)
for link in root.links:
if link.path is not None and link not in visited:
try:
index = flat.index(link)
except ValueError: # Can happen if max_levels is used
continue
hf = flat[index]
if hf not in visited:
yield hf
visited.add(hf)
for hf in depth_first(hf, flat, visited):
if hf not in visited:
yield hf
visited.add(hf)
def traverse(path_to_html_file, max_levels=sys.maxint, verbose=0, encoding=None):
'''
Recursively traverse all links in the HTML file.
:param max_levels: Maximum levels of recursion. Must be non-negative. 0
implies that no links in hte root HTML file are followed.
:param encoding: Specify character encoding of HTML files. If `None` it is
auto-detected.
:return: A pair of lists (breadth_first, depth_first). Each list contains
:class:`HTMLFile` objects.
'''
assert max_levels >= 0
level = 0
flat = [HTMLFile(path_to_html_file, level, encoding, verbose)]
next_level = list(flat)
while level < max_levels and len(next_level) > 0:
level += 1
nl = []
for hf in next_level:
rejects = []
for link in hf.links:
if link.path is None or link.path in flat:
continue
try:
nf = HTMLFile(link.path, level, encoding, verbose)
nl.append(nf)
flat.append(nf)
except IgnoreFile, err:
rejects.append(link)
if not err.doesnt_exist or verbose > 1:
print str(err)
for link in rejects:
hf.links.remove(link)
next_level = list(nl)
return flat, list(depth_first(flat[0], flat))
if __name__ == '__main__':
breadth_first, depth_first = traverse(sys.argv[1], verbose=2)
print 'Breadth first...'
for f in breadth_first: print f
print '\n\nDepth first...'
for f in depth_first: print f

1020
src/calibre/ebooks/html.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ from lxml import etree
from calibre.ebooks.lit import LitError
from calibre.ebooks.lit.maps import OPF_MAP, HTML_MAP
import calibre.ebooks.lit.mssha1 as mssha1
from calibre.ebooks import DRMError
from calibre import plugins
lzx, lxzerror = plugins['lzx']
msdes, msdeserror = plugins['msdes']
@ -543,7 +544,10 @@ class LitReader(object):
raise LitError('Directory entry had 64bit name length.')
if namelen > remaining - 3:
raise LitError('Read past end of directory chunk')
name, chunk = chunk[:namelen].decode('utf-8'), chunk[namelen:]
try:
name, chunk = chunk[:namelen].decode('utf-8'), chunk[namelen:]
except UnicodeDecodeError:
break
section, chunk, remaining = encint(chunk, remaining)
offset, chunk, remaining = encint(chunk, remaining)
size, chunk, remaining = encint(chunk, remaining)
@ -650,7 +654,7 @@ class LitReader(object):
raise LitError('Unable to decrypt title key!')
self.bookkey = bookkey[1:9]
else:
raise LitError('Cannot extract content from a DRM protected ebook')
raise DRMError()
def _calculate_deskey(self):
hashfiles = ['/meta', '/DRMStorage/DRMSource']

View File

@ -22,6 +22,8 @@ __docformat__ = "epytext"
preferred_source_formats = [
'LIT',
'MOBI',
'EPUB',
'ODT',
'HTML',
'HTM',
'XHTM',
@ -163,7 +165,7 @@ def option_parser(usage, gui_mode=False):
help=_('''The regular expression used to detect chapter titles.'''
''' It is searched for in heading tags (h1-h6). Defaults to %default'''))
chapter.add_option('--chapter-attr', default='$,,$',
help=_('Detect a chapter beginning at an element having the specified attribute. The format for this option is tagname regexp,attribute name,attribute value regexp. For example to match all heading tags that have the attribute class="chapter" you would use "h\d,class,chapter". Default is %default'''))
help=_('Detect a chapter beginning at an element having the specified attribute. The format for this option is tagname regexp,attribute name,attribute value regexp. For example to match all heading tags that have the attribute class="chapter" you would use "h\d,class,chapter". You can set the attribute to "none" to match only on tag names. So for example, to match all <h2> tags, you would use "h2,none,". Default is %default'''))
chapter.add_option('--page-break-before-tag', dest='page_break', default='h[12]',
help=_('''If html2lrf does not find any page breaks in the '''
'''html file and cannot detect chapter headings, it will '''

View File

@ -1,12 +1,14 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Convert any ebook file into a LRF file.'''
import sys, os, logging, shutil, tempfile, glob, re
import sys, os, logging, shutil, tempfile, re
from calibre.ebooks import UnknownFormatError
from calibre.ebooks.lrf import option_parser as _option_parser
from calibre import __appname__, setup_cli_handlers, extract
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks.lrf.lit.convert_from import process_file as lit2lrf
from calibre.ebooks.lrf.pdf.convert_from import process_file as pdf2lrf
from calibre.ebooks.lrf.rtf.convert_from import process_file as rtf2lrf
@ -28,12 +30,18 @@ def largest_file(files):
def find_htmlfile(dir):
ext_pat = re.compile(r'\.(x){0,1}htm(l){0,1}', re.IGNORECASE)
toc_pat = re.compile(r'toc', re.IGNORECASE)
toc_files, files = [], []
for f in map(lambda x:os.path.join(dir, x), os.listdir(dir)):
name, ext = os.path.splitext(f)
if ext and ext_pat.match(ext):
toc_files.append(f) if toc_pat.search(f) else files.append(f)
a = toc_files if toc_files else files
index_pat = re.compile(r'index', re.IGNORECASE)
toc_files, index_files, files = [], [], []
for root, dirs, _files in os.walk(dir):
for f in _files:
f = os.path.abspath(os.path.join(root, f))
ext = os.path.splitext(f)[1]
if ext and ext_pat.match(ext):
toc_files.append(f) if toc_pat.search(f) else \
index_files.append(f) if index_pat.search(f) else \
files.append(f)
a = toc_files if toc_files else index_files if index_files else files
if a:
return largest_file(a)
@ -74,7 +82,7 @@ def handle_archive(path):
candidates = map(lambda x:os.path.join(cdir, x), os.listdir(cdir))
for ext in exts:
for f in candidates:
if f.lower().endswith(ext):
if f.lower().endswith('.'+ext):
files.append(f)
file = largest_file(files)
if not file:
@ -83,6 +91,21 @@ def handle_archive(path):
file = file.decode(sys.getfilesystemencoding())
return tdir, file
def odt2lrf(path, options, logger):
from calibre.ebooks.odt.to_oeb import Extract
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
if logger is None:
level = logging.DEBUG if options.verbose else logging.INFO
logger = logging.getLogger('odt2lrf')
setup_cli_handlers(logger, level)
with TemporaryDirectory('_odt2lrf') as tdir:
opf = Extract()(path, tdir)
options.use_spine = True
options.encoding = 'utf-8'
html_process_file(opf.replace('metadata.opf', 'index.html'), options, logger)
def process_file(path, options, logger=None):
path = os.path.abspath(os.path.expanduser(path))
tdir = None
@ -103,7 +126,7 @@ def process_file(path, options, logger=None):
fmt = '.lrs' if options.lrs else '.lrf'
options.output = os.path.splitext(os.path.basename(path))[0] + fmt
options.output = os.path.abspath(os.path.expanduser(options.output))
if ext in ['zip', 'rar']:
if ext in ['zip', 'rar', 'oebzip']:
newpath = None
try:
tdir, newpath = handle_archive(path)
@ -132,8 +155,10 @@ def process_file(path, options, logger=None):
convertor = mobi2lrf
elif ext == 'fb2':
convertor = fb22lrf
elif ext == 'odt':
convertor = odt2lrf
if not convertor:
raise UnknownFormatError('Coverting from %s to LRF is not supported.'%ext)
raise UnknownFormatError(_('Converting from %s to LRF is not supported.')%ext)
convertor(path, options, logger)
finally:
os.chdir(cwd)

View File

@ -7,16 +7,18 @@ __docformat__ = 'restructuredtext en'
Based on ideas from comiclrf created by FangornUK.
'''
import os, sys, traceback, shutil
import os, sys, shutil, traceback, textwrap
from uuid import uuid4
from calibre import extract, detect_ncpus, terminal_controller, \
__appname__, __version__
from calibre import extract, terminal_controller, __appname__, __version__
from calibre.utils.config import Config, StringConfig
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.threadpool import ThreadPool, WorkRequest
from calibre.parallel import Server, ParallelJob
from calibre.utils.terminfo import ProgressBar
from calibre.ebooks.lrf.pylrs.pylrs import Book, BookSetting, ImageStream, ImageBlock
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.epub.from_html import config as html2epub_config, convert as html2epub
try:
from calibre.utils.PythonMagickWand import \
NewMagickWand, NewPixelWand, \
@ -27,7 +29,7 @@ try:
MagickGetImageHeight, \
MagickResizeImage, MagickSetImageType, \
GrayscaleType, CatromFilter, MagickSetImagePage, \
MagickBorderImage, MagickSharpenImage, \
MagickBorderImage, MagickSharpenImage, MagickDespeckleImage, \
MagickQuantizeImage, RGBColorspace, \
MagickWriteImage, DestroyPixelWand, \
DestroyMagickWand, CloneMagickWand, \
@ -38,14 +40,14 @@ except:
PROFILES = {
# Name : (width, height) in pixels
'prs500':(584, 754),
'prs500':(584, 754),
}
def extract_comic(path_to_comic_file):
'''
Un-archive the comic file.
'''
tdir = PersistentTemporaryDirectory(suffix='comic_extract')
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
extract(path_to_comic_file, tdir)
return tdir
@ -79,151 +81,200 @@ def find_pages(dir, sort_on_mtime=False, verbose=False):
class PageProcessor(list):
'''
Contains the actual image rendering logic. See :method:`__call__` and
Contains the actual image rendering logic. See :method:`render` and
:method:`process_pages`.
'''
def __init__(self, path_to_page, dest, opts, num):
self.path_to_page = path_to_page
self.opts = opts
self.num = num
self.dest = dest
self.rotate = False
list.__init__(self)
self.path_to_page = path_to_page
self.opts = opts
self.num = num
self.dest = dest
self.rotate = False
self.render()
def __call__(self):
try:
img = NewMagickWand()
if img < 0:
def render(self):
img = NewMagickWand()
if img < 0:
raise RuntimeError('Cannot create wand.')
if not MagickReadImage(img, self.path_to_page):
raise IOError('Failed to read image from: %'%self.path_to_page)
width = MagickGetImageWidth(img)
height = MagickGetImageHeight(img)
if self.num == 0: # First image so create a thumbnail from it
thumb = CloneMagickWand(img)
if thumb < 0:
raise RuntimeError('Cannot create wand.')
if not MagickReadImage(img, self.path_to_page):
raise IOError('Failed to read image from: %'%self.path_to_page)
width = MagickGetImageWidth(img)
height = MagickGetImageHeight(img)
if self.num == 0: # First image so create a thumbnail from it
thumb = CloneMagickWand(img)
if thumb < 0:
MagickThumbnailImage(thumb, 60, 80)
MagickWriteImage(thumb, os.path.join(self.dest, 'thumbnail.png'))
DestroyMagickWand(thumb)
self.pages = [img]
if width > height:
if self.opts.landscape:
self.rotate = True
else:
split1, split2 = map(CloneMagickWand, (img, img))
DestroyMagickWand(img)
if split1 < 0 or split2 < 0:
raise RuntimeError('Cannot create wand.')
MagickThumbnailImage(thumb, 60, 80)
MagickWriteImage(thumb, os.path.join(self.dest, 'thumbnail.png'))
DestroyMagickWand(thumb)
self.pages = [img]
if width > height:
if self.opts.landscape:
self.rotate = True
else:
split1, split2 = map(CloneMagickWand, (img, img))
if split1 < 0 or split2 < 0:
raise RuntimeError('Cannot create wand.')
DestroyMagickWand(img)
MagickCropImage(split1, (width/2)-1, height, 0, 0)
MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages()
except Exception, err:
print 'Failed to process page: %s'%os.path.basename(self.path_to_page)
print 'Error:', err
if self.opts.verbose:
traceback.print_exc()
MagickCropImage(split1, (width/2)-1, height, 0, 0)
MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages()
def process_pages(self):
for i, wand in enumerate(self.pages):
pw = NewPixelWand()
if pw < 0:
raise RuntimeError('Cannot create wand.')
PixelSetColor(pw, 'white')
MagickSetImageBorderColor(wand, pw)
if self.rotate:
MagickRotateImage(wand, pw, -90)
try:
if pw < 0:
raise RuntimeError('Cannot create wand.')
PixelSetColor(pw, 'white')
# 25 percent fuzzy trim?
MagickTrimImage(wand, 25*65535/100)
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent
if not self.opts.dont_normalize:
MagickNormalizeImage(wand)
sizex = MagickGetImageWidth(wand)
sizey = MagickGetImageHeight(wand)
SCRWIDTH, SCRHEIGHT = PROFILES[self.opts.profile]
if self.opts.keep_aspect_ratio:
# Preserve the aspect ratio by adding border
aspect = float(sizex) / float(sizey)
if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)):
newsizey = SCRHEIGHT
newsizex = int(newsizey * aspect)
deltax = (SCRWIDTH - newsizex) / 2
deltay = 0
else:
newsizex = SCRWIDTH
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (SCRHEIGHT - newsizey) / 2
MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0)
MagickSetImageBorderColor(wand, pw)
MagickBorderImage(wand, pw, deltax, deltay)
else:
MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0)
if self.rotate:
MagickRotateImage(wand, pw, -90)
# 25 percent fuzzy trim?
MagickTrimImage(wand, 25*65535/100)
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent
if not self.opts.dont_normalize:
MagickNormalizeImage(wand)
sizex = MagickGetImageWidth(wand)
sizey = MagickGetImageHeight(wand)
if not self.opts.dont_sharpen:
MagickSharpenImage(wand, 0.0, 1.0)
SCRWIDTH, SCRHEIGHT = PROFILES[self.opts.profile]
MagickSetImageType(wand, GrayscaleType)
MagickQuantizeImage(wand, self.opts.colors, RGBColorspace, 0, 1, 0)
dest = '%d_%d.png'%(self.num, i)
dest = os.path.join(self.dest, dest)
MagickWriteImage(wand, dest+'8')
os.rename(dest+'8', dest)
self.append(dest)
DestroyPixelWand(pw)
wand = DestroyMagickWand(wand)
if self.opts.keep_aspect_ratio:
# Preserve the aspect ratio by adding border
aspect = float(sizex) / float(sizey)
if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)):
newsizey = SCRHEIGHT
newsizex = int(newsizey * aspect)
deltax = (SCRWIDTH - newsizex) / 2
deltay = 0
else:
newsizex = SCRWIDTH
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (SCRHEIGHT - newsizey) / 2
MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0)
MagickSetImageBorderColor(wand, pw)
MagickBorderImage(wand, pw, deltax, deltay)
elif self.opts.wide:
# Keep aspect and Use device height as scaled image width so landscape mode is clean
aspect = float(sizex) / float(sizey)
screen_aspect = float(SCRWIDTH) / float(SCRHEIGHT)
# Get dimensions of the landscape mode screen
# Add 25px back to height for the battery bar.
wscreenx = SCRHEIGHT + 25
wscreeny = int(wscreenx / screen_aspect)
if aspect <= screen_aspect:
newsizey = wscreeny
newsizex = int(newsizey * aspect)
deltax = (wscreenx - newsizex) / 2
deltay = 0
else:
newsizex = wscreenx
newsizey = int(newsizex / aspect)
deltax = 0
deltay = (wscreeny - newsizey) / 2
MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0)
MagickSetImageBorderColor(wand, pw)
MagickBorderImage(wand, pw, deltax, deltay)
else:
MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0)
if not self.opts.dont_sharpen:
MagickSharpenImage(wand, 0.0, 1.0)
MagickSetImageType(wand, GrayscaleType)
if self.opts.despeckle:
MagickDespeckleImage(wand)
MagickQuantizeImage(wand, self.opts.colors, RGBColorspace, 0, 1, 0)
dest = '%d_%d.png'%(self.num, i)
dest = os.path.join(self.dest, dest)
MagickWriteImage(wand, dest+'8')
os.rename(dest+'8', dest)
self.append(dest)
finally:
if pw > 0:
DestroyPixelWand(pw)
DestroyMagickWand(wand)
class Progress(object):
def render_pages(tasks, dest, opts, notification=None):
'''
Entry point for the job server.
'''
failures, pages = [], []
with ImageMagick():
for num, path in tasks:
try:
pages.extend(PageProcessor(path, dest, opts, num))
msg = _('Rendered %s')
except:
failures.append(path)
msg = _('Failed %s')
if opts.verbose:
msg += '\n' + traceback.format_exc()
msg = msg%path
if notification is not None:
notification(0.5, msg)
return pages, failures
class JobManager(object):
'''
Simple job manager responsible for keeping track of overall progress.
'''
def __init__(self, total, update):
self.total = total
self.update = update
self.done = 0
self.add_job = lambda j: j
self.output = lambda j: j
self.start_work = lambda j: j
self.job_done = lambda j: j
def __call__(self, req, res):
def status_update(self, job):
self.done += 1
self.update(float(self.done)/self.total,
_('Rendered %s')%os.path.basename(req.callable.path_to_page))
#msg = msg%os.path.basename(job.args[0])
self.update(float(self.done)/self.total, job.msg)
def process_pages(pages, opts, update):
'''
Render all identified comic pages.
'''
if not _imagemagick_loaded:
raise RuntimeError('Failed to load ImageMagick')
with ImageMagick():
tdir = PersistentTemporaryDirectory('_comic2lrf_pp')
processed_pages = [PageProcessor(path, tdir, opts, i) for i, path in enumerate(pages)]
tp = ThreadPool(detect_ncpus())
update(0, '')
notify = Progress(len(pages), update)
for pp in processed_pages:
tp.putRequest(WorkRequest(pp, callback=notify))
tp.wait()
ans, failures = [], []
tdir = PersistentTemporaryDirectory('_comic2lrf_pp')
job_manager = JobManager(len(pages), update)
server = Server()
jobs = []
tasks = server.split(pages)
for task in tasks:
jobs.append(ParallelJob('render_pages', lambda s:s, job_manager=job_manager,
args=[task, tdir, opts]))
server.add_job(jobs[-1])
server.wait()
server.killall()
server.close()
ans, failures = [], []
for pp in processed_pages:
if len(pp) == 0:
failures.append(os.path.basename(pp.path_to_page))
else:
ans += pp
return ans, failures, tdir
for job in jobs:
if job.result is None:
raise Exception(_('Failed to process comic: %s\n\n%s')%(job.exception, job.traceback))
pages, failures_ = job.result
ans += pages
failures += failures_
return ans, failures, tdir
def config(defaults=None):
desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks')
@ -231,32 +282,36 @@ def config(defaults=None):
c = Config('comic', desc)
else:
c = StringConfig(defaults, desc)
c.add_opt('title', ['-t', '--title'],
c.add_opt('title', ['-t', '--title'],
help=_('Title for generated ebook. Default is to use the filename.'))
c.add_opt('author', ['-a', '--author'],
help=_('Set the author in the metadata of the generated ebook. Default is %default'),
c.add_opt('author', ['-a', '--author'],
help=_('Set the author in the metadata of the generated ebook. Default is %default'),
default=_('Unknown'))
c.add_opt('output', ['-o', '--output'],
help=_('Path to output LRF file. By default a file is created in the current directory.'))
c.add_opt('output', ['-o', '--output'],
help=_('Path to output file. By default a file is created in the current directory.'))
c.add_opt('colors', ['-c', '--colors'], type='int', default=64,
help=_('Number of colors for grayscale image conversion. Default: %default'))
c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False,
help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
help=_('Disable sharpening.'))
c.add_opt('landscape', ['-l', '--landscape'], default=False,
c.add_opt('landscape', ['-l', '--landscape'], default=False,
help=_("Don't split landscape images into two portrait images"))
c.add_opt('wide', ['-w', '--wide-aspect'], default=False,
help=_("Keep aspect ratio and scale image using screen height as image width for viewing in landscape mode."))
c.add_opt('right2left', ['--right2left'], default=False, action='store_true',
help=_('Used for right-to-left publications like manga. Causes landscape pages to be split into portrait pages from right to left.'))
c.add_opt('no_sort', ['--no-sort'], default=False,
c.add_opt('despeckle', ['-d', '--despeckle'], default=False,
help=_('Enable Despeckle. Reduces speckle noise. May greatly increase processing time.'))
c.add_opt('no_sort', ['--no-sort'], default=False,
help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(),
help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys())
c.add_opt('verbose', ['--verbose'], default=0, action='count',
c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(),
help=_('Choose a profile for the device you are generating this file for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. This is suitable for any reader with the same screen size. Choices are %s')%PROFILES.keys())
c.add_opt('verbose', ['-v', '--verbose'], default=0, action='count',
help=_('Be verbose, useful for debugging. Can be specified multiple times for greater verbosity.'))
c.add_opt('no_progress_bar', ['--no-progress-bar'], default=False,
c.add_opt('no_progress_bar', ['--no-progress-bar'], default=False,
help=_("Don't show progress bar."))
return c
@ -265,9 +320,41 @@ def option_parser():
return c.option_parser(usage=_('''\
%prog [options] comic.cb[z|r]
Convert a comic in a CBZ or CBR file to an LRF ebook.
Convert a comic in a CBZ or CBR file to an ebook.
'''))
def create_epub(pages, profile, opts, thumbnail=None):
wrappers = []
WRAPPER = textwrap.dedent('''\
<html>
<head>
<title>Page #%d</title>
<style type="text/css">@page {margin:0pt; padding: 0pt;}</style>
</head>
<body style="margin: 0pt; padding: 0pt">
<div style="text-align:center">
<img src="%s" alt="comic page #%d" />
</div>
</body>
</html>
''')
dir = os.path.dirname(pages[0])
for i, page in enumerate(pages):
wrapper = WRAPPER%(i+1, os.path.basename(page), i+1)
page = os.path.join(dir, 'page_%d.html'%(i+1))
open(page, 'wb').write(wrapper)
wrappers.append(page)
mi = MetaInformation(opts.title, [opts.author])
opf = OPFCreator(dir, mi)
opf.create_manifest([(w, None) for w in wrappers])
opf.create_spine(wrappers)
metadata = os.path.join(dir, 'metadata.opf')
opf.render(open(metadata, 'wb'))
opts2 = html2epub_config('margin_left=0\nmargin_right=0\nmargin_top=0\nmargin_bottom=0').parse()
opts2.output = opts.output
html2epub(metadata, opts2)
def create_lrf(pages, profile, opts, thumbnail=None):
width, height = PROFILES[profile]
ps = {}
@ -290,14 +377,15 @@ def create_lrf(pages, profile, opts, thumbnail=None):
book.append(_page)
book.renderLrf(open(opts.output, 'wb'))
print _('Output written to'), opts.output
def do_convert(path_to_file, opts, notification=lambda m, p: p):
def do_convert(path_to_file, opts, notification=lambda m, p: p, output_format='lrf'):
source = path_to_file
if not opts.title:
opts.title = os.path.splitext(os.path.basename(source))
opts.title = os.path.splitext(os.path.basename(source))[0]
if not opts.output:
opts.output = os.path.abspath(os.path.splitext(os.path.basename(source))[0]+'.lrf')
opts.output = os.path.abspath(os.path.splitext(os.path.basename(source))[0]+'.'+output_format)
tdir = extract_comic(source)
pages = find_pages(tdir, sort_on_mtime=opts.no_sort, verbose=opts.verbose)
if not pages:
@ -312,12 +400,16 @@ def do_convert(path_to_file, opts, notification=lambda m, p: p):
thumbnail = os.path.join(tdir2, 'thumbnail.png')
if not os.access(thumbnail, os.R_OK):
thumbnail = None
create_lrf(pages, opts.profile, opts, thumbnail=thumbnail)
if output_format == 'lrf':
create_lrf(pages, opts.profile, opts, thumbnail=thumbnail)
else:
create_epub(pages, opts.profile, opts, thumbnail=thumbnail)
shutil.rmtree(tdir)
shutil.rmtree(tdir2)
def main(args=sys.argv, notification=None):
def main(args=sys.argv, notification=None, output_format='lrf'):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
@ -331,8 +423,8 @@ def main(args=sys.argv, notification=None):
notification = pb.update
source = os.path.abspath(args[1])
do_convert(source, opts, notification)
print _('Output written to'), opts.output
do_convert(source, opts, notification, output_format=output_format)
return 0
if __name__ == '__main__':

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, shutil, logging
from tempfile import mkdtemp
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks import ConversionError
from calibre.ebooks import ConversionError, DRMError
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata.opf import OPF
from calibre.ebooks.metadata.epub import OCFDirReader
@ -27,6 +27,8 @@ def generate_html(pathtoepub, logger):
os.rmdir(tdir)
try:
ZipFile(pathtoepub).extractall(tdir)
if os.path.exists(os.path.join(tdir, 'META-INF', 'encryption.xml')):
raise DRMError(os.path.basename(pathtoepub))
except:
if os.path.exists(tdir) and os.path.isdir(tdir):
shutil.rmtree(tdir)

View File

@ -1,16 +1,22 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Anatoly Shipitsin <norguhtar at gmail.com>'
"""
Convert .fb2 files to .lrf
"""
import os, sys, tempfile, shutil, logging
import os, sys, shutil, logging
from base64 import b64decode
from lxml import etree
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre import setup_cli_handlers, __appname__
from calibre import setup_cli_handlers
from calibre.resources import fb2_xsl
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.metadata import MetaInformation
def option_parser():
parser = lrf_option_parser(
@ -31,29 +37,42 @@ def extract_embedded_content(doc):
data = b64decode(elem.text.strip())
open(fname, 'wb').write(data)
def generate_html(fb2file, encoding, logger):
from lxml import etree
tdir = tempfile.mkdtemp(prefix=__appname__+'_fb2_')
cwd = os.getcwdu()
os.chdir(tdir)
def to_html(fb2file, tdir):
cwd = os.getcwd()
try:
logger.info('Parsing XML...')
os.chdir(tdir)
print 'Parsing XML...'
parser = etree.XMLParser(recover=True, no_network=True)
doc = etree.parse(fb2file, parser)
extract_embedded_content(doc)
logger.info('Converting XML to HTML...')
print 'Converting XML to HTML...'
styledoc = etree.fromstring(fb2_xsl)
transform = etree.XSLT(styledoc)
result = transform(doc)
html = os.path.join(tdir, 'index.html')
f = open(html, 'wb')
f.write(transform.tostring(result))
f.close()
open('index.html', 'wb').write(transform.tostring(result))
try:
mi = get_metadata(open(fb2file, 'rb'))
except:
mi = MetaInformation(None, None)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(fb2file))[0]
if not mi.authors:
mi.authors = [_('Unknown')]
opf = OPFCreator(tdir, mi)
opf.create_manifest([('index.html', None)])
opf.create_spine(['index.html'])
opf.render(open('metadata.opf', 'wb'))
return os.path.join(tdir, 'metadata.opf')
finally:
os.chdir(cwd)
return html
def generate_html(fb2file, encoding, logger):
tdir = PersistentTemporaryDirectory('_fb22lrf')
to_html(fb2file, tdir)
return os.path.join(tdir, 'index.html')
def process_file(path, options, logger=None):
if logger is None:
level = logging.DEBUG if options.verbose else logging.INFO

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
@ -8,12 +8,10 @@ from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.lrf.html.convert_from import process_file
from calibre.web.feeds.main import option_parser as feeds_option_parser
from calibre.web.feeds.main import run_recipe
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre import sanitize_file_name
from calibre.ptempfile import TemporaryDirectory
from calibre import sanitize_file_name, strftime
import sys, os, time
import parser
import sys, os
def option_parser():
parser = feeds_option_parser()
@ -36,25 +34,25 @@ def main(args=sys.argv, notification=None, handler=None):
recipe_arg = args[1] if len(args) > 1 else None
tdir = PersistentTemporaryDirectory('_feeds2lrf')
opts.output_dir = tdir
recipe = run_recipe(opts, recipe_arg, parser, notification=notification, handler=handler)
htmlfile = os.path.join(tdir, 'index.html')
if not os.access(htmlfile, os.R_OK):
raise RuntimeError(_('Fetching of recipe failed: ')+recipe_arg)
lparser = lrf_option_parser('')
ropts = lparser.parse_args(['html2lrf']+recipe.html2lrf_options)[0]
parser.merge_options(ropts, opts)
if not opts.output:
ext = '.lrs' if opts.lrs else '.lrf'
fname = recipe.title + time.strftime(recipe.timefmt)+ext
opts.output = os.path.join(os.getcwd(), sanitize_file_name(fname))
print 'Generating LRF...'
process_file(htmlfile, opts)
with TemporaryDirectory('_feeds2lrf') as tdir:
opts.output_dir = tdir
recipe = run_recipe(opts, recipe_arg, parser, notification=notification, handler=handler)
htmlfile = os.path.join(tdir, 'index.html')
if not os.access(htmlfile, os.R_OK):
raise RuntimeError(_('Fetching of recipe failed: ')+recipe_arg)
lparser = lrf_option_parser('')
ropts = lparser.parse_args(['html2lrf']+recipe.html2lrf_options)[0]
parser.merge_options(ropts, opts)
if not opts.output:
ext = '.lrs' if opts.lrs else '.lrf'
fname = recipe.title + strftime(recipe.timefmt)+ext
opts.output = os.path.join(os.getcwd(), sanitize_file_name(fname))
print 'Generating LRF...'
process_file(htmlfile, opts)
return 0
if __name__ == '__main__':

View File

@ -410,6 +410,7 @@ class HTMLConverter(object, LoggingInterface):
for key in sel[0].split(','):
val = self.parse_style_properties(sel[1])
key = key.strip().lower()
if '+' in key: continue
if ':' in key:
key, sep, pseudo = key.partition(':')
if key in pdict:
@ -437,7 +438,7 @@ class HTMLConverter(object, LoggingInterface):
for s in props.split(';'):
l = s.split(':',1)
if len(l)==2:
key = str(l[0].strip()).lower()
key = l[0].strip().lower()
val = l[1].strip()
prop [key] = val
return prop
@ -539,7 +540,7 @@ class HTMLConverter(object, LoggingInterface):
return tb
for page in list(self.book.pages()[index+1:]):
for c in page.contents:
if isinstance(c, (TextBlock, ImageBlock)):
if isinstance(c, (TextBlock, ImageBlock, Canvas)):
return c
raise ConversionError(_('Could not parse file: %s')%self.file_name)
@ -667,10 +668,12 @@ class HTMLConverter(object, LoggingInterface):
ascii_text = item.text
if not item.fragment and item.abspath in self.tops:
self.book.addTocEntry(ascii_text, self.tops[item.abspath])
else:
url = item.abspath+item.fragment
elif item.abspath:
url = item.abspath+(item.fragment if item.fragment else '')
if url in self.targets:
self.book.addTocEntry(ascii_text, self.targets[url])
def end_page(self):
@ -777,11 +780,11 @@ class HTMLConverter(object, LoggingInterface):
@param css: A dict
'''
src = tag.string if hasattr(tag, 'string') else tag
if len(src) > 32767:
if len(src) > 32760:
pos = 0
while pos < len(src):
self.add_text(src[pos:pos+32767], css, pseudo_css, force_span_use)
pos += 32767
self.add_text(src[pos:pos+32760], css, pseudo_css, force_span_use)
pos += 32760
return
src = src.replace('\r\n', '\n').replace('\r', '\n')
@ -1446,10 +1449,11 @@ class HTMLConverter(object, LoggingInterface):
pass
if not self.disable_chapter_detection and \
(self.chapter_attr[0].match(tagname) and \
tag.has_key(self.chapter_attr[1]) and \
self.chapter_attr[2].match(tag[self.chapter_attr[1]])):
(self.chapter_attr[1].lower() == 'none' or \
(tag.has_key(self.chapter_attr[1]) and \
self.chapter_attr[2].match(tag[self.chapter_attr[1]])))):
self.log_debug('Detected chapter %s', tagname)
self.end_page()
self.end_page()
self.page_break_found = True
if self.options.add_chapters_to_toc:
@ -1562,6 +1566,10 @@ class HTMLConverter(object, LoggingInterface):
if tagname == 'ol':
old_counter = self.list_counter
self.list_counter = 1
try:
self.list_counter = int(tag['start'])
except:
pass
prev_bs = self.current_block.blockStyle
self.end_current_block()
attrs = self.current_block.blockStyle.attrs
@ -1749,7 +1757,7 @@ class HTMLConverter(object, LoggingInterface):
try:
self.process_table(tag, tag_css)
except Exception, err:
self.log_warning(_('An error occurred while processing a table: %s. Ignoring table markup.'), str(err))
self.log_warning(_('An error occurred while processing a table: %s. Ignoring table markup.'), unicode(err))
self.log_debug('', exc_info=True)
self.log_debug(_('Bad table:\n%s'), str(tag)[:300])
self.in_table = False

View File

@ -11,7 +11,7 @@
</head>
<h1>Demo of <span style='font-family:monospace'>html2lrf</span></h1>
<p>
This document contains a demonstration of the capabilities of <span style='font-family:monospace'>html2lrf</span>, the HTML to LRF converter from <em>libprs500.</em> To obtain libprs500 visit<br/><span style='font:sans-serif'>https://libprs500.kovidgoyal.net</span>
This document contains a demonstration of the capabilities of <span style='font-family:monospace'>html2lrf</span>, the HTML to LRF converter from <em>calibre.</em> To obtain calibre visit<br/><span style='font:sans-serif'>http://calibre.kovidgoyal.net</span>
</p>
<br/>
<h2 id="toc">Table of Contents</h2>

View File

@ -676,7 +676,10 @@ def main(args=sys.argv):
if options.get_thumbnail:
print "Thumbnail:", td
if options.get_cover:
ext, data = lrf.get_cover()
try:
ext, data = lrf.get_cover()
except: # Fails on books created by LRFCreator 1.0
ext, data = None, None
if data:
cover = os.path.splitext(os.path.basename(args[1]))[0]+"_cover."+ext
open(cover, 'wb').write(data)

View File

@ -5,10 +5,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, subprocess, logging
from functools import partial
from calibre import isosx, setup_cli_handlers, filename_to_utf8, iswindows, islinux
from calibre.ebooks import ConversionError
from calibre.ebooks import ConversionError, DRMError
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.metadata.pdf import get_metadata
PDFTOHTML = 'pdftohtml'
popen = subprocess.Popen
@ -20,7 +23,7 @@ if iswindows and hasattr(sys, 'frozen'):
if islinux and getattr(sys, 'frozen_path', False):
PDFTOHTML = os.path.join(getattr(sys, 'frozen_path'), 'pdftohtml')
def generate_html(pathtopdf, logger):
def generate_html(pathtopdf, tdir):
'''
Convert the pdf into html.
@return: Path to a temporary file containing the HTML.
@ -29,10 +32,10 @@ def generate_html(pathtopdf, logger):
pathtopdf = pathtopdf.encode(sys.getfilesystemencoding())
if not os.access(pathtopdf, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtopdf
tdir = PersistentTemporaryDirectory('pdftohtml')
index = os.path.join(tdir, 'index.html')
# This is neccessary as pdftohtml doesn't always (linux) respect absolute paths
cmd = (PDFTOHTML, '-enc', 'UTF-8', '-noframes', '-p', '-nomerge', pathtopdf, os.path.basename(index))
pathtopdf = os.path.abspath(pathtopdf)
cmd = (PDFTOHTML, '-enc', 'UTF-8', '-noframes', '-p', '-nomerge', pathtopdf, os.path.basename(index))
cwd = os.getcwd()
try:
@ -44,16 +47,30 @@ def generate_html(pathtopdf, logger):
raise ConversionError(_('Could not find pdftohtml, check it is in your PATH'), True)
else:
raise
logger.info(p.stdout.read())
print p.stdout.read()
ret = p.wait()
if ret != 0:
err = p.stderr.read()
raise ConversionError, err
if not os.path.exists(index) or os.stat(index).st_size < 100:
raise ConversionError(os.path.basename(pathtopdf) + _(' does not allow copying of text.'), True)
raw = open(index).read(4000)
if not '<br' in raw:
raise DRMError()
raw = open(index, 'rb').read()
open(index, 'wb').write('<!-- created by calibre\'s pdftohtml -->\n'+raw)
if not '<br' in raw[:4000]:
raise ConversionError(os.path.basename(pathtopdf) + _(' is an image based PDF. Only conversion of text based PDFs is supported.'), True)
try:
mi = get_metadata(open(pathtopdf, 'rb'))
except:
mi = MetaInformation(None, None)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(pathtopdf))[0]
if not mi.authors:
mi.authors = [_('Unknown')]
opf = OPFCreator(tdir, mi)
opf.create_manifest([('index.html', None)])
opf.create_spine(['index.html'])
opf.render(open('metadata.opf', 'wb'))
finally:
os.chdir(cwd)
return index
@ -72,7 +89,8 @@ def process_file(path, options, logger=None):
logger = logging.getLogger('pdf2lrf')
setup_cli_handlers(logger, level)
pdf = os.path.abspath(os.path.expanduser(path))
htmlfile = generate_html(pdf, logger)
tdir = PersistentTemporaryDirectory('_pdf2lrf')
htmlfile = generate_html(pdf, tdir)
if not options.output:
ext = '.lrs' if options.lrs else '.lrf'
options.output = os.path.abspath(os.path.basename(os.path.splitext(path)[0]) + ext)

View File

@ -118,7 +118,6 @@ def writeLineWidth(f, width):
def writeUnicode(f, string, encoding):
if isinstance(string, str):
string = string.decode(encoding)
string = string.encode("utf-16-le")
length = len(string)
if length > 65535:

View File

@ -603,7 +603,7 @@ class Book(Delegator):
def renderLrs(self, lrsFile, encoding="UTF-8"):
if isinstance(lrsFile, basestring):
if isinstance(lrsFile, basestring):
lrsFile = codecs.open(lrsFile, "wb", encoding=encoding)
self.render(lrsFile, outputEncodingName=encoding)
lrsFile.close()

View File

@ -1,19 +1,20 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, tempfile, subprocess, shutil, logging, glob
import os, sys, shutil, logging, glob
from lxml import etree
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks import ConversionError
from calibre import isosx, setup_cli_handlers, __appname__
from calibre import setup_cli_handlers
from calibre.libwand import convert, WandException
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre.ebooks.lrf.rtf.xsl import xhtml
UNRTF = 'unrtf'
if isosx and hasattr(sys, 'frameworks_dir'):
UNRTF = os.path.join(getattr(sys, 'frameworks_dir'), UNRTF)
from calibre.ebooks.rtf2xml.ParseRtf import RtfInvalidCodeException
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator
def option_parser():
parser = lrf_option_parser(
@ -37,32 +38,6 @@ def convert_images(html, logger):
continue
return html
def generate_html(rtfpath, logger):
tdir = tempfile.mkdtemp(prefix=__appname__+'_')
cwd = os.path.abspath(os.getcwd())
os.chdir(tdir)
try:
logger.info('Converting to HTML...')
sys.stdout.flush()
handle, path = tempfile.mkstemp(dir=tdir, suffix='.html')
file = os.fdopen(handle, 'wb')
cmd = ' '.join([UNRTF, '"'+rtfpath+'"'])
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
raw = p.stdout.read()
ret = p.wait()
if ret != 0:
if len(raw) > 1000: #unrtf crashes occassionally on OSX and windows but still convert correctly
raw += '</body>\n</html>'
else:
logger.critical(p.stderr.read())
raise ConversionError, 'unrtf failed with error code: %d'%(ret,)
file.write(convert_images(raw, logger))
file.close()
return path
finally:
os.chdir(cwd)
def process_file(path, options, logger=None):
if logger is None:
level = logging.DEBUG if options.verbose else logging.INFO
@ -72,8 +47,8 @@ def process_file(path, options, logger=None):
f = open(rtf, 'rb')
mi = get_metadata(f, 'rtf')
f.close()
html = generate_html2(rtf, logger)
tdir = os.path.dirname(html)
tdir = PersistentTemporaryDirectory('_rtf2lrf')
html = generate_html(rtf, tdir)
cwd = os.getcwdu()
try:
if not options.output:
@ -111,12 +86,12 @@ def main(args=sys.argv, logger=None):
return 0
def generate_xml(rtfpath):
def generate_xml(rtfpath, tdir):
from calibre.ebooks.rtf2xml.ParseRtf import ParseRtf
tdir = tempfile.mkdtemp(prefix=__appname__+'_')
ofile = os.path.join(tdir, 'index.xml')
cwd = os.getcwdu()
os.chdir(tdir)
rtfpath = os.path.abspath(rtfpath)
try:
parser = ParseRtf(
in_file = rtfpath,
@ -162,24 +137,27 @@ def generate_xml(rtfpath):
return ofile
def generate_html2(rtfpath, logger):
from lxml import etree
logger.info('Converting RTF to XML...')
xml = generate_xml(rtfpath)
def generate_html(rtfpath, tdir):
print 'Converting RTF to XML...'
rtfpath = os.path.abspath(rtfpath)
try:
xml = generate_xml(rtfpath, tdir)
except RtfInvalidCodeException:
raise Exception(_('This RTF file has a feature calibre does not support. Convert it to HTML and then convert it.'))
tdir = os.path.dirname(xml)
cwd = os.getcwdu()
os.chdir(tdir)
try:
logger.info('Parsing XML...')
print 'Parsing XML...'
parser = etree.XMLParser(recover=True, no_network=True)
try:
doc = etree.parse(xml, parser)
except:
raise
logger.info('Parsing failed. Trying to clean up XML...')
print 'Parsing failed. Trying to clean up XML...'
soup = BeautifulStoneSoup(open(xml, 'rb').read())
doc = etree.fromstring(str(soup))
logger.info('Converting XML to HTML...')
print 'Converting XML to HTML...'
styledoc = etree.fromstring(xhtml)
transform = etree.XSLT(styledoc)
@ -187,8 +165,22 @@ def generate_html2(rtfpath, logger):
tdir = os.path.dirname(xml)
html = os.path.join(tdir, 'index.html')
f = open(html, 'wb')
f.write(transform.tostring(result))
res = transform.tostring(result)
res = res[:100].replace('xmlns:html', 'xmlns') + res[100:]
f.write(res)
f.close()
try:
mi = get_metadata(open(rtfpath, 'rb'))
except:
mi = MetaInformation(None, None)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(rtfpath))[0]
if not mi.authors:
mi.authors = [_('Unknown')]
opf = OPFCreator(tdir, mi)
opf.create_manifest([('index.html', None)])
opf.create_spine(['index.html'])
opf.render(open('metadata.opf', 'wb'))
finally:
os.chdir(cwd)
return html

View File

@ -5,12 +5,14 @@ Convert .txt files to .lrf
"""
import os, sys, codecs, logging
from calibre.ptempfile import PersistentTemporaryFile
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks import ConversionError
from calibre.ebooks.lrf.html.convert_from import process_file as html_process_file
from calibre.ebooks.markdown import markdown
from calibre import setup_cli_handlers
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf import OPFCreator
def option_parser():
parser = lrf_option_parser(
@ -23,7 +25,7 @@ _('''%prog [options] mybook.txt
return parser
def generate_html(txtfile, encoding, logger):
def generate_html(txtfile, encoding, tdir):
'''
Convert txtfile to html and return a PersistentTemporaryFile object pointing
to the file with the HTML.
@ -44,15 +46,19 @@ def generate_html(txtfile, encoding, logger):
else:
txt = codecs.open(txtfile, 'rb', enc).read()
logger.info('Converting text to HTML...')
print 'Converting text to HTML...'
md = markdown.Markdown(
extensions=['footnotes', 'tables', 'toc'],
safe_mode=False,
)
html = md.convert(txt)
p = PersistentTemporaryFile('.html', dir=os.path.dirname(txtfile))
p.close()
codecs.open(p.name, 'wb', 'utf8').write(html)
html = '<html><body>'+md.convert(txt)+'</body></html>'
p = os.path.join(tdir, 'index.html')
open(p, 'wb').write(html.encode('utf-8'))
mi = MetaInformation(os.path.splitext(os.path.basename(txtfile))[0], [_('Unknown')])
opf = OPFCreator(tdir, mi)
opf.create_manifest([(os.path.join(tdir, 'index.html'), None)])
opf.create_spine([os.path.join(tdir, 'index.html')])
opf.render(open(os.path.join(tdir, 'metadata.opf'), 'wb'))
return p
def process_file(path, options, logger=None):
@ -63,7 +69,8 @@ def process_file(path, options, logger=None):
txt = os.path.abspath(os.path.expanduser(path))
if not hasattr(options, 'debug_html_generation'):
options.debug_html_generation = False
htmlfile = generate_html(txt, options.encoding, logger)
tdir = PersistentTemporaryDirectory('_txt2lrf')
htmlfile = generate_html(txt, options.encoding, tdir)
options.encoding = 'utf-8'
if not options.debug_html_generation:
options.force_page_break = 'h2'
@ -73,9 +80,9 @@ def process_file(path, options, logger=None):
options.output = os.path.abspath(os.path.expanduser(options.output))
if not options.title:
options.title = os.path.splitext(os.path.basename(path))[0]
html_process_file(htmlfile.name, options, logger)
html_process_file(htmlfile, options, logger)
else:
print open(htmlfile.name, 'rb').read()
print open(htmlfile, 'rb').read()
def main(args=sys.argv, logger=None):
parser = option_parser()

View File

@ -2,10 +2,10 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Convert websites into LRF files.'''
import sys, time, tempfile, shutil, os, logging, imp, inspect, re
import sys, tempfile, shutil, os, logging, imp, inspect, re
from urlparse import urlsplit
from calibre import __appname__, setup_cli_handlers, CommandLineError
from calibre import __appname__, setup_cli_handlers, CommandLineError, strftime
from calibre.ebooks.lrf import option_parser as lrf_option_parser
from calibre.ebooks.lrf.html.convert_from import process_file
@ -128,7 +128,7 @@ def process_profile(args, options, logger=None):
title = profile.title
if not title:
title = urlsplit(options.url).netloc
options.title = title + time.strftime(profile.timefmt, time.localtime())
options.title = title + strftime(profile.timefmt)
options.match_regexps += profile.match_regexps
options.preprocess_regexps = profile.preprocess_regexps

View File

@ -30,7 +30,7 @@ class AssociatedPress(DefaultProfile):
('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'),
('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'),
('AP Political News', 'http://hosted.ap.org/lineups/POLITICSHEADS-rss_2.0.xml?SITE=ORMED&SECTION=HOME'),
('AP Washington News', 'http://hosted.ap.org/lineups/WASHINGTONHEADS-rss_2.0.xml?SITE=NYPLA&SECTION=HOME'),
('AP Washington State News', 'http://hosted.ap.org/lineups/WASHINGTONHEADS-rss_2.0.xml?SITE=NYPLA&SECTION=HOME'),
('AP Technology News', 'http://hosted.ap.org/lineups/TECHHEADS-rss_2.0.xml?SITE=CTNHR&SECTION=HOME'),
('AP Health News', 'http://hosted.ap.org/lineups/HEALTHHEADS-rss_2.0.xml?SITE=FLDAY&SECTION=HOME'),
('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'),

View File

@ -15,6 +15,24 @@ from calibre.constants import __version__ as VERSION
from calibre import relpath
from calibre.utils.config import OptionParser
def string_to_authors(raw):
raw = raw.replace('&&', u'\uffff')
authors = [a.strip().replace(u'\uffff', '&') for a in raw.split('&')]
return authors
def authors_to_string(authors):
return ' & '.join([a.replace('&', '&&') for a in authors])
def author_to_author_sort(author):
tokens = author.split()
tokens = tokens[-1:] + tokens[:-1]
if len(tokens) > 1:
tokens[0] += ','
return ' '.join(tokens)
def authors_to_sort_string(authors):
return ' & '.join(map(author_to_author_sort, authors))
def get_parser(extension):
''' Return an option parser with the basic metadata options already setup'''
parser = OptionParser(usage='%prog [options] myfile.'+extension+'\n\nRead and write metadata from an ebook file.')
@ -43,7 +61,7 @@ class Resource(object):
def __init__(self, href_or_path, basedir=os.getcwd(), is_path=True):
self._href = None
self._basedir = None
self._basedir = basedir
self.path = None
self.fragment = ''
try:
@ -55,7 +73,7 @@ class Resource(object):
if is_path:
path = href_or_path
if not os.path.isabs(path):
path = os.path.abspath(os.path.join(path, basedir))
path = os.path.abspath(os.path.join(basedir, path))
if isinstance(path, str):
path = path.decode(sys.getfilesystemencoding())
self.path = path
@ -141,6 +159,10 @@ class ResourceCollection(object):
def remove(self, resource):
self._resources.remove(resource)
def replace(self, start, end, items):
'Same as list[start:end] = items'
self._resources[start:end] = items
@staticmethod
def from_directory_contents(top, topdown=True):
@ -167,18 +189,17 @@ class MetaInformation(object):
for attr in ('author_sort', 'title_sort', 'comments', 'category',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language'):
'manifest', 'spine', 'toc', 'cover', 'language', 'book_producer'):
if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr))
def __init__(self, title, authors=[_('Unknown')]):
'''
@param title: title or "Unknown" or a MetaInformation object
@param authors: List of strings or []
'''
mi = None
if isinstance(title, MetaInformation):
if hasattr(title, 'title') and hasattr(title, 'authors'):
mi = title
title = mi.title
authors = mi.authors
@ -186,42 +207,32 @@ class MetaInformation(object):
self.author = authors # Needed for backward compatibility
#: List of strings or []
self.authors = authors
#: Sort text for author
self.author_sort = None if not mi else mi.author_sort
self.title_sort = None if not mi else mi.title_sort
self.comments = None if not mi else mi.comments
self.category = None if not mi else mi.category
self.publisher = None if not mi else mi.publisher
self.series = None if not mi else mi.series
self.series_index = None if not mi else mi.series_index
self.rating = None if not mi else mi.rating
self.isbn = None if not mi else mi.isbn
self.tags = [] if not mi else mi.tags
self.language = None if not mi else mi.language # Typically a string describing the language
self.tags = getattr(mi, 'tags', [])
#: mi.cover_data = (ext, data)
self.cover_data = mi.cover_data if (mi and hasattr(mi, 'cover_data')) else (None, None)
self.application_id = mi.application_id if (mi and hasattr(mi, 'application_id')) else None
self.manifest = getattr(mi, 'manifest', None)
self.toc = getattr(mi, 'toc', None)
self.spine = getattr(mi, 'spine', None)
self.guide = getattr(mi, 'guide', None)
self.cover = getattr(mi, 'cover', None)
self.cover_data = getattr(mi, 'cover_data', (None, None))
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer',
):
setattr(self, x, getattr(mi, x, None))
def smart_update(self, mi):
'''
Merge the information in C{mi} into self. In case of conflicts, the information
in C{mi} takes precedence, unless the information in mi is NULL.
'''
if mi.title and mi.title.lower() != 'unknown':
if mi.title and mi.title != _('Unknown'):
self.title = mi.title
if mi.authors and mi.authors[0].lower() != 'unknown':
if mi.authors and mi.authors[0] != _('Unknown'):
self.authors = mi.authors
for attr in ('author_sort', 'title_sort', 'comments', 'category',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'application_id', 'manifest', 'spine', 'toc',
'cover', 'language', 'guide'):
'cover', 'language', 'guide', 'book_producer'):
if hasattr(mi, attr):
val = getattr(mi, attr)
if val is not None:
@ -242,6 +253,8 @@ class MetaInformation(object):
ans += ((' (' + self.author_sort + ')') if self.author_sort else '') + u'\n'
if self.publisher:
ans += u'Publisher: '+ unicode(self.publisher) + u'\n'
if self.book_producer:
ans += u'Producer : '+ unicode(self.book_producer) + u'\n'
if self.category:
ans += u'Category : ' + unicode(self.category) + u'\n'
if self.comments:

View File

@ -6,14 +6,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read meta information from epub files'''
import sys, os
from calibre.utils.zipfile import ZipFile, BadZipfile
from cStringIO import StringIO
from contextlib import closing
from calibre.utils.zipfile import ZipFile, BadZipfile, safe_replace
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre.ebooks.metadata.opf import OPF, OPFReader, OPFCreator
from calibre.ebooks.metadata import get_parser, MetaInformation
from calibre.ebooks.metadata.opf2 import OPF
class EPubException(Exception):
pass
@ -43,20 +43,22 @@ class Container(dict):
raise EPubException("<rootfile/> element malformed")
class OCF(object):
MIMETYPE = 'application/epub+zip'
CONTAINER_PATH = 'META-INF/container.xml'
MIMETYPE = 'application/epub+zip'
CONTAINER_PATH = 'META-INF/container.xml'
ENCRYPTION_PATH = 'META-INF/encryption.xml'
def __init__(self):
raise NotImplementedError('Abstract base class')
class OCFReader(OCF):
def __init__(self):
try:
mimetype = self.open('mimetype').read().rstrip()
if mimetype != OCF.MIMETYPE:
raise EPubException
except (KeyError, EPubException):
raise EPubException("not an .epub OCF container")
print 'WARNING: Invalid mimetype declaration', mimetype
except:
print 'WARNING: Epub doesn\'t contain a mimetype declaration'
try:
with closing(self.open(OCF.CONTAINER_PATH)) as f:
@ -66,35 +68,27 @@ class OCFReader(OCF):
try:
with closing(self.open(self.container[OPF.MIMETYPE])) as f:
self.opf = OPFReader(f, self.root)
self.opf = OPF(f, self.root)
except KeyError:
raise EPubException("missing OPF package file")
class OCFZipReader(OCFReader):
def __init__(self, stream, mode='r'):
def __init__(self, stream, mode='r', root=None):
try:
self.archive = ZipFile(stream, mode)
self.archive = ZipFile(stream, mode=mode)
except BadZipfile:
raise EPubException("not a ZIP .epub OCF container")
self.root = getattr(stream, 'name', os.getcwd())
self.root = root
if self.root is None:
self.root = os.getcwdu()
if hasattr(stream, 'name'):
self.root = os.path.abspath(os.path.dirname(stream.name))
super(OCFZipReader, self).__init__()
def open(self, name, mode='r'):
return StringIO(self.archive.read(name))
class OCFZipWriter(OCFZipReader):
def __init__(self, stream):
OCFZipReader.__init__(self, stream, mode='a')
def set_metadata(self, mi):
name = self.container[OPF.MIMETYPE]
stream = StringIO()
opf = OPFCreator(self.root, mi)
opf.render(stream)
self.archive.delete(name)
self.archive.writestr(name, stream.getvalue())
class OCFDirReader(OCFReader):
def __init__(self, path):
self.root = path
@ -109,12 +103,22 @@ def get_metadata(stream):
return OCFZipReader(stream).opf
def set_metadata(stream, mi):
OCFZipWriter(stream).set_metadata(mi)
reader = OCFZipReader(stream, root=os.getcwdu())
reader.opf.smart_update(mi)
newopf = StringIO(reader.opf.render())
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)
def option_parser():
parser = get_parser('epub')
parser.remove_option('--category')
parser.add_option('--tags', default=None, help=_('A comma separated list of tags to set'))
parser.add_option('--tags', default=None,
help=_('A comma separated list of tags to set'))
parser.add_option('--series', default=None,
help=_('The series to which this book belongs'))
parser.add_option('--series-index', default=None,
help=_('The series index'))
parser.add_option('--language', default=None,
help=_('The book language'))
return parser
def main(args=sys.argv):
@ -124,19 +128,36 @@ def main(args=sys.argv):
parser.print_help()
return 1
stream = open(args[1], 'r+b')
mi = MetaInformation(OCFZipReader(stream).opf)
mi = MetaInformation(OCFZipReader(stream, root=os.getcwdu()).opf)
changed = False
if opts.title:
mi.title = opts.title
changed = True
if opts.authors:
mi.authors = opts.authors.split(',')
changed = True
if opts.tags:
mi.tags = opts.tags.split(',')
changed = True
if opts.comment:
mi.comments = opts.comment
set_metadata(stream, mi)
print unicode(mi)
changed = True
if opts.series:
mi.series = opts.series
changed = True
if opts.series_index:
mi.series_index = opts.series_index
changed = True
if opts.language is not None:
mi.language = opts.language
changed = True
if changed:
stream.seek(0)
set_metadata(stream, mi)
stream.seek(0)
print unicode(MetaInformation(OCFZipReader(stream, root=os.getcwdu()).opf))
stream.close()
return 0
if __name__ == '__main__':

View File

@ -33,8 +33,8 @@ def get_metadata(stream):
exts = ['.jpg']
cdata = (exts[0][1:], b64decode(binary.string.strip()))
if comments and len(comments) > 1:
comments = comments.p.contents[0]
if comments:
comments = u''.join(comments.findAll(text=True))
series = soup.find("sequence")
mi = MetaInformation(title, author)
mi.comments = comments

View File

@ -0,0 +1,62 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Ashish Kulkarni <kulkarni.ashish@gmail.com>'
'''Read meta information from IMP files'''
import sys, os
from calibre.ebooks.metadata import MetaInformation
MAGIC = ['\x00\x01BOOKDOUG', '\x00\x02BOOKDOUG']
def get_metadata(stream):
""" Return metadata as a L{MetaInfo} object """
title = 'Unknown'
mi = MetaInformation(title, ['Unknown'])
stream.seek(0)
try:
if stream.read(10) not in MAGIC:
print >>sys.stderr, u'Couldn\'t read IMP header from file'
return mi
def cString(skip=0):
result = ''
while 1:
data = stream.read(1)
if data == '\x00':
if not skip: return result
skip -= 1
result, data = '', ''
result += data
stream.read(38) # skip past some uninteresting headers
_, category, title, author = cString(), cString(), cString(1), cString(2)
if title:
mi.title = title
if author:
src = author.split('&')
authors = []
for au in src:
authors += au.split(',')
mi.authors = authors
mi.author = author
if category:
mi.category = category
except Exception, err:
msg = u'Couldn\'t read metadata from imp: %s with error %s'%(mi.title, unicode(err))
print >>sys.stderr, msg.encode('utf8')
return mi
def main(args=sys.argv):
if len(args) != 2:
print >>sys.stderr, _('Usage: imp-meta file.imp')
print >>sys.stderr, _('No filename specified.')
return 1
path = os.path.abspath(os.path.expanduser(args[1]))
print get_metadata(open(path, 'rb'))
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -39,7 +39,7 @@ def cover_from_isbn(isbn, timeout=5.):
_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(timeout)
try:
src = browser.open('http://www.librarything.com/isbn/'+isbn).read()
src = browser.open('http://www.librarything.com/isbn/'+isbn).read().decode('utf-8', 'replace')
s = BeautifulSoup(src)
url = s.find('td', attrs={'class':'left'})
if url is None:

View File

@ -9,9 +9,12 @@ from calibre.ebooks.metadata.fb2 import get_metadata as fb2_metadata
from calibre.ebooks.lrf.meta import get_metadata as lrf_metadata
from calibre.ebooks.metadata.pdf import get_metadata as pdf_metadata
from calibre.ebooks.metadata.lit import get_metadata as lit_metadata
from calibre.ebooks.metadata.imp import get_metadata as imp_metadata
from calibre.ebooks.metadata.rb import get_metadata as rb_metadata
from calibre.ebooks.metadata.epub import get_metadata as epub_metadata
from calibre.ebooks.metadata.html import get_metadata as html_metadata
from calibre.ebooks.mobi.reader import get_metadata as mobi_metadata
from calibre.ebooks.metadata.odt import get_metadata as odt_metadata
from calibre.ebooks.metadata.opf import OPFReader
from calibre.ebooks.metadata.rtf import set_metadata as set_rtf_metadata
from calibre.ebooks.lrf.meta import set_metadata as set_lrf_metadata
@ -21,8 +24,8 @@ from calibre.ebooks.metadata import MetaInformation
_METADATA_PRIORITIES = [
'html', 'htm', 'xhtml', 'xhtm',
'rtf', 'fb2', 'pdf', 'prc',
'epub', 'lit', 'lrf', 'mobi',
'rtf', 'fb2', 'pdf', 'prc', 'odt',
'epub', 'lit', 'lrf', 'mobi', 'rb', 'imp'
]
# The priorities for loading metadata from different file types
@ -41,23 +44,28 @@ def metadata_from_formats(formats):
for path in formats:
ext = path_to_ext(path)
stream = open(path, 'rb')
mi.smart_update(get_metadata(stream, stream_type=ext, use_libprs_metadata=True))
try:
mi.smart_update(get_metadata(stream, stream_type=ext, use_libprs_metadata=True))
except:
continue
if getattr(mi, 'application_id', None) is not None:
return mi
if not mi.title:
mi.title = 'Unknown'
mi.title = _('Unknown')
if not mi.authors:
mi.authors = ['Unknown']
mi.authors = [_('Unknown')]
return mi
def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
if stream_type: stream_type = stream_type.lower()
if stream_type in ('html', 'html', 'xhtml', 'xhtm'):
if stream_type in ('html', 'html', 'xhtml', 'xhtm', 'xml'):
stream_type = 'html'
if stream_type in ('mobi', 'prc'):
stream_type = 'mobi'
if stream_type in ('odt', 'ods', 'odp', 'odg', 'odf'):
stream_type = 'odt'
opf = None
if hasattr(stream, 'name'):
@ -68,18 +76,20 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
if use_libprs_metadata and getattr(opf, 'application_id', None) is not None:
return opf
try:
func = eval(stream_type + '_metadata')
mi = func(stream)
except NameError:
mi = MetaInformation(None, None)
mi = MetaInformation(None, None)
if prefs['read_file_metadata']:
try:
func = eval(stream_type + '_metadata')
mi = func(stream)
except NameError:
pass
name = os.path.basename(getattr(stream, 'name', ''))
base = metadata_from_filename(name)
if not base.authors:
base.authors = ['Unknown']
base.authors = [_('Unknown')]
if not base.title:
base.title = 'Unknown'
base.title = _('Unknown')
base.smart_update(mi)
if opf is not None:
base.smart_update(opf)

View File

@ -1,7 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<?python
from uuid import uuid4
?>
<ncx version="2005-1"
xml:lang="en"
xmlns="http://www.daisy.org/z3986/2005/ncx/"
encoding="UTF-8"
xmlns:py="http://genshi.edgewall.org/"
>
<head>
@ -14,11 +17,11 @@
<docTitle><text>Table of Contents</text></docTitle>
<py:def function="navpoint(np, level)">
${'%*s'%(4*level,'')}<navPoint playOrder="${str(np.play_order)}">
${'%*s'%(4*level,'')}<navPoint id="${str(uuid4())}" playOrder="${str(np.play_order)}">
${'%*s'%(4*level,'')}<navLabel>
${'%*s'%(4*level,'')}<text>${np.text}</text>
${'%*s'%(4*level,'')}</navLabel>
${'%*s'%(4*level,'')}<content src="${str(np.href)+(('#' + str(np.fragment)) if np.fragment else '')}" />
${'%*s'%(4*level,'')}<content src="${unicode(np.href)+(('#' + unicode(np.fragment)) if np.fragment else '')}" />
<py:for each="np2 in np">${navpoint(np2, level+1)}</py:for>
${'%*s'%(4*level,'')}</navPoint>
</py:def>

View File

@ -0,0 +1,266 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (C) 2006 Søren Roug, European Environment Agency
#
# This is free software. You may redistribute it under the terms
# of the Apache license and the GNU General Public License Version
# 2 or at your option any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Contributor(s):
#
import zipfile, sys, re
import xml.sax.saxutils
from cStringIO import StringIO
from odf.namespaces import OFFICENS, DCNS, METANS
from calibre.ebooks.metadata import MetaInformation, string_to_authors
whitespace = re.compile(r'\s+')
fields = {
'title': (DCNS,u'title'),
'description': (DCNS,u'description'),
'subject': (DCNS,u'subject'),
'creator': (DCNS,u'creator'),
'date': (DCNS,u'date'),
'language': (DCNS,u'language'),
'generator': (METANS,u'generator'),
'initial-creator': (METANS,u'initial-creator'),
'keyword': (METANS,u'keyword'),
'editing-duration': (METANS,u'editing-duration'),
'editing-cycles': (METANS,u'editing-cycles'),
'printed-by': (METANS,u'printed-by'),
'print-date': (METANS,u'print-date'),
'creation-date': (METANS,u'creation-date'),
'user-defined': (METANS,u'user-defined'),
#'template': (METANS,u'template'),
}
def normalize(str):
"""
The normalize-space function returns the argument string with whitespace
normalized by stripping leading and trailing whitespace and replacing
sequences of whitespace characters by a single space.
"""
return whitespace.sub(' ', str).strip()
class MetaCollector:
"""
The MetaCollector is a pseudo file object, that can temporarily ignore write-calls
It could probably be replaced with a StringIO object.
"""
def __init__(self):
self._content = []
self.dowrite = True
def write(self, str):
if self.dowrite:
self._content.append(str)
def content(self):
return ''.join(self._content)
class odfmetaparser(xml.sax.saxutils.XMLGenerator):
""" Parse a meta.xml file with an event-driven parser and replace elements.
It would probably be a cleaner approach to use a DOM based parser and
then manipulate in memory.
Small issue: Reorders elements
"""
def __init__(self, deletefields={}, yieldfields={}, addfields={}):
self.deletefields = deletefields
self.yieldfields = yieldfields
self.addfields = addfields
self._mimetype = ''
self.output = MetaCollector()
self._data = []
self.seenfields = {}
xml.sax.saxutils.XMLGenerator.__init__(self, self.output, 'utf-8')
def startElementNS(self, name, qname, attrs):
self._data = []
field = name
# I can't modify the template until the tool replaces elements at the same
# location and not at the end
# if name == (METANS,u'template'):
# self._data = [attrs.get((XLINKNS,u'title'),'')]
if name == (METANS,u'user-defined'):
field = attrs.get((METANS,u'name'))
if field in self.deletefields:
self.output.dowrite = False
elif field in self.yieldfields:
del self.addfields[field]
xml.sax.saxutils.XMLGenerator.startElementNS(self, name, qname, attrs)
else:
xml.sax.saxutils.XMLGenerator.startElementNS(self, name, qname, attrs)
self._tag = field
def endElementNS(self, name, qname):
field = name
if name == (METANS,u'user-defined'):
field = self._tag
if name == (OFFICENS,u'meta'):
for k,v in self.addfields.items():
if len(v) > 0:
if type(k) == type(''):
xml.sax.saxutils.XMLGenerator.startElementNS(self,(METANS,u'user-defined'),None,{(METANS,u'name'):k})
xml.sax.saxutils.XMLGenerator.characters(self, v)
xml.sax.saxutils.XMLGenerator.endElementNS(self, (METANS,u'user-defined'),None)
else:
xml.sax.saxutils.XMLGenerator.startElementNS(self, k, None, {})
xml.sax.saxutils.XMLGenerator.characters(self, v)
xml.sax.saxutils.XMLGenerator.endElementNS(self, k, None)
if isinstance(self._tag, tuple):
texttag = self._tag[1]
else:
texttag = self._tag
self.seenfields[texttag] = self.data()
if field in self.deletefields:
self.output.dowrite = True
else:
xml.sax.saxutils.XMLGenerator.endElementNS(self, name, qname)
def characters(self, content):
xml.sax.saxutils.XMLGenerator.characters(self, content)
self._data.append(content)
def meta(self):
return self.output.content()
def data(self):
return normalize(''.join(self._data))
def get_metadata(stream):
zin = zipfile.ZipFile(stream, 'r')
odfs = odfmetaparser()
parser = xml.sax.make_parser()
parser.setFeature(xml.sax.handler.feature_namespaces, 1)
parser.setContentHandler(odfs)
content = zin.read('meta.xml')
parser.parse(StringIO(content))
data = odfs.seenfields
mi = MetaInformation(None, [])
if data.has_key('title'):
mi.title = data['title']
if data.has_key('creator'):
mi.authors = string_to_authors(data['creator'])
if data.has_key('description'):
mi.comments = data['description']
if data.has_key('language'):
mi.language = data['language']
if data.get('keywords', ''):
mi.tags = data['keywords'].split(',')
return mi
def main(args=sys.argv):
if len(args) != 2:
print 'Usage: %s file.odt'%args[0]
return 1
mi = get_metadata(open(args[1], 'rb'))
print mi
return 0
if __name__ == '__main__':
sys.exit(main())
#now = time.localtime()[:6]
#outputfile = "-"
#writemeta = False # Do we change any meta data?
#usenormalize = False
#
#try:
# opts, args = getopt.getopt(sys.argv[1:], "cdlI:A:a:o:x:X:")
#except getopt.GetoptError:
# exitwithusage()
#
#if len(opts) == 0:
# opts = [ ('-l','') ]
#
#for o, a in opts:
# if o in ('-a','-A','-I'):
# writemeta = True
# if a.find(":") >= 0:
# k,v = a.split(":",1)
# else:
# k,v = (a, "")
# if len(k) == 0:
# exitwithusage()
# k = fields.get(k,k)
# addfields[k] = unicode(v,'utf-8')
# if o == '-a':
# yieldfields[k] = True
# if o == '-I':
# deletefields[k] = True
# if o == '-d':
# writemeta = True
# addfields[(DCNS,u'date')] = "%04d-%02d-%02dT%02d:%02d:%02d" % now
# deletefields[(DCNS,u'date')] = True
# if o == '-c':
# usenormalize = True
# if o == '-l':
# Xfields = fields.values()
# if o == "-x":
# xfields.append(fields.get(a,a))
# if o == "-X":
# Xfields.append(fields.get(a,a))
# if o == "-o":
# outputfile = a
#
## The specification says we should change the element to our own,
## and must not export the original identifier.
#if writemeta:
# addfields[(METANS,u'generator')] = TOOLSVERSION
# deletefields[(METANS,u'generator')] = True
#
#odfs = odfmetaparser()
#parser = xml.sax.make_parser()
#parser.setFeature(xml.sax.handler.feature_namespaces, 1)
#parser.setContentHandler(odfs)
#
#if len(args) == 0:
# zin = zipfile.ZipFile(sys.stdin,'r')
#else:
# if not zipfile.is_zipfile(args[0]):
# exitwithusage()
# zin = zipfile.ZipFile(args[0], 'r')
#
#content = zin.read('meta.xml')
#parser.parse(StringIO(content))
#
#if writemeta:
# if outputfile == '-':
# if sys.stdout.isatty():
# sys.stderr.write("Won't write ODF file to terminal\n")
# sys.exit(1)
# zout = zipfile.ZipFile(sys.stdout,"w")
# else:
# zout = zipfile.ZipFile(outputfile,"w")
#
#
#
# # Loop through the input zipfile and copy the content to the output until we
# # get to the meta.xml. Then substitute.
# for zinfo in zin.infolist():
# if zinfo.filename == "meta.xml":
# # Write meta
# zi = zipfile.ZipInfo("meta.xml", now)
# zi.compress_type = zipfile.ZIP_DEFLATED
# zout.writestr(zi,odfs.meta() )
# else:
# payload = zin.read(zinfo.filename)
# zout.writestr(zinfo, payload)
#
# zout.close()
#zin.close()

View File

@ -7,7 +7,7 @@ import cStringIO
import uuid
from urllib import unquote, quote
from calibre import __appname__
from calibre.constants import __appname__, __version__
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
from calibre.ebooks.lrf import entity_to_unicode
@ -28,7 +28,10 @@ class ManifestItem(Resource):
if item.has_key('href'):
href = item['href']
if unquote(href) == href:
href = quote(href)
try:
href = quote(href)
except KeyError:
pass
res = ManifestItem(href, basedir=basedir, is_path=False)
mt = item.get('media-type', '').strip()
if mt:
@ -241,7 +244,7 @@ class OPF(MetaInformation):
def get_title(self):
title = self.soup.package.metadata.find('dc:title')
if title:
if title and title.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip()
return self.default_title.strip()
@ -253,7 +256,7 @@ class OPF(MetaInformation):
role = elem.get('opf:role')
if not role:
role = 'aut'
if role == 'aut':
if role == 'aut' and elem.string:
raw = self.ENTITY_PATTERN.sub(entity_to_unicode, elem.string)
au = raw.split(',')
ans = []
@ -293,13 +296,13 @@ class OPF(MetaInformation):
def get_category(self):
category = self.soup.find('dc:type')
if category:
if category and category.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip()
return None
def get_publisher(self):
publisher = self.soup.find('dc:publisher')
if publisher:
if publisher and publisher.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip()
return None
@ -308,7 +311,7 @@ class OPF(MetaInformation):
scheme = item.get('scheme')
if not scheme:
scheme = item.get('opf:scheme')
if scheme is not None and scheme.lower() == 'isbn':
if scheme is not None and scheme.lower() == 'isbn' and item.string:
return str(item.string).strip()
return None
@ -320,7 +323,10 @@ class OPF(MetaInformation):
def get_application_id(self):
for item in self.soup.package.metadata.findAll('dc:identifier'):
if item.has_key('scheme') and item['scheme'] == __appname__:
scheme = item.get('scheme', None)
if scheme is None:
scheme = item.get('opf:scheme', None)
if scheme in ['libprs500', 'calibre']:
return str(item.string).strip()
return None
@ -353,7 +359,7 @@ class OPF(MetaInformation):
def get_series_index(self):
s = self.soup.package.metadata.find('series-index')
if s:
if s and s.string:
try:
return int(str(s.string).strip())
except:
@ -361,11 +367,8 @@ class OPF(MetaInformation):
return None
def get_rating(self):
xm = self.soup.package.metadata.find('x-metadata')
if not xm:
return None
s = xm.find('rating')
if s:
s = self.soup.package.metadata.find('rating')
if s and s.string:
try:
return int(str(s.string).strip())
except:
@ -483,7 +486,7 @@ class OPFCreator(MetaInformation):
Set the toc. You must call :method:`create_spine` before calling this
method.
`toc`: A :class:`TOC` object
:param toc: A :class:`TOC` object
'''
self.toc = toc
@ -491,12 +494,21 @@ class OPFCreator(MetaInformation):
self.guide = Guide.from_opf_guide(guide_element, self.base_path)
self.guide.set_basedir(self.base_path)
def render(self, opf_stream, ncx_stream=None):
def render(self, opf_stream, ncx_stream=None, ncx_manifest_entry=None):
from calibre.resources import opf_template
from calibre.utils.genshi.template import MarkupTemplate
template = MarkupTemplate(opf_template)
if self.manifest:
self.manifest.set_basedir(self.base_path)
if ncx_manifest_entry is not None:
if not os.path.isabs(ncx_manifest_entry):
ncx_manifest_entry = os.path.join(self.base_path, ncx_manifest_entry)
remove = [i for i in self.manifest if i.id == 'ncx']
for item in remove:
self.manifest.remove(item)
self.manifest.append(ManifestItem(ncx_manifest_entry, self.base_path))
self.manifest[-1].id = 'ncx'
self.manifest[-1].mime_type = 'application/x-dtbncx+xml'
if not self.guide:
self.guide = Guide()
if self.cover:
@ -506,7 +518,7 @@ class OPFCreator(MetaInformation):
self.guide.set_cover(cover)
self.guide.set_basedir(self.base_path)
opf = template.generate(__appname__=__appname__, mi=self).render('xml')
opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
opf_stream.write(opf)
opf_stream.flush()
toc = getattr(self, 'toc', None)

View File

@ -8,6 +8,7 @@
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:title py:with="attrs={'opf:files-as':mi.title_sort}" py:attrs="attrs">${mi.title}</dc:title>
<dc:creator opf:role="aut" py:for="i, author in enumerate(mi.authors)" py:with="attrs={'opf:file-as':mi.author_sort if i==0 else None}" py:attrs="attrs">${author}</dc:creator>
<dc:contributor opf:role="bkp" py:with="attrs={'opf:files-as':__appname__}" py:attrs="attrs">${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]</dc:contributor>
<dc:identifier opf:scheme="${__appname__}" id="${__appname__}_id">${mi.application_id}</dc:identifier>
<dc:language>${mi.language if mi.language else 'Unknown'}</dc:language>
@ -16,13 +17,19 @@
<dc:publisher py:if="mi.publisher">${mi.publisher}</dc:publisher>
<dc:identifier opf:scheme="ISBN" py:if="mi.isbn">${mi.isbn}</dc:identifier>
<series py:if="mi.series">${mi.series}</series>
<series-index py:if="mi.series_index is not None">${mi.series_index}</series-index>
<series_index py:if="mi.series_index is not None">${mi.series_index}</series_index>
<rating py:if="mi.rating is not None">${mi.rating}</rating>
<py:for each="tag in mi.tags">
<dc:subject py:if="mi.tags is not None">${tag}</dc:subject>
</py:for>
</metadata>
<manifest py:if="getattr(mi, 'manifest', None)">
<py:for each="ref in mi.manifest">
<item id="${ref.id}" href="${ref.href()}" media-type="${ref.mime_type}" />
</py:for>
</manifest>
<guide py:if="getattr(mi, 'guide', None)">
<py:for each="ref in mi.guide">
<reference type="${ref.type}" href="${ref.href()}" py:with="attrs={'title': ref.title if ref.title else None}" py:attrs="attrs" />
@ -36,10 +43,5 @@
</py:for>
</spine>
<manifest py:if="getattr(mi, 'manifest', None)">
<py:for each="ref in mi.manifest">
<item id="${ref.id}" href="${ref.href()}" media-type="${ref.mime_type}" />
</py:for>
</manifest>
</package>

View File

@ -0,0 +1,892 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
lxml based OPF parser.
'''
import sys, unittest, functools, os, mimetypes, uuid, glob
from urllib import unquote
from urlparse import urlparse
from lxml import etree
from calibre.ebooks.chardet import xml_to_unicode
from calibre import relpath
from calibre.constants import __appname__, __version__
from calibre.ebooks.metadata.toc import TOC
from calibre.ebooks.metadata import MetaInformation
class Resource(object):
'''
Represents a resource (usually a file on the filesystem or a URL pointing
to the web. Such resources are commonly referred to in OPF files.
They have the interface:
:member:`path`
:member:`mime_type`
:method:`href`
'''
def __init__(self, href_or_path, basedir=os.getcwd(), is_path=True):
self._href = None
self._basedir = basedir
self.path = None
self.fragment = ''
try:
self.mime_type = mimetypes.guess_type(href_or_path)[0]
except:
self.mime_type = None
if self.mime_type is None:
self.mime_type = 'application/octet-stream'
if is_path:
path = href_or_path
if not os.path.isabs(path):
path = os.path.abspath(os.path.join(basedir, path))
if isinstance(path, str):
path = path.decode(sys.getfilesystemencoding())
self.path = path
else:
href_or_path = href_or_path
url = urlparse(href_or_path)
if url[0] not in ('', 'file'):
self._href = href_or_path
else:
pc = url[2]
if isinstance(pc, unicode):
pc = pc.encode('utf-8')
pc = pc.decode('utf-8')
self.path = os.path.abspath(os.path.join(basedir, pc.replace('/', os.sep)))
self.fragment = url[-1]
def href(self, basedir=None):
'''
Return a URL pointing to this resource. If it is a file on the filesystem
the URL is relative to `basedir`.
`basedir`: If None, the basedir of this resource is used (see :method:`set_basedir`).
If this resource has no basedir, then the current working directory is used as the basedir.
'''
if basedir is None:
if self._basedir:
basedir = self._basedir
else:
basedir = os.getcwd()
if self.path is None:
return self._href
f = self.fragment.encode('utf-8') if isinstance(self.fragment, unicode) else self.fragment
frag = '#'+f if self.fragment else ''
if self.path == basedir:
return ''+frag
try:
rpath = relpath(self.path, basedir)
except OSError: # On windows path and basedir could be on different drives
rpath = self.path
if isinstance(rpath, unicode):
rpath = rpath.encode('utf-8')
return rpath.replace(os.sep, '/')+frag
def set_basedir(self, path):
self._basedir = path
def basedir(self):
return self._basedir
def __repr__(self):
return 'Resource(%s, %s)'%(repr(self.path), repr(self.href()))
class ResourceCollection(object):
def __init__(self):
self._resources = []
def __iter__(self):
for r in self._resources:
yield r
def __len__(self):
return len(self._resources)
def __getitem__(self, index):
return self._resources[index]
def __bool__(self):
return len(self._resources) > 0
def __str__(self):
resources = map(repr, self)
return '[%s]'%', '.join(resources)
def __repr__(self):
return str(self)
def append(self, resource):
if not isinstance(resource, Resource):
raise ValueError('Can only append objects of type Resource')
self._resources.append(resource)
def remove(self, resource):
self._resources.remove(resource)
def replace(self, start, end, items):
'Same as list[start:end] = items'
self._resources[start:end] = items
@staticmethod
def from_directory_contents(top, topdown=True):
collection = ResourceCollection()
for spec in os.walk(top, topdown=topdown):
path = os.path.abspath(os.path.join(spec[0], spec[1]))
res = Resource.from_path(path)
res.set_basedir(top)
collection.append(res)
return collection
def set_basedir(self, path):
for res in self:
res.set_basedir(path)
class ManifestItem(Resource):
@staticmethod
def from_opf_manifest_item(item, basedir):
href = item.get('href', None)
if href:
res = ManifestItem(href, basedir=basedir, is_path=True)
mt = item.get('media-type', '').strip()
if mt:
res.mime_type = mt
return res
@apply
def media_type():
def fget(self):
return self.mime_type
def fset(self, val):
self.mime_type = val
return property(fget=fget, fset=fset)
def __unicode__(self):
return u'<item id="%s" href="%s" media-type="%s" />'%(self.id, self.href(), self.media_type)
def __str__(self):
return unicode(self).encode('utf-8')
def __repr__(self):
return unicode(self)
def __getitem__(self, index):
if index == 0:
return self.href()
if index == 1:
return self.media_type
raise IndexError('%d out of bounds.'%index)
class Manifest(ResourceCollection):
@staticmethod
def from_opf_manifest_element(items, dir):
m = Manifest()
for item in items:
try:
m.append(ManifestItem.from_opf_manifest_item(item, dir))
id = item.get('id', '')
if not id:
id = 'id%d'%m.next_id
m[-1].id = id
m.next_id += 1
except ValueError:
continue
return m
@staticmethod
def from_paths(entries):
'''
`entries`: List of (path, mime-type) If mime-type is None it is autodetected
'''
m = Manifest()
for path, mt in entries:
mi = ManifestItem(path, is_path=True)
if mt:
mi.mime_type = mt
mi.id = 'id%d'%m.next_id
m.next_id += 1
m.append(mi)
return m
def add_item(self, path, mime_type=None):
mi = ManifestItem(path, is_path=True)
if mime_type:
mi.mime_type = mime_type
mi.id = 'id%d'%self.next_id
self.next_id += 1
self.append(mi)
return mi.id
def __init__(self):
ResourceCollection.__init__(self)
self.next_id = 1
def item(self, id):
for i in self:
if i.id == id:
return i
def id_for_path(self, path):
path = os.path.normpath(os.path.abspath(path))
for i in self:
if i.path and os.path.normpath(i.path) == path:
return i.id
def path_for_id(self, id):
for i in self:
if i.id == id:
return i.path
class Spine(ResourceCollection):
class Item(Resource):
def __init__(self, idfunc, *args, **kwargs):
Resource.__init__(self, *args, **kwargs)
self.is_linear = True
self.id = idfunc(self.path)
@staticmethod
def from_opf_spine_element(itemrefs, manifest):
s = Spine(manifest)
for itemref in itemrefs:
idref = itemref.get('idref', None)
if idref is not None:
r = Spine.Item(s.manifest.id_for_path,
s.manifest.path_for_id(idref), is_path=True)
r.is_linear = itemref.get('linear', 'yes') == 'yes'
s.append(r)
return s
@staticmethod
def from_paths(paths, manifest):
s = Spine(manifest)
for path in paths:
try:
s.append(Spine.Item(s.manifest.id_for_path, path, is_path=True))
except:
continue
return s
def __init__(self, manifest):
ResourceCollection.__init__(self)
self.manifest = manifest
def replace(self, start, end, ids):
'''
Replace the items between start (inclusive) and end (not inclusive) with
with the items identified by ids. ids can be a list of any length.
'''
items = []
for id in ids:
path = self.manifest.path_for_id(id)
if path is None:
raise ValueError('id %s not in manifest')
items.append(Spine.Item(lambda x: id, path, is_path=True))
ResourceCollection.replace(start, end, items)
def linear_items(self):
for r in self:
if r.is_linear:
yield r.path
def nonlinear_items(self):
for r in self:
if not r.is_linear:
yield r.path
def items(self):
for i in self:
yield i.path
class Guide(ResourceCollection):
class Reference(Resource):
@staticmethod
def from_opf_resource_item(ref, basedir):
title, href, type = ref.get('title', ''), ref.get('href'), ref.get('type')
res = Guide.Reference(href, basedir, is_path=False)
res.title = title
res.type = type
return res
def __repr__(self):
ans = '<reference type="%s" href="%s" '%(self.type, self.href())
if self.title:
ans += 'title="%s" '%self.title
return ans + '/>'
@staticmethod
def from_opf_guide(references, base_dir=os.getcwdu()):
coll = Guide()
for ref in references:
try:
ref = Guide.Reference.from_opf_resource_item(ref, base_dir)
coll.append(ref)
except:
continue
return coll
def set_cover(self, path):
map(self.remove, [i for i in self if 'cover' in i.type.lower()])
for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
self.append(Guide.Reference(path, is_path=True))
self[-1].type = type
self[-1].title = ''
class MetadataField(object):
def __init__(self, name, is_dc=True, formatter=None, none_is=None):
self.name = name
self.is_dc = is_dc
self.formatter = formatter
self.none_is = none_is
def __real_get__(self, obj, type=None):
ans = obj.get_metadata_element(self.name)
if ans is None:
return None
ans = obj.get_text(ans)
if ans is None:
return ans
if self.formatter is not None:
try:
ans = self.formatter(ans)
except:
return None
return ans
def __get__(self, obj, type=None):
ans = self.__real_get__(obj, type)
if ans is None:
ans = self.none_is
return ans
def __set__(self, obj, val):
elem = obj.get_metadata_element(self.name)
if elem is None:
elem = obj.create_metadata_element(self.name, ns='dc' if self.is_dc else 'opf')
elem.text = unicode(val)
class OPF(object):
MIMETYPE = 'application/oebps-package+xml'
PARSER = etree.XMLParser(recover=True)
NAMESPACES = {
None : "http://www.idpf.org/2007/opf",
'dc' : "http://purl.org/dc/elements/1.1/",
'opf' : "http://www.idpf.org/2007/opf",
}
xpn = NAMESPACES.copy()
xpn.pop(None)
xpn['re'] = 'http://exslt.org/regular-expressions'
XPath = functools.partial(etree.XPath, namespaces=xpn)
TEXT = XPath('string()')
metadata_path = XPath('descendant::*[re:match(name(), "metadata", "i")]')
metadata_elem_path = XPath('descendant::*[re:match(name(), $name, "i")]')
series_path = XPath('descendant::*[re:match(name(), "series$", "i")]')
authors_path = XPath('descendant::*[re:match(name(), "creator", "i") and (@role="aut" or @opf:role="aut")]')
bkp_path = XPath('descendant::*[re:match(name(), "contributor", "i") and (@role="bkp" or @opf:role="bkp")]')
tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]')
isbn_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
'(re:match(@scheme, "isbn", "i") or re:match(@opf:scheme, "isbn", "i"))]')
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]')
guide_path = XPath('descendant::*[re:match(name(), "guide", "i")]/*[re:match(name(), "reference", "i")]')
title = MetadataField('title')
publisher = MetadataField('publisher')
language = MetadataField('language')
comments = MetadataField('description')
category = MetadataField('category')
series_index = MetadataField('series_index', is_dc=False, formatter=int, none_is=1)
rating = MetadataField('rating', is_dc=False, formatter=int)
def __init__(self, stream, basedir=os.getcwdu()):
if not hasattr(stream, 'read'):
stream = open(stream, 'rb')
self.basedir = self.base_dir = basedir
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
self.root = etree.fromstring(raw, self.PARSER)
self.metadata = self.metadata_path(self.root)
if not self.metadata:
raise ValueError('Malformed OPF file: No <metadata> element')
self.metadata = self.metadata[0]
self.unquote_urls()
self.manifest = Manifest()
m = self.manifest_path(self.root)
if m:
self.manifest = Manifest.from_opf_manifest_element(m, basedir)
self.spine = None
s = self.spine_path(self.root)
if s:
self.spine = Spine.from_opf_spine_element(s, self.manifest)
self.guide = None
guide = self.guide_path(self.root)
self.guide = Guide.from_opf_guide(guide, basedir) if guide else None
self.cover_data = (None, None)
self.find_toc()
def find_toc(self):
self.toc = None
try:
spine = self.XPath('descendant::*[re:match(name(), "spine", "i")]')(self.root)
toc = None
if spine:
spine = spine[0]
toc = spine.get('toc', None)
if toc is None and self.guide:
for item in self.guide:
if item.type and item.type.lower() == 'toc':
toc = item.path
if toc is None:
for item in self.manifest:
if 'toc' in item.href().lower():
toc = item.path
if toc is None: return
self.toc = TOC(base_path=self.base_dir)
if toc.lower() in ('ncx', 'ncxtoc'):
path = self.manifest.path_for_id(toc)
if path:
self.toc.read_ncx_toc(path)
else:
f = glob.glob(os.path.join(self.base_dir, '*.ncx'))
if f:
self.toc.read_ncx_toc(f[0])
else:
self.toc.read_html_toc(toc)
except:
pass
def get_text(self, elem):
return u''.join(self.TEXT(elem))
def itermanifest(self):
return self.manifest_path(self.root)
def create_manifest_item(self, href, media_type):
ids = [i.get('id', None) for i in self.itermanifest()]
id = None
for c in xrange(1, sys.maxint):
id = 'id%d'%c
if id not in ids:
break
if not media_type:
media_type = 'application/xhtml+xml'
ans = etree.Element('{%s}item'%self.NAMESPACES['opf'],
attrib={'id':id, 'href':href, 'media-type':media_type})
ans.tail = '\n\t\t'
return ans
def replace_manifest_item(self, item, items):
items = [self.create_manifest_item(*i) for i in items]
for i, item2 in enumerate(items):
item2.set('id', item.get('id')+'.%d'%(i+1))
manifest = item.getparent()
index = manifest.index(item)
manifest[index:index+1] = items
return [i.get('id') for i in items]
def iterspine(self):
return self.spine_path(self.root)
def create_spine_item(self, idref):
ans = etree.Element('{%s}itemref'%self.NAMESPACES['opf'], idref=idref)
ans.tail = '\n\t\t'
return ans
def replace_spine_items_by_idref(self, idref, new_idrefs):
items = list(map(self.create_spine_item, new_idrefs))
spine = self.XPath('/opf:package/*[re:match(name(), "spine", "i")]')(self.root)[0]
old = [i for i in self.iterspine() if i.get('idref', None) == idref]
for x in old:
i = spine.index(x)
spine[i:i+1] = items
def create_guide_element(self):
e = etree.SubElement(self.root, '{%s}guide'%self.NAMESPACES['opf'])
e.text = '\n '
e.tail = '\n'
return e
def remove_guide(self):
self.guide = None
for g in self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'}):
self.root.remove(g)
def create_guide_item(self, type, title, href):
e = etree.Element('{%s}reference'%self.NAMESPACES['opf'],
type=type, title=title, href=href)
e.tail='\n'
return e
def add_guide_item(self, type, title, href):
g = self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'})[0]
g.append(self.create_guide_item(type, title, href))
def iterguide(self):
return self.guide_path(self.root)
def unquote_urls(self):
def get_href(item):
raw = unquote(item.get('href', ''))
if not isinstance(raw, unicode):
raw = raw.decode('utf-8')
return raw
for item in self.itermanifest():
item.set('href', get_href(item))
for item in self.iterguide():
item.set('href', get_href(item))
@apply
def authors():
def fget(self):
ans = []
for elem in self.authors_path(self.metadata):
ans.extend([x.strip() for x in self.get_text(elem).split(',')])
return ans
def fset(self, val):
remove = list(self.authors_path(self.metadata))
for elem in remove:
self.metadata.remove(elem)
for author in val:
elem = self.create_metadata_element('creator', ns='dc',
attrib={'{%s}role'%self.NAMESPACES['opf']:'aut'})
elem.text = author
return property(fget=fget, fset=fset)
@apply
def author_sort():
def fget(self):
matches = self.authors_path(self.metadata)
if matches:
ans = matches[0].get('opf:file-as', None)
return ans if ans else matches[0].get('file-as', None)
def fset(self, val):
matches = self.authors_path(self.metadata)
if matches:
matches[0].set('file-as', unicode(val))
return property(fget=fget, fset=fset)
@apply
def tags():
def fget(self):
ans = []
for tag in self.tags_path(self.metadata):
ans.append(self.get_text(tag))
return ans
def fset(self, val):
for tag in list(self.tags_path(self.metadata)):
self.metadata.remove(tag)
for tag in val:
elem = self.create_metadata_element('subject', ns='dc')
elem.text = unicode(tag)
return property(fget=fget, fset=fset)
@apply
def isbn():
def fget(self):
for match in self.isbn_path(self.metadata):
return match.text if match.text else None
def fset(self, val):
matches = self.isbn_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('identifier', ns='dc',
attrib={'{%s}scheme'%self.NAMESPACES['opf']:'ISBN'})]
matches[0].text = unicode(val)
return property(fget=fget, fset=fset)
@apply
def series():
def fget(self):
for match in self.series_path(self.metadata):
return match.text if match.text else None
def fset(self, val):
matches = self.series_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('series')]
matches[0].text = unicode(val)
return property(fget=fget, fset=fset)
@apply
def book_producer():
def fget(self):
for match in self.bkp_path(self.metadata):
return match.text if match.text else None
def fset(self, val):
matches = self.bkp_path(self.metadata)
if not matches:
matches = [self.create_metadata_element('contributor', ns='dc',
attrib={'{%s}role'%self.NAMESPACES['opf']:'bkp'})]
matches[0].text = unicode(val)
return property(fget=fget, fset=fset)
@apply
def cover():
def fget(self):
if self.guide is not None:
for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
for item in self.guide:
if item.type.lower() == t:
return item.path
def fset(self, path):
if self.guide is not None:
self.guide.set_cover(path)
for item in list(self.iterguide()):
if 'cover' in item.get('type', ''):
item.getparent().remove(item)
else:
g = self.create_guide_element()
self.guide = Guide()
self.guide.set_cover(path)
etree.SubElement(g, 'opf:reference', nsmap=self.NAMESPACES,
attrib={'type':'cover', 'href':self.guide[-1].href()})
id = self.manifest.id_for_path(self.cover)
if id is None:
for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
for item in self.guide:
if item.type.lower() == t:
self.create_manifest_item(item.href(), mimetypes.guess_type(path)[0])
return property(fget=fget, fset=fset)
def get_metadata_element(self, name):
matches = self.metadata_elem_path(self.metadata, name=name)
if matches:
return matches[-1]
def create_metadata_element(self, name, attrib=None, ns='opf'):
elem = etree.SubElement(self.metadata, '{%s}%s'%(self.NAMESPACES[ns], name),
attrib=attrib, nsmap=self.NAMESPACES)
elem.tail = '\n'
return elem
def render(self, encoding='utf-8'):
return etree.tostring(self.root, encoding='utf-8', pretty_print=True)
def smart_update(self, mi):
for attr in ('author_sort', 'title_sort', 'comments', 'category',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'language', 'tags', 'title', 'authors'):
val = getattr(mi, attr, None)
if val is not None and val != [] and val != (None, None):
setattr(self, attr, val)
class OPFCreator(MetaInformation):
def __init__(self, base_path, *args, **kwargs):
'''
Initialize.
@param base_path: An absolute path to the directory in which this OPF file
will eventually be. This is used by the L{create_manifest} method
to convert paths to files into relative paths.
'''
MetaInformation.__init__(self, *args, **kwargs)
self.base_path = os.path.abspath(base_path)
if self.application_id is None:
self.application_id = str(uuid.uuid4())
if not isinstance(self.toc, TOC):
self.toc = None
if not self.authors:
self.authors = [_('Unknown')]
if self.guide is None:
self.guide = Guide()
if self.cover:
self.guide.set_cover(self.cover)
def create_manifest(self, entries):
'''
Create <manifest>
`entries`: List of (path, mime-type) If mime-type is None it is autodetected
'''
entries = map(lambda x: x if os.path.isabs(x[0]) else
(os.path.abspath(os.path.join(self.base_path, x[0])), x[1]),
entries)
self.manifest = Manifest.from_paths(entries)
self.manifest.set_basedir(self.base_path)
def create_manifest_from_files_in(self, files_and_dirs):
entries = []
def dodir(dir):
for spec in os.walk(dir):
root, files = spec[0], spec[-1]
for name in files:
path = os.path.join(root, name)
if os.path.isfile(path):
entries.append((path, None))
for i in files_and_dirs:
if os.path.isdir(i):
dodir(i)
else:
entries.append((i, None))
self.create_manifest(entries)
def create_spine(self, entries):
'''
Create the <spine> element. Must first call :method:`create_manifest`.
`entries`: List of paths
'''
entries = map(lambda x: x if os.path.isabs(x) else
os.path.abspath(os.path.join(self.base_path, x)), entries)
self.spine = Spine.from_paths(entries, self.manifest)
def set_toc(self, toc):
'''
Set the toc. You must call :method:`create_spine` before calling this
method.
:param toc: A :class:`TOC` object
'''
self.toc = toc
def create_guide(self, guide_element):
self.guide = Guide.from_opf_guide(guide_element, self.base_path)
self.guide.set_basedir(self.base_path)
def render(self, opf_stream, ncx_stream=None, ncx_manifest_entry=None):
from calibre.resources import opf_template
from calibre.utils.genshi.template import MarkupTemplate
template = MarkupTemplate(opf_template)
if self.manifest:
self.manifest.set_basedir(self.base_path)
if ncx_manifest_entry is not None:
if not os.path.isabs(ncx_manifest_entry):
ncx_manifest_entry = os.path.join(self.base_path, ncx_manifest_entry)
remove = [i for i in self.manifest if i.id == 'ncx']
for item in remove:
self.manifest.remove(item)
self.manifest.append(ManifestItem(ncx_manifest_entry, self.base_path))
self.manifest[-1].id = 'ncx'
self.manifest[-1].mime_type = 'application/x-dtbncx+xml'
if not self.guide:
self.guide = Guide()
if self.cover:
cover = self.cover
if not os.path.isabs(cover):
cover = os.path.abspath(os.path.join(self.base_path, cover))
self.guide.set_cover(cover)
self.guide.set_basedir(self.base_path)
opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
opf_stream.write(opf)
opf_stream.flush()
toc = getattr(self, 'toc', None)
if toc is not None and ncx_stream is not None:
toc.render(ncx_stream, self.application_id)
ncx_stream.flush()
class OPFTest(unittest.TestCase):
def setUp(self):
import cStringIO
self.stream = cStringIO.StringIO(
'''\
<?xml version="1.0" encoding="UTF-8"?>
<package version="2.0" xmlns="http://www.idpf.org/2007/opf" >
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:title>A Cool &amp; &copy; &#223; Title</dc:title>
<creator opf:role="aut" file-as="Monkey">Monkey Kitchen, Next</creator>
<dc:subject>One</dc:subject><dc:subject>Two</dc:subject>
<dc:identifier scheme="ISBN">123456789</dc:identifier>
<x-metadata>
<series>A one book series</series>
</x-metadata>
</metadata>
<manifest>
<item id="1" href="a%20%7E%20b" media-type="text/txt" />
</manifest>
</package>
'''
)
self.opf = OPF(self.stream, os.getcwd())
def testReading(self):
opf = self.opf
self.assertEqual(opf.title, u'A Cool & \xa9 \xdf Title')
self.assertEqual(opf.authors, u'Monkey Kitchen,Next'.split(','))
self.assertEqual(opf.author_sort, 'Monkey')
self.assertEqual(opf.tags, ['One', 'Two'])
self.assertEqual(opf.isbn, '123456789')
self.assertEqual(opf.series, 'A one book series')
self.assertEqual(opf.series_index, None)
self.assertEqual(list(opf.itermanifest())[0].get('href'), 'a ~ b')
def testWriting(self):
for test in [('title', 'New & Title'), ('authors', ['One', 'Two']),
('author_sort', "Kitchen"), ('tags', ['Three']),
('isbn', 'a'), ('rating', 3), ('series_index', 1)]:
setattr(self.opf, *test)
self.assertEqual(getattr(self.opf, test[0]), test[1])
self.opf.render()
def suite():
return unittest.TestLoader().loadTestsFromTestCase(OPFTest)
def test():
unittest.TextTestRunner(verbosity=2).run(suite())
if __name__ == '__main__':
sys.exit(test())

View File

@ -5,12 +5,11 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.pyPdf import PdfFileReader
from pyPdf import PdfFileReader
def get_metadata(stream):
""" Return metadata as a L{MetaInfo} object """
title = 'Unknown'
mi = MetaInformation(title, ['Unknown'])
mi = MetaInformation(_('Unknown'), [_('Unknown')])
stream.seek(0)
try:
info = PdfFileReader(stream).getDocumentInfo()

View File

@ -0,0 +1,68 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Ashish Kulkarni <kulkarni.ashish@gmail.com>'
'''Read meta information from RB files'''
import sys, os, struct
from calibre.ebooks.metadata import MetaInformation
MAGIC = '\xb0\x0c\xb0\x0c\x02\x00NUVO\x00\x00\x00\x00'
def get_metadata(stream):
""" Return metadata as a L{MetaInfo} object """
title = 'Unknown'
mi = MetaInformation(title, ['Unknown'])
stream.seek(0)
try:
if not stream.read(14) == MAGIC:
print >>sys.stderr, u'Couldn\'t read RB header from file'
return mi
stream.read(10)
read_i32 = lambda: struct.unpack('<I', stream.read(4))[0]
stream.seek(read_i32())
toc_count = read_i32()
for i in range(toc_count):
stream.read(32)
length, offset, flag = read_i32(), read_i32(), read_i32()
if flag == 2: break
else:
print >>sys.stderr, u'Couldn\'t find INFO from RB file'
return mi
stream.seek(offset)
info = stream.read(length).splitlines()
for line in info:
if not '=' in line:
continue
key, value = line.split('=')
if key.strip() == 'TITLE':
mi.title = value.strip()
elif key.strip() == 'AUTHOR':
src = value.split('&')
authors = []
for au in src:
authors += au.split(',')
mi.authors = authors
mi.author = value
except Exception, err:
msg = u'Couldn\'t read metadata from rb: %s with error %s'%(mi.title, unicode(err))
print >>sys.stderr, msg.encode('utf8')
raise
return mi
def main(args=sys.argv):
if len(args) != 2:
print >>sys.stderr, _('Usage: rb-meta file.rb')
print >>sys.stderr, _('No filename specified.')
return 1
path = os.path.abspath(os.path.expanduser(args[1]))
print get_metadata(open(path, 'rb'))
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, glob, sys
import os, glob
from urlparse import urlparse
from urllib import unquote
@ -21,18 +21,41 @@ class NCXSoup(BeautifulStoneSoup):
class TOC(list):
def __init__(self, href=None, fragment=None, text=None, parent=None, play_order=0,
base_path=os.getcwd()):
base_path=os.getcwd(), type='unknown'):
self.href = href
self.fragment = fragment
if not self.fragment:
self.fragment = None
self.text = text
self.parent = parent
self.base_path = base_path
self.play_order = play_order
self.type = type
def add_item(self, href, fragment, text):
play_order = (self[-1].play_order if len(self) else self.play_order) + 1
def count(self, type):
return len([i for i in self.flat() if i.type == type])
def purge(self, types, max=0):
remove = []
for entry in self.flat():
if entry.type in types:
remove.append(entry)
remove = remove[max:]
for entry in remove:
if entry.parent is None:
continue
entry.parent.remove(entry)
return remove
def remove(self, entry):
list.remove(self, entry)
entry.parent = None
def add_item(self, href, fragment, text, play_order=None, type='unknown'):
if play_order is None:
play_order = (self[-1].play_order if len(self) else self.play_order) + 1
self.append(TOC(href=href, fragment=fragment, text=text, parent=self,
base_path=self.base_path, play_order=play_order))
base_path=self.base_path, play_order=play_order, type=type))
return self[-1]
def top_level_items(self):
@ -48,14 +71,24 @@ class TOC(list):
depth = c + 1
return depth
def flat(self):
'Depth first iteration over the tree rooted at self'
yield self
for obj in self:
for i in obj.flat():
yield i
@apply
def abspath():
doc='Return the file this toc entry points to as a absolute path to a file on the system.'
def fget(self):
if self.href is None:
return None
path = self.href.replace('/', os.sep)
if not os.path.isabs(path):
path = os.path.join(self.base_path, path)
return path
return property(fget=fget, doc=doc)
def read_from_opf(self, opfreader):
@ -85,15 +118,15 @@ class TOC(list):
self.read_html_toc(toc)
except:
print 'WARNING: Could not read Table of Contents:'
import traceback
traceback.print_exc(file=sys.stdout)
print 'Continuing anyway'
print 'WARNING: Could not read Table of Contents. Continuing anyway.'
else:
path = opfreader.manifest.item(toc.lower())
path = getattr(path, 'path', path)
if path and os.access(path, os.R_OK):
self.read_ncx_toc(path)
try:
self.read_ncx_toc(path)
except Exception, err:
print 'WARNING: Invalid NCX file:', err
return
cwd = os.path.abspath(self.base_path)
m = glob.glob(os.path.join(cwd, '*.ncx'))
@ -106,14 +139,16 @@ class TOC(list):
soup = NCXSoup(xml_to_unicode(open(toc, 'rb').read())[0])
def process_navpoint(np, dest):
play_order = np.get('playOrder', 1)
play_order = np.get('playOrder', None)
if play_order is None:
play_order = int(np.get('playorder', 1))
href = fragment = text = None
nl = np.find('navlabel')
if nl is not None:
text = u''
for txt in nl.findAll('text'):
text += ''.join([unicode(s) for s in txt.findAll(text=True)])
content = elem.find('content')
content = np.find('content')
if content is None or not content.has_key('src') or not txt:
return
@ -143,8 +178,20 @@ class TOC(list):
continue
purl = urlparse(unquote(a['href']))
href, fragment = purl[2], purl[5]
if not fragment:
fragment = None
else:
fragment = fragment.strip()
href = href.strip()
txt = ''.join([unicode(s).strip() for s in a.findAll(text=True)])
self.add_item(href, fragment, txt)
add = True
for i in self.flat():
if i.href == href and i.fragment == fragment:
add = False
break
if add:
self.add_item(href, fragment, txt)
def render(self, stream, uid):
from calibre.resources import ncx_template

View File

@ -12,7 +12,8 @@ try:
except ImportError:
import Image as PILImage
from calibre import __appname__
from calibre import __appname__, entity_to_unicode
from calibre.ebooks import DRMError
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag
from calibre.ebooks.mobi import MobiError
from calibre.ebooks.mobi.huffcdic import HuffReader
@ -160,12 +161,12 @@ class MobiReader(object):
self.book_header = BookHeader(self.sections[0][0], self.ident)
self.name = self.name.decode(self.book_header.codec, 'replace')
def extract_content(self, output_dir=os.getcwdu()):
output_dir = os.path.abspath(output_dir)
if self.book_header.encryption_type != 0:
raise MobiError('Cannot extract content from a DRM protected ebook')
raise DRMError(self.name)
processed_records = self.extract_text()
self.add_anchors()
@ -176,15 +177,17 @@ class MobiReader(object):
self.processed_html = \
re.compile('<head>', re.IGNORECASE).sub(
'<head>\n'
'\n<head>\n'
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\n'
'<style type="text/css">\n'
'blockquote { margin: 0em 0em 0em 1.25em; text-align: justify; }\n'
'p { margin: 0em; text-align: justify; }\n'
'.bold { font-weight: bold; }\n'
'.italic { font-style: italic; }\n'
'</style>\n',
self.processed_html)
soup = BeautifulSoup(self.processed_html.replace('> <', '>\n<'))
soup = BeautifulSoup(self.processed_html)
self.cleanup_soup(soup)
guide = soup.find('guide')
for elem in soup.findAll(['metadata', 'guide']):
@ -210,6 +213,11 @@ class MobiReader(object):
self.processed_html = re.sub(r'<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.processed_html)
if self.book_header.ancient and '<html' not in self.mobi_html[:300].lower():
self.processed_html = '<html><p>'+self.processed_html.replace('\n\n', '<p>')+'</html>'
self.processed_html = self.processed_html.replace('> <', '>\n<')
self.processed_html = self.processed_html.replace('<b>', '<span class="bold">')
self.processed_html = self.processed_html.replace('<i>', '<span class="italic">')
self.processed_html = self.processed_html.replace('</b>', '</span>')
self.processed_html = self.processed_html.replace('</i>', '</span>')
def cleanup_soup(self, soup):
for tag in soup.recursiveChildGenerator():
@ -256,17 +264,19 @@ class MobiReader(object):
if ref.type.lower() == 'toc':
toc = ref.href()
if toc:
index = self.processed_html.find('<a name="%s"'%toc.partition('#')[-1])
index = self.processed_html.find('<a id="%s" name="%s"'%(toc.partition('#')[-1], toc.partition('#')[-1]))
tocobj = None
ent_pat = re.compile(r'&(\S+?);')
if index > -1:
raw = '<html><body>'+self.processed_html[index:]
soup = BeautifulSoup(raw)
tocobj = TOC()
for a in soup.findAll('a', href=True):
try:
text = ''.join(a.findAll(text=True)).strip()
text = u''.join(a.findAll(text=True)).strip()
except:
text = ''
text = ent_pat.sub(entity_to_unicode, text)
tocobj.add_item(toc.partition('#')[0], a['href'][1:], text)
if tocobj is not None:
opf.set_toc(tocobj)
@ -341,12 +351,14 @@ class MobiReader(object):
pos = 0
self.processed_html = ''
for end in positions:
if end == 0:
continue
oend = end
l = self.mobi_html.find('<', end)
r = self.mobi_html.find('>', end)
if r > -1 and r < l: # Move out of tag
end = r+1
self.processed_html += self.mobi_html[pos:end] + '<a name="filepos%d"></a>'%oend
self.processed_html += self.mobi_html[pos:end] + '<a id="filepos%d" name="filepos%d"></a>'%(oend, oend)
pos = end
self.processed_html += self.mobi_html[pos:]

View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Handle the Open Document Format
'''

View File

@ -0,0 +1,72 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Convert an ODT file into a Open Ebook
'''
import os, sys
from odf.odf2xhtml import ODF2XHTML
from calibre import CurrentDir, walk
from calibre.utils.zipfile import ZipFile
from calibre.utils.config import OptionParser
from calibre.ebooks.metadata.odt import get_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator
class Extract(ODF2XHTML):
def extract_pictures(self, zf):
if not os.path.exists('Pictures'):
os.makedirs('Pictures')
for name in zf.namelist():
if name.startswith('Pictures'):
data = zf.read(name)
with open(name, 'wb') as f:
f.write(data)
def __call__(self, path, odir):
if not os.path.exists(odir):
os.makedirs(odir)
path = os.path.abspath(path)
with CurrentDir(odir):
print 'Extracting ODT file...'
html = self.odf2xhtml(path)
with open('index.html', 'wb') as f:
f.write(html.encode('utf-8'))
with open(path, 'rb') as f:
zf = ZipFile(f, 'r')
self.extract_pictures(zf)
f.seek(0)
mi = get_metadata(f)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(path))
if not mi.authors:
mi.authors = [_('Unknown')]
opf = OPFCreator(os.path.abspath(os.getcwdu()), mi)
opf.create_manifest([(os.path.abspath(f), None) for f in walk(os.getcwd())])
opf.create_spine([os.path.abspath('index.html')])
with open('metadata.opf', 'wb') as f:
opf.render(f)
return os.path.abspath('metadata.opf')
def option_parser():
parser = OptionParser('%prog [options] file.odt')
parser.add_option('-o', '--output-dir', default='.',
help=_('The output directory. Defaults to the current directory.'))
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) < 2:
parser.print_help()
print 'No ODT file specified'
return 1
Extract()(args[1], os.path.abspath(opts.output_dir))
print 'Extracted to', os.path.abspath(opts.output_dir)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -3,7 +3,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """
import sys, os, re, StringIO, traceback
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \
QModelIndex
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QDialogButtonBox, QApplication
@ -20,8 +21,8 @@ def _config():
c = Config('gui', 'preferences for the calibre GUI')
c.add_opt('frequently_used_directories', default=[],
help=_('Frequently used directories'))
c.add_opt('send_to_device_by_default', default=True,
help=_('Send downloaded periodical content to device automatically'))
c.add_opt('send_to_storage_card_by_default', default=False,
help=_('Send file to storage card instead of main memory by default'))
c.add_opt('save_to_disk_single_format', default='lrf',
help=_('The format to use when saving single files to disk'))
c.add_opt('confirm_delete', default=False,
@ -36,6 +37,8 @@ def _config():
help=_('Notify when a new version is available'))
c.add_opt('use_roman_numerals_for_series_number', default=True,
help=_('Use Roman numerals for series number'))
c.add_opt('sort_by_popularity', default=False,
help=_('Sort tags list by popularity'))
c.add_opt('cover_flow_queue_length', default=6,
help=_('Number of covers to show in the cover browsing mode'))
c.add_opt('LRF_conversion_defaults', default=[],
@ -159,7 +162,7 @@ class TableView(QTableView):
else:
cols = dynamic[key]
if not cols:
cols = [True for i in range(self.model().columnCount(self))]
cols = [True for i in range(self.model().columnCount(QModelIndex()))]
for i in range(len(cols)):
hidden = self.isColumnHidden(i)
@ -195,6 +198,7 @@ class FileIconProvider(QFileIconProvider):
'prc' : 'mobi',
'azw' : 'mobi',
'mobi' : 'mobi',
'epub' : 'epub',
}
def __init__(self):
@ -221,7 +225,7 @@ class FileIconProvider(QFileIconProvider):
return icon
def icon_from_ext(self, ext):
key = self.key_from_ext(ext)
key = self.key_from_ext(ext.lower() if ext else '')
return self.cached_icon(key)
def load_icon(self, fileinfo):
@ -303,7 +307,7 @@ class FileDialog(QObject):
QObject.connect(self.fd, SIGNAL('accepted()'), self.save_dir)
self.accepted = self.fd.exec_() == QFileDialog.Accepted
else:
dir = dynamic.get(self.dialog_name, default=os.path.expanduser('~'))
dir = dynamic.get(self.dialog_name, os.path.expanduser('~'))
self.selected_files = []
if mode == QFileDialog.AnyFile:
f = qstring_to_unicode(

View File

@ -5,24 +5,68 @@ __docformat__ = 'restructuredtext en'
'''
'''
import textwrap
import textwrap, os
from PyQt4.QtCore import QCoreApplication
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
class BookInfo(QDialog, Ui_BookInfo):
def __init__(self, parent, info):
def __init__(self, parent, view, row):
QDialog.__init__(self, parent)
Ui_BookInfo.__init__(self)
self.setupUi(self)
self.setWindowTitle(info[_('Title')])
desktop = QCoreApplication.instance().desktop()
screen_height = desktop.availableGeometry().height() - 100
self.resize(self.size().width(), screen_height)
self.view = view
self.current_row = None
self.refresh(row)
self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
self.connect(self.next_button, SIGNAL('clicked()'), self.next)
self.connect(self.previous_button, SIGNAL('clicked()'), self.previous)
self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path)
def slave(self, current, previous):
row = current.row()
self.refresh(row)
def open_book_path(self, path):
if os.sep in unicode(path):
QDesktopServices.openUrl(QUrl('file:'+path))
else:
format = unicode(path)
path = self.view.model().db.format_abspath(self.current_row, format)
if path is not None:
QDesktopServices.openUrl(QUrl('file:'+path))
def next(self):
row = self.view.currentIndex().row()
ni = self.view.model().index(row+1, 0)
if ni.isValid():
self.view.setCurrentIndex(ni)
def previous(self):
row = self.view.currentIndex().row()
ni = self.view.model().index(row-1, 0)
if ni.isValid():
self.view.setCurrentIndex(ni)
def refresh(self, row):
if isinstance(row, QModelIndex):
row = row.row()
if row == self.current_row:
return
self.previous_button.setEnabled(False if row == 0 else True)
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
self.current_row = row
info = self.view.model().get_book_info(row)
self.setWindowTitle(info[_('Title')])
self.title.setText('<b>'+info.pop(_('Title')))
self.comments.setText(info.pop(_('Comments'), ''))
@ -37,6 +81,15 @@ class BookInfo(QDialog, Ui_BookInfo):
rows = u''
self.text.setText('')
self.data = info
if _('Path') in info.keys():
p = info[_('Path')]
info[_('Path')] = '<a href="%s">%s</a>'%(p, p)
if _('Formats') in info.keys():
formats = info[_('Formats')].split(',')
info[_('Formats')] = ''
for f in formats:
f = f.strip()
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
for key in info.keys():
txt = info[key]
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))

View File

@ -6,14 +6,14 @@
<x>0</x>
<y>0</y>
<width>917</width>
<height>780</height>
<height>783</height>
</rect>
</property>
<property name="windowTitle" >
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" >
<item>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" colspan="2" >
<widget class="QLabel" name="title" >
<property name="text" >
<string>TextLabel</string>
@ -23,51 +23,68 @@
</property>
</widget>
</item>
<item>
<widget class="QSplitter" name="splitter" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<widget class="QGraphicsView" name="cover" />
<widget class="QWidget" name="" >
<layout class="QVBoxLayout" >
<item row="1" column="0" >
<widget class="QGraphicsView" name="cover" />
</item>
<item row="1" column="1" >
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<widget class="QLabel" name="text" >
<property name="text" >
<string>TextLabel</string>
</property>
<property name="alignment" >
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Comments</string>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QTextBrowser" name="comments" />
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QLabel" name="text" >
<widget class="QPushButton" name="previous_button" >
<property name="text" >
<string>TextLabel</string>
<string>&amp;Previous</string>
</property>
<property name="alignment" >
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap" >
<bool>true</bool>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/previous.svg</normaloff>:/images/previous.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Comments</string>
<widget class="QPushButton" name="next_button" >
<property name="text" >
<string>&amp;Next</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/next.svg</normaloff>:/images/next.svg</iconset>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QTextBrowser" name="comments" />
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<resources>
<include location="../images.qrc" />
</resources>
<connections/>
</ui>

View File

@ -57,6 +57,8 @@ class ComicConf(QDialog, Ui_Dialog):
self.opt_dont_sharpen.setChecked(opts.dont_sharpen)
self.opt_landscape.setChecked(opts.landscape)
self.opt_no_sort.setChecked(opts.no_sort)
self.opt_despeckle.setChecked(opts.despeckle)
self.opt_wide.setChecked(opts.wide)
self.opt_right2left.setChecked(opts.right2left)
for opt in self.config.option_set.preferences:
@ -79,4 +81,4 @@ class ComicConf(QDialog, Ui_Dialog):
else:
raise Exception('Bad coding')
self.config.set(opt.name, val)
return QDialog.accept(self)
return QDialog.accept(self)

View File

@ -100,21 +100,21 @@
</property>
</widget>
</item>
<item row="7" column="0" >
<item row="8" column="0" >
<widget class="QCheckBox" name="opt_landscape" >
<property name="text" >
<string>&amp;Landscape</string>
</property>
</widget>
</item>
<item row="9" column="0" >
<item row="10" column="0" >
<widget class="QCheckBox" name="opt_no_sort" >
<property name="text" >
<string>Don't so&amp;rt</string>
</property>
</widget>
</item>
<item row="10" column="1" >
<item row="12" column="1" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
@ -124,13 +124,27 @@
</property>
</widget>
</item>
<item row="8" column="0" >
<item row="9" column="0" >
<widget class="QCheckBox" name="opt_right2left" >
<property name="text" >
<string>&amp;Right to left</string>
</property>
</widget>
</item>
<item row="11" column="0" >
<widget class="QCheckBox" name="opt_despeckle" >
<property name="text" >
<string>De&amp;speckle</string>
</property>
</widget>
</item>
<item row="7" column="0" >
<widget class="QCheckBox" name="opt_wide" >
<property name="text" >
<string>&amp;Wide</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>

View File

@ -7,7 +7,7 @@ from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant
from calibre import islinux
from calibre.gui2.dialogs.config_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, warning_dialog
from calibre.utils.config import prefs
from calibre.gui2.widgets import FilenamePattern
from calibre.ebooks import BOOK_EXTENSIONS
@ -20,8 +20,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
Ui_Dialog.__init__(self)
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self.setupUi(self)
self.item1 = QListWidgetItem(QIcon(':/images/metadata.svg'), _('Basic'), self.category_list)
self.item2 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list)
self.item1 = QListWidgetItem(QIcon(':/images/metadata.svg'), _('General'), self.category_list)
self.item2 = QListWidgetItem(QIcon(':/images/lookfeel.svg'), _('Interface'), self.category_list)
self.item3 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list)
self.db = db
self.current_cols = columns
path = prefs['library_path']
@ -66,6 +67,23 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.single_format.setCurrentIndex(BOOK_EXTENSIONS.index(single_format))
self.cover_browse.setValue(config['cover_flow_queue_length'])
self.confirm_delete.setChecked(config['confirm_delete'])
from calibre.translations.compiled import translations
from calibre.translations import language_codes
from calibre.startup import get_lang
lang = get_lang()
if lang is not None and language_codes.has_key(lang):
self.language.addItem(language_codes[lang], QVariant(lang))
items = [(l, language_codes[l]) for l in translations.keys() if l != lang]
if lang != 'en':
items.append(('en', 'English'))
items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
for item in items:
self.language.addItem(item[1], QVariant(item[0]))
self.output_format.setCurrentIndex(0 if prefs['output_format'] == 'LRF' else 1)
self.pdf_metadata.setChecked(prefs['read_file_metadata'])
def compact(self, toggled):
d = Vacuum(self, self.db)
@ -97,8 +115,15 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['confirm_delete'] = bool(self.confirm_delete.isChecked())
pattern = self.filename_pattern.commit()
prefs['filename_pattern'] = pattern
prefs['read_file_metadata'] = bool(self.pdf_metadata.isChecked())
config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()]
config['cover_flow_queue_length'] = self.cover_browse.value()
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())
of = str(self.output_format.currentText())
if of != prefs['output_format'] and 'epub' in of.lower():
warning_dialog(self, 'Warning',
'<p>EPUB support is still in beta. If you find bugs, please report them by opening a <a href="http://calibre.kovidgoyal.net">ticket</a>.').exec_()
prefs['output_format'] = of
if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'),

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>709</width>
<height>685</height>
<height>676</height>
</rect>
</property>
<property name="windowTitle" >
@ -22,15 +22,9 @@
<layout class="QHBoxLayout" >
<item>
<widget class="QListWidget" name="category_list" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Minimum" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize" >
<size>
<width>100</width>
<width>140</width>
<height>16777215</height>
</size>
</property>
@ -40,10 +34,22 @@
<bold>true</bold>
</font>
</property>
<property name="mouseTracking" >
<bool>true</bool>
</property>
<property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="iconSize" >
<size>
<width>32</width>
<height>32</height>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="movement" >
@ -55,11 +61,8 @@
<property name="isWrapping" stdset="0" >
<bool>false</bool>
</property>
<property name="gridSize" >
<size>
<width>80</width>
<height>80</height>
</size>
<property name="spacing" >
<number>25</number>
</property>
<property name="viewMode" >
<enum>QListView::IconMode</enum>
@ -78,14 +81,6 @@
<number>0</number>
</property>
<widget class="QWidget" name="page_3" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>640</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<layout class="QVBoxLayout" name="_2" >
@ -125,33 +120,6 @@
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="roman_numerals" >
<property name="text" >
<string>Use &amp;Roman numerals for series number</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>&amp;Number of covers to show in browse mode (after restart):</string>
</property>
<property name="buddy" >
<cstring>cover_browse</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="cover_browse" />
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="new_version_notification" >
<property name="text" >
@ -166,9 +134,22 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="pdf_metadata" >
<property name="toolTip" >
<string>If you disable this setting, metadatas is guessed from the filename instead. This can be configured in the Advanced section.</string>
</property>
<property name="text" >
<string>Read &amp;metadata from files</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2" >
<item row="0" column="0" >
<item row="1" column="0" >
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>Format for &amp;single file save:</string>
@ -178,10 +159,10 @@
</property>
</widget>
</item>
<item row="0" column="1" >
<item row="1" column="1" >
<widget class="QComboBox" name="single_format" />
</item>
<item row="1" column="0" >
<item row="2" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>&amp;Priority for conversion jobs:</string>
@ -191,10 +172,10 @@
</property>
</widget>
</item>
<item row="1" column="1" >
<item row="2" column="1" >
<widget class="QComboBox" name="priority" />
</item>
<item row="2" column="0" >
<item row="3" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>Default network &amp;timeout:</string>
@ -204,7 +185,7 @@
</property>
</widget>
</item>
<item row="2" column="1" >
<item row="3" column="1" >
<widget class="QSpinBox" name="timeout" >
<property name="toolTip" >
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
@ -223,72 +204,48 @@
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QComboBox" name="language" />
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_7" >
<property name="text" >
<string>Choose &amp;language (requires restart):</string>
</property>
<property name="buddy" >
<cstring>language</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QComboBox" name="output_format" >
<property name="toolTip" >
<string>The default output format for ebook conversions.</string>
</property>
<item>
<property name="text" >
<string>LRF</string>
</property>
</item>
<item>
<property name="text" >
<string>EPUB</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0" >
<widget class="QLabel" name="label_8" >
<property name="text" >
<string>&amp;Output format:</string>
</property>
<property name="buddy" >
<cstring>output_format</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2" >
<property name="title" >
<string>Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="1" >
<widget class="QComboBox" name="toolbar_button_size" >
<item>
<property name="text" >
<string>Large</string>
</property>
</item>
<item>
<property name="text" >
<string>Medium</string>
</property>
</item>
<item>
<property name="text" >
<string>Small</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>&amp;Button size in toolbar</string>
</property>
<property name="buddy" >
<cstring>toolbar_button_size</cstring>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QCheckBox" name="show_toolbar_text" >
<property name="text" >
<string>Show &amp;text in toolbar buttons</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Select visible &amp;columns in library view</string>
</property>
<layout class="QGridLayout" name="_4" >
<item row="0" column="0" >
<widget class="QListWidget" name="columns" >
<property name="selectionMode" >
<enum>QAbstractItemView::NoSelection</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="dirs_box" >
<property name="title" >
@ -388,15 +345,102 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="page" >
<layout class="QGridLayout" name="gridLayout_3" >
<item row="0" column="0" >
<widget class="QCheckBox" name="roman_numerals" >
<property name="text" >
<string>Use &amp;Roman numerals for series number</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>&amp;Number of covers to show in browse mode (after restart):</string>
</property>
<property name="buddy" >
<cstring>cover_browse</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="cover_browse" />
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QGroupBox" name="groupBox_2" >
<property name="title" >
<string>Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="1" >
<widget class="QComboBox" name="toolbar_button_size" >
<item>
<property name="text" >
<string>Large</string>
</property>
</item>
<item>
<property name="text" >
<string>Medium</string>
</property>
</item>
<item>
<property name="text" >
<string>Small</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>&amp;Button size in toolbar</string>
</property>
<property name="buddy" >
<cstring>toolbar_button_size</cstring>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QCheckBox" name="show_toolbar_text" >
<property name="text" >
<string>Show &amp;text in toolbar buttons</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0" >
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Select visible &amp;columns in library view</string>
</property>
<layout class="QGridLayout" name="_4" >
<item row="0" column="0" >
<widget class="QListWidget" name="columns" >
<property name="selectionMode" >
<enum>QAbstractItemView::NoSelection</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>638</height>
</rect>
</property>
<layout class="QVBoxLayout" >
<item>
<layout class="QHBoxLayout" >
@ -489,8 +533,8 @@
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>231</x>
<y>502</y>
<x>235</x>
<y>671</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
@ -505,8 +549,8 @@
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>299</x>
<y>502</y>
<x>303</x>
<y>671</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
@ -521,12 +565,12 @@
<slot>setCurrentIndex(int)</slot>
<hints>
<hint type="sourcelabel" >
<x>176</x>
<y>184</y>
<x>142</x>
<y>117</y>
</hint>
<hint type="destinationlabel" >
<x>294</x>
<y>9</y>
<x>256</x>
<y>7</y>
</hint>
</hints>
</connection>

View File

@ -0,0 +1,274 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
The GUI for conversion to EPUB.
'''
import os
from PyQt4.Qt import QDialog, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, \
QTextEdit, QCheckBox, Qt, QPixmap, QIcon, QListWidgetItem, SIGNAL
from lxml.etree import XPath
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.epub_ui import Ui_Dialog
from calibre.gui2 import error_dialog, choose_images, pixmap_to_data
from calibre.ebooks.epub.from_any import SOURCE_FORMATS, config
from calibre.ebooks.metadata import MetaInformation
from calibre.ptempfile import PersistentTemporaryFile
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.metadata import authors_to_string, string_to_authors
class Config(QDialog, Ui_Dialog):
def __init__(self, parent, db, row=None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.connect(self.category_list, SIGNAL('itemEntered(QListWidgetItem *)'),
self.show_category_help)
self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover)
self.cover_changed = False
self.db = db
self.id = None
self.row = row
if row is not None:
self.id = db.id(row)
base = config().as_string() + '\n\n'
defaults = self.db.conversion_options(self.id, 'epub')
defaults = base + (defaults if defaults else '')
self.config = config(defaults=defaults)
else:
self.config = config()
self.initialize()
self.get_source_format()
self.category_list.setCurrentRow(0)
if self.row is None:
self.setWindowTitle(_('Bulk convert to EPUB'))
else:
self.setWindowTitle(_(u'Convert %s to EPUB')%unicode(self.title.text()))
def initialize(self):
self.__w = []
self.__w.append(QIcon(':/images/dialog_information.svg'))
self.item1 = QListWidgetItem(self.__w[-1], _('Metadata'), self.category_list)
self.__w.append(QIcon(':/images/lookfeel.svg'))
self.item2 = QListWidgetItem(self.__w[-1], _('Look & Feel'), self.category_list)
self.__w.append(QIcon(':/images/page.svg'))
self.item3 = QListWidgetItem(self.__w[-1], _('Page Setup'), self.category_list)
self.__w.append(QIcon(':/images/chapters.svg'))
self.item4 = QListWidgetItem(self.__w[-1], _('Chapter Detection'), self.category_list)
self.setup_tooltips()
self.initialize_options()
def set_help(self, msg):
if msg and getattr(msg, 'strip', lambda:True)():
self.help_view.setPlainText(msg)
def setup_tooltips(self):
for opt in self.config.option_set.preferences:
g = getattr(self, 'opt_'+opt.name, False)
if opt.help and g:
help = opt.help.replace('%default', str(opt.default))
g._help = help
g.setToolTip(help.replace('<', '&lt;').replace('>', '&gt;'))
g.setWhatsThis(help.replace('<', '&lt;').replace('>', '&gt;'))
g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip()))
def show_category_help(self, item):
text = unicode(item.text())
help = {
_('Metadata') : _('Specify metadata such as title and author for the book.\n\nMetadata will be updated in the database as well as the generated EPUB file.'),
_('Look & Feel') : _('Adjust the look of the generated EPUB file by specifying things like font sizes.'),
_('Page Setup') : _('Specify the page layout settings like margins.'),
_('Chapter Detection') : _('Fine tune the detection of chapter and section headings.'),
}
self.set_help(help[text])
def select_cover(self):
files = choose_images(self, 'change cover dialog',
_('Choose cover for ') + unicode(self.title.text()))
if not files:
return
_file = files[0]
if _file:
_file = os.path.abspath(_file)
if not os.access(_file, os.R_OK):
d = error_dialog(self.window, _('Cannot read'),
_('You do not have permission to read the file: ') + _file)
d.exec_()
return
cf, cover = None, None
try:
cf = open(_file, "rb")
cover = cf.read()
except IOError, e:
d = error_dialog(self.window, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_()
if cover:
pix = QPixmap()
pix.loadFromData(cover)
if pix.isNull():
d = error_dialog(self.window, _file + _(" is not a valid picture"))
d.exec_()
else:
self.cover_path.setText(_file)
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
def initialize_metadata_options(self):
all_series = self.db.all_series()
all_series.sort(cmp=lambda x, y : cmp(x[1], y[1]))
for series in all_series:
self.series.addItem(series[1])
self.series.setCurrentIndex(-1)
if self.row is not None:
mi = self.db.get_metadata(self.id, index_is_id=True)
self.title.setText(mi.title)
if mi.authors:
self.author.setText(authors_to_string(mi.authors))
else:
self.author.setText('')
self.publisher.setText(mi.publisher if mi.publisher else '')
self.author_sort.setText(mi.author_sort if mi.author_sort else '')
self.tags.setText(', '.join(mi.tags if mi.tags else []))
self.comment.setText(mi.comments if mi.comments else '')
if mi.series:
self.series.setCurrentIndex(self.series.findText(mi.series))
if mi.series_index is not None:
self.series_index.setValue(mi.series_index)
cover = self.db.cover(self.id, index_is_id=True)
if cover:
pm = QPixmap()
pm.loadFromData(cover)
if not pm.isNull():
self.cover.setPixmap(pm)
def get_title_and_authors(self):
title = unicode(self.title.text()).strip()
if not title:
title = _('Unknown')
authors = unicode(self.author.text()).strip()
authors = string_to_authors(authors) if authors else [_('Unknown')]
return title, authors
def get_metadata(self):
title, authors = self.get_title_and_authors()
mi = MetaInformation(title, authors)
publisher = unicode(self.publisher.text())
if publisher:
mi.publisher = publisher
author_sort = unicode(self.author_sort.text())
if author_sort:
mi.author_sort = author_sort
comments = unicode(self.comment.toPlainText())
if comments:
mi.comments = comments
mi.series_index = int(self.series_index.value())
if self.series.currentIndex() > -1:
mi.series = unicode(self.series.currentText())
tags = [t.strip() for t in unicode(self.tags.text()).split(',')]
if tags:
mi.tags = tags
return mi
def read_settings(self):
for pref in self.config.option_set.preferences:
g = getattr(self, 'opt_'+pref.name, False)
if g:
if isinstance(g, (QSpinBox, QDoubleSpinBox)):
self.config.set(pref.name, g.value())
elif isinstance(g, (QLineEdit, QTextEdit)):
func = getattr(g, 'toPlainText', getattr(g, 'text', None))()
val = unicode(func)
self.config.set(pref.name, val if val else None)
elif isinstance(g, QComboBox):
self.config.set(pref.name, unicode(g.currentText()))
elif isinstance(g, QCheckBox):
self.config.set(pref.name, bool(g.isChecked()))
if self.row is not None:
self.db.set_conversion_options(self.id, 'epub', self.config.src)
def initialize_options(self):
self.initialize_metadata_options()
values = self.config.parse()
for pref in self.config.option_set.preferences:
g = getattr(self, 'opt_'+pref.name, False)
if g:
val = getattr(values, pref.name)
if val is None:
continue
if isinstance(g, (QSpinBox, QDoubleSpinBox)):
g.setValue(val)
elif isinstance(g, (QLineEdit, QTextEdit)):
getattr(g, 'setPlainText', g.setText)(val)
elif isinstance(g, QComboBox):
for value in pref.choices:
g.addItem(value)
g.setCurrentIndex(g.findText(val))
elif isinstance(g, QCheckBox):
g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked)
def get_source_format(self):
self.source_format = None
if self.row is not None:
temp = self.db.formats(self.id, index_is_id=True)
if not temp:
error_dialog(self.parent(), _('Cannot convert'),
_('This book has no available formats')).exec_()
available_formats = [f.upper().strip() for f in temp.split(',')]
choices = [fmt.upper() for fmt in SOURCE_FORMATS if fmt.upper() in available_formats]
if not choices:
error_dialog(self.parent(), _('No available formats'),
_('Cannot convert %s as this book has no supported formats')%(self.title.text())).exec_()
elif len(choices) == 1:
self.source_format = choices[0]
else:
d = ChooseFormatDialog(self.parent(), _('Choose the format to convert to EPUB'), choices)
if d.exec_() == QDialog.Accepted:
self.source_format = d.format()
def accept(self):
for opt in ('chapter', 'level1_toc', 'level2_toc'):
text = unicode(getattr(self, 'opt_'+opt).text())
if text:
try:
XPath(text,namespaces={'re':'http://exslt.org/regular-expressions'})
except Exception, err:
error_dialog(self, _('Invalid XPath expression'),
_('The expression %s is invalid. Error: %s')%(text, err)
).exec_()
return
mi = self.get_metadata()
self.read_settings()
self.cover_file = None
if self.row is not None:
self.db.set_metadata(self.id, mi)
self.mi = self.db.get_metadata(self.id, index_is_id=True)
opf = OPFCreator(os.getcwdu(), self.mi)
self.opf_file = PersistentTemporaryFile('.opf')
opf.render(self.opf_file)
self.opf_file.close()
if self.cover_changed:
self.db.set_cover(self.id, pixmap_to_data(self.cover.pixmap()))
cover = self.db.cover(self.id, index_is_id=True)
if cover:
cf = PersistentTemporaryFile('.jpeg')
cf.write(cover)
cf.close()
self.cover_file = cf
self.opts = self.config.parse()
QDialog.accept(self)

View File

@ -0,0 +1,805 @@
<ui version="4.0" >
<class>Dialog</class>
<widget class="QDialog" name="Dialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>868</width>
<height>670</height>
</rect>
</property>
<property name="windowTitle" >
<string>Convert to EPUB</string>
</property>
<property name="windowIcon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
</property>
<property name="sizeGripEnabled" >
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QListWidget" name="category_list" >
<property name="maximumSize" >
<size>
<width>172</width>
<height>16777215</height>
</size>
</property>
<property name="font" >
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="mouseTracking" >
<bool>true</bool>
</property>
<property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="iconSize" >
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="flow" >
<enum>QListView::TopToBottom</enum>
</property>
<property name="isWrapping" stdset="0" >
<bool>false</bool>
</property>
<property name="spacing" >
<number>10</number>
</property>
<property name="viewMode" >
<enum>QListView::IconMode</enum>
</property>
<property name="uniformItemSizes" >
<bool>true</bool>
</property>
<property name="currentRow" >
<number>-1</number>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stack" >
<property name="currentIndex" >
<number>0</number>
</property>
<widget class="QWidget" name="metadata_page" >
<layout class="QGridLayout" name="gridLayout_4" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout_2" >
<item>
<widget class="QGroupBox" name="groupBox_4" >
<property name="title" >
<string>Book Cover</string>
</property>
<layout class="QGridLayout" name="_2" >
<item row="1" column="0" >
<layout class="QVBoxLayout" name="_4" >
<property name="spacing" >
<number>6</number>
</property>
<property name="margin" >
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>Change &amp;cover image:</string>
</property>
<property name="buddy" >
<cstring>cover_path</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5" >
<property name="spacing" >
<number>6</number>
</property>
<property name="margin" >
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="cover_path" >
<property name="readOnly" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="cover_button" >
<property name="toolTip" >
<string>Browse for an image to use as the cover of this book.</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2" >
<item>
<layout class="QGridLayout" name="_7" >
<item row="0" column="0" >
<widget class="QLabel" name="label" >
<property name="text" >
<string>&amp;Title: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QLineEdit" name="title" >
<property name="toolTip" >
<string>Change the title of this book</string>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>&amp;Author(s): </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>author</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="author" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>Author So&amp;rt:</string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>author_sort</cstring>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QLineEdit" name="author_sort" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by a comma</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>&amp;Publisher: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="3" column="1" >
<widget class="QLineEdit" name="publisher" >
<property name="toolTip" >
<string>Change the publisher of this book</string>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>Ta&amp;gs: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>tags</cstring>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QLineEdit" name="tags" >
<property name="toolTip" >
<string>Tags categorize the book. This is particularly useful while searching. &lt;br>&lt;br>They can be any words or phrases, separated by commas.</string>
</property>
</widget>
</item>
<item row="5" column="0" >
<widget class="QLabel" name="label_7" >
<property name="text" >
<string>&amp;Series:</string>
</property>
<property name="textFormat" >
<enum>Qt::PlainText</enum>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="5" column="1" >
<widget class="QComboBox" name="series" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Preferred" >
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip" >
<string>List of known series. You can add new series.</string>
</property>
<property name="whatsThis" >
<string>List of known series. You can add new series.</string>
</property>
<property name="editable" >
<bool>true</bool>
</property>
<property name="insertPolicy" >
<enum>QComboBox::InsertAlphabetically</enum>
</property>
<property name="sizeAdjustPolicy" >
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="6" column="1" >
<widget class="QSpinBox" name="series_index" >
<property name="enabled" >
<bool>true</bool>
</property>
<property name="toolTip" >
<string>Series index.</string>
</property>
<property name="whatsThis" >
<string>Series index.</string>
</property>
<property name="prefix" >
<string>Book </string>
</property>
<property name="minimum" >
<number>1</number>
</property>
<property name="maximum" >
<number>10000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>200</height>
</size>
</property>
<property name="title" >
<string>Comments</string>
</property>
<layout class="QGridLayout" name="_8" >
<item row="0" column="0" >
<widget class="QTextEdit" name="comment" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>180</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="lookandfeel_page" >
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<layout class="QGridLayout" name="gridLayout_6" >
<item row="0" column="0" >
<widget class="QLabel" name="label_26" >
<property name="text" >
<string>Source en&amp;coding:</string>
</property>
<property name="buddy" >
<cstring>opt_encoding</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2" >
<widget class="QLineEdit" name="opt_encoding" />
</item>
<item row="1" column="0" colspan="2" >
<widget class="QLabel" name="label_18" >
<property name="text" >
<string>Base &amp;font size:</string>
</property>
<property name="buddy" >
<cstring>opt_base_font_size2</cstring>
</property>
</widget>
</item>
<item row="1" column="2" >
<widget class="QDoubleSpinBox" name="opt_base_font_size2" >
<property name="suffix" >
<string> pt</string>
</property>
<property name="decimals" >
<number>0</number>
</property>
<property name="minimum" >
<double>0.000000000000000</double>
</property>
<property name="maximum" >
<double>30.000000000000000</double>
</property>
<property name="singleStep" >
<double>1.000000000000000</double>
</property>
<property name="value" >
<double>30.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_remove_paragraph_spacing" >
<property name="text" >
<string>Remove &amp;spacing between paragraphs</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Override &amp;CSS</string>
</property>
<layout class="QGridLayout" name="gridLayout_3" >
<item row="0" column="0" >
<widget class="QTextEdit" name="opt_override_css" />
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="pagesetup_page" >
<layout class="QGridLayout" name="_13" >
<item row="0" column="0" >
<widget class="QLabel" name="label_11" >
<property name="text" >
<string>&amp;Profile:</string>
</property>
<property name="buddy" >
<cstring>opt_profile</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QComboBox" name="opt_profile" >
<property name="currentIndex" >
<number>-1</number>
</property>
<property name="minimumContentsLength" >
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_12" >
<property name="text" >
<string>&amp;Left Margin:</string>
</property>
<property name="buddy" >
<cstring>opt_margin_left</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QSpinBox" name="opt_margin_left" >
<property name="suffix" >
<string> pt</string>
</property>
<property name="maximum" >
<number>200</number>
</property>
<property name="value" >
<number>20</number>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_13" >
<property name="text" >
<string>&amp;Right Margin:</string>
</property>
<property name="buddy" >
<cstring>opt_margin_right</cstring>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QSpinBox" name="opt_margin_right" >
<property name="suffix" >
<string> pt</string>
</property>
<property name="maximum" >
<number>200</number>
</property>
<property name="value" >
<number>20</number>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_14" >
<property name="text" >
<string>&amp;Top Margin:</string>
</property>
<property name="buddy" >
<cstring>opt_margin_top</cstring>
</property>
</widget>
</item>
<item row="3" column="1" >
<widget class="QSpinBox" name="opt_margin_top" >
<property name="suffix" >
<string> pt</string>
</property>
<property name="maximum" >
<number>200</number>
</property>
<property name="value" >
<number>10</number>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_15" >
<property name="text" >
<string>&amp;Bottom Margin:</string>
</property>
<property name="buddy" >
<cstring>opt_margin_bottom</cstring>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QSpinBox" name="opt_margin_bottom" >
<property name="suffix" >
<string> pt</string>
</property>
<property name="maximum" >
<number>200</number>
</property>
<property name="value" >
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="chapterdetection_page" >
<layout class="QVBoxLayout" name="_14" >
<item>
<widget class="QGroupBox" name="groupBox_6" >
<property name="title" >
<string>Automatic &amp;chapter detection</string>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="1" column="0" >
<widget class="QLabel" name="label_17" >
<property name="text" >
<string>&amp;XPath:</string>
</property>
<property name="buddy" >
<cstring>opt_chapter</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="opt_chapter" />
</item>
<item row="0" column="0" colspan="2" >
<widget class="QLabel" name="label_8" >
<property name="text" >
<string>&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">
p, li { white-space: pre-wrap; }
&lt;/style>&lt;/head>&lt;body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;">
&lt;p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You can control how calibre detects chapters using a XPath expression. To learn how to use XPath expressions see the &lt;a href="https://calibre.kovidgoyal.net/user_manual/xpath.html">&lt;span style=" text-decoration: underline; color:#0000ff;">XPath tutorial&lt;/span>&lt;/a>&lt;/p>&lt;/body>&lt;/html></string>
</property>
<property name="textFormat" >
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QComboBox" name="opt_chapter_mark" />
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_9" >
<property name="text" >
<string>Chapter &amp;mark:</string>
</property>
<property name="buddy" >
<cstring>opt_chapter_mark</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_7" >
<property name="title" >
<string>Automatic &amp;Table of Contents</string>
</property>
<layout class="QGridLayout" name="gridLayout_5" >
<item row="2" column="1" >
<widget class="QSpinBox" name="opt_max_toc_links" />
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_10" >
<property name="text" >
<string>Number of &amp;links to add to Table of Contents</string>
</property>
<property name="buddy" >
<cstring>opt_max_toc_links</cstring>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QCheckBox" name="opt_no_chapters_in_toc" >
<property name="text" >
<string>Do not add &amp;detected chapters to the Table of Contents</string>
</property>
</widget>
</item>
<item row="3" column="1" >
<widget class="QSpinBox" name="opt_toc_threshold" />
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_16" >
<property name="text" >
<string>Chapter &amp;threshold</string>
</property>
<property name="buddy" >
<cstring>opt_toc_threshold</cstring>
</property>
</widget>
</item>
<item row="0" column="0" >
<widget class="QCheckBox" name="opt_use_auto_toc" >
<property name="text" >
<string>&amp;Force use of auto-generated Table of Contents</string>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QLineEdit" name="opt_level1_toc" />
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_19" >
<property name="text" >
<string>Level &amp;1 TOC</string>
</property>
<property name="buddy" >
<cstring>opt_level1_toc</cstring>
</property>
</widget>
</item>
<item row="5" column="0" >
<widget class="QLabel" name="label_20" >
<property name="text" >
<string>Level &amp;2 TOC</string>
</property>
<property name="buddy" >
<cstring>opt_level2_toc</cstring>
</property>
</widget>
</item>
<item row="5" column="1" >
<widget class="QLineEdit" name="opt_level2_toc" />
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QTextBrowser" name="help_view" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="acceptRichText" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ImageView</class>
<extends>QLabel</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../images.qrc" />
<include location="../images.qrc" />
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>222</x>
<y>652</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>290</x>
<y>658</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>category_list</sender>
<signal>currentRowChanged(int)</signal>
<receiver>stack</receiver>
<slot>setCurrentIndex(int)</slot>
<hints>
<hint type="sourcelabel" >
<x>88</x>
<y>42</y>
</hint>
<hint type="destinationlabel" >
<x>659</x>
<y>12</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -14,6 +14,7 @@ from calibre.gui2.widgets import FontFamilyModel
from calibre.ebooks.lrf import option_parser
from calibre.ptempfile import PersistentTemporaryFile
from calibre.constants import __appname__
from calibre.ebooks.metadata import authors_to_string, string_to_authors, authors_to_sort_string
font_family_model = None
@ -199,7 +200,11 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
self.id = self.db.id(row)
self.gui_title.setText(db.title(row))
au = self.db.authors(row)
self.gui_author.setText(au if au else '')
if au:
au = [a.strip().replace('|', ',') for a in au.split(',')]
self.gui_author.setText(authors_to_string(au))
else:
self.gui_author.setText('')
aus = self.db.author_sort(row)
self.gui_author_sort.setText(aus if aus else '')
pub = self.db.publisher(row)
@ -350,14 +355,16 @@ class LRFSingleDialog(QDialog, Ui_LRFSingleDialog):
def write_metadata(self):
title = qstring_to_unicode(self.gui_title.text())
self.db.set_title(self.id, title)
au = qstring_to_unicode(self.gui_author.text()).split(',')
if au: self.db.set_authors(self.id, au)
au = unicode(self.gui_author.text())
if au:
self.db.set_authors(self.id, string_to_authors(au))
aus = qstring_to_unicode(self.gui_author_sort.text())
if not aus:
t = self.db.authors(self.id, index_is_id=True)
if not t:
t = 'Unknown'
aus = t.split(',')[0].strip()
t = _('Unknown')
aus = [a.strip().replace('|', ',') for a in t.split(',')]
aus = authors_to_sort_string(aus)
self.db.set_author_sort(self.id, aus)
self.db.set_publisher(self.id, qstring_to_unicode(self.gui_publisher.text()))
self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(','))

View File

@ -115,17 +115,9 @@
<item row="0" column="0" >
<widget class="QStackedWidget" name="stack" >
<property name="currentIndex" >
<number>3</number>
<number>0</number>
</property>
<widget class="QWidget" name="metadata_page" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>642</width>
<height>458</height>
</rect>
</property>
<layout class="QHBoxLayout" >
<item>
<widget class="QGroupBox" name="groupBox_4" >
@ -263,7 +255,7 @@
</sizepolicy>
</property>
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by a comma</string>
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
@ -431,14 +423,6 @@
</layout>
</widget>
<widget class="QWidget" name="lookandfeel_page" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>642</width>
<height>458</height>
</rect>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="label_8" >
@ -697,14 +681,6 @@
</layout>
</widget>
<widget class="QWidget" name="pagesetup_page" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>642</width>
<height>458</height>
</rect>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="label_11" >
@ -742,7 +718,7 @@
<string> px</string>
</property>
<property name="maximum" >
<number>100</number>
<number>200</number>
</property>
<property name="value" >
<number>20</number>
@ -765,7 +741,7 @@
<string> px</string>
</property>
<property name="maximum" >
<number>100</number>
<number>200</number>
</property>
<property name="value" >
<number>20</number>
@ -788,7 +764,7 @@
<string> px</string>
</property>
<property name="maximum" >
<number>100</number>
<number>200</number>
</property>
<property name="value" >
<number>10</number>
@ -811,7 +787,7 @@
<string> px</string>
</property>
<property name="maximum" >
<number>100</number>
<number>200</number>
</property>
<property name="value" >
<number>0</number>
@ -854,14 +830,6 @@
</layout>
</widget>
<widget class="QWidget" name="chapterdetection_page" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>642</width>
<height>458</height>
</rect>
</property>
<layout class="QVBoxLayout" >
<item>
<widget class="QGroupBox" name="groupBox_6" >
@ -1017,7 +985,7 @@
<string>&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">
p, li { white-space: pre-wrap; }
&lt;/style>&lt;/head>&lt;body style=" font-family:'Candara'; font-size:11pt; font-weight:400; font-style:normal;">
&lt;/style>&lt;/head>&lt;body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;">
&lt;p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;">&lt;/p>&lt;/body>&lt;/html></string>
</property>
</widget>

View File

@ -9,6 +9,7 @@ from PyQt4.QtGui import QDialog
from calibre.gui2 import qstring_to_unicode
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
def __init__(self, window, rows, db):
@ -29,6 +30,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
id, name = i
self.series.addItem(name)
for f in self.db.all_formats():
self.remove_format.addItem(f)
self.remove_format.setCurrentIndex(-1)
self.series.lineEdit().setText('')
QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.series_changed)
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.series_changed)
@ -46,7 +52,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
for id in self.ids:
au = qstring_to_unicode(self.authors.text())
if au:
au = au.split(',')
au = string_to_authors(au)
self.db.set_authors(id, au)
aus = qstring_to_unicode(self.author_sort.text())
if aus:
@ -66,7 +72,11 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.db.unapply_tags(id, remove_tags)
if self.write_series:
self.db.set_series(id, qstring_to_unicode(self.series.currentText()))
self.changed = True
if self.remove_format.currentIndex() > -1:
self.db.remove_format(id, unicode(self.remove_format.currentText()), index_is_id=True)
self.changed = True
def series_changed(self):
self.write_series = True

View File

@ -52,7 +52,7 @@
<item row="0" column="1" colspan="2" >
<widget class="QLineEdit" name="authors" >
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by a comma</string>
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
@ -84,6 +84,9 @@
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>rating</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2" >
@ -211,6 +214,19 @@
</property>
</widget>
</item>
<item row="7" column="1" >
<widget class="QComboBox" name="remove_format" />
</item>
<item row="7" column="0" >
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>Remove &amp;format:</string>
</property>
<property name="buddy" >
<cstring>remove_format</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -17,6 +17,7 @@ from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.password import PasswordDialog
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
from calibre import islinux
from calibre.utils.config import prefs
@ -100,7 +101,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
old_extensions, new_extensions, paths = set(), set(), {}
for row in range(self.formats.count()):
fmt = self.formats.item(row)
ext, path = fmt.ext, fmt.path
ext, path = fmt.ext.lower(), fmt.path
if 'unknown' in ext.lower():
ext = None
if path:
@ -109,8 +110,8 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
else:
old_extensions.add(ext)
for ext in new_extensions:
self.db.add_format(self.row, ext, open(paths[ext], "rb"))
db_extensions = set(self.db.formats(self.row).split(','))
self.db.add_format(self.row, ext, open(paths[ext], 'rb'))
db_extensions = set([f.lower() for f in self.db.formats(self.row).split(',')])
extensions = new_extensions.union(old_extensions)
for ext in db_extensions:
if ext not in extensions:
@ -144,7 +145,9 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
QObject.connect(self.tag_editor_button, SIGNAL('clicked()'),
self.edit_tags)
QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
self.remove_unused_series)
self.remove_unused_series)
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
self.deduce_author_sort)
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
self.timeout = float(prefs['network_timeout'])
self.title.setText(db.title(row))
@ -153,7 +156,11 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
isbn = ''
self.isbn.setText(isbn)
au = self.db.authors(row)
self.authors.setText(au if au else '')
if au:
au = [a.strip().replace('|', ',') for a in au.split(',')]
self.authors.setText(authors_to_string(au))
else:
self.authors.setText('')
aus = self.db.author_sort(row)
self.author_sort.setText(aus if aus else '')
pub = self.db.publisher(row)
@ -195,6 +202,11 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
if not pm.isNull():
self.cover.setPixmap(pm)
def deduce_author_sort(self):
au = unicode(self.authors.text())
authors = string_to_authors(au)
self.author_sort.setText(authors_to_sort_string(authors))
def swap_title_author(self):
title = self.title.text()
self.title.setText(self.authors.text())
@ -281,7 +293,7 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
def fetch_metadata(self):
isbn = qstring_to_unicode(self.isbn.text())
title = qstring_to_unicode(self.title.text())
author = qstring_to_unicode(self.authors.text()).split(',')[0]
author = string_to_authors(unicode(self.authors.text()))[0]
publisher = qstring_to_unicode(self.publisher.text())
if isbn or title or author or publisher:
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
@ -290,10 +302,10 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
book = d.selected_book()
if book:
self.title.setText(book.title)
self.authors.setText(', '.join(book.authors))
self.authors.setText(authors_to_string(book.authors))
if book.author_sort: self.author_sort.setText(book.author_sort)
self.publisher.setText(book.publisher)
self.isbn.setText(book.isbn)
if book.publisher: self.publisher.setText(book.publisher)
if book.isbn: self.isbn.setText(book.isbn)
summ = book.comments
if summ:
prefix = qstring_to_unicode(self.comments.toPlainText())
@ -323,8 +335,9 @@ class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog):
self.sync_formats()
title = qstring_to_unicode(self.title.text())
self.db.set_title(self.id, title)
au = qstring_to_unicode(self.authors.text()).split(',')
if au: self.db.set_authors(self.id, au)
au = unicode(self.authors.text())
if au:
self.db.set_authors(self.id, string_to_authors(au))
aus = qstring_to_unicode(self.author_sort.text())
if aus:
self.db.set_author_sort(self.id, aus)

View File

@ -5,7 +5,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>796</width>
<width>923</width>
<height>715</height>
</rect>
</property>
@ -98,7 +98,7 @@
<item row="1" column="1" >
<widget class="QLineEdit" name="authors" >
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by a comma</string>
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
@ -116,11 +116,29 @@
</widget>
</item>
<item row="2" column="1" colspan="2" >
<widget class="QLineEdit" name="author_sort" >
<property name="toolTip" >
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QLineEdit" name="author_sort" >
<property name="toolTip" >
<string>Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="auto_author_sort" >
<property name="toolTip" >
<string>Automatically create the author sort entry based on the current author entry</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/auto_author_sort.svg</normaloff>:/images/auto_author_sort.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_6" >
@ -320,16 +338,11 @@
<property name="title" >
<string>Comments</string>
</property>
<widget class="QTextEdit" name="comments" >
<property name="geometry" >
<rect>
<x>9</x>
<y>39</y>
<width>354</width>
<height>557</height>
</rect>
</property>
</widget>
<layout class="QGridLayout" name="gridLayout_4" >
<item row="0" column="0" >
<widget class="QTextEdit" name="comments" />
</item>
</layout>
</widget>
</item>
<item>
@ -436,8 +449,8 @@
<property name="title" >
<string>Book Cover</string>
</property>
<layout class="QGridLayout" name="gridLayout_2" >
<item row="0" column="0" >
<layout class="QVBoxLayout" name="verticalLayout_4" >
<item>
<widget class="ImageView" name="cover" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
@ -456,11 +469,14 @@
</property>
</widget>
</item>
<item row="1" column="0" >
<item>
<layout class="QVBoxLayout" >
<property name="spacing" >
<number>6</number>
</property>
<property name="sizeConstraint" >
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="margin" >
<number>0</number>
</property>
@ -507,7 +523,7 @@
</item>
</layout>
</item>
<item row="2" column="0" >
<item>
<layout class="QHBoxLayout" >
<item>
<widget class="QPushButton" name="fetch_cover_button" >

View File

@ -16,7 +16,7 @@ class PasswordDialog(QDialog, Ui_Dialog):
self.cfg_key = re.sub(r'[^0-9a-zA-Z]', '_', name)
un = dynamic[self.cfg_key+'__un']
pw = dynamic[self.cfg_key+'__un']
pw = dynamic[self.cfg_key+'__pw']
if not un: un = ''
if not pw: pw = ''
self.gui_username.setText(un)

View File

@ -0,0 +1,113 @@
<ui version="4.0" >
<class>Dialog</class>
<widget class="QDialog" name="Dialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>727</width>
<height>432</height>
</rect>
</property>
<property name="windowTitle" >
<string>Dialog</string>
</property>
<property name="windowIcon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/dialog_warning.svg</normaloff>:/images/dialog_warning.svg</iconset>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="msg" >
<property name="text" >
<string>TextLabel</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<widget class="QLabel" name="label" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../images.qrc" >:/images/dialog_warning.svg</pixmap>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer" >
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextEdit" name="details" />
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../images.qrc" />
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1991,7 +1991,7 @@
cx="343.99899"
rx="8.0010004"
r="36"
style="fill:url(#linearGradient5167);fill-opacity:1"
style="fill-opacity:1"
sodipodi:cx="343.99899"
sodipodi:cy="92"
sodipodi:rx="36"

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.0"
width="64"
height="64"
id="svg2453">
<defs
id="defs2455" />
<g
id="layer1">
<image
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGwAAAAzCAYAAAByvu3vAAAAAXNSR0IArs4c6QAAAARnQU1BAACx jwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAADV9JREFU eF7tW2mQVNUVZmZYBBHQGIQIJSpLgAQSk7IkCYb8cEskxCrLqkRMYmFRQFwqJKUBklCEAIIS0QQn IRHZNxUQEwmbsokI0z0Lw8wwzAzMxqzdPfv0TE/3yTn3vvPufb3N6x6EtNVNveqet9x37vnOdr9z SQH89Ep+EkcDBFjykzga6JU4oiYlFdEwqYbE0kASsMTCK+lhCYZXErAkYImmgQSTN5nDkoAlmAYS TNxuPSwQCCTYlL7Y4nYL2Bd7+ok3uyRgCYZZRMAuz5kDF6ZPh4vTfxzjQc/E/lwBPlP92hqhvgD+ o097YSG4d+8Gz/t7ox9790LTJyfBe/EiBLwdGgQ4khhKhfXW7Czw7KHx3jfHdOPzjYcPQaDDa7xf DtFRUYH37gEPXqf7G+gZ/N2wbx/4aussUHeUl+E1vAePaPK68d3NJ0+At6TE8rycszyktOFTUUTA socNg1O9esFncRyn8Rn9sDPGSXym+PHHLZO4snSpkOGszcN5442QP3UqVK9ZA/62NjltnLeehy/P ni1k08ck+XLuGAW+epfl/a7t28T8g++lvxsOHDDulYqt37QJzuB5OsLJS+czjGv0O3PIECj80Q+h 4cP/WA1MAOaP6PcRAcsdN068PKNXis1DCsQHCW39nRp1HFLipV/80iJo1bJlQrn6uJF+s5LoflJy wf33g7e4KGTipXPmmoDxWDTPc3fdjYDVWwHbsd0EwDKXlBRoPHjQAphry5aQeyPpg4ETxpCaBlXL lsuxjGjAPhYOtasAmLQq9iJSHP/NFicFjw68DhgLfGX5CluAOXDss+b4EmAaL++++6CrudkSXMrm /krIqitTADZ6NPhc7GHSa1w7dgpvYQXTHOhvR1oaNBxiwKRaXVu2mp7VvYFJXTg0Xbm2bDYjQoRo KK73CDAGJHf8BCib9QzUpv8dPO++C+733gHX5s1QOm8e5E6YIBTUnbeG9TATsGCwQ8MOGwWB5+hF 3ixBq3r5ZYuhls6dZ4Q5NaYEbAz43OxhBmA7dxpys8HJ92YgYI0HDymnoJCIHsZeHmyc6rwcRxmB lIH0QxGtq6nJ8DTOZ6E+Zgsw8YIUEpYnKT0qd8xoqN+4Abo8DRFjbpfHA7VvpkP28K9YvQXH0ycW C2COlFSgfJU5eAg4+t0Q4jEZBBiOL2Sc+DXwt7SY8jFg+rsVYNYc5rYApnkaARbVw6wG5hwwEJwD B2oh03qdAW366EhEPfIFW4Dx5CRg0nIv/mQGdJSXmy/wlhRD9erVaNErodWREfJi74VCKPj+NGWx mAfiAYwm57zpJlE9tmVnQzNWh1XLlyOAA6Xl4rjkZRzGnP0HoDwOe4CZIVHe3lPApCelQN0//wWt GRlQk/4mZI8YGRJtOI3Upq8z5IxMVtgDTChX5Yaixx4Df3u7qYS2czmQc+edZh5zoEJrX3/dEjLo D19dHRR8b6rhEfEBxhWWt6zUYhQVixaKcSVY1nDn2ffBdQWs+fRp8/0EHleSSk4ZDWrT003AIkFm CzB2WRE6xo8HX021KUDA1wmFDz8Cn2qJXOQrrH4a9+8P8bSO4mLIHjY8xMrshkSSJXPwYGgvyLeM 7dmz2wi5XIBI6ya5qCDgz7UMiexhTUePmu8nT3P27mPkMZViSGfu3e9JI4/CBtoCjBPl2dRU8PCg hght+fmQOWiwlpylogiAAlwTBbp8mhBSEirXZaWmPMEuYMLDMHe1FxRYPLh+02ZLjhTVHB707dn3 76sEmFEw2MxhJmDHjysPews9TORvowDB6MVzooV6SFgKMnnbgJGC8++9VwFg1J6UQxzoTap0l8WJ UFi/ftBeVGwCxuV6Z2UlZN58s8XKYgIMn+0oK1Ne7u+Cwgcf1LxWVZHOG/pD65kzVwkwrUq0UXQw YM3HT4Df64Wmw4dxvXeXadxcE1AUKP/tbxQ0wsViZDqsC2dZllcuXmwaAK/GWz47Dc6+fQ3lp4pv upeEyB07Djqra0LDIio7a+hQS1i0C5goOgZi0bFtGyZyJ7h3vYNgPRSmpJZy5NyN6yuP2x5gV7no 4AiSP2UK5u7vivWbvryhuZCeCh9+CHwNHkO3RKeRafcQMEHHaMmbNUBunD1ihAhHTOPkfn0yVCxY CF7kAvlDua755Emo/P0f4NyYMSELabuAmWE0LRUcRi5gJQiL1apPkufyrFkWg7mWOYxl1SkreU6G xMxbboHyhQugy1x2RAaKJ2ErJHLR0ZaVGeItlCHzvzMFsm6/HUqfmQ2NR6xriebjx6Dij4sh7xvf BEfffuYiOriaiw0wXogbIcrIhWJMAzBSkqM/hkOnVebrAZgEiak5Fa4vPPAANCInGTB4T1FwKBMP 1TWesQ0YWURbfp6ZFPUwS2W9z6V4uNasLLiychXkIzXk6NPHQrZGoqjsA2YkbALGAOes8ZsX9oJZ SU2xlMk8++sGmEk8GEsPlJkdIXfcV6Fu7VpEi0nfHpK/PLDpPcSAI6MsHNioQanMrkQSk+I1FRvM LcpwheErBi6RlVsVhpqSvCF7lpVgpncR8Fm3DRUMjLJYZbdRAfucchjrgtOGSaMZoDH3WootLYpY 0Xr8tj2MlFRDVqBSo2oCdGGVNm2aaIWo1oUkNxVLopfxqqzl63Y9jAHjvMBtC2I/cseOhcrfLQBv 0UUtsFit9Vp6GFeJ5S+9BPXr10Pdun9AycwnMTXo6zBFeRGw9evf0gwtNCraAoyUSoopfuIJpQiy BOFpRj9ow0az6FAEJ1WNnGTlmosmEY4MtguYqBIxN1WtWAH1GzZA3dtvQ8MH+wRNpbMvZuzWogCd ux6AtTidFs1Ta4YKDgaUcxzpJQ+XTv42xSIFQxYTYFm3fhk7pbLHZDbZjJAYwHXGxYceMRevoSFQ ApWBS4DS556DSz9/Kq51mGA6sPnXWV0VNil3d5LCTvCiXTA4WLn63EHtlV27wrP1vdOgyWDr+X3W 9ooyTgKj6ejHIWKVv/iiJof0MpKDSOLWnJyI0+gWMLEINghVmmjZs8+qwcT6TkXcztJSLDSmmCU+ hy2O4Rem/QCacfFIn5ZPTlnWTrF4mKCmLlzoDpuw10vncT8sTHvFyGEcNVzbtloYHJMQIKbjQLR+ WDBgkprSdyu4tm830ofqjUnyOhWajh2LHzDZX1ICnE3rbeHmpCAqUXY1NEDlkqVYxk8WhDBxjyUz nxJ7IgK+Lil4pw+KcA+HbunXCrArS/8Uli3P+tKtuG6URsAmWLVqpQUw7gI4eveG4FZIdA9TXCIj UYHMBs+fc70ADI2h+cSJ+AGzdnINygnd1rVpo2VQsTrXyxu/Hzeq1Jp7K/hmX109lPz0Zz0nf+P0 MGLE5bYDndWncJSCrMkuy5xoz4W416jmzJYNFjjBHh4NsJYM1d6hFzQd+QjDupWaM7vauAxq+fRU /ICFWzdxZVb6/PPYE1OcXrQYFfD5wL1jB5yfNElrOMZO/ppsfYyAsS01HvhvSCHERVXe5MnQhvnD j9sKav+2VixPzMLAIGxFvhs7Bvwd+u6s4C0CioWnscvnz8ee2Dqo+esbcOnpp8E5YIA5rr69gcbO HjUKOutqry5gctUui4gcZDjK0L0b9n8IndSj8huqwS8qDIjpqHrtL6L64fWGsKYeNDB7ksNoZ9Q5 3CEVumVBJv1MbIRm3zbMbIAyYA5DXpHHX3ghRKHhPEynprgyVvtJlLFyFCNvFmuxKJ9ui45IzIS+ 44g5xJzhw4H2d5zHtnzuxImQM/IOXHP0tWzQ0Zt28bRXeuJh7GUVixapVozW6uDQJ0HSWiBaB5vo rrZMLtPVGi8aYJZNOVp41XlFYfzIyXovXfocAKNJahOVSVOyD/qClkMnXQsu8/VFNffPQra5hWE6 4gVMlEYGn0YhJ3fyJK1/Zli7QXFJb9K3G8h1KCm1evWrWsWnknZ0wBSJQJFFGYT8TZ7lxO4FM0lx bXOjvg17DlNB4b51gPTr3AbnbwaT79HPc5uh5MmZFuu6svTPov0QPC7t0+AGZlRzDLooy2qp5Pa8 PMi75x5zCaLvO2SFssyCvcHqjSpM/gTz6i5soOqsfLCu2KD5POlWjtsbimbMgPbz521NJWxIJGHy vvVtcAwaBE5c81BHuefHoKhjnMV3qfgtlUqbeui8/m4n/p09cqTYxt3TTydWsVeWLIEc3ESagd10 9iLON6RcBxYIhY8+KrZyh/9IWd07dwl9ZQbJG6w3J+qSlhC0Xi399Xwct/udUvp7I+Ywn8sNNCEf ho9rc9QZ+/K424pcSmur2LgT8v76Oux8dxqhKRpVGg1S9Rxt02v++AjU4MahqlWvQPWrr0DVypXg 2rrF0tMLO5oxjN/bLvWFe+671RfuMCZmSHmrfdOL4GGR6X37Q8d3Z0C0GCK3yPVR9RAXz9ti+a9v Iq+EfcCerPHIF+6ZMIDJzfiCK4zSqr5aAgSPo9rjkRUh84cqIuKVRTqHahHJN5pnjd98Noona83B 2PxdRRO7c4jy/8Nie7XdFybeff9fekj+h74Es6AkYEnAEkwDCSZu0sOSgCWYBhJM3KSHJRhg/wMS tH7wUoCi5gAAAABJRU5ErkJggg== "
x="0.3636362"
y="19.590906"
width="63.63636"
height="22.09091"
id="image2509" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -5145,7 +5145,7 @@
id="stop16206" />
</linearGradient>
<path
style="fill:url(#linearGradient34419);fill-rule:evenodd"
style="fill-rule:evenodd"
d="M 87.500999,62.279999 C 87.882781,62.284241 87.476154,61.794221 87.500999,62.279999 z "
id="path16208" />
<linearGradient

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 483 KiB

View File

@ -0,0 +1,325 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg2936"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docname="list-remove-jakob.svgz"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
sodipodi:docbase="/home/jakob/dev/kde/src/kdebase/runtime/pics/oxygen/scalable/actions">
<defs
id="defs2938">
<linearGradient
inkscape:collect="always"
xlink:href="#XMLID_4_"
id="linearGradient4284"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4999996,0,0,2.4999996,174,-145.99998)"
x1="-13.757333"
y1="76.708466"
x2="-62.424866"
y2="104.80668" />
<linearGradient
id="linearGradient3207">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3209" />
<stop
style="stop-color:#252525;stop-opacity:0;"
offset="1"
id="stop3211" />
</linearGradient>
<linearGradient
id="linearGradient5412"
gradientUnits="userSpaceOnUse"
x1="28"
y1="57.5"
x2="28"
y2="0">
<stop
offset="0"
style="stop-color:#fff14d;stop-opacity:1;"
id="stop5414" />
<stop
offset="1"
style="stop-color:#f8ffa0;stop-opacity:0;"
id="stop5416" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="linearGradient4291"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
x1="73.742638"
y1="15.336544"
x2="80"
y2="19.281664" />
<linearGradient
id="linearGradient3030"
inkscape:collect="always">
<stop
id="stop3032"
offset="0"
style="stop-color:#000000;stop-opacity:0.77902622" />
<stop
id="stop3034"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3030"
id="radialGradient4275"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4999996,0,0,2.4999996,174,-145.99998)"
cx="-44"
cy="84"
fx="-60"
fy="100"
r="24" />
<filter
id="filter3387"
height="1.249912"
y="-0.12495601"
width="1.2041403"
x="-0.10207015"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur3389"
stdDeviation="0.44655691"
inkscape:collect="always" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="linearGradient4289"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
x1="73.742638"
y1="15.336544"
x2="80"
y2="19.281664" />
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9792,0,0,0.9725,133.0002,20.8762)"
r="55.148"
cy="-0.2148"
cx="48"
id="XMLID_4_">
<stop
id="stop3082"
style="stop-color:#ff0101;stop-opacity:1;"
offset="0" />
<stop
id="stop3090"
style="stop-color:#800000;stop-opacity:1;"
offset="1" />
</radialGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#XMLID_4_"
id="radialGradient4271"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4999996,0,0,2.4999996,174,-145.99998)"
cx="-44"
cy="84"
fx="-40"
fy="96"
r="20" />
<linearGradient
id="linearGradient3202">
<stop
style="stop-color:#ff8787;stop-opacity:1;"
offset="0"
id="stop3204" />
<stop
style="stop-color:#ff8787;stop-opacity:0;"
offset="1"
id="stop3206" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3202"
id="linearGradient4268"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4999996,0,0,2.4999996,17.28126,-145.99998)"
x1="11.68106"
y1="60.539303"
x2="11.68106"
y2="108.0104" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="linearGradient4265"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4999996,0,0,2.4999996,17.25592,-145.99998)"
x1="6.6976352"
y1="52"
x2="11.68106"
y2="96.001434" />
<linearGradient
id="linearGradient3260"
inkscape:collect="always">
<stop
id="stop3262"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3264"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3260"
id="linearGradient4262"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4999996,0,0,2.4999996,17.25592,-145.99998)"
x1="26.697636"
y1="96"
x2="14.697635"
y2="72" />
<filter
id="filter3191"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur3193"
stdDeviation="0.2025"
inkscape:collect="always" />
</filter>
<linearGradient
id="linearGradient3225">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3227" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3229" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3225"
id="linearGradient4259"
gradientUnits="userSpaceOnUse"
x1="97.622581"
y1="77.512512"
x2="98.097946"
y2="105.10625"
gradientTransform="translate(-36.000006,-20.000008)" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.5078057"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="layer1"
height="128px"
width="128px"
gridtolerance="10000"
inkscape:window-width="976"
inkscape:window-height="904"
inkscape:window-x="260"
inkscape:window-y="43"
showgrid="false">
<inkscape:grid
type="xygrid"
id="grid2944"
spacingx="4px"
spacingy="4px"
empspacing="2" />
</sodipodi:namedview>
<metadata
id="metadata2941">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:url(#linearGradient4284);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 19.156259,43.999993 C 10.862452,43.999993 4.0000112,50.862435 4.0000112,59.156236 L 4.0000112,68.843735 C 4.0000112,77.137559 10.862452,83.999983 19.156259,83.999983 L 108.84376,83.999983 C 117.13756,83.999983 124,77.137559 124,68.843735 L 124,59.156236 C 124,50.862435 117.13756,43.999993 108.84376,43.999993 L 19.156259,43.999993 z "
id="path3012"
sodipodi:nodetypes="ccccccccc"
clip-path="none" />
<path
sodipodi:nodetypes="cccccc"
transform="matrix(1.2499999,0,0,1.2499999,24.889073,28.928032)"
id="path3221"
d="M 69.875971,12.057888 C 68.798883,12.123171 67.34775,12.277052 66.875971,12.995388 L 68.465655,24.133449 L 79,23.37409 L 79,22.90534 C 80.740958,20.33518 74.219552,11.998548 69.875971,12.057888 z"
style="opacity:0.55056176;fill:url(#linearGradient4291);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3387)"
clip-path="none" />
<path
sodipodi:nodetypes="ccccccccc"
id="path3028"
d="M 19.156259,43.999993 C 10.862452,43.999993 4.000007,47.134768 4.000007,55.428569 C 4.000008,53.062884 4.00001,63.906397 4.000011,68.843735 C 4.000011,77.137559 10.862452,83.999983 19.156259,83.999983 L 108.84376,83.999983 C 117.13756,83.999983 124,77.137559 124,68.843735 L 124,59.156236 C 124,50.862435 117.13756,43.999993 108.84376,43.999993 L 19.156259,43.999993 z "
style="opacity:0.58052434;fill:url(#radialGradient4275);fill-opacity:1;stroke:none;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
clip-path="none" />
<path
style="opacity:0.55056176;fill:url(#linearGradient4289);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3387)"
d="M 69.875971,12.057888 C 68.798883,12.123171 67.34775,12.277052 66.875971,12.995388 L 68.465655,24.133449 L 79,23.37409 L 79,22.90534 C 80.740958,20.33518 74.219552,11.998548 69.875971,12.057888 z"
id="path3217"
transform="matrix(-1.2499999,0,0,1.2499999,103.11092,28.928032)"
sodipodi:nodetypes="cccccc"
clip-path="none" />
<path
style="fill:url(#radialGradient4271);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 19.156259,53.999993 C 16.321336,53.999993 14.00001,56.321312 14.00001,59.156236 L 14.00001,68.843735 C 14.00001,71.678658 16.321336,73.999984 19.156259,73.999984 L 108.84376,73.999984 C 111.67868,73.999984 114.00001,71.678658 114.00001,68.843735 L 114.00001,59.156236 C 114.00001,56.321312 111.67868,53.999993 108.84376,53.999993 L 19.156259,53.999993 z "
id="rect2407"
clip-path="none"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:url(#linearGradient4268);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 19.156259,53.999993 C 16.321336,53.999993 14.00001,56.321312 14.00001,59.156236 L 14.00001,68.843735 C 14.00001,69.118235 14.114379,69.361784 14.156259,69.624985 C 22.300473,70.798908 30.88461,71.499985 39.781255,71.499985 C 68.535185,71.499985 94.295855,64.92541 111.26562,54.624988 C 110.54253,54.237113 109.71726,53.999993 108.84376,53.999993 L 19.156259,53.999993 z "
id="path3038"
clip-path="none"
sodipodi:nodetypes="ccccsccc" />
<path
style="fill:url(#linearGradient4265);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
d="M 19.156259,53.999993 C 16.321336,53.999993 14.00001,56.321312 14.00001,59.156236 L 14.00001,61.656236 C 14.00001,58.821311 16.321336,54.999988 19.156259,54.999988 L 19.156259,53.999993 z M 108.39942,55.279287 C 91.501435,64.347036 63.84326,71.265635 37.125005,71.265609 C 32.676683,71.265609 28.27615,71.040734 24.000009,70.718735 C 29.150991,71.186035 34.44854,71.499985 39.859381,71.499985 C 66.784535,71.499985 91.068755,65.69071 107.98439,56.499987 C 108.00428,56.489187 108.04263,56.510811 108.0625,56.499987 C 109.1689,55.896911 110.22406,55.257212 111.26562,54.624988 C 110.92486,54.442187 110.55239,54.263038 110.17188,54.156238 C 110.1469,54.149613 110.11889,54.162513 110.09376,54.156238 C 109.71282,54.055113 109.33032,54.006787 108.92189,53.999993 L 108.84376,53.999993 L 107.67188,53.999993 L 108.39942,55.279287 z "
id="path3062"
sodipodi:nodetypes="ccccccscssscsscccc"
clip-path="none" />
<path
sodipodi:nodetypes="ccccccscssscsscccc"
id="path3087"
d="M 19.156259,53.999993 C 16.321336,53.999993 14.00001,56.321312 14.00001,59.156236 L 14.00001,61.656236 C 14.00001,58.821311 16.321336,54.999988 19.156259,54.999988 L 19.156259,53.999993 z M 108.64356,55.279287 C 91.745585,64.347036 63.84326,71.265635 37.125005,71.265609 C 32.676683,71.265609 28.27615,71.040734 24.000009,70.718735 C 29.150991,71.186035 34.44854,71.499985 39.859381,71.499985 C 66.784535,71.499985 91.068755,65.69071 107.98439,56.499987 C 108.00428,56.489187 108.04263,56.510811 108.0625,56.499987 C 109.1689,55.896911 110.22406,55.257212 111.26562,54.624988 C 110.92486,54.442187 110.55239,54.263038 110.17188,54.156238 C 110.1469,54.149613 110.11889,54.162513 110.09376,54.156238 C 109.71282,54.055113 109.33032,54.006787 108.92189,53.999993 L 108.84376,53.999993 L 107.67188,53.999993 L 108.64356,55.279287 z "
style="fill:url(#linearGradient4262);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1"
clip-path="none" />
<path
id="path3099"
d="M 46.062496,59.999992 C 44.928527,59.999992 43.999996,60.92852 43.999996,62.06249 L 43.999996,65.93749 C 43.999996,67.07146 44.928527,67.99999 46.062496,67.99999 L 81.9375,67.99999 C 83.07147,67.99999 84,67.07146 84,65.93749 L 84,62.06249 C 84,60.92852 83.07147,59.999992 81.9375,59.999992 L 46.062496,59.999992 z "
style="fill:none;fill-opacity:1;stroke:url(#linearGradient4259);stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter3191)"
clip-path="none"
transform="matrix(2.4999996,0,0,2.4999996,-95.999962,-95.99996)"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,503 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg1307"
sodipodi:version="0.32"
inkscape:version="0.43"
version="1.0"
sodipodi:docbase="/home/pinheiro/Documents/pics/new oxygen/svg"
sodipodi:docname="rss_tag.svg">
<defs
id="defs1309">
<linearGradient
inkscape:collect="always"
id="linearGradient12560">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop12562" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop12564" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient11615">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop11617" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop11619" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient11584">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop11586" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop11588" />
</linearGradient>
<linearGradient
y2="3.1118"
x2="17.0464"
y1="7.6073999"
x1="17.0464"
gradientUnits="userSpaceOnUse"
id="linearGradient2959">
<stop
id="stop2961"
style="stop-color:#EEEEEE"
offset="0" />
<stop
id="stop2963"
style="stop-color:#CECECE"
offset="0.2909" />
<stop
id="stop2965"
style="stop-color:#888888"
offset="0.85" />
<stop
id="stop2967"
style="stop-color:#555555"
offset="1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient7033">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop7035" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop7037" />
</linearGradient>
<linearGradient
id="linearGradient5259">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5261" />
<stop
id="stop5267"
offset="0.5"
style="stop-color:#7f7f7f;stop-opacity:0.33935019;" />
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="1"
id="stop5263" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3291">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3293" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop3295" />
</linearGradient>
<linearGradient
id="linearGradient2316">
<stop
id="stop2318"
offset="0"
style="stop-color:#dd6a0e;stop-opacity:1;" />
<stop
id="stop2320"
offset="1"
style="stop-color:#ffb66d;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3075">
<stop
id="stop3077"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0.42597079"
id="stop3093" />
<stop
style="stop-color:#f1f1f1;stop-opacity:1;"
offset="0.5892781"
id="stop3085" />
<stop
id="stop3087"
offset="0.80219781"
style="stop-color:#eaeaea;stop-opacity:1;" />
<stop
id="stop3079"
offset="1"
style="stop-color:#dfdfdf;stop-opacity:1;" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3291"
id="radialGradient10686"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.197802,0,92.82166)"
cx="63.912209"
cy="115.70919"
fx="42.094791"
fy="115.7093"
r="63.912209" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3075"
id="radialGradient11644"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.844258,1.256048e-15,-4.710318e-16,1.606667,44.38044,-98.18508)"
spreadMethod="reflect"
cx="-52.250774"
cy="128.00081"
fx="-52.250774"
fy="128.00081"
r="36.937431" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2316"
id="linearGradient11646"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.835095,0,0,0.835095,6.816147,12.32049)"
x1="32.39278"
y1="79.018364"
x2="83.208656"
y2="79.018364" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5259"
id="linearGradient11648"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.267368,-6.264141e-2)"
x1="24.851341"
y1="60.846405"
x2="-35.981007"
y2="112.08296" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5259"
id="linearGradient11650"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.267368,-6.264141e-2)"
x1="24.851341"
y1="60.846405"
x2="-35.981007"
y2="112.08296" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5259"
id="linearGradient11652"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.267368,-6.264141e-2)"
x1="24.851341"
y1="60.846405"
x2="-35.981007"
y2="112.08296" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2316"
id="linearGradient11654"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.835095,0,0,0.835095,7.613067,13.11741)"
x1="32.39278"
y1="79.018364"
x2="83.208656"
y2="79.018364" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7033"
id="linearGradient11656"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.144757,-0.233352)"
x1="96.437851"
y1="14.713447"
x2="96.397697"
y2="23.267729" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2959"
id="linearGradient11658"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.965789,0,0,0.944211,3.248297,-0.448682)"
x1="111.30237"
y1="-18.911451"
x2="108.5625"
y2="26.541067" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11584"
id="linearGradient11660"
gradientUnits="userSpaceOnUse"
x1="65.073738"
y1="53.097416"
x2="62.605522"
y2="102.24165" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2959"
id="linearGradient11662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.989491,0.144757,0.128566)"
x1="112.14121"
y1="0.22972308"
x2="108.5625"
y2="41.496986" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11584"
id="linearGradient11664"
gradientUnits="userSpaceOnUse"
x1="236.57014"
y1="-50.274925"
x2="2.61567"
y2="111.73157" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11584"
id="linearGradient11666"
gradientUnits="userSpaceOnUse"
x1="95.915977"
y1="-33.667568"
x2="32.102207"
y2="129.69464" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient11615"
id="radialGradient11668"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.381766,0.111574,-0.139672,0.47791,70.02209,5.232857)"
cx="99.498825"
cy="33.076019"
fx="92.406448"
fy="33.504173"
r="17.845808" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient12560"
id="linearGradient12566"
x1="96.686058"
y1="28.999111"
x2="109.04183"
y2="41.42416"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2316"
id="linearGradient15192"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.835095,0,0,0.835095,5.637308,11.14165)"
x1="32.39278"
y1="79.018364"
x2="83.208656"
y2="79.018364" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.5652975"
inkscape:cx="24.19134"
inkscape:cy="66.08179"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:document-units="px"
inkscape:grid-bbox="true"
guidetolerance="0.1px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1106"
inkscape:window-height="958"
inkscape:window-x="722"
inkscape:window-y="85">
<sodipodi:guide
orientation="horizontal"
position="32.487481"
id="guide2204" />
</sodipodi:namedview>
<metadata
id="metadata1312">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
<dc:contributor>
<cc:Agent>
<dc:title>Oxygen team</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/LGPL/2.1/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
<cc:requires
rdf:resource="http://web.resource.org/cc/SourceCode" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
sodipodi:nodetypes="ccc"
id="path2276"
d="M 50.892799,3.2812959 L 50.892799,0.48658747 L 50.892799,3.2812959 z "
style="fill:#ffffff;fill-opacity:0.75688076;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
sodipodi:type="arc"
style="opacity:0.38139535;fill:url(#radialGradient10686);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path3289"
sodipodi:cx="63.912209"
sodipodi:cy="115.70919"
sodipodi:rx="63.912209"
sodipodi:ry="12.641975"
d="M 127.82442 115.70919 A 63.912209 12.641975 0 1 1 0,115.70919 A 63.912209 12.641975 0 1 1 127.82442 115.70919 z"
transform="matrix(1,0,0,0.416667,0.144757,74.63816)" />
<g
id="g11627"
transform="matrix(0.99373,0,0,0.99373,9.698994e-4,0.76812)">
<path
sodipodi:nodetypes="ccccccccccccssss"
id="rect1410"
d="M 65.957252,16.860398 C 62.907696,16.584597 59.770313,17.609836 57.426002,19.954148 L 3.301002,74.047898 C -0.89408299,78.242983 -0.89408309,85.009061 3.301002,89.204148 L 33.457252,119.3604 C 37.652337,123.55549 44.418418,123.55548 48.613502,119.3604 L 102.70726,65.235398 C 105.19809,62.744566 105.83999,59.365361 105.83226,56.141648 L 105.76976,27.454148 C 105.76976,21.521399 100.98376,16.735398 95.051007,16.735398 C 95.051007,16.735398 66.443365,16.827319 65.957252,16.860398 z M 94.801007,21.985398 C 98.463627,21.823968 100.95726,24.470845 100.95726,27.547898 C 100.95726,30.624951 98.471807,33.110398 95.394757,33.110398 C 92.317697,33.110399 89.801007,30.624951 89.801007,27.547898 C 89.801007,24.663161 91.983037,22.1096 94.801007,21.985398 z "
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.5999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.04313725" />
<rect
transform="matrix(0.707107,-0.707107,0.707107,0.707107,0,0)"
rx="7.8982348"
ry="7.8982348"
y="64.655273"
x="-51.673248"
height="44.167801"
width="73.874908"
id="rect3166"
style="opacity:1;fill:url(#radialGradient11644);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.5999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.04313725" />
<path
id="path3173"
d="M 75.885718,57.507812 L 34.284739,99.108794"
style="opacity:0.35655739;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:url(#linearGradient11646);stroke-width:6.68100023;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="opacity:0.22950822;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient11648);stroke-width:3.00000095;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="rect5257"
width="73.874908"
height="44.167801"
x="-51.673248"
y="64.655273"
ry="7.8982348"
rx="7.8982348"
transform="matrix(0.707107,-0.707107,0.707107,0.707107,0,0)" />
<rect
transform="matrix(0.707107,-0.707107,0.707107,0.707107,0,0)"
rx="7.8982348"
ry="7.8982348"
y="64.655273"
x="-51.673248"
height="44.167801"
width="73.874908"
id="rect5269"
style="opacity:0.22950822;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient11650);stroke-width:4.70000124;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<rect
style="opacity:0.22950822;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient11652);stroke-width:1.50000048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="rect5271"
width="73.874908"
height="44.167801"
x="-51.673248"
y="64.655273"
ry="7.8982348"
rx="7.8982348"
transform="matrix(0.707107,-0.707107,0.707107,0.707107,0,0)" />
<path
style="opacity:0.35655739;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:url(#linearGradient11654);stroke-width:4.28100014;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 76.682637,58.304731 L 35.081658,99.905718"
id="path6146" />
<path
sodipodi:nodetypes="cccccccc"
id="path7023"
d="M 95.974287,17.823297 C 85.481516,17.823297 75.240344,17.739431 64.747574,17.739431 C 62.985501,17.739431 60.491741,18.681986 58.875807,20.29792 C 57.215559,22.024579 55.471445,23.415775 53.811196,25.142433 C 56.225419,22.728211 61.024042,19.417529 64.90428,19.417529 C 72.325291,19.417529 92.255327,19.319227 95.974287,19.385637 C 99.686057,19.385637 103.02976,21.506741 104.92901,24.79634 C 104.24024,22.852996 100.25567,17.823297 95.974287,17.823297 z "
style="fill:url(#linearGradient11656);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="opacity:1;fill:url(#linearGradient11658);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.28100014;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 100.82818,26.326961 C 98.938027,24.599124 97.412757,21.656447 97.412757,18.406034 C 97.412757,14.872675 99.380507,11.582117 101.55558,8.9316543 C 103.73064,6.2811911 106.84326,4.8075247 109.13903,4.2829843 C 111.4348,3.758444 114.9341,4.1607878 117.10916,5.8173273 C 119.28424,7.4738667 120.58279,10.076472 120.58279,13.60983 C 120.58279,17.143189 118.5547,20.594653 116.37963,23.245116 C 114.20456,25.895577 110.39259,27.695387 108.09681,28.219928 C 107.19159,28.426755 106.70156,28.478743 105.74762,28.384108 L 105.74762,32.025222 C 106.6546,31.947861 107.12437,31.847174 108.09681,31.62499 C 113.50868,30.388482 117.9234,27.25568 120.80298,23.746728 C 123.68255,20.237776 125.33012,16.130459 125.33012,12.032608 C 125.33012,7.9347583 123.68255,4.5770463 120.80298,2.3839513 C 118.5758,0.68772922 115.44044,-0.39061945 111.62798,-0.21262935 C 110.5112,-0.16049075 109.32294,0.008836555 108.09681,0.28898284 C 102.68496,1.5254904 98.240037,4.6877993 95.360457,8.196751 C 92.480887,11.705703 90.863507,15.783513 90.863507,19.881364 C 90.863497,23.979214 92.480887,27.36643 95.360457,29.559527 C 95.900377,29.970733 97.848257,30.831489 97.141137,30.651272 C 97.134377,30.649551 99.104327,31.902937 98.996487,31.335711 C 100.80737,30.597386 101.2053,27.858014 100.82818,26.326961 z "
id="path9411"
sodipodi:nodetypes="csssssssccsssscssssscc" />
<path
id="path10706"
d="M 90.5625,16.75 C 82.493814,16.777403 66.333335,16.850191 65.96875,16.875 C 62.919193,16.5992 59.781811,17.624438 57.4375,19.96875 L 7.84375,69.53125 C 19.373301,71.513576 35.736073,74.116497 58.96875,77.5 C 67.242732,78.704986 77.262405,79.536346 87.875,80.09375 L 102.71875,65.25 C 105.20958,62.759169 105.85148,59.379963 105.84375,56.15625 L 105.78125,27.46875 C 105.78125,21.536002 100.99525,16.75 95.0625,16.75 C 95.062499,16.75 93.252062,16.740866 90.5625,16.75 z M 94.875,21.96875 C 98.4908,21.851975 100.96875,24.509486 100.96875,27.5625 C 100.96875,30.639554 98.4833,33.125 95.40625,33.125 C 92.329187,33.125002 89.8125,30.639553 89.8125,27.5625 C 89.812497,24.677764 91.99453,22.124202 94.8125,22 C 94.841114,21.998739 94.846529,21.969669 94.875,21.96875 z "
style="opacity:0.11065572;fill:url(#linearGradient11660);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.5999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.04313725" />
<path
sodipodi:nodetypes="ccsssssssccsssscsssss"
id="path7046"
d="M 97.375359,32.751517 C 99.475509,31.977786 100.76571,30.212919 100.93539,28.016764 C 98.978277,26.206069 97.644757,23.293743 97.644757,19.887456 C 97.644747,16.184655 99.682197,12.7363 101.93432,9.9587343 C 104.18643,7.1811689 107.4093,5.6368332 109.78639,5.0871385 C 112.16349,4.537444 115.78674,4.9590821 118.03885,6.6950607 C 120.29097,8.431039 121.63552,11.158452 121.63552,14.861251 C 121.63552,18.564052 119.53559,22.18103 117.28347,24.958595 C 115.03136,27.73616 111.08436,29.622279 108.70726,30.171974 C 107.76997,30.38872 106.74637,30.487599 105.75864,30.388425 L 105.75864,34.204148 C 106.69775,34.123076 107.70037,33.973164 108.70726,33.740324 C 114.31083,32.44452 118.88193,29.161485 121.86351,25.484262 C 124.84508,21.80704 126.55101,17.502757 126.55101,13.208394 C 126.55101,8.9140326 124.84508,5.3953022 121.86351,3.0970377 C 119.55744,1.3194736 116.31101,0.18941291 112.36351,0.37593852 C 111.20717,0.43057734 109.97682,0.60802477 108.70726,0.90160537 C 103.1037,2.1974095 98.501327,5.5113663 95.519757,9.1885892 C 92.538177,12.865812 90.863507,17.139173 90.863507,21.433535 C 90.863497,25.727897 92.538177,29.277549 95.519757,31.575814 C 96.078797,32.006739 96.705929,32.408418 97.375359,32.751517 z "
style="opacity:1;fill:url(#linearGradient11662);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.28100014;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
id="path11592"
d="M 87.21875,16.75 C 78.71516,16.782629 66.287762,16.853292 65.96875,16.875 C 62.919193,16.5992 59.781811,17.624438 57.4375,19.96875 L 7.84375,69.53125 C 14.356712,70.651052 22.410305,71.98654 32.375,73.53125 C 43.387521,56.22581 59.726155,36.241802 71.1875,26.65625 C 75.832301,22.771629 83.170429,19.475338 91.53125,16.75 C 91.224346,16.750624 90.898695,16.748858 90.5625,16.75 C 89.553914,16.753425 88.433549,16.745339 87.21875,16.75 z M 105.59375,25.4375 C 104.03905,26.123892 102.48669,26.807023 100.96875,27.5 C 100.96905,27.523802 100.96875,27.538648 100.96875,27.5625 C 100.96875,30.639554 98.4833,33.125 95.40625,33.125 C 94.072055,33.125001 92.839017,32.655165 91.875,31.875 C 85.192675,35.231446 79.793639,38.393861 77.28125,40.90625 C 69.040453,49.147048 61.755175,64.657329 56.59375,77.15625 C 57.378226,77.271014 58.168037,77.383388 58.96875,77.5 C 67.242732,78.704986 77.262405,79.536346 87.875,80.09375 L 102.71875,65.25 C 105.20958,62.759169 105.85148,59.379963 105.84375,56.15625 L 105.78125,27.46875 C 105.78125,26.773506 105.71934,26.095391 105.59375,25.4375 z "
style="opacity:0.20081967;fill:url(#linearGradient11664);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.5999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.04313725" />
<path
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.28100014;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 100.93539,27.951048 C 98.978277,26.121121 97.644757,23.177863 97.644757,19.735398 C 97.644747,15.99327 99.142637,12.292464 101.39476,9.4853983 C 103.64687,6.6783321 106.33017,5.0096813 108.70726,4.4541483 C 111.08436,3.8986155 113.7364,4.3247318 115.98851,6.0791483 C 118.24063,7.8335645 119.80101,10.805771 119.80101,14.547898 C 119.80101,18.290026 118.24063,22.053332 115.98851,24.860398 C 113.7364,27.667463 111.08436,29.273615 108.70726,29.829148 C 107.76997,30.048196 106.78874,30.148125 105.80101,30.047898 L 105.80101,34.204148 C 106.74012,34.122215 107.70037,33.970711 108.70726,33.735398 C 114.31083,32.425831 118.88193,29.107927 121.86351,25.391648 C 124.84508,21.67537 126.55101,17.325371 126.55101,12.985398 C 126.55101,8.6454257 124.84508,5.0893227 121.86351,2.7666483 C 119.55744,0.97020468 116.31101,-0.17185845 112.36351,0.016648255 C 111.20717,0.071867415 109.97682,0.25119951 108.70726,0.54789827 C 103.1037,1.8574651 98.501327,5.2066196 95.519757,8.9228983 C 92.538177,12.639177 90.863507,16.957925 90.863507,21.297898 C 90.863497,25.637871 92.538177,29.225223 95.519757,31.547898 C 96.078797,31.9834 96.694077,32.357405 97.363507,32.704148 C 97.369937,32.707482 97.388307,32.700823 97.394757,32.704148 C 99.463657,31.922199 100.76571,30.170528 100.93539,27.951048 z "
id="path8516"
sodipodi:nodetypes="csssssssccsssscssssscc" />
<path
sodipodi:nodetypes="ccccscscccsscsccccc"
style="opacity:0.14344261;fill:url(#linearGradient11666);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.5999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.04313725"
d="M 87.21875,16.75 C 78.71516,16.782629 66.287762,16.853292 65.96875,16.875 C 62.919193,16.5992 59.781811,17.624438 57.4375,19.96875 L 7.84375,69.53125 C 14.356712,70.651052 59.726155,36.241802 71.1875,26.65625 C 75.832301,22.771629 83.170429,19.475338 91.53125,16.75 C 91.224346,16.750624 90.898695,16.748858 90.5625,16.75 C 89.553914,16.753425 88.433549,16.745339 87.21875,16.75 z M 105.59375,25.4375 C 104.03905,26.123892 102.48669,26.807023 100.96875,27.5 C 100.96905,27.523802 100.96875,27.538648 100.96875,27.5625 C 100.96875,30.639554 98.4833,33.125 95.40625,33.125 C 94.072055,33.125001 92.839017,32.655165 91.875,31.875 C 85.192675,35.231446 79.793639,38.393861 77.28125,40.90625 C 69.040453,49.147048 77.262405,79.536346 87.875,80.09375 L 102.71875,65.25 C 105.20958,62.759169 105.85148,59.379963 105.84375,56.15625 L 105.78125,27.46875 C 105.78125,26.773506 105.71934,26.095391 105.59375,25.4375 z "
id="path11602" />
<path
style="opacity:0.60655738;fill:url(#radialGradient11668);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.28100014;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 90.96875 26.46875 C 91.712717 28.766017 93.069493 30.685101 94.9375 32.125 C 95.493036 32.553222 96.084767 32.971552 96.75 33.3125 C 99.406083 32.361987 100.76114 29.807363 100.21875 26.90625 C 100.06618 26.766783 99.897677 26.623256 99.75 26.46875 L 90.96875 26.46875 z M 112.6875 26.46875 C 110.84997 27.679808 108.83212 28.486717 107.40625 28.8125 C 106.50671 29.018029 106.04171 29.062792 105.09375 28.96875 L 105.09375 30.9375 L 105.09375 32.59375 L 105.09375 34.78125 C 106.02697 34.700686 107.03067 34.54388 108.03125 34.3125 C 113.39667 33.07177 117.81612 29.961838 120.78125 26.46875 L 112.6875 26.46875 z "
transform="matrix(1.00631,0,0,1.00631,-9.76019e-4,-0.772966)"
id="path11610" />
<path
id="path14317"
d="M 74.706878,56.328972 L 33.105899,97.929959"
style="opacity:0.35655739;fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:url(#linearGradient15192);stroke-width:10.01680565;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
style="opacity:0.53688528;fill:#c5c5c5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 89.656712,30.354292 C 90.147952,32.680027 92.244725,34.411058 94.787893,34.411059 C 97.331055,34.411059 99.4673,32.680028 99.958536,30.354292 C 99.103765,32.286307 97.092249,33.645631 94.787893,33.645631 C 92.483533,33.645631 90.511491,32.286307 89.656712,30.354292 z "
id="path11672" />
<path
style="fill:url(#linearGradient12566);fill-opacity:1.0;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.30327869"
d="M 100.18163,26.901091 C 101.33249,30.951757 103.83808,29.993287 105.1151,33.72654 L 105.1151,36.081713 C 100.54929,34.644208 98.892965,34.944024 96.686058,33.322037 C 98.825095,32.488947 100.73865,30.734861 100.18163,26.901091 z "
id="path12550"
sodipodi:nodetypes="ccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -19,13 +19,13 @@ NONE = QVariant()
class JobManager(QAbstractTableModel):
wait_icon = QVariant(QIcon(':/images/jobs.svg'))
running_icon = QVariant(QIcon(':/images/exec.svg'))
error_icon = QVariant(QIcon(':/images/dialog_error.svg'))
done_icon = QVariant(QIcon(':/images/ok.svg'))
def __init__(self):
QAbstractTableModel.__init__(self)
self.wait_icon = QVariant(QIcon(':/images/jobs.svg'))
self.running_icon = QVariant(QIcon(':/images/exec.svg'))
self.error_icon = QVariant(QIcon(':/images/dialog_error.svg'))
self.done_icon = QVariant(QIcon(':/images/ok.svg'))
self.jobs = []
self.server = Server()
self.add_job = Dispatcher(self._add_job)
@ -42,13 +42,13 @@ class JobManager(QAbstractTableModel):
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal:
if orientation == Qt.Horizontal:
if section == 0: text = _("Job")
elif section == 1: text = _("Status")
elif section == 2: text = _("Progress")
elif section == 3: text = _('Running time')
return QVariant(text)
else:
else:
return QVariant(section+1)
def data(self, index, role):
@ -58,7 +58,7 @@ class JobManager(QAbstractTableModel):
row, col = index.row(), index.column()
job = self.jobs[row]
if role == Qt.DisplayRole:
if role == Qt.DisplayRole:
if col == 0:
desc = job.description
if not desc:
@ -145,7 +145,7 @@ class JobManager(QAbstractTableModel):
return True
return False
def run_job(self, done, func, args=[], kwargs={},
def run_job(self, done, func, args=[], kwargs={},
description=None):
job = ParallelJob(func, done, self, args=args, kwargs=kwargs,
description=description)
@ -159,15 +159,15 @@ class JobManager(QAbstractTableModel):
def kill_job(self, row, view):
job = self.jobs[row]
if isinstance(job, DeviceJob):
error_dialog(view, _('Cannot kill job'),
error_dialog(view, _('Cannot kill job'),
_('Cannot kill jobs that communicate with the device')).exec_()
return
if job.has_run:
error_dialog(view, _('Cannot kill job'),
error_dialog(view, _('Cannot kill job'),
_('Job has already run')).exec_()
return
if not job.is_running:
error_dialog(view, _('Cannot kill job'),
error_dialog(view, _('Cannot kill job'),
_('Cannot kill waiting job')).exec_()
return

View File

@ -1,6 +1,6 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, textwrap, traceback, time
import os, textwrap, traceback, time, re
from datetime import timedelta, datetime
from operator import attrgetter
@ -13,7 +13,7 @@ from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor,
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex
from calibre import preferred_encoding
from calibre import strftime
from calibre.ptempfile import PersistentTemporaryFile
from calibre.library.database import LibraryDatabase, text_to_tokens
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config
@ -100,7 +100,7 @@ class BooksModel(QAbstractTableModel):
QAbstractTableModel.__init__(self, parent)
self.db = None
self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher', 'tags', 'series']
self.editable_cols = [0, 1, 4, 5, 6]
self.editable_cols = [0, 1, 4, 5, 6, 7]
self.default_image = QImage(':/images/book.svg')
self.sorted_on = (3, Qt.AscendingOrder)
self.last_search = '' # The last search performed on this model
@ -128,7 +128,7 @@ class BooksModel(QAbstractTableModel):
for row in rows:
if self.cover_cache:
id = self.db.id(row)
self.cover_cache.refresh(id)
self.cover_cache.refresh([id])
if row == current_row:
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
self.get_book_display_info(row))
@ -168,6 +168,11 @@ class BooksModel(QAbstractTableModel):
def search_tokens(self, text):
return text_to_tokens(text)
def books_added(self, num):
if num > 0:
self.beginInsertRows(QModelIndex(), 0, num-1)
self.endInsertRows()
def search(self, text, refinement, reset=True):
tokens, OR = self.search_tokens(text)
@ -201,9 +206,13 @@ class BooksModel(QAbstractTableModel):
LibraryDatabase.sizeof_old_database(path) > 0
def columnCount(self, parent):
if parent and parent.isValid():
return 0
return len(self.cols)
def rowCount(self, parent):
if parent and parent.isValid():
return 0
return self.db.rows() if self.db else 0
def count(self):
@ -226,6 +235,7 @@ class BooksModel(QAbstractTableModel):
else:
formats = _('None')
data[_('Formats')] = formats
data[_('Path')] = self.db.abspath(idx)
comments = self.db.comments(idx)
if not comments:
comments = _('None')
@ -248,7 +258,10 @@ class BooksModel(QAbstractTableModel):
for i in range(1, k):
ids.extend([idx-i, idx+i])
ids = ids + [i for i in range(l, r, 1) if i not in ids]
ids = [self.db.id(i) for i in ids]
try:
ids = [self.db.id(i) for i in ids]
except IndexError:
return
self.cover_cache.set_cache(ids)
def current_changed(self, current, previous, emit_signal=True):
@ -261,6 +274,8 @@ class BooksModel(QAbstractTableModel):
return data
def get_book_info(self, index):
if isinstance(index, int):
index = self.index(index, 0)
data = self.current_changed(index, None, False)
row = index.row()
data[_('Title')] = self.db.title(row)
@ -311,11 +326,17 @@ class BooksModel(QAbstractTableModel):
ans = []
for row in (row.row() for row in rows):
format = None
for f in self.db.formats(row).split(','):
if f.lower() in formats:
fmts = self.db.formats(row)
if not fmts:
return []
db_formats = set(fmts.lower().split(','))
available_formats = set([f.lower() for f in formats])
u = available_formats.intersection(db_formats)
for f in formats:
if f.lower() in u:
format = f
break
if format:
if format is not None:
pt = PersistentTemporaryFile(suffix='.'+format)
pt.write(self.db.format(row, format))
pt.flush()
@ -360,7 +381,7 @@ class BooksModel(QAbstractTableModel):
elif col == 1:
au = self.db.authors(row)
if au:
au = au.split(',')
au = [a.strip().replace('|', ',') for a in au.split(',')]
return QVariant("\n".join(au))
elif col == 2:
size = self.db.max_size(row)
@ -370,7 +391,7 @@ class BooksModel(QAbstractTableModel):
dt = self.db.timestamp(row)
if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime(BooksView.TIME_FMT).decode(preferred_encoding, 'replace'))
return QVariant(strftime(BooksView.TIME_FMT, dt.timetuple()))
elif col == 4:
r = self.db.rating(row)
r = r/2 if r else 0
@ -430,8 +451,19 @@ class BooksModel(QAbstractTableModel):
if col == 4:
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
column = self.cols[col]
self.db.set(row, column, val)
if col == 7:
pat = re.compile(r'\[(\d+)\]')
match = pat.search(val)
id = self.db.id(row)
if match is not None:
self.db.set_series_index(id, int(match.group(1)))
val = pat.sub('', val)
val = val.strip()
if val:
self.db.set_series(id, val)
else:
column = self.cols[col]
self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index)
if col == self.sorted_on[0]:
@ -559,17 +591,17 @@ class DeviceBooksModel(BooksModel):
self.marked_for_deletion = {}
def mark_for_deletion(self, id, rows):
self.marked_for_deletion[id] = self.indices(rows)
def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows)
for row in rows:
indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, id, succeeded=True):
if not self.marked_for_deletion.has_key(id):
def deletion_done(self, job, succeeded=True):
if not self.marked_for_deletion.has_key(job):
return
rows = self.marked_for_deletion.pop(id)
rows = self.marked_for_deletion.pop(job)
for row in rows:
if not succeeded:
indices = self.row_indices(self.index(row, 0))
@ -662,9 +694,13 @@ class DeviceBooksModel(BooksModel):
self.reset()
def columnCount(self, parent):
if parent and parent.isValid():
return 0
return 5
def rowCount(self, parent):
if parent and parent.isValid():
return 0
return len(self.map)
def set_database(self, db):
@ -690,7 +726,7 @@ class DeviceBooksModel(BooksModel):
dt = item.datetime
dt = datetime(*dt[0:6])
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
data[_('Timestamp')] = dt.strftime('%a %b %d %H:%M:%S %Y')
data[_('Timestamp')] = strftime('%a %b %d %H:%M:%S %Y', dt.timetuple())
data[_('Tags')] = ', '.join(item.tags)
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
@ -731,7 +767,7 @@ class DeviceBooksModel(BooksModel):
dt = self.db[self.map[row]].datetime
dt = datetime(*dt[0:6])
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime(BooksView.TIME_FMT))
return QVariant(strftime(BooksView.TIME_FMT, dt.timetuple()))
elif col == 4:
tags = self.db[self.map[row]].tags
if tags:
@ -841,6 +877,13 @@ class SearchBox(QLineEdit):
self.prev_search = text
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
def search_from_tokens(self, tokens, all):
ans = u' '.join([u'%s:%s'%x for x in tokens])
if not all:
ans = '[' + ans + ']'
self.set_search_string(ans)
def set_search_string(self, txt):
self.normalize_state()
self.setText(txt)

View File

@ -108,6 +108,7 @@ class _Canvas(QGraphicsRectItem):
line = block.peek()
y += block.bs.topskip
block_consumed = False
line.height = min(line.height, self.max_y-block.bs.topskip) # LRF files from TOR have Plot elements with their height set to 800
while y + line.height <= self.max_y:
block.commit()
if isinstance(line, QGraphicsItem):

View File

@ -450,7 +450,7 @@ class Line(QGraphicsItem):
if self.current_link is not None:
self.end_link()
# We justify is line is small and it doesn't have links in it
# We justify if line is small and it doesn't have links in it
# If it has links, justification would cause the boundingrect of the link to
# be too small
if self.current_width >= 0.85 * self.line_length and len(self.links) == 0:

View File

@ -1,11 +1,11 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, textwrap, collections, traceback, time
import os, sys, textwrap, collections, traceback, time, re
from xml.parsers.expat import ExpatError
from functools import partial
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QUrl
from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \
QToolButton, QDialog, QDesktopServices
QToolButton, QDialog, QDesktopServices, QFileDialog
from PyQt4.QtSvg import QSvgRenderer
from calibre import __version__, __appname__, islinux, sanitize_file_name, \
@ -34,20 +34,20 @@ from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from calibre.gui2.dialogs.jobs import JobsDialog
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebooks, set_conversion_defaults, fetch_news
from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.search import SearchDialog
from calibre.gui2.dialogs.user_profiles import UserProfiles
import calibre.gui2.dialogs.comicconf as ComicConf
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.book_info import BookInfo
from calibre.ebooks.metadata.meta import set_metadata
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.html import gui_main as html2oeb
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
from calibre.library.database2 import LibraryDatabase2, CoverCache
from calibre.parallel import JobKilled
from calibre.utils.filenames import ascii_filename
from calibre.gui2.widgets import WarningDialog
class Main(MainWindow, Ui_MainWindow):
@ -122,13 +122,13 @@ class Main(MainWindow, Ui_MainWindow):
sm.addAction(_('Send to storage card by default'))
sm.actions()[-1].setCheckable(True)
def default_sync(checked):
config.set('send_to_device_by_default', bool(checked))
config.set('send_to_storage_card_by_default', bool(checked))
QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_main_memory)
QObject.disconnect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card)
QObject.connect(self.action_sync, SIGNAL("triggered(bool)"), self.sync_to_card if checked else self.sync_to_main_memory)
QObject.connect(sm.actions()[-1], SIGNAL('toggled(bool)'), default_sync)
sm.actions()[-1].setChecked(config.get('send_to_device_by_default'))
sm.actions()[-1].setChecked(config.get('send_to_storage_card_by_default'))
default_sync(sm.actions()[-1].isChecked())
self.sync_menu = sm # Needed
md = QMenu()
@ -178,7 +178,7 @@ class Main(MainWindow, Ui_MainWindow):
cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert'))
cm.addSeparator()
cm.addAction(_('Set defaults for conversion to LRF'))
cm.addAction(_('Set defaults for conversion'))
cm.addAction(_('Set defaults for conversion of comics'))
self.action_convert.setMenu(cm)
QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single)
@ -214,7 +214,17 @@ class Main(MainWindow, Ui_MainWindow):
self.show()
self.stack.setCurrentIndex(0)
db = LibraryDatabase2(self.library_path)
try:
db = LibraryDatabase2(self.library_path)
except OSError, err:
error_dialog(self, _('Bad database location'), unicode(err)).exec_()
dir = unicode(QFileDialog.getExistingDirectory(self,
_('Choose a location for your ebook library.'), os.path.expanduser('~')))
if not dir:
QCoreApplication.exit(1)
else:
self.library_path = dir
db = LibraryDatabase2(self.library_path)
self.library_view.set_database(db)
if self.olddb is not None:
from PyQt4.QtGui import QProgressDialog
@ -223,10 +233,13 @@ class Main(MainWindow, Ui_MainWindow):
pd.setCancelButton(None)
pd.setWindowTitle(_('Migrating database'))
pd.show()
db.migrate_old(self.olddb, pd)
number_of_books = db.migrate_old(self.olddb, pd)
self.olddb.close()
if number_of_books == 0:
os.remove(self.olddb.dbpath)
self.olddb = None
prefs['library_path'] = self.library_path
self.library_view.sortByColumn(3, Qt.DescendingOrder)
self.library_view.sortByColumn(*dynamic.get('sort_column', (3, Qt.DescendingOrder)))
if not self.library_view.restore_column_widths():
self.library_view.resizeColumnsToContents()
self.library_view.resizeRowsToContents()
@ -234,6 +247,16 @@ class Main(MainWindow, Ui_MainWindow):
self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache
self.tags_view.setVisible(False)
self.match_all.setVisible(False)
self.match_any.setVisible(False)
self.popularity.setVisible(False)
self.tags_view.set_database(db, self.match_all, self.popularity)
self.connect(self.tags_view, SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self.search.search_from_tokens)
self.connect(self.status_bar.tag_view_button, SIGNAL('toggled(bool)'), self.toggle_tags_view)
self.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
self.tags_view.model().reinit)
########################### Cover Flow ################################
self.cover_flow = None
if CoverFlow is not None:
@ -273,6 +296,18 @@ class Main(MainWindow, Ui_MainWindow):
self.status_bar.book_info.book_data.setMaximumHeight(1000)
self.setMaximumHeight(available_height())
def toggle_tags_view(self, show):
if show:
self.tags_view.setVisible(True)
self.match_all.setVisible(True)
self.match_any.setVisible(True)
self.popularity.setVisible(True)
self.tags_view.setFocus(Qt.OtherFocusReason)
else:
self.tags_view.setVisible(False)
self.match_all.setVisible(False)
self.match_any.setVisible(False)
self.popularity.setVisible(False)
def sync_cf_to_listview(self, index, *args):
if not hasattr(index, 'row') and self.library_view.currentIndex().row() != index:
@ -411,8 +446,8 @@ class Main(MainWindow, Ui_MainWindow):
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
for mi, formats in duplicates:
files += '<li>'+mi.title+'</li>\n'
d = question_dialog(self, _('Duplicates found!'), files+'</ul></p>')
if d.exec_() == QMessageBox.Yes:
d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'</ul></p>', self)
if d.exec_() == QDialog.Accepted:
for mi, formats in duplicates:
self.library_view.model().db.import_book(mi, formats )
@ -459,39 +494,58 @@ class Main(MainWindow, Ui_MainWindow):
if to_device:
self.status_bar.showMessage(_('Uploading books to device.'), 2000)
def _add_books(self, paths, to_device):
on_card = False if self.stack.currentIndex() != 2 else True
def _add_books(self, paths, to_device, on_card=None):
if on_card is None:
on_card = self.stack.currentIndex() == 2
# Get format and metadata information
formats, metadata, names, infos = [], [], [], []
for book in paths:
format = os.path.splitext(book)[1]
format = format[1:] if format else None
stream = open(book, 'rb')
mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
try:
mi = get_metadata(stream, stream_type=format, use_libprs_metadata=True)
except:
mi = MetaInformation(None, None)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(book))[0]
if not mi.authors:
mi.authors = [_('Unknown')]
formats.append(format)
metadata.append(mi)
names.append(os.path.basename(book))
if not mi.authors:
mi.authors = ['Unknown']
infos.append({'title':mi.title, 'authors':', '.join(mi.authors),
'cover':self.default_thumbnail, 'tags':[]})
if not to_device:
model = self.current_view().model()
duplicates = model.add_books(paths, formats, metadata)
model = self.library_view.model()
html_pat = re.compile(r'\.x{0,1}htm(l{0,1})\s*$', re.IGNORECASE)
paths = list(paths)
for i, path in enumerate(paths):
if html_pat.search(path) is not None:
try:
paths[i] = html2oeb(path)
except:
traceback.print_exc()
continue
if paths[i] is None:
paths[i] = path
else:
formats[i] = 'zip'
duplicates, number_added = model.add_books(paths, formats, metadata)
if duplicates:
files = _('<p>Books with the same title as the following already exist in the database. Add them anyway?<ul>')
for mi in duplicates[2]:
files += '<li>'+mi.title+'</li>\n'
d = question_dialog(self, _('Duplicates found!'), files+'</ul></p>')
if d.exec_() == QMessageBox.Yes:
model.add_books(*duplicates, **dict(add_duplicates=True))
model.resort()
model.research()
d = WarningDialog(_('Duplicates found!'), _('Duplicates found!'), files+'</ul></p>', parent=self)
if d.exec_() == QDialog.Accepted:
num = model.add_books(*duplicates, **dict(add_duplicates=True))[1]
number_added += num
#self.library_view.sortByColumn(3, Qt.DescendingOrder)
#model.research()
model.books_added(number_added)
else:
self.upload_books(paths, names, infos, on_card=on_card)
self.upload_books(paths, list(map(sanitize_file_name, names)), infos, on_card=on_card)
def upload_books(self, files, names, metadata, on_card=False, memory=None):
'''
@ -557,9 +611,9 @@ class Main(MainWindow, Ui_MainWindow):
else:
view = self.memory_view if self.stack.currentIndex() == 1 else self.card_view
paths = view.model().paths(rows)
id = self.remove_paths(paths)
self.delete_memory[id] = (paths, view.model())
view.model().mark_for_deletion(id, rows)
job = self.remove_paths(paths)
self.delete_memory[job] = (paths, view.model())
view.model().mark_for_deletion(job, rows)
self.status_bar.showMessage(_('Deleting books from device.'), 1000)
def remove_paths(self, paths):
@ -570,7 +624,7 @@ class Main(MainWindow, Ui_MainWindow):
Called once deletion is done on the device
'''
for view in (self.memory_view, self.card_view):
view.model().deletion_done(id, bool(job.exception))
view.model().deletion_done(job, bool(job.exception))
if job.exception is not None:
self.device_job_exception(job)
return
@ -743,29 +797,32 @@ class Main(MainWindow, Ui_MainWindow):
self.news_menu.set_custom_feeds(feeds)
def fetch_news(self, data):
pt = PersistentTemporaryFile(suffix='_feeds2lrf.lrf')
pt.close()
args = ['feeds2lrf', '--output', pt.name, '--debug']
if data['username']:
args.extend(['--username', data['username']])
if data['password']:
args.extend(['--password', data['password']])
args.append(data['script'] if data['script'] else data['title'])
job = self.job_manager.run_job(Dispatcher(self.news_fetched), 'feeds2lrf', args=[args],
description=_('Fetch news from ')+data['title'])
self.conversion_jobs[job] = (pt, 'lrf')
func, args, desc, fmt, temp_files = fetch_news(data)
self.status_bar.showMessage(_('Fetching news from ')+data['title'], 2000)
job = self.job_manager.run_job(Dispatcher(self.news_fetched), func, args=args,
description=desc)
self.conversion_jobs[job] = (temp_files, fmt)
self.status_bar.showMessage(_('Fetching news from ')+data['title'], 2000)
def news_fetched(self, job):
pt, fmt = self.conversion_jobs.pop(job)
temp_files, fmt = self.conversion_jobs.pop(job)
pt = temp_files[0]
if job.exception is not None:
self.job_exception(job)
return
to_device = self.device_connected and fmt in self.device_manager.device_class.FORMATS
self._add_books([pt.name], to_device)
to_device = self.device_connected and fmt.lower() in self.device_manager.device_class.FORMATS
self._add_books([pt.name], to_device,
on_card=config.get('send_to_storage_card_by_default') and self.device_connected and bool(self.device_manager.device.card_prefix()))
if to_device:
self.status_bar.showMessage(_('News fetched. Uploading to device.'), 2000)
self.persistent_files.append(pt)
try:
if not to_device:
for f in temp_files:
if os.path.exists(f.name):
os.remove(f.name)
except:
pass
############################################################################
@ -789,189 +846,60 @@ class Main(MainWindow, Ui_MainWindow):
others.append(r)
return comics, others
def convert_bulk_others(self, rows):
d = LRFBulkDialog(self)
d.exec_()
if d.result() != QDialog.Accepted:
return
bad_rows = []
self.status_bar.showMessage(_('Starting Bulk conversion of %d books')%len(rows), 2000)
if rows and hasattr(rows[0], 'row'):
rows = [r.row() for r in rows]
for i, row in enumerate(rows):
cmdline = list(d.cmdline)
mi = self.library_view.model().db.get_metadata(row)
if mi.title:
cmdline.extend(['--title', mi.title])
if mi.authors:
cmdline.extend(['--author', ','.join(mi.authors)])
if mi.publisher:
cmdline.extend(['--publisher', mi.publisher])
if mi.comments:
cmdline.extend(['--comment', mi.comments])
data = None
for fmt in LRF_PREFERRED_SOURCE_FORMATS:
try:
data = self.library_view.model().db.format(row, fmt.upper())
break
except:
continue
if data is None:
bad_rows.append(row)
continue
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
cover = self.library_view.model().db.cover(row)
if cover:
cf = PersistentTemporaryFile('.jpeg')
cf.write(cover)
cf.close()
cmdline.extend(['--cover', cf.name])
cmdline.extend(['-o', of.name])
cmdline.append(pt.name)
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'any2lrf', args=[cmdline],
description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title)))
self.conversion_jobs[job] = (d.cover_file, pt, of, d.output_format,
self.library_view.model().db.id(row))
res = []
for row in bad_rows:
title = self.library_view.model().db.title(row)
res.append('<li>%s</li>'%title)
if res:
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), len(rows), '\n'.join(res))
warning_dialog(self, _('Could not convert some books'), msg).exec_()
def convert_bulk(self, checked):
comics, others = self.get_books_for_conversion()
if others:
self.convert_bulk_others(others)
if comics:
opts = ComicConf.get_bulk_conversion_options(self)
if opts:
for i, row in enumerate(comics):
options = opts.copy()
mi = self.library_view.model().db.get_metadata(row)
if mi.title:
options.title = mi.title
if mi.authors:
opts.author = ','.join(mi.authors)
data = None
for fmt in ['cbz', 'cbr']:
try:
data = self.library_view.model().db.format(row, fmt.upper())
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
setattr(options, 'output', of.name)
options.verbose = 1
args = [pt.name, options]
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'comic2lrf', args=args,
description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title)))
self.conversion_jobs[job] = (None, pt, of, 'lrf',
self.library_view.model().db.id(row))
def set_conversion_defaults(self, checked):
d = LRFSingleDialog(self, None, None)
d.exec_()
r = self.get_books_for_conversion()
if r is None:
return
comics, others = r
def set_comic_conversion_defaults(self, checked):
ComicConf.set_conversion_defaults(self)
def convert_single_others(self, rows):
changed = False
for row in rows:
d = LRFSingleDialog(self, self.library_view.model().db, row)
if d.selected_format:
d.exec_()
if d.result() == QDialog.Accepted:
cmdline = d.cmdline
data = self.library_view.model().db.format(row, d.selected_format)
pt = PersistentTemporaryFile('.'+d.selected_format.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
cmdline.extend(['-o', of.name])
cmdline.append(pt.name)
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'any2lrf', args=[cmdline],
description=_('Convert book: ')+d.title())
self.conversion_jobs[job] = (d.cover_file, pt, of, d.output_format, d.id)
changed = True
jobs, changed = convert_bulk_ebooks(self, self.library_view.model().db, comics, others)
for func, args, desc, fmt, id, temp_files in jobs:
job = self.job_manager.run_job(Dispatcher(self.book_converted),
func, args=args, description=desc)
self.conversion_jobs[job] = (temp_files, fmt, id)
if changed:
self.library_view.model().resort(reset=False)
self.library_view.model().research()
def set_conversion_defaults(self, checked):
set_conversion_defaults(False, self, self.library_view.model().db)
def set_comic_conversion_defaults(self, checked):
set_conversion_defaults(True, self, self.library_view.model().db)
def convert_single(self, checked):
comics, others = self.get_books_for_conversion()
if others:
self.convert_single_others(others)
changed = False
db = self.library_view.model().db
for row in comics:
mi = db.get_metadata(row)
title = author = _('Unknown')
if mi.title:
title = mi.title
if mi.authors:
author = ','.join(mi.authors)
defaults = db.conversion_options(db.id(row), 'comic')
opts, defaults = ComicConf.get_conversion_options(self, defaults, title, author)
if defaults is not None:
db.set_conversion_options(db.id(row), 'comic', defaults)
if opts is None: continue
for fmt in ['cbz', 'cbr']:
try:
data = db.format(row, fmt.upper())
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt)
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
opts.output = of.name
opts.verbose = 1
args = [pt.name, opts]
changed = True
r = self.get_books_for_conversion()
if r is None: return
comics, others = r
jobs, changed = convert_single_ebook(self, self.library_view.model().db, comics, others)
for func, args, desc, fmt, id, temp_files in jobs:
job = self.job_manager.run_job(Dispatcher(self.book_converted),
'comic2lrf', args=args,
description=_('Convert comic: ')+opts.title)
self.conversion_jobs[job] = (None, pt, of, 'lrf',
self.library_view.model().db.id(row))
func, args=args, description=desc)
self.conversion_jobs[job] = (temp_files, fmt, id)
if changed:
self.library_view.model().resort(reset=False)
self.library_view.model().research()
def book_converted(self, job):
of, fmt, book_id = self.conversion_jobs.pop(job)[2:]
if job.exception is not None:
self.job_exception(job)
return
data = open(of.name, 'rb')
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
data.close()
self.status_bar.showMessage(job.description + (' completed'), 2000)
temp_files, fmt, book_id = self.conversion_jobs.pop(job)
try:
if job.exception is not None:
self.job_exception(job)
return
data = open(temp_files[-1].name, 'rb')
self.library_view.model().db.add_format(book_id, fmt, data, index_is_id=True)
data.close()
self.status_bar.showMessage(job.description + (' completed'), 2000)
finally:
for f in temp_files:
try:
if os.path.exists(f.name):
os.remove(f.name)
except:
pass
#############################View book######################################
@ -1086,16 +1014,22 @@ class Main(MainWindow, Ui_MainWindow):
self.library_view.set_visible_columns(d.final_columns)
self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly)
self.save_menu.actions()[2].setText(_('Save only %s format to disk')%config.get('save_to_disk_single_format').upper())
if self.library_path != d.database_location:
try:
newloc = d.database_location
if not os.path.exists(os.path.join(newloc, 'metadata.db')):
if os.access(self.library_path, os.R_OK):
from PyQt4.QtGui import QProgressDialog
pd = QProgressDialog('', '', 0, 100, self)
pd.setWindowModality(Qt.ApplicationModal)
pd.setCancelButton(None)
pd.setWindowTitle(_('Copying database'))
pd.show()
self.status_bar.showMessage(_('Copying library to ')+newloc)
self.setCursor(Qt.BusyCursor)
self.library_view.setEnabled(False)
self.library_view.model().db.move_library_to(newloc)
self.library_view.model().db.move_library_to(newloc, pd)
else:
try:
db = LibraryDatabase2(newloc)
@ -1134,8 +1068,7 @@ class Main(MainWindow, Ui_MainWindow):
return
index = self.library_view.currentIndex()
if index.isValid():
info = self.library_view.model().get_book_info(index)
BookInfo(self, info).show()
BookInfo(self, self.library_view, index).show()
############################################################################
@ -1184,7 +1117,13 @@ class Main(MainWindow, Ui_MainWindow):
self.device_error_dialog.show()
def job_exception(self, job):
try:
if job.exception[0] == 'DRMError':
error_dialog(self, _('Conversion Error'),
_('<p>Could not convert: %s<p>It is a <a href="http://wiki.mobileread.com/wiki/DRM">DRM</a>ed book. You must first remove the DRM using 3rd party tools.')%job.description.split(':')[-1]).exec_()
return
except:
pass
only_msg = getattr(job.exception, 'only_msg', False)
try:
print job.console_text()
@ -1207,6 +1146,12 @@ class Main(MainWindow, Ui_MainWindow):
self.library_path = prefs['library_path']
self.olddb = None
if self.library_path is None: # Need to migrate to new database layout
QMessageBox.information(self, 'Database format changed',
'''\
<p>calibre's book storage format has changed. Instead of storing book files in a database, the
files are now stored in a folder on your filesystem. You will now be asked to choose the folder
in which you want to store your books files. Any existing books will be automatically migrated.
''')
self.database_path = prefs['database_path']
if not os.access(os.path.dirname(self.database_path), os.W_OK):
error_dialog(self, _('Database does not exist'),
@ -1222,7 +1167,6 @@ class Main(MainWindow, Ui_MainWindow):
home = os.path.dirname(self.database_path)
if not os.path.exists(home):
home = os.getcwd()
from PyQt4.QtGui import QFileDialog
dir = unicode(QFileDialog.getExistingDirectory(self,
_('Choose a location for your ebook library.'), home))
if not dir:
@ -1243,6 +1187,7 @@ class Main(MainWindow, Ui_MainWindow):
def write_settings(self):
config.set('main_window_geometry', self.saveGeometry())
dynamic.set('sort_column', self.library_view.model().sorted_on)
self.library_view.write_settings()
if self.device_connected:
self.memory_view.write_settings()

View File

@ -24,14 +24,6 @@
<normaloff>:/library</normaloff>:/library</iconset>
</property>
<widget class="QWidget" name="centralwidget" >
<property name="geometry" >
<rect>
<x>0</x>
<y>79</y>
<width>865</width>
<height>716</height>
</rect>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<layout class="QHBoxLayout" >
@ -242,60 +234,95 @@
</sizepolicy>
</property>
<property name="currentIndex" >
<number>2</number>
<number>0</number>
</property>
<widget class="QWidget" name="library" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
</rect>
</property>
<layout class="QVBoxLayout" >
<layout class="QVBoxLayout" name="verticalLayout_2" >
<item>
<widget class="BooksView" name="library_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<widget class="QRadioButton" name="match_any" >
<property name="text" >
<string>Match any</string>
</property>
<property name="checked" >
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="match_all" >
<property name="text" >
<string>Match all</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="popularity" >
<property name="text" >
<string>Sort by &amp;popularity</string>
</property>
</widget>
</item>
<item>
<widget class="TagsView" name="tags_view" >
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="animated" >
<bool>true</bool>
</property>
<property name="headerHidden" >
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="BooksView" name="library_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="main_memory" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
</rect>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="memory_view" >
@ -331,14 +358,6 @@
</layout>
</widget>
<widget class="QWidget" name="page" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>857</width>
<height>552</height>
</rect>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="card_view" >
@ -378,14 +397,6 @@
</layout>
</widget>
<widget class="QToolBar" name="tool_bar" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>865</width>
<height>79</height>
</rect>
</property>
<property name="minimumSize" >
<size>
<width>0</width>
@ -425,14 +436,6 @@
<addaction name="action_view" />
</widget>
<widget class="QStatusBar" name="statusBar" >
<property name="geometry" >
<rect>
<x>0</x>
<y>795</y>
<width>865</width>
<height>27</height>
</rect>
</property>
<property name="mouseTracking" >
<bool>true</bool>
</property>
@ -564,6 +567,11 @@
<extends>QTableView</extends>
<header>library.h</header>
</customwidget>
<customwidget>
<class>TagsView</class>
<extends>QTreeView</extends>
<header>tags.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc" />

View File

@ -1,116 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Manage the PyQt build system pyrcc4, pylupdate4, lrelease and friends.
'''
import sys, os, subprocess, cStringIO, compiler, re
from functools import partial
from PyQt4.uic import compileUi
check_call = partial(subprocess.check_call, shell=True)
sys.path.insert(1, os.path.abspath('..%s..'%os.sep))
from calibre import __appname__
from calibre.path import path
def find_forms():
forms = []
for root, dirs, files in os.walk('.'):
for name in files:
if name.endswith('.ui'):
forms.append(os.path.abspath(os.path.join(root, name)))
return forms
def form_to_compiled_form(form):
return form.rpartition('.')[0]+'_ui.py'
def build_forms(forms):
for form in forms:
compiled_form = form_to_compiled_form(form)
if not os.path.exists(compiled_form) or os.stat(form).st_mtime > os.stat(compiled_form).st_mtime:
print 'Compiling form', form
buf = cStringIO.StringIO()
compileUi(form, buf)
dat = buf.getvalue()
dat = dat.replace('__appname__', __appname__)
dat = dat.replace('import images_rc', 'from calibre.gui2 import images_rc')
dat = dat.replace('from library import', 'from calibre.gui2.library import')
dat = dat.replace('from widgets import', 'from calibre.gui2.widgets import')
dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(?<!\\)",.+?\)', re.DOTALL).sub(r'_("\1")', dat)
# Workaround bug in Qt 4.4 on Windows
if form.endswith('dialogs%sconfig.ui'%os.sep) or form.endswith('dialogs%slrf_single.ui'%os.sep):
print 'Implementing Workaround for buggy pyuic in form', form
dat = re.sub(r'= QtGui\.QTextEdit\(self\..*?\)', '= QtGui.QTextEdit()', dat)
dat = re.sub(r'= QtGui\.QListWidget\(self\..*?\)', '= QtGui.QListWidget()', dat)
open(compiled_form, 'wb').write(dat)
def build_images():
p = path('images')
mtime = p.mtime
for x in p.walk():
mtime = max(x.mtime, mtime)
images = path('images_rc.py')
if not images.exists() or mtime > images.mtime:
print 'Compiling images...'
files = []
for x in p.walk():
if '.svn' in x or '.bzr' in x or x.isdir():
continue
alias = ' alias="library"' if x == p/'library.png' else ''
files.append('<file%s>%s</file>'%(alias, x))
qrc = '<RCC>\n<qresource prefix="/">\n%s\n</qresource>\n</RCC>'%'\n'.join(files)
f = open('images.qrc', 'wb')
f.write(qrc)
f.close()
check_call(' '.join(['pyrcc4', '-o', images, 'images.qrc']))
compiler.compileFile(images)
os.utime(images, None)
os.utime(images, None)
print 'Size of images:', '%.2f MB'%(path(images+'c').size/(1024*1024.))
def build(forms):
build_forms(forms)
build_images()
def clean(forms):
for form in forms:
compiled_form = form_to_compiled_form(form)
if os.path.exists(compiled_form):
print 'Removing compiled form', compiled_form
os.unlink(compiled_form)
print 'Removing compiled images'
os.unlink('images_rc.py')
os.unlink('images_rc.pyc')
def main(args=sys.argv):
if not os.getcwd().endswith('gui2'):
raise Exception('Must be run from the gui2 directory')
forms = find_forms()
if len(args) == 1:
args.append('all')
if args[1] == 'all':
build(forms)
elif args[1] == 'clean':
clean(forms)
elif args[1] == 'test':
build(forms)
print 'Running main.py'
subprocess.call('python main.py', shell=True)
else:
print 'Usage: %s [all|clean|test]'%(args[0])
return 1
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,10 +1,10 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import re
import re, collections
from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \
QVBoxLayout, QSizePolicy, QToolButton, QIcon
from PyQt4.QtCore import Qt, QSize, SIGNAL
from PyQt4.QtCore import Qt, QSize, SIGNAL, QCoreApplication
from calibre import fit_image, preferred_encoding
from calibre.gui2 import qstring_to_unicode
@ -47,6 +47,13 @@ class BookInfoDisplay(QFrame):
def mouseReleaseEvent(self, ev):
self.emit(SIGNAL('mr(int)'), 1)
WEIGHTS = collections.defaultdict(lambda : 100)
WEIGHTS[_('Path')] = 0
WEIGHTS[_('Formats')] = 1
WEIGHTS[_('Comments')] = 2
WEIGHTS[_('Series')] = 3
WEIGHTS[_('Tags')] = 4
def __init__(self, clear_message):
QFrame.__init__(self)
@ -73,8 +80,10 @@ class BookInfoDisplay(QFrame):
rows = u''
self.book_data.setText('')
self.data = data
for key in data.keys():
self.data = data.copy()
keys = data.keys()
keys.sort(cmp=lambda x, y: cmp(self.WEIGHTS[x], self.WEIGHTS[y]))
for key in keys:
txt = data[key]
#txt = '<br />\n'.join(textwrap.wrap(txt, 120))
if isinstance(key, str):
@ -140,13 +149,28 @@ class CoverFlowButton(QToolButton):
def disable(self, reason):
self.setDisabled(True)
self.setToolTip(_('<p>Browsing books by their covers is disabled.<br>Import of pictureflow module failed:<br>')+reason)
class TagViewButton(QToolButton):
def __init__(self, parent=None):
QToolButton.__init__(self, parent)
self.setIconSize(QSize(80, 80))
self.setIcon(QIcon(':/images/tags.svg'))
self.setToolTip(_('Click to browse books by tags'))
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding))
self.setCheckable(True)
self.setChecked(False)
self.setAutoRaise(True)
class StatusBar(QStatusBar):
def __init__(self, jobs_dialog):
QStatusBar.__init__(self)
self.movie_button = MovieButton(QMovie(':/images/jobs-animated.mng'), jobs_dialog)
self.cover_flow_button = CoverFlowButton()
self.tag_view_button = TagViewButton()
self.addPermanentWidget(self.cover_flow_button)
self.addPermanentWidget(self.tag_view_button)
self.addPermanentWidget(self.movie_button)
self.book_info = BookInfoDisplay(self.clearMessage)
self.connect(self.book_info, SIGNAL('show_book_info()'), self.show_book_info)
@ -186,6 +210,7 @@ class StatusBar(QStatusBar):
if self.movie_button.movie.state() == QMovie.Running:
self.movie_button.movie.jumpToFrame(0)
self.movie_button.movie.setPaused(True)
QCoreApplication.instance().alert(self, 5000)
if __name__ == '__main__':
# Used to create the animated status icon

165
src/calibre/gui2/tags.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Browsing book collection by tags.
'''
from PyQt4.Qt import QAbstractItemModel, Qt, QVariant, QTreeView, QModelIndex, \
QFont, SIGNAL, QSize, QColor, QIcon
from calibre.gui2 import config
NONE = QVariant()
class TagsView(QTreeView):
def __init__(self, *args):
QTreeView.__init__(self, *args)
self.setUniformRowHeights(True)
self.setCursor(Qt.PointingHandCursor)
self.setIconSize(QSize(30, 30))
def set_database(self, db, match_all, popularity):
self._model = TagsModel(db)
self.popularity = popularity
self.match_all = match_all
self.setModel(self._model)
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
self.popularity.setChecked(config['sort_by_popularity'])
self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
def sort_changed(self, state):
config.set('sort_by_popularity', state == Qt.Checked)
self.model().refresh()
def toggle(self, index):
if self._model.toggle(index):
self.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self._model.tokens(), self.match_all.isChecked())
class TagsModel(QAbstractItemModel):
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('Tags')]
row_map = {0: 'author', 1:'series', 2:'format', 3:'publisher', 4:'tag'}
def __init__(self, db):
QAbstractItemModel.__init__(self)
self.db = db
self.ignore_next_search = False
self._data = {}
self.refresh()
self.bold_font = QFont()
self.bold_font.setBold(True)
self.bold_font = QVariant(self.bold_font)
self.status_map = [QColor(200,200,200, 0), QIcon(':/images/plus.svg'), QIcon(':/images/minus.svg')]
self.status_map = list(map(QVariant, self.status_map))
self.cmap = [QIcon(':/images/user_profile.svg'), QIcon(':/images/series.svg'), QIcon(':/images/book.svg'), QIcon(':/images/publisher.png'), QIcon(':/images/tags.svg')]
self.cmap = list(map(QVariant, self.cmap))
self.db.add_listener(self.database_changed)
def database_changed(self, event, ids):
self.refresh()
def refresh(self):
old_data = self._data
self._data = self.db.get_categories(config['sort_by_popularity'])
for key in old_data.keys():
for tag in old_data[key]:
try:
index = self._data[key].index(tag)
if index > -1:
self._data[key][index].state = tag.state
except:
continue
self.reset()
def reinit(self, *args, **kwargs):
if not self.ignore_next_search:
for category in self._data.values():
for tag in category:
tag.state = 0
self.reset()
self.ignore_next_search = False
def toggle(self, index):
if index.parent().isValid():
category = self.row_map[index.parent().row()]
tag = self._data[category][index.row()]
tag.state = (tag.state + 1)%3
self.ignore_next_search = True
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
return True
return False
def tokens(self):
ans = []
for key in self.row_map.values():
for tag in self._data[key]:
if tag.state > 0:
if tag.state == 2:
tag = '!'+tag
ans.append((key, tag))
return ans
def index(self, row, col, parent=QModelIndex()):
if parent.isValid():
if parent.parent().isValid(): # parent is a tag
return QModelIndex()
try:
category = self.row_map[parent.row()]
except KeyError:
return QModelIndex()
if col == 0 and row < len(self._data[category]):
return self.createIndex(row, col, parent.row())
return QModelIndex()
if col == 0 and row < len(self.categories):
return self.createIndex(row, col, -1)
return QModelIndex()
def parent(self, index):
if not index.isValid() or index.internalId() < 0:
return QModelIndex()
return self.createIndex(index.internalId(), 0, -1)
def rowCount(self, parent):
if not parent or not parent.isValid():
return len(self.categories)
if not parent.parent().isValid():
return len(self._data[self.row_map[parent.row()]])
return 0
def columnCount(self, parent):
return 1
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEnabled
def category_data(self, index, role):
if role == Qt.DisplayRole:
row = index.row()
return QVariant(self.categories[row])
if role == Qt.FontRole:
return self.bold_font
if role == Qt.SizeHintRole:
return QVariant(QSize(100, 40))
if role == Qt.DecorationRole:
return self.cmap[index.row()]
return NONE
def tag_data(self, index, role):
category = self.row_map[index.parent().row()]
if role == Qt.DisplayRole:
return QVariant(self._data[category][index.row()].as_string())
if role == Qt.DecorationRole:
return self.status_map[self._data[category][index.row()].state]
return NONE
def data(self, index, role):
if not index.parent().isValid():
return self.category_data(index, role)
if not index.parent().parent().isValid():
return self.tag_data(index, role)
return NONE

380
src/calibre/gui2/tools.py Normal file
View File

@ -0,0 +1,380 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
'''
Logic for setting up conversion jobs
'''
import os
from PyQt4.Qt import QDialog
from calibre.utils.config import prefs
from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog
from calibre.gui2.dialogs.epub import Config as EPUBConvert
import calibre.gui2.dialogs.comicconf as ComicConf
from calibre.gui2 import warning_dialog
from calibre.ptempfile import PersistentTemporaryFile
from calibre.ebooks.lrf import preferred_source_formats as LRF_PREFERRED_SOURCE_FORMATS
from calibre.ebooks.metadata.opf import OPFCreator
from calibre.ebooks.epub.from_any import SOURCE_FORMATS as EPUB_PREFERRED_SOURCE_FORMATS
def convert_single_epub(parent, db, comics, others):
changed = False
jobs = []
for row in others:
temp_files = []
d = EPUBConvert(parent, db, row)
if d.source_format is not None:
d.exec_()
if d.result() == QDialog.Accepted:
opts = d.opts
data = db.format(row, d.source_format)
pt = PersistentTemporaryFile('.'+d.source_format.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.epub')
of.close()
opts.output = of.name
opts.from_opf = d.opf_file.name
opts.verbose = 2
args = [opts, pt.name]
if d.cover_file:
temp_files.append(d.cover_file)
opts.cover = d.cover_file.name
temp_files.extend([d.opf_file, pt, of])
jobs.append(('any2epub', args, _('Convert book: ')+d.mi.title,
'EPUB', db.id(row), temp_files))
changed = True
for row in comics:
mi = db.get_metadata(row)
title = author = _('Unknown')
if mi.title:
title = mi.title
if mi.authors:
author = ','.join(mi.authors)
defaults = db.conversion_options(db.id(row), 'comic')
opts, defaults = ComicConf.get_conversion_options(parent, defaults, title, author)
if defaults is not None:
db.set_conversion_options(db.id(row), 'comic', defaults)
if opts is None: continue
for fmt in ['cbz', 'cbr']:
try:
data = db.format(row, fmt.upper())
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt)
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.epub')
of.close()
opts.output = of.name
opts.verbose = 2
args = [pt.name, opts]
changed = True
jobs.append(('comic2epub', args, _('Convert comic: ')+opts.title,
'EPUB', db.id(row), [pt, of]))
return jobs, changed
def convert_single_lrf(parent, db, comics, others):
changed = False
jobs = []
for row in others:
temp_files = []
d = LRFSingleDialog(parent, db, row)
if d.selected_format:
d.exec_()
if d.result() == QDialog.Accepted:
cmdline = d.cmdline
data = db.format(row, d.selected_format)
pt = PersistentTemporaryFile('.'+d.selected_format.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
cmdline.extend(['-o', of.name])
cmdline.append(pt.name)
if d.cover_file:
temp_files.append(d.cover_file)
temp_files.extend([pt, of])
jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(),
'LRF', db.id(row), temp_files))
changed = True
for row in comics:
mi = db.get_metadata(row)
title = author = _('Unknown')
if mi.title:
title = mi.title
if mi.authors:
author = ','.join(mi.authors)
defaults = db.conversion_options(db.id(row), 'comic')
opts, defaults = ComicConf.get_conversion_options(parent, defaults, title, author)
if defaults is not None:
db.set_conversion_options(db.id(row), 'comic', defaults)
if opts is None: continue
for fmt in ['cbz', 'cbr']:
try:
data = db.format(row, fmt.upper())
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt)
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
opts.output = of.name
opts.verbose = 1
args = [pt.name, opts]
changed = True
jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title,
'LRF', db.id(row), [pt, of]))
return jobs, changed
def convert_bulk_epub(parent, db, comics, others):
if others:
d = EPUBConvert(parent, db)
if d.exec_() != QDialog.Accepted:
others = []
else:
opts = d.opts
opts.verbose = 2
if comics:
comic_opts = ComicConf.get_bulk_conversion_options(parent)
if not comic_opts:
comics = []
bad_rows = []
jobs = []
total = sum(map(len, (others, comics)))
if total == 0:
return
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
for i, row in enumerate(others+comics):
if row in others:
data = None
for fmt in EPUB_PREFERRED_SOURCE_FORMATS:
try:
data = db.format(row, fmt.upper())
break
except:
continue
if data is None:
bad_rows.append(row)
continue
options = opts.copy()
mi = db.get_metadata(row)
opf = OPFCreator(os.getcwdu(), mi)
opf_file = PersistentTemporaryFile('.opf')
opf.render(opf_file)
opf_file.close()
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.epub')
of.close()
cover = db.cover(row)
cf = None
if cover:
cf = PersistentTemporaryFile('.jpeg')
cf.write(cover)
cf.close()
options.cover = cf.name
options.output = of.name
options.from_opf = opf_file.name
args = [options, pt.name]
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
temp_files = [cf] if cf is not None else []
temp_files.extend([opf_file, pt, of])
jobs.append(('any2epub', args, desc, 'EPUB', db.id(row), temp_files))
else:
options = comic_opts.copy()
mi = db.get_metadata(row)
if mi.title:
options.title = mi.title
if mi.authors:
options.author = ','.join(mi.authors)
data = None
for fmt in ['cbz', 'cbr']:
try:
data = db.format(row, fmt.upper())
if data:
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.epub')
of.close()
setattr(options, 'output', of.name)
options.verbose = 1
args = [pt.name, options]
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
jobs.append(('comic2epub', args, desc, 'EPUB', db.id(row), [pt, of]))
if bad_rows:
res = []
for row in bad_rows:
title = db.title(row)
res.append('<li>%s</li>'%title)
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res))
warning_dialog(parent, _('Could not convert some books'), msg).exec_()
return jobs, False
def convert_bulk_lrf(parent, db, comics, others):
if others:
d = LRFBulkDialog(parent)
if d.exec_() != QDialog.Accepted:
others = []
if comics:
comic_opts = ComicConf.get_bulk_conversion_options(parent)
if not comic_opts:
comics = []
bad_rows = []
jobs = []
total = sum(map(len, (others, comics)))
if total == 0:
return
parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000)
for i, row in enumerate(others+comics):
if row in others:
cmdline = list(d.cmdline)
mi = db.get_metadata(row)
if mi.title:
cmdline.extend(['--title', mi.title])
if mi.authors:
cmdline.extend(['--author', ','.join(mi.authors)])
if mi.publisher:
cmdline.extend(['--publisher', mi.publisher])
if mi.comments:
cmdline.extend(['--comment', mi.comments])
data = None
for fmt in LRF_PREFERRED_SOURCE_FORMATS:
try:
data = db.format(row, fmt.upper())
break
except:
continue
if data is None:
bad_rows.append(row)
continue
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
cover = db.cover(row)
cf = None
if cover:
cf = PersistentTemporaryFile('.jpeg')
cf.write(cover)
cf.close()
cmdline.extend(['--cover', cf.name])
cmdline.extend(['-o', of.name])
cmdline.append(pt.name)
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
temp_files = [cf] if cf is not None else []
temp_files.extend([pt, of])
jobs.append(('any2lrf', [cmdline], desc, 'LRF', db.id(row), temp_files))
else:
options = comic_opts.copy()
mi = db.get_metadata(row)
if mi.title:
options.title = mi.title
if mi.authors:
options.author = ','.join(mi.authors)
data = None
for fmt in ['cbz', 'cbr']:
try:
data = db.format(row, fmt.upper())
if data:
break
except:
continue
pt = PersistentTemporaryFile('.'+fmt.lower())
pt.write(data)
pt.close()
of = PersistentTemporaryFile('.lrf')
of.close()
setattr(options, 'output', of.name)
options.verbose = 1
args = [pt.name, options]
desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title))
jobs.append(('comic2lrf', args, desc, 'LRF', db.id(row), [pt, of]))
if bad_rows:
res = []
for row in bad_rows:
title = db.title(row)
res.append('<li>%s</li>'%title)
msg = _('<p>Could not convert %d of %d books, because no suitable source format was found.<ul>%s</ul>')%(len(res), total, '\n'.join(res))
warning_dialog(parent, _('Could not convert some books'), msg).exec_()
return jobs, False
def set_conversion_defaults_lrf(comic, parent, db):
if comic:
ComicConf.set_conversion_defaults(parent)
else:
LRFSingleDialog(parent, None, None).exec_()
def set_conversion_defaults_epub(comic, parent, db):
if comic:
ComicConf.set_conversion_defaults(parent)
else:
d = EPUBConvert(parent, db)
d.setWindowTitle(_('Set conversion defaults'))
d.exec_()
def _fetch_news(data, fmt):
pt = PersistentTemporaryFile(suffix='_feeds2%s.%s'%(fmt.lower(), fmt.lower()))
pt.close()
args = ['feeds2%s'%fmt.lower(), '--output', pt.name, '--debug']
if data['username']:
args.extend(['--username', data['username']])
if data['password']:
args.extend(['--password', data['password']])
args.append(data['script'] if data['script'] else data['title'])
return 'feeds2'+fmt.lower(), [args], _('Fetch news from ')+data['title'], fmt.upper(), [pt]
def convert_single_ebook(*args):
fmt = prefs['output_format'].lower()
if fmt == 'lrf':
return convert_single_lrf(*args)
elif fmt == 'epub':
return convert_single_epub(*args)
def convert_bulk_ebooks(*args):
fmt = prefs['output_format'].lower()
if fmt == 'lrf':
return convert_bulk_lrf(*args)
elif fmt == 'epub':
return convert_bulk_epub(*args)
def set_conversion_defaults(comic, parent, db):
fmt = prefs['output_format'].lower()
if fmt == 'lrf':
return set_conversion_defaults_lrf(comic, parent, db)
elif fmt == 'epub':
return set_conversion_defaults_epub(comic, parent, db)
def fetch_news(data):
fmt = prefs['output_format'].lower()
return _fetch_news(data, fmt)

View File

@ -3,10 +3,10 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Miscellaneous widgets used in the GUI
'''
import re, os
import re, os, traceback
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
QListWidgetItem, QTextCharFormat, QApplication, \
QSyntaxHighlighter, QCursor, QColor, QWidget, \
QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \
QAbstractItemDelegate, QPixmap, QStyle, QFontMetrics
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
QObject, QRegExp, QString, QSettings
@ -19,8 +19,16 @@ from calibre import fit_image
from calibre.utils.fontconfig import find_font_families
from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.utils.config import prefs
from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog
class WarningDialog(QDialog, Ui_WarningDialog):
def __init__(self, title, msg, details, parent=None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.setWindowTitle(title)
self.msg.setText(msg)
self.details.setText(details)
class FilenamePattern(QWidget, Ui_Form):
@ -246,7 +254,12 @@ class FontFamilyModel(QAbstractListModel):
def __init__(self, *args):
QAbstractListModel.__init__(self, *args)
self.families = find_font_families()
try:
self.families = find_font_families()
except:
self.families = []
print 'WARNING: Could not load fonts'
traceback.print_exc()
self.families.sort()
self.families[:0] = ['None']

View File

@ -318,7 +318,7 @@ def do_show_metadata(db, id, as_opf):
mi = OPFCreator(os.getcwd(), mi)
mi.render(sys.stdout)
else:
print mi
print unicode(mi).encode(preferred_encoding)
def command_show_metadata(args, dbpath):
parser = get_parser(_(

View File

@ -822,17 +822,18 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
'''
Rebuild self.data and self.cache. Filter results are lost.
'''
FIELDS = {'title' : 'sort',
'authors': 'author_sort',
'publisher': 'publisher',
'size': 'size',
'date': 'timestamp',
'timestamp':'timestamp',
'formats':'formats',
'rating': 'rating',
'tags':'tags',
'series': 'series',
}
FIELDS = {
'title' : 'sort',
'authors' : 'author_sort',
'publisher' : 'publisher',
'size' : 'size',
'date' : 'timestamp',
'timestamp' : 'timestamp',
'formats' : 'formats',
'rating' : 'rating',
'tags' : 'tags',
'series' : 'series',
}
field = FIELDS[sort_field]
order = 'ASC'
if not ascending:
@ -894,6 +895,11 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def id(self, index):
return self.data[index][0]
def row(self, id):
for r, record in enumerate(self.data):
if record[0] == id:
return r
def title(self, index, index_is_id=False):
if not index_is_id:
@ -904,7 +910,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return _('Unknown')
def authors(self, index, index_is_id=False):
''' Authors as a comma separated list or None'''
'''
Authors as a comma separated list or None.
In the comma separated list, commas in author names are replaced by | symbols
'''
if not index_is_id:
return self.data[index][2]
try:
@ -970,9 +979,15 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return ans[0]
def series_index(self, index, index_is_id=False):
ans = None
if not index_is_id:
return self.data[index][10]
return self.conn.execute('SELECT series_index FROM books WHERE id=?', (index,)).fetchone()[0]
ans = self.data[index][10]
else:
ans = self.conn.execute('SELECT series_index FROM books WHERE id=?', (index,)).fetchone()[0]
try:
return int(ans)
except:
return 1
def books_in_series(self, series_id):
'''
@ -1212,6 +1227,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
self.conn.commit()
row = self.row(id)
if row is not None:
self.data[row][9] = series
def remove_unused_series(self):
for id, in self.conn.execute('SELECT id FROM series').fetchall():
@ -1220,8 +1238,12 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
self.conn.commit()
def set_series_index(self, id, idx):
idx = int(idx)
self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id))
self.conn.commit()
row = self.row(id)
if row is not None:
self.data[row][10] = idx
def set_rating(self, id, rating):
rating = int(rating)
@ -1342,7 +1364,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
Convenience method to return metadata as a L{MetaInformation} object.
'''
aum = self.authors(idx, index_is_id=index_is_id)
if aum: aum = aum.split(',')
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
mi.comments = self.comments(idx, index_is_id=index_is_id)
@ -1418,6 +1440,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
fmts = ''
for fmt in fmts.split(','):
data = self.format(idx, fmt, index_is_id=index_is_id)
if not data:
continue
fname = name +'.'+fmt.lower()
fname = sanitize_file_name(fname)
f = open(os.path.join(base, fname), 'w+b')
@ -1492,7 +1516,6 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def import_book_directory(self, dirpath):
dirpath = os.path.abspath(dirpath)
formats = []
for path in os.listdir(dirpath):
path = os.path.abspath(os.path.join(dirpath, path))
if os.path.isdir(path) or not os.access(path, os.R_OK):
@ -1507,6 +1530,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
if not formats:
return
mi = metadata_from_formats(formats)
if mi.title is None:
return
@ -1537,8 +1561,12 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
for id in indices:
try:
data = self.format(id, format, index_is_id=True)
if not data:
failures.append((id, self.title(id, index_is_id=True)))
continue
except:
failures.append((id, self.title(id, index_is_id=True)))
continue
title = self.title(id, index_is_id=True)
au = self.authors(id, index_is_id=True)
if not au:

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
@ -6,7 +6,8 @@ __docformat__ = 'restructuredtext en'
'''
The database used to store ebook metadata
'''
import os, re, sys, shutil, cStringIO, glob, collections
import os, re, sys, shutil, cStringIO, glob, collections, textwrap, \
operator, itertools, functools, traceback
import sqlite3 as sqlite
from itertools import repeat
@ -15,6 +16,8 @@ from PyQt4.QtGui import QApplication, QPixmap, QImage
__app = None
from calibre.library.database import LibraryDatabase
from calibre.ebooks.metadata import string_to_authors
from calibre.constants import preferred_encoding
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
filesystem_encoding = sys.getfilesystemencoding()
@ -47,6 +50,7 @@ class CoverCache(QThread):
self.load_queue_lock = QReadWriteLock(QReadWriteLock.Recursive)
self.cache = {}
self.cache_lock = QReadWriteLock()
self.id_map_stale = True
self.keep_running = True
def build_id_map(self):
@ -60,6 +64,7 @@ class CoverCache(QThread):
except:
continue
self.id_map_lock.unlock()
self.id_map_stale = False
def set_cache(self, ids):
@ -79,9 +84,9 @@ class CoverCache(QThread):
def run(self):
while self.keep_running:
if self.id_map is None:
if self.id_map is None or self.id_map_stale:
self.build_id_map()
while True:
while True: # Load images from the load queue
self.load_queue_lock.lockForWrite()
try:
id = self.load_queue.popleft()
@ -101,6 +106,8 @@ class CoverCache(QThread):
self.id_map_lock.lockForRead()
if id in self.id_map.keys():
path = self.id_map[id]
else:
self.id_map_stale = True
self.id_map_lock.unlock()
if path and os.access(path, os.R_OK):
try:
@ -139,7 +146,7 @@ class CoverCache(QThread):
self.cache_lock.unlock()
self.load_queue_lock.lockForWrite()
for id in ids:
self.load_queue.append_left(id)
self.load_queue.appendleft(id)
self.load_queue_lock.unlock()
class Concatenate(object):
@ -159,6 +166,176 @@ class Concatenate(object):
return self.ans[:-len(self.sep)]
return self.ans
class ResultCache(object):
'''
Stores sorted and filtered metadata in memory.
'''
METHOD_MAP = {
'title' : 'title',
'authors' : 'author_sort',
'author' : 'author_sort',
'publisher' : 'publisher',
'size' : 'size',
'date' : 'timestamp',
'timestamp' : 'timestamp',
'rating' : 'rating',
'tags' : 'tags',
'series' : 'series',
}
def __init__(self):
self._map = self._map_filtered = self._data = []
def __getitem__(self, row):
return self._data[self._map_filtered[row]]
def __len__(self):
return len(self._map_filtered)
def __iter__(self):
for id in self._map_filtered:
yield self._data[id]
def remove(self, id):
self._data[id] = None
if id in self._map:
self._map.remove(id)
if id in self._map_filtered:
self._map_filtered.remove(id)
def set(self, row, col, val):
id = self._map_filtered[row]
self._data[id][col] = val
def index(self, id, cache=False):
x = self._map if cache else self._map_filtered
return x.index(id)
def row(self, id):
return self.index(id)
def refresh_ids(self, conn, ids):
for id in ids:
self._data[id] = conn.execute('SELECT * from meta WHERE id=?', (id,)).fetchone()
return map(self.row, ids)
def books_added(self, ids, conn):
if not ids:
return
self._data.extend(repeat(None, max(ids)-len(self._data)+2))
for id in ids:
self._data[id] = conn.execute('SELECT * from meta WHERE id=?', (id,)).fetchone()
self._map[0:0] = ids
self._map_filtered[0:0] = ids
def refresh(self, db, field, ascending):
field = field.lower()
method = getattr(self, 'sort_on_' + self.METHOD_MAP[field])
# Fast mapping from sorted row numbers to ids
self._map = map(operator.itemgetter(0), method('ASC' if ascending else 'DESC', db)) # Preserves sort order
# Fast mapping from sorted, filtered row numbers to ids
# At the moment it is the same as self._map
self._map_filtered = list(self._map)
temp = db.conn.execute('SELECT * FROM meta').fetchall()
# Fast mapping from ids to data.
# Can be None for ids that dont exist (i.e. have been deleted)
self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else []
for r in temp:
self._data[r[0]] = r
def filter(self, filters, refilter=False, OR=False):
'''
Filter data based on filters. All the filters must match for an item to
be accepted. Matching is case independent regexp matching.
@param filters: A list of SearchToken objects
@param refilter: If True filters are applied to the results of the previous
filtering.
@param OR: If True, keeps a match if any one of the filters matches. If False,
keeps a match only if all the filters match
'''
if not refilter:
self._map_filtered = list(self._map)
if filters:
remove = []
for id in self._map_filtered:
if OR:
keep = False
for token in filters:
if token.match(self._data[id]):
keep = True
break
if not keep:
remove.append(id)
else:
for token in filters:
if not token.match(self._data[id]):
remove.append(id)
break
for id in remove:
self._map_filtered.remove(id)
def sort_on_title(self, order, db):
return db.conn.execute('SELECT id FROM books ORDER BY sort ' + order).fetchall()
def sort_on_author_sort(self, order, db):
return db.conn.execute('SELECT id FROM books ORDER BY author_sort,sort ' + order).fetchall()
def sort_on_timestamp(self, order, db):
return db.conn.execute('SELECT id FROM books ORDER BY id ' + order).fetchall()
def sort_on_publisher(self, order, db):
no_publisher = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_publishers_link) ORDER BY books.sort').fetchall()
ans = []
for r in db.conn.execute('SELECT id FROM publishers ORDER BY name '+order).fetchall():
publishers_id = r[0]
ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_publishers_link WHERE publisher=?) ORDER BY books.sort '+order, (publishers_id,)).fetchall()
ans = (no_publisher + ans) if order == 'ASC' else (ans + no_publisher)
return ans
def sort_on_size(self, order, db):
return db.conn.execute('SELECT id FROM meta ORDER BY size ' + order).fetchall()
def sort_on_rating(self, order, db):
no_rating = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_ratings_link) ORDER BY books.sort').fetchall()
ans = []
for r in db.conn.execute('SELECT id FROM ratings ORDER BY rating '+order).fetchall():
ratings_id = r[0]
ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_ratings_link WHERE rating=?) ORDER BY books.sort', (ratings_id,)).fetchall()
ans = (no_rating + ans) if order == 'ASC' else (ans + no_rating)
return ans
def sort_on_series(self, order, db):
no_series = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_series_link) ORDER BY books.sort').fetchall()
ans = []
for r in db.conn.execute('SELECT id FROM series ORDER BY name '+order).fetchall():
series_id = r[0]
ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_series_link WHERE series=?) ORDER BY books.series_index,books.id '+order, (series_id,)).fetchall()
ans = (no_series + ans) if order == 'ASC' else (ans + no_series)
return ans
def sort_on_tags(self, order, db):
no_tags = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_tags_link) ORDER BY books.sort').fetchall()
ans = []
for r in db.conn.execute('SELECT id FROM tags ORDER BY name '+order).fetchall():
tag_id = r[0]
ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_tags_link WHERE tag=?) ORDER BY books.sort '+order, (tag_id,)).fetchall()
ans = (no_tags + ans) if order == 'ASC' else (ans + no_tags)
return ans
class Tag(unicode):
def __init__(self, name):
unicode.__init__(self, name)
self.count = 0
self.state = 0
def as_string(self):
return u'[%d] %s'%(self.count, self)
class LibraryDatabase2(LibraryDatabase):
'''
@ -204,26 +381,105 @@ class LibraryDatabase2(LibraryDatabase):
def __init__(self, library_path, row_factory=False):
if not os.path.exists(library_path):
os.makedirs(library_path)
self.listeners = set([])
self.library_path = os.path.abspath(library_path)
self.row_factory = row_factory
self.dbpath = os.path.join(library_path, 'metadata.db')
if isinstance(self.dbpath, unicode):
self.dbpath = self.dbpath.encode(filesystem_encoding)
self.connect()
# Upgrade database
while True:
meth = getattr(self, 'upgrade_version_%d'%self.user_version, None)
if meth is None:
break
else:
print 'Upgrading database to version %d...'%(self.user_version+1)
meth()
self.conn.commit()
self.user_version += 1
self.data = ResultCache()
self.filter = self.data.filter
self.refresh = functools.partial(self.data.refresh, self)
self.index = self.data.index
self.refresh_ids = functools.partial(self.data.refresh_ids, self.conn)
self.row = self.data.row
def initialize_database(self):
from calibre.resources import metadata_sqlite
self.conn.executescript(metadata_sqlite)
self.user_version = 1
def upgrade_version_1(self):
'''
Normalize indices.
'''
self.conn.executescript(textwrap.dedent('''\
DROP INDEX authors_idx;
CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE, sort COLLATE NOCASE);
DROP INDEX series_idx;
CREATE INDEX series_idx ON series (name COLLATE NOCASE);
CREATE INDEX series_sort_idx ON books (series_index, id);
'''))
def upgrade_version_2(self):
''' Fix Foreign key constraints for deleting from link tables. '''
script = textwrap.dedent('''\
DROP TRIGGER fkc_delete_books_%(ltable)s_link;
CREATE TRIGGER fkc_delete_on_%(table)s
BEFORE DELETE ON %(table)s
BEGIN
SELECT CASE
WHEN (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=OLD.id) > 0
THEN RAISE(ABORT, 'Foreign key violation: %(table)s is still referenced')
END;
END;
DELETE FROM %(table)s WHERE (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=%(table)s.id) < 1;
''')
self.conn.executescript(script%dict(ltable='authors', table='authors', ltable_col='author'))
self.conn.executescript(script%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
self.conn.executescript(script%dict(ltable='tags', table='tags', ltable_col='tag'))
self.conn.executescript(script%dict(ltable='series', table='series', ltable_col='series'))
def path(self, index, index_is_id=False):
'Return the relative path to the directory containing this books files as a unicode string.'
id = index if index_is_id else self.id()
id = index if index_is_id else self.id(index)
path = self.conn.execute('SELECT path FROM books WHERE id=?', (id,)).fetchone()[0].replace('/', os.sep)
return path
def abspath(self, index, index_is_id=False):
'Return the absolute path to the directory containing this books files as a unicode string.'
path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
if not os.path.exists(path):
os.makedirs(path)
return path
def construct_path_name(self, id):
'''
Construct the directory name for this book based on its metadata.
'''
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
path = author + '/' + title + ' (%d)'%id
return path
def construct_file_name(self, id):
'''
Construct the file name for this book based on its metadata.
'''
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
name = title + ' - ' + author
return name
def set_path(self, index, index_is_id=False):
'''
Set the path to the directory containing this books files based on its
@ -231,27 +487,64 @@ class LibraryDatabase2(LibraryDatabase):
are copied and it is deleted.
'''
id = index if index_is_id else self.id(index)
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
path = author + '/' + title + ' (%d)'%id
path = self.construct_path_name(id)
current_path = self.path(id, index_is_id=True).replace(os.sep, '/')
if path == current_path:
formats = self.formats(id, index_is_id=True)
formats = formats.split(',') if formats else []
# Check if the metadata used to construct paths has changed
fname = self.construct_file_name(id)
changed = False
for format in formats:
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]
if name and name != fname:
changed = True
break
if path == current_path and not changed:
return
tpath = os.path.join(self.library_path, *path.split('/'))
if not os.path.exists(tpath):
os.makedirs(tpath)
spath = os.path.join(self.library_path, *current_path.split('/'))
if current_path and os.path.exists(spath):
for f in os.listdir(spath):
copyfile(os.path.join(spath, f), os.path.join(tpath, f))
if current_path and os.path.exists(spath): # Migrate existing files
cdata = self.cover(id, index_is_id=True)
if cdata is not None:
open(os.path.join(tpath, 'cover.jpg'), 'wb').write(cdata)
for format in formats:
# Get data as string (can't use file as source and target files may be the same)
f = self.format(id, format, index_is_id=True, as_file=False)
if not f:
continue
stream = cStringIO.StringIO(f)
self.add_format(id, format, stream, index_is_id=True, path=tpath)
self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id))
self.conn.commit()
# Delete not needed directories
norm = lambda x : os.path.abspath(os.path.normcase(x))
if current_path and os.path.exists(spath):
shutil.rmtree(spath)
if norm(spath) != norm(tpath):
shutil.rmtree(spath)
parent = os.path.dirname(spath)
if len(os.listdir(parent)) == 0:
shutil.rmtree(parent)
def add_listener(self, listener):
'''
Add a listener. Will be called on change events with two arguments.
Event name and list of affected ids.
'''
self.listeners.add(listener)
def notify(self, event, ids=[]):
'Notify all listeners'
for listener in self.listeners:
try:
listener(event, ids)
except:
traceback.print_exc()
continue
def cover(self, index, index_is_id=False, as_file=False, as_image=False):
'''
Return the cover image as a bytestring (in JPEG format) or None.
@ -288,13 +581,32 @@ class LibraryDatabase2(LibraryDatabase):
p.loadFromData(data)
p.save(path)
def format(self, index, format, index_is_id=False, as_file=False, mode='r+b'):
'''
Return the ebook format as a bytestring or `None` if the format doesn't exist,
or we don't have permission to write to the ebook file.
`as_file`: If True the ebook format is returned as a file object opened in `mode`
'''
def all_formats(self):
formats = self.conn.execute('SELECT format from data').fetchall()
if not formats:
return set([])
return set([f[0] for f in formats])
def formats(self, index, index_is_id=False):
''' Return available formats as a comma separated list or None if there are no available formats '''
id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
try:
formats = self.conn.execute('SELECT format FROM data WHERE book=?', (id,)).fetchall()
name = self.conn.execute('SELECT name FROM data WHERE book=?', (id,)).fetchone()[0]
formats = map(lambda x:x[0], formats)
except:
return None
ans = []
for format in formats:
_format = ('.' + format.lower()) if format else ''
if os.access(os.path.join(path, name+_format), os.R_OK|os.W_OK):
ans.append(format)
return ','.join(ans)
def format_abspath(self, index, format, index_is_id=False):
'Return absolute path to the ebook file of format `format`'
id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]
@ -302,87 +614,180 @@ class LibraryDatabase2(LibraryDatabase):
format = ('.' + format.lower()) if format else ''
path = os.path.join(path, name+format)
if os.access(path, os.R_OK|os.W_OK):
f = open(path, mode)
return f if as_file else f.read()
return path
def format(self, index, format, index_is_id=False, as_file=False, mode='r+b'):
'''
Return the ebook format as a bytestring or `None` if the format doesn't exist,
or we don't have permission to write to the ebook file.
def add_format(self, index, format, stream, index_is_id=False):
`as_file`: If True the ebook format is returned as a file object opened in `mode`
'''
path = self.format_abspath(index, format, index_is_id=index_is_id)
if path is not None:
f = open(path, mode)
return f if as_file else f.read()
self.remove_format(id, format, index_is_id=True)
def add_format(self, index, format, stream, index_is_id=False, path=None):
id = index if index_is_id else self.id(index)
authors = self.authors(id, index_is_id=True)
if not authors:
authors = _('Unknown')
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
author = sanitize_file_name(authors.split(',')[0][:self.PATH_LIMIT]).decode(filesystem_encoding)
title = sanitize_file_name(self.title(id, index_is_id=True)[:self.PATH_LIMIT]).decode(filesystem_encoding)
if path is None:
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()
if name:
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format))
name = title + ' - ' + author
name = self.construct_file_name(id)
ext = ('.' + format.lower()) if format else ''
shutil.copyfileobj(stream, open(os.path.join(path, name+ext), 'wb'))
dest = os.path.join(path, name+ext)
pdir = os.path.dirname(dest)
if not os.path.exists(pdir):
os.makedirs(pdir)
with open(dest, 'wb') as f:
shutil.copyfileobj(stream, f)
stream.seek(0, 2)
size=stream.tell()
self.conn.execute('INSERT INTO data (book,format,uncompressed_size,name) VALUES (?,?,?,?)',
(id, format.upper(), size, name))
self.conn.commit()
self.notify('metadata', [id])
def delete_book(self, id):
'''
Removes book from self.cache, self.data and underlying database.
Removes book from the result cache and the underlying database.
'''
try:
self.cache.pop(self.index(id, cache=True))
self.data.pop(self.index(id, cache=False))
except TypeError: #If data and cache are the same object
pass
self.data.remove(id)
path = os.path.join(self.library_path, self.path(id, True))
if os.path.exists(path):
shutil.rmtree(path)
parent = os.path.dirname(path)
if len(os.listdir(parent)) == 0:
shutil.rmtree(parent)
self.conn.execute('DELETE FROM books WHERE id=?', (id,))
self.conn.commit()
self.clean()
self.notify('delete', [id])
def remove_format(self, index, format, index_is_id=False):
id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True))
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]
name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()
name = name[0] if name else False
if name:
ext = ('.' + format.lower()) if format else ''
path = os.path.join(path, name+ext)
if os.access(path, os.W_OK):
os.unlink(path)
try:
os.remove(path)
except:
pass
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format.upper()))
self.conn.commit()
self.notify('metadata', [id])
def clean(self):
'''
Remove orphaned entries.
'''
st = 'DELETE FROM %(table)s WHERE (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=%(table)s.id) < 1;'
self.conn.execute(st%dict(ltable='authors', table='authors', ltable_col='author'))
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
self.conn.commit()
def get_categories(self, sort_on_count=False):
categories = {}
def get(name, category, field='name'):
ans = self.conn.execute('SELECT DISTINCT %s FROM %s'%(field, name)).fetchall()
ans = [x[0].strip() for x in ans]
try:
ans.remove('')
except ValueError: pass
categories[category] = list(map(Tag, ans))
tags = categories[category]
if name != 'data':
for tag in tags:
id = self.conn.execute('SELECT id FROM %s WHERE %s=?'%(name, field), (tag,)).fetchone()
if id:
id = id[0]
tag.id = id
for tag in tags:
if tag.id is not None:
tag.count = self.conn.execute('SELECT COUNT(id) FROM books_%s_link WHERE %s=?'%(name, category), (tag.id,)).fetchone()[0]
else:
for tag in tags:
tag.count = self.conn.execute('SELECT COUNT(format) FROM data WHERE format=?', (tag,)).fetchone()[0]
tags.sort(reverse=sort_on_count, cmp=(lambda x,y:cmp(x.count,y.count)) if sort_on_count else cmp)
for x in (('authors', 'author'), ('tags', 'tag'), ('publishers', 'publisher'),
('series', 'series')):
get(*x)
get('data', 'format', 'format')
return categories
def set(self, row, column, val):
'''
Convenience method for setting the title, authors, publisher or rating
'''
id = self.data[row][0]
col = {'title':1, 'authors':2, 'publisher':3, 'rating':4, 'tags':7}[column]
self.data.set(row, col, val)
if column == 'authors':
val = string_to_authors(val)
self.set_authors(id, val, notify=False)
elif column == 'title':
self.set_title(id, val, notify=False)
elif column == 'publisher':
self.set_publisher(id, val, notify=False)
elif column == 'rating':
self.set_rating(id, val)
elif column == 'tags':
self.set_tags(id, val.split(','), append=False, notify=False)
self.data.refresh_ids(self.conn, [id])
self.set_path(id, True)
self.notify('metadata', [id])
def set_metadata(self, id, mi):
'''
Set metadata for the book `id` from the `MetaInformation` object `mi`
'''
if mi.title:
self.set_title(id, mi.title)
if not mi.authors:
mi.authors = ['Unknown']
mi.authors = [_('Unknown')]
authors = []
for a in mi.authors:
authors += a.split('&')
self.set_authors(id, authors)
self.set_authors(id, authors, notify=False)
if mi.author_sort:
self.set_author_sort(id, mi.author_sort)
if mi.publisher:
self.set_publisher(id, mi.publisher)
self.set_publisher(id, mi.publisher, notify=False)
if mi.rating:
self.set_rating(id, mi.rating)
if mi.series:
self.set_series(id, mi.series)
self.set_series(id, mi.series, notify=False)
if mi.cover_data[1] is not None:
self.set_cover(id, mi.cover_data[1])
if mi.tags:
self.set_tags(id, mi.tags, notify=False)
if mi.comments:
self.set_comment(id, mi.comments)
self.set_path(id, True)
self.notify('metadata', [id])
def set_authors(self, id, authors):
def set_authors(self, id, authors, notify=True):
'''
`authors`: A list of authors.
'''
self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM authors WHERE (SELECT COUNT(id) FROM books_authors_link WHERE author=authors.id) < 1')
for a in authors:
if not a:
continue
a = a.strip()
a = a.strip().replace(',', '|')
if not isinstance(a, unicode):
a = a.decode(preferred_encoding, 'replace')
author = self.conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone()
if author:
aid = author[0]
@ -395,20 +800,103 @@ class LibraryDatabase2(LibraryDatabase):
except sqlite.IntegrityError: # Sometimes books specify the same author twice in their metadata
pass
self.set_path(id, True)
self.notify('metadata', [id])
def set_title(self, id, title):
def set_title(self, id, title, notify=True):
if not title:
return
if not isinstance(title, unicode):
title = title.decode(preferred_encoding, 'replace')
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
self.set_path(id, True)
self.notify('metadata', [id])
def set_publisher(self, id, publisher, notify=True):
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1')
if publisher:
if not isinstance(publisher, unicode):
publisher = publisher.decode(preferred_encoding, 'replace')
pub = self.conn.execute('SELECT id from publishers WHERE name=?', (publisher,)).fetchone()
if pub:
aid = pub[0]
else:
aid = self.conn.execute('INSERT INTO publishers(name) VALUES (?)', (publisher,)).lastrowid
self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid))
self.conn.commit()
self.notify('metadata', [id])
def set_tags(self, id, tags, append=False, notify=True):
'''
@param tags: list of strings
@param append: If True existing tags are not removed
'''
if not append:
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1')
for tag in set(tags):
tag = tag.lower().strip()
if not tag:
continue
if not isinstance(tag, unicode):
tag = tag.decode(preferred_encoding, 'replace')
t = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone()
if t:
tid = t[0]
else:
tid = self.conn.execute('INSERT INTO tags(name) VALUES(?)', (tag,)).lastrowid
if not self.conn.execute('SELECT book FROM books_tags_link WHERE book=? AND tag=?',
(id, tid)).fetchone():
self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)',
(id, tid))
self.conn.commit()
self.notify('metadata', [id])
def set_series(self, id, series, notify=True):
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1')
if series:
if not isinstance(series, unicode):
series = series.decode(preferred_encoding, 'replace')
s = self.conn.execute('SELECT id from series WHERE name=?', (series,)).fetchone()
if s:
aid = s[0]
else:
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
self.conn.commit()
try:
row = self.row(id)
if row is not None:
self.data.set(row, 9, series)
except ValueError:
pass
self.notify('metadata', [id])
def set_series_index(self, id, idx, notify=True):
if idx is None:
idx = 1
idx = int(idx)
self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id))
self.conn.commit()
try:
row = self.row(id)
if row is not None:
self.data.set(row, 10, idx)
except ValueError:
pass
self.notify('metadata', [id])
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
'''
Add a book to the database. self.data and self.cache are not updated.
Add a book to the database. The result cache is not updated.
@param paths: List of paths to book files of file-like objects
'''
formats, metadata, uris = iter(formats), iter(metadata), iter(uris)
duplicates = []
ids = []
for path in paths:
mi = metadata.next()
format = formats.next()
@ -424,6 +912,7 @@ class LibraryDatabase2(LibraryDatabase):
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
(mi.title, uri, series_index, aus))
id = obj.lastrowid
ids.append(id)
self.set_path(id, True)
self.conn.commit()
self.set_metadata(id, mi)
@ -434,13 +923,15 @@ class LibraryDatabase2(LibraryDatabase):
if not hasattr(path, 'read'):
stream.close()
self.conn.commit()
if ids:
self.data.books_added(ids, self.conn)
if duplicates:
paths = tuple(duplicate[0] for duplicate in duplicates)
formats = tuple(duplicate[1] for duplicate in duplicates)
metadata = tuple(duplicate[2] for duplicate in duplicates)
uris = tuple(duplicate[3] for duplicate in duplicates)
return (paths, formats, metadata, uris)
return None
return (paths, formats, metadata, uris), len(ids)
return None, len(ids)
def import_book(self, mi, formats):
series_index = 1 if mi.series_index is None else mi.series_index
@ -457,12 +948,24 @@ class LibraryDatabase2(LibraryDatabase):
stream = open(path, 'rb')
self.add_format(id, ext, stream, index_is_id=True)
self.conn.commit()
def move_library_to(self, newloc):
self.data.books_added([id], self.conn)
self.notify('add', [id])
def move_library_to(self, newloc, progress=None):
header = _(u'<p>Copying books to %s<br><center>')%newloc
books = self.conn.execute('SELECT id, path, title FROM books').fetchall()
if progress is not None:
progress.setValue(0)
progress.setLabelText(header)
QCoreApplication.processEvents()
progress.setAutoReset(False)
progress.setRange(0, len(books))
if not os.path.exists(newloc):
os.makedirs(newloc)
old_dirs = set([])
for book in self.conn.execute('SELECT id, path FROM books').fetchall():
for i, book in enumerate(books):
if progress is not None:
progress.setLabelText(header+_(u'Copying <b>%s</b>')%book[2])
path = book[1]
if not path:
continue
@ -471,8 +974,11 @@ class LibraryDatabase2(LibraryDatabase):
tdir = os.path.join(newloc, dir)
if os.path.exists(tdir):
shutil.rmtree(tdir)
shutil.copytree(srcdir, tdir)
if os.path.exists(srcdir):
shutil.copytree(srcdir, tdir)
old_dirs.add(srcdir)
if progress is not None:
progress.setValue(i+1)
dbpath = os.path.join(newloc, os.path.basename(self.dbpath))
shutil.copyfile(self.dbpath, dbpath)
@ -486,6 +992,9 @@ class LibraryDatabase2(LibraryDatabase):
shutil.rmtree(dir)
except:
pass
if progress is not None:
progress.reset()
progress.hide()
def migrate_old(self, db, progress):
@ -494,7 +1003,9 @@ class LibraryDatabase2(LibraryDatabase):
progress.setLabelText(header)
QCoreApplication.processEvents()
db.conn.row_factory = lambda cursor, row : tuple(row)
db.conn.text_factory = lambda x : unicode(x, 'utf-8', 'replace')
books = db.conn.execute('SELECT id, title, sort, timestamp, uri, series_index, author_sort, isbn FROM books ORDER BY id ASC').fetchall()
progress.setAutoReset(False)
progress.setRange(0, len(books))
for book in books:
@ -533,4 +1044,7 @@ books_series_link feeds
self.conn.commit()
progress.setLabelText(_('Compacting database'))
self.vacuum()
progress.reset()
return len(books)

Some files were not shown because too many files have changed in this diff Show More