KG merged libiMobileDevice wrapper
This commit is contained in:
GRiker 2013-05-15 09:59:38 -06:00
commit 6dfcd92008
25 changed files with 706 additions and 172 deletions

View File

@ -3,7 +3,7 @@ __license__ = 'GPL v3'
__copyright__ = '4 February 2011, desUBIKado' __copyright__ = '4 February 2011, desUBIKado'
__author__ = 'desUBIKado' __author__ = 'desUBIKado'
__version__ = 'v0.09' __version__ = 'v0.09'
__date__ = '02, December 2012' __date__ = '14, May 2013'
''' '''
http://www.weblogssl.com/ http://www.weblogssl.com/
''' '''
@ -56,15 +56,16 @@ class weblogssl(BasicNewsRecipe):
,(u'Zona FandoM', u'http://feeds.weblogssl.com/zonafandom') ,(u'Zona FandoM', u'http://feeds.weblogssl.com/zonafandom')
,(u'Fandemia', u'http://feeds.weblogssl.com/fandemia') ,(u'Fandemia', u'http://feeds.weblogssl.com/fandemia')
,(u'Tendencias', u'http://feeds.weblogssl.com/trendencias') ,(u'Tendencias', u'http://feeds.weblogssl.com/trendencias')
,(u'Beb\xe9s y m\xe1s', u'http://feeds.weblogssl.com/bebesymas') ,(u'Tendencias Belleza', u'http://feeds.weblogssl.com/trendenciasbelleza')
,(u'Tendencias Hombre', u'http://feeds.weblogssl.com/trendenciashombre')
,(u'Tendencias Shopping', u'http://feeds.weblogssl.com/trendenciasshopping')
,(u'Directo al paladar', u'http://feeds.weblogssl.com/directoalpaladar') ,(u'Directo al paladar', u'http://feeds.weblogssl.com/directoalpaladar')
,(u'Compradicci\xf3n', u'http://feeds.weblogssl.com/compradiccion') ,(u'Compradicci\xf3n', u'http://feeds.weblogssl.com/compradiccion')
,(u'Decoesfera', u'http://feeds.weblogssl.com/decoesfera') ,(u'Decoesfera', u'http://feeds.weblogssl.com/decoesfera')
,(u'Embelezzia', u'http://feeds.weblogssl.com/embelezzia') ,(u'Embelezzia', u'http://feeds.weblogssl.com/embelezzia')
,(u'Vit\xf3nica', u'http://feeds.weblogssl.com/vitonica') ,(u'Vit\xf3nica', u'http://feeds.weblogssl.com/vitonica')
,(u'Ambiente G', u'http://feeds.weblogssl.com/ambienteg') ,(u'Ambiente G', u'http://feeds.weblogssl.com/ambienteg')
,(u'Tendencias Belleza', u'http://feeds.weblogssl.com/trendenciasbelleza') ,(u'Beb\xe9s y m\xe1s', u'http://feeds.weblogssl.com/bebesymas')
,(u'Tendencias Hombre', u'http://feeds.weblogssl.com/trendenciashombre')
,(u'Peques y m\xe1s', u'http://feeds.weblogssl.com/pequesymas') ,(u'Peques y m\xe1s', u'http://feeds.weblogssl.com/pequesymas')
,(u'Motorpasi\xf3n', u'http://feeds.weblogssl.com/motorpasion') ,(u'Motorpasi\xf3n', u'http://feeds.weblogssl.com/motorpasion')
,(u'Motorpasi\xf3n F1', u'http://feeds.weblogssl.com/motorpasionf1') ,(u'Motorpasi\xf3n F1', u'http://feeds.weblogssl.com/motorpasionf1')
@ -119,23 +120,6 @@ class weblogssl(BasicNewsRecipe):
return soup return soup
# Para obtener la url original del articulo a partir de la de "feedsportal"
# El siguiente código es gracias al usuario "bosplans" de www.mobileread.com
# http://www.mobileread.com/forums/showthread.php?t=130297
def get_article_url(self, article): def get_article_url(self, article):
link = article.get('link', None)
if link is None:
return article
# if link.split('/')[-4]=="xataka2":
# return article.get('feedburner_origlink', article.get('link', article.get('guid')))
if link.split('/')[-4]=="xataka2":
return article.get('guid', None) return article.get('guid', None)
if link.split('/')[-1]=="story01.htm":
link=link.split('/')[-2]
a=['0B','0C','0D','0E','0F','0G','0N' ,'0L0S','0A']
b=['.' ,'/' ,'?' ,'-' ,'=' ,'&' ,'.com','www.','0']
for i in range(0,len(a)):
link=link.replace(a[i],b[i])
link="http://"+link
return link

View File

@ -38,7 +38,7 @@ binary_includes = [
'/lib/libz.so.1', '/lib/libz.so.1',
'/usr/lib/libtiff.so.5', '/usr/lib/libtiff.so.5',
'/lib/libbz2.so.1', '/lib/libbz2.so.1',
'/usr/lib/libpoppler.so.28', '/usr/lib/libpoppler.so.37',
'/usr/lib/libxml2.so.2', '/usr/lib/libxml2.so.2',
'/usr/lib/libopenjpeg.so.2', '/usr/lib/libopenjpeg.so.2',
'/usr/lib/libxslt.so.1', '/usr/lib/libxslt.so.1',

View File

@ -378,7 +378,7 @@ class Py2App(object):
@flush @flush
def add_poppler(self): def add_poppler(self):
info('\nAdding poppler') info('\nAdding poppler')
for x in ('libpoppler.28.dylib',): for x in ('libpoppler.37.dylib',):
self.install_dylib(os.path.join(SW, 'lib', x)) self.install_dylib(os.path.join(SW, 'lib', x))
for x in ('pdftohtml', 'pdftoppm', 'pdfinfo'): for x in ('pdftohtml', 'pdftoppm', 'pdfinfo'):
self.install_dylib(os.path.join(SW, 'bin', x), False) self.install_dylib(os.path.join(SW, 'bin', x), False)

View File

@ -66,10 +66,8 @@ else:
filesystem_encoding = 'utf-8' filesystem_encoding = 'utf-8'
# On linux, unicode arguments to os file functions are coerced to an ascii # On linux, unicode arguments to os file functions are coerced to an ascii
# bytestring if sys.getfilesystemencoding() == 'ascii', which is # bytestring if sys.getfilesystemencoding() == 'ascii', which is
# just plain dumb. So issue a warning. # just plain dumb. This is fixed by the icu.py module which, when
print ('WARNING: You do not have the LANG environment variable set correctly. ' # imported changes ascii to utf-8
'This will cause problems with non-ascii filenames. '
'Set it to something like en_US.UTF-8.\n')
except: except:
filesystem_encoding = 'utf-8' filesystem_encoding = 'utf-8'

View File

@ -240,7 +240,8 @@ class ANDROID(USBMS):
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E', 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS', 'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD', 'XT894'] 'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD', 'XT894', '_USB',
]
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -251,7 +252,9 @@ class ANDROID(USBMS):
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E', 'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E',
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1', 'XT894'] 'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1', 'XT894',
'_USB',
]
OSX_MAIN_MEM = 'Android Device Main Memory' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
import cStringIO, ctypes, datetime, os, re, shutil, sys, tempfile, time import cStringIO, ctypes, datetime, os, re, shutil, sys, tempfile, time
from calibre import fit_image, confirm_config_name, osx_version, strftime as _strftime from calibre import fit_image, confirm_config_name, strftime as _strftime
from calibre.constants import ( from calibre.constants import (
__appname__, __version__, isosx, iswindows, cache_dir as _cache_dir) __appname__, __version__, isosx, iswindows, cache_dir as _cache_dir)
from calibre.devices.errors import OpenFeedback, UserFeedback from calibre.devices.errors import OpenFeedback, UserFeedback
@ -1609,7 +1609,6 @@ class ITUNES(DriverBase):
if self.verbose: if self.verbose:
logger().info(" failed to write artwork") logger().info(" failed to write artwork")
elif format == 'pdf': elif format == 'pdf':
if self.verbose: if self.verbose:
logger().info(" unable to set PDF cover via automation interface") logger().info(" unable to set PDF cover via automation interface")
@ -1647,6 +1646,7 @@ class ITUNES(DriverBase):
def _create_new_book(self, fpath, metadata, path, db_added, lb_added, thumb, format): def _create_new_book(self, fpath, metadata, path, db_added, lb_added, thumb, format):
''' '''
''' '''
from calibre.utils.date import parse_date
if self.verbose: if self.verbose:
logger().info(" %s._create_new_book()" % self.__class__.__name__) logger().info(" %s._create_new_book()" % self.__class__.__name__)

View File

@ -15,7 +15,9 @@ __copyright__ = '2013, Gregory Riker'
import os, sys import os, sys
from collections import OrderedDict from collections import OrderedDict
from ctypes import * from ctypes import (
c_int, c_long, c_void_p, c_char_p, Structure, POINTER, byref, cdll, c_char, c_ulonglong,
c_uint, c_ubyte, create_string_buffer, string_at)
from calibre.constants import DEBUG, islinux, isosx, iswindows from calibre.constants import DEBUG, islinux, isosx, iswindows
from calibre.devices.idevice.parse_xml import XmlPropertyListParser from calibre.devices.idevice.parse_xml import XmlPropertyListParser

View File

@ -50,10 +50,10 @@ class PRST1(USBMS):
VENDOR_NAME = 'SONY' VENDOR_NAME = 'SONY'
WINDOWS_MAIN_MEM = re.compile( WINDOWS_MAIN_MEM = re.compile(
r'(PRS-T(1|2)&)' r'(PRS-T(1|2|2N)&)'
) )
WINDOWS_CARD_A_MEM = re.compile( WINDOWS_CARD_A_MEM = re.compile(
r'(PRS-T(1|2)__SD&)' r'(PRS-T(1|2|2N)__SD&)'
) )
MAIN_MEMORY_VOLUME_LABEL = 'SONY Reader Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'SONY Reader Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'SONY Reader Storage Card' STORAGE_CARD_VOLUME_LABEL = 'SONY Reader Storage Card'
@ -66,7 +66,7 @@ class PRST1(USBMS):
EXTRA_CUSTOMIZATION_MESSAGE = [ EXTRA_CUSTOMIZATION_MESSAGE = [
_('Comma separated list of metadata fields ' _('Comma separated list of metadata fields '
'to turn into collections on the device. Possibilities include: ')+\ 'to turn into collections on the device. Possibilities include: ')+
'series, tags, authors', 'series, tags, authors',
_('Upload separate cover thumbnails for books') + _('Upload separate cover thumbnails for books') +
':::'+_('Normally, the SONY readers get the cover image from the' ':::'+_('Normally, the SONY readers get the cover image from the'
@ -194,11 +194,11 @@ class PRST1(USBMS):
time_offsets = {} time_offsets = {}
for i, row in enumerate(cursor): for i, row in enumerate(cursor):
try: try:
comp_date = int(os.path.getmtime(self.normalize_path(prefix + row[0])) * 1000); comp_date = int(os.path.getmtime(self.normalize_path(prefix + row[0])) * 1000)
except (OSError, IOError, TypeError): except (OSError, IOError, TypeError):
# In case the db has incorrect path info # In case the db has incorrect path info
continue continue
device_date = int(row[1]); device_date = int(row[1])
offset = device_date - comp_date offset = device_date - comp_date
time_offsets.setdefault(offset, 0) time_offsets.setdefault(offset, 0)
time_offsets[offset] = time_offsets[offset] + 1 time_offsets[offset] = time_offsets[offset] + 1
@ -345,7 +345,7 @@ class PRST1(USBMS):
# Insert the sequence Id if it doesn't # Insert the sequence Id if it doesn't
query = ('INSERT INTO sqlite_sequence (name, seq) ' query = ('INSERT INTO sqlite_sequence (name, seq) '
'SELECT ?, ? ' 'SELECT ?, ? '
'WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = ?)'); 'WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = ?)')
cursor.execute(query, (table, sequence_id, table,)) cursor.execute(query, (table, sequence_id, table,))
cursor.close() cursor.close()

View File

@ -875,6 +875,9 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
self.client_device_kind = result.get('deviceKind', '') self.client_device_kind = result.get('deviceKind', '')
self._debug('Client device kind', self.client_device_kind) self._debug('Client device kind', self.client_device_kind)
self.client_device_name = result.get('deviceName', self.client_device_kind)
self._debug('Client device name', self.client_device_name)
self.max_book_packet_len = result.get('maxBookContentPacketLen', self.max_book_packet_len = result.get('maxBookContentPacketLen',
self.BASE_PACKET_LEN) self.BASE_PACKET_LEN)
self._debug('max_book_packet_len', self.max_book_packet_len) self._debug('max_book_packet_len', self.max_book_packet_len)
@ -946,6 +949,8 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
return False return False
def get_gui_name(self): def get_gui_name(self):
if getattr(self, 'client_device_name', None):
return self.gui_name_template%(self.gui_name, self.client_device_name)
if getattr(self, 'client_device_kind', None): if getattr(self, 'client_device_kind', None):
return self.gui_name_template%(self.gui_name, self.client_device_kind) return self.gui_name_template%(self.gui_name, self.client_device_kind)
return self.gui_name return self.gui_name

View File

@ -208,7 +208,7 @@ class ParagraphStyle(object):
# Misc. # Misc.
'text_indent', 'text_align', 'line_height', 'direction', 'background_color', 'text_indent', 'text_align', 'line_height', 'direction', 'background_color',
'numbering', 'numbering', 'font_family', 'font_size',
) )
def __init__(self, pPr=None): def __init__(self, pPr=None):
@ -232,6 +232,8 @@ class ParagraphStyle(object):
for s in XPath('./w:pStyle[@w:val]')(pPr): for s in XPath('./w:pStyle[@w:val]')(pPr):
self.linked_style = get(s, 'w:val') self.linked_style = get(s, 'w:val')
self.font_family = self.font_size = inherit
self._css = None self._css = None
def update(self, other): def update(self, other):
@ -271,10 +273,16 @@ class ParagraphStyle(object):
if val is not inherit: if val is not inherit:
c['margin-%s' % edge] = val c['margin-%s' % edge] = val
for x in ('text_indent', 'text_align', 'line_height', 'background_color'): if self.line_height not in {inherit, '1'}:
c['line-height'] = self.line_height
for x in ('text_indent', 'text_align', 'background_color', 'font_family', 'font_size'):
val = getattr(self, x) val = getattr(self, x)
if val is not inherit: if val is not inherit:
if x == 'font_size':
val = '%.3gpt' % val
c[x.replace('_', '-')] = val c[x.replace('_', '-')] = val
return self._css return self._css
# TODO: keepNext must be done at markup level # TODO: keepNext must be done at markup level

View File

@ -113,6 +113,14 @@ def read_vert_align(parent, dest):
if val and val in {'baseline', 'subscript', 'superscript'}: if val and val in {'baseline', 'subscript', 'superscript'}:
ans = val ans = val
setattr(dest, 'vert_align', ans) setattr(dest, 'vert_align', ans)
def read_font_family(parent, dest):
ans = inherit
for col in XPath('./w:rFonts[@w:ascii]')(parent):
val = get(col, 'w:ascii')
if val:
ans = val
setattr(dest, 'font_family', ans)
# }}} # }}}
class RunStyle(object): class RunStyle(object):
@ -122,7 +130,7 @@ class RunStyle(object):
'rtl', 'shadow', 'smallCaps', 'strike', 'vanish', 'rtl', 'shadow', 'smallCaps', 'strike', 'vanish',
'border_color', 'border_style', 'border_width', 'padding', 'color', 'highlight', 'background_color', 'border_color', 'border_style', 'border_width', 'padding', 'color', 'highlight', 'background_color',
'letter_spacing', 'font_size', 'text_decoration', 'vert_align', 'lang', 'letter_spacing', 'font_size', 'text_decoration', 'vert_align', 'lang', 'font_family'
} }
toggle_properties = { toggle_properties = {
@ -141,7 +149,7 @@ class RunStyle(object):
): ):
setattr(self, p, binary_property(rPr, p)) setattr(self, p, binary_property(rPr, p))
for x in ('text_border', 'color', 'highlight', 'shd', 'letter_spacing', 'sz', 'underline', 'vert_align', 'lang'): for x in ('text_border', 'color', 'highlight', 'shd', 'letter_spacing', 'sz', 'underline', 'vert_align', 'lang', 'font_family'):
f = globals()['read_%s' % x] f = globals()['read_%s' % x]
f(rPr, self) f(rPr, self)
@ -164,6 +172,18 @@ class RunStyle(object):
if val is inherit: if val is inherit:
setattr(self, p, getattr(parent, p)) setattr(self, p, getattr(parent, p))
def get_border_css(self, ans):
for x in ('color', 'style', 'width'):
val = getattr(self, 'border_'+x)
if x == 'width' and val is not inherit:
val = '%.3gpt' % val
if val is not inherit:
ans['border-%s' % x] = val
def clear_border_css(self):
for x in ('color', 'style', 'width'):
setattr(self, 'border_'+x, inherit)
@property @property
def css(self): def css(self):
if self._css is None: if self._css is None:
@ -188,12 +208,7 @@ class RunStyle(object):
if self.vanish is True: if self.vanish is True:
c['display'] = 'none' c['display'] = 'none'
for x in ('color', 'style', 'width'): self.get_border_css(c)
val = getattr(self, 'border_'+x)
if x == 'width' and val is not inherit:
val = '%.3gpt' % val
if val is not inherit:
c['border-%s' % x] = val
if self.padding is not inherit: if self.padding is not inherit:
c['padding'] = '%.3gpt' % self.padding c['padding'] = '%.3gpt' % self.padding
@ -212,6 +227,10 @@ class RunStyle(object):
if self.b: if self.b:
c['font-weight'] = 'bold' c['font-weight'] = 'bold'
if self.font_family is not inherit:
c['font-family'] = self.font_family
return self._css return self._css
def same_border(self, other): def same_border(self, other):

View File

@ -167,7 +167,9 @@ class DOCX(object):
@property @property
def document_relationships(self): def document_relationships(self):
name = self.document_name return self.get_relationships(self.document_name)
def get_relationships(self, name):
base = '/'.join(name.split('/')[:-1]) base = '/'.join(name.split('/')[:-1])
by_id, by_type = {}, {} by_id, by_type = {}, {}
parts = name.split('/') parts = name.split('/')

View File

@ -22,7 +22,7 @@ def dump(path):
zf.extractall(dest) zf.extractall(dest)
for f in walk(dest): for f in walk(dest):
if f.endswith('.xml'): if f.endswith('.xml') or f.endswith('.rels'):
with open(f, 'r+b') as stream: with open(f, 'r+b') as stream:
raw = stream.read() raw = stream.read()
root = etree.fromstring(raw) root = etree.fromstring(raw)

View File

@ -0,0 +1,132 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re
from collections import namedtuple
from calibre.ebooks.docx.block_styles import binary_property, inherit
from calibre.ebooks.docx.names import XPath, get
from calibre.utils.filenames import ascii_filename
from calibre.utils.fonts.scanner import font_scanner, NoFonts
from calibre.utils.fonts.utils import panose_to_css_generic_family, is_truetype_font
Embed = namedtuple('Embed', 'name key subsetted')
def has_system_fonts(name):
try:
return bool(font_scanner.fonts_for_family(name))
except NoFonts:
return False
def get_variant(bold=False, italic=False):
return {(False, False):'Regular', (False, True):'Italic',
(True, False):'Bold', (True, True):'BoldItalic'}[(bold, italic)]
class Family(object):
def __init__(self, elem, embed_relationships):
self.name = self.family_name = get(elem, 'w:name')
self.alt_names = tuple(get(x, 'w:val') for x in XPath('./w:altName')(elem))
if self.alt_names and not has_system_fonts(self.name):
for x in self.alt_names:
if has_system_fonts(x):
self.family_name = x
break
self.embedded = {}
for x in ('Regular', 'Bold', 'Italic', 'BoldItalic'):
for y in XPath('./w:embed%s[@r:id]' % x)(elem):
rid = get(y, 'r:id')
key = get(y, 'w:fontKey')
subsetted = get(y, 'w:subsetted') in {'1', 'true', 'on'}
if rid in embed_relationships:
self.embedded[x] = Embed(embed_relationships[rid], key, subsetted)
self.generic_family = 'auto'
for x in XPath('./w:family[@w:val]')(elem):
self.generic_family = get(x, 'w:val', 'auto')
ntt = binary_property(elem, 'notTrueType')
self.is_ttf = ntt is inherit or not ntt
self.panose1 = None
self.panose_name = None
for x in XPath('./w:panose1[@w:val]')(elem):
try:
v = get(x, 'w:val')
v = tuple(int(v[i:i+2], 16) for i in xrange(0, len(v), 2))
except (TypeError, ValueError, IndexError):
pass
else:
self.panose1 = v
self.panose_name = panose_to_css_generic_family(v)
self.css_generic_family = {'roman':'serif', 'swiss':'sans-serif', 'modern':'monospace',
'decorative':'fantasy', 'script':'cursive'}.get(self.generic_family, None)
self.css_generic_family = self.css_generic_family or self.panose_name or 'serif'
class Fonts(object):
def __init__(self):
self.fonts = {}
self.used = set()
def __call__(self, root, embed_relationships, docx, dest_dir):
for elem in XPath('//w:font[@w:name]')(root):
self.fonts[get(elem, 'w:name')] = Family(elem, embed_relationships)
def family_for(self, name, bold=False, italic=False):
f = self.fonts.get(name, None)
if f is None:
return 'serif'
variant = get_variant(bold, italic)
self.used.add((name, variant))
name = f.name if variant in f.embedded else f.family_name
return '"%s", %s' % (name.replace('"', ''), f.css_generic_family)
def embed_fonts(self, dest_dir, docx):
defs = []
dest_dir = os.path.join(dest_dir, 'fonts')
for name, variant in self.used:
f = self.fonts[name]
if variant in f.embedded:
if not os.path.exists(dest_dir):
os.mkdir(dest_dir)
fname = self.write(name, dest_dir, docx, variant)
if fname is not None:
d = {'font-family':'"%s"' % name.replace('"', ''), 'src': 'url("fonts/%s")' % fname}
if 'Bold' in variant:
d['font-weight'] = 'bold'
if 'Italic' in variant:
d['font-style'] = 'italic'
d = ['%s: %s' % (k, v) for k, v in d.iteritems()]
d = ';\n\t'.join(d)
defs.append('@font-face {\n\t%s\n}\n' % d)
return '\n'.join(defs)
def write(self, name, dest_dir, docx, variant):
f = self.fonts[name]
ef = f.embedded[variant]
raw = docx.read(ef.name)
prefix = raw[:32]
if ef.key:
key = re.sub(r'[^A-Fa-f0-9]', '', ef.key)
key = bytearray(reversed(tuple(int(key[i:i+2], 16) for i in xrange(0, len(key), 2))))
prefix = bytearray(prefix)
prefix = bytes(bytearray(prefix[i]^key[i % len(key)] for i in xrange(len(prefix))))
if not is_truetype_font(prefix):
return None
ext = 'otf' if prefix.startswith(b'OTTO') else 'ttf'
fname = ascii_filename('%s - %s.%s' % (name, variant, ext))
with open(os.path.join(dest_dir, fname), 'wb') as dest:
dest.write(prefix)
dest.write(raw[32:])
return fname

View File

@ -13,6 +13,7 @@ DOCPROPS = 'http://schemas.openxmlformats.org/package/2006/relationships/metada
APPPROPS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties' APPPROPS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties'
STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles' STYLES = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'
NUMBERING = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering' NUMBERING = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering'
FONTS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable'
namespaces = { namespaces = {
'mo': 'http://schemas.microsoft.com/office/mac/office/2008/main', 'mo': 'http://schemas.microsoft.com/office/mac/office/2008/main',

View File

@ -214,6 +214,8 @@ class Numbering(object):
p.set('list-template', val) p.set('list-template', val)
self.update_counter(counter, ilvl, d.levels) self.update_counter(counter, ilvl, d.levels)
templates = {}
def commit(current_run): def commit(current_run):
if not current_run: if not current_run:
return return
@ -244,6 +246,9 @@ class Numbering(object):
span.append(gc) span.append(gc)
child.append(span) child.append(span)
span = SPAN(child.get('list-template')) span = SPAN(child.get('list-template'))
last = templates.get(lvlid, '')
if span.text and len(span.text) > len(last):
templates[lvlid] = span.text
child.insert(0, span) child.insert(0, span)
for attr in ('list-lvl', 'list-id', 'list-template'): for attr in ('list-lvl', 'list-id', 'list-template'):
child.attrib.pop(attr, None) child.attrib.pop(attr, None)
@ -272,8 +277,14 @@ class Numbering(object):
commit(current_run) commit(current_run)
for wrap in body.xpath('//ol[@lvlid]'): for wrap in body.xpath('//ol[@lvlid]'):
wrap.attrib.pop('lvlid') lvlid = wrap.attrib.pop('lvlid')
wrap.tag = 'div' wrap.tag = 'div'
text = ''
maxtext = templates.get(lvlid, '').replace('.', '')[:-1]
for li in wrap.iterchildren('li'):
t = li[0].text
if t and len(t) > len(text):
text = t
for i, li in enumerate(wrap.iterchildren('li')): for i, li in enumerate(wrap.iterchildren('li')):
li.tag = 'div' li.tag = 'div'
li.attrib.pop('value', None) li.attrib.pop('value', None)
@ -281,7 +292,8 @@ class Numbering(object):
obj = object_map[li] obj = object_map[li]
bs = styles.para_cache[obj] bs = styles.para_cache[obj]
if i == 0: if i == 0:
wrap.set('style', 'display:table; margin-left: %s' % (bs.css.get('margin-left', 0))) m = len(maxtext) # Move the table left to simulate the behavior of a list (number is to the left of text margin)
wrap.set('style', 'display:table; margin-left: -%dem; padding-left: %s' % (m, bs.css.get('margin-left', 0)))
bs.css.pop('margin-left', None) bs.css.pop('margin-left', None)
for child in li: for child in li:
child.set('style', 'display:table-cell') child.set('style', 'display:table-cell')

View File

@ -97,7 +97,8 @@ class Styles(object):
def get(self, key, default=None): def get(self, key, default=None):
return self.id_map.get(key, default) return self.id_map.get(key, default)
def __call__(self, root): def __call__(self, root, fonts):
self.fonts = fonts
for s in XPath('//w:style')(root): for s in XPath('//w:style')(root):
s = Style(s) s = Style(s)
if s.style_id: if s.style_id:
@ -246,6 +247,9 @@ class Styles(object):
for attr in ans.all_properties: for attr in ans.all_properties:
setattr(ans, attr, self.run_val(parent_styles, direct_formatting, attr)) setattr(ans, attr, self.run_val(parent_styles, direct_formatting, attr))
if ans.font_family is not inherit:
ans.font_family = self.fonts.family_for(ans.font_family, ans.b, ans.i)
return ans return ans
def resolve(self, obj): def resolve(self, obj):
@ -254,6 +258,55 @@ class Styles(object):
if obj.tag.endswith('}r'): if obj.tag.endswith('}r'):
return self.resolve_run(obj) return self.resolve_run(obj)
def cascade(self, layers):
self.body_font_family = 'serif'
self.body_font_size = '10pt'
for p, runs in layers.iteritems():
char_styles = [self.resolve_run(r) for r in runs]
block_style = self.resolve_paragraph(p)
c = Counter()
for s in char_styles:
if s.font_family is not inherit:
c[s.font_family] += 1
if c:
family = c.most_common(1)[0][0]
block_style.font_family = family
for s in char_styles:
if s.font_family == family:
s.font_family = inherit
sizes = [s.font_size for s in char_styles if s.font_size is not inherit]
if sizes:
sz = block_style.font_size = sizes[0]
for s in char_styles:
if s.font_size == sz:
s.font_size = inherit
block_styles = [self.resolve_paragraph(p) for p in layers]
c = Counter()
for s in block_styles:
if s.font_family is not inherit:
c[s.font_family] += 1
if c:
self.body_font_family = family = c.most_common(1)[0][0]
for s in block_styles:
if s.font_family == family:
s.font_family = inherit
c = Counter()
for s in block_styles:
if s.font_size is not inherit:
c[s.font_size] += 1
if c:
sz = c.most_common(1)[0][0]
for s in block_styles:
if s.font_size == sz:
s.font_size = inherit
self.body_font_size = '%.3gpt' % sz
def resolve_numbering(self, numbering): def resolve_numbering(self, numbering):
# When a numPr element appears inside a paragraph style, the lvl info # When a numPr element appears inside a paragraph style, the lvl info
# must be discarder and pStyle used instead. # must be discarder and pStyle used instead.
@ -290,13 +343,18 @@ class Styles(object):
h = hash(frozenset(css.iteritems())) h = hash(frozenset(css.iteritems()))
return self.classes.get(h, (None, None))[0] return self.classes.get(h, (None, None))[0]
def generate_css(self): def generate_css(self, dest_dir, docx):
ef = self.fonts.embed_fonts(dest_dir, docx)
prefix = textwrap.dedent( prefix = textwrap.dedent(
'''\ '''\
body { font-family: %s; font-size: %s }
p { text-indent: 1.5em } p { text-indent: 1.5em }
ul, ol, p { margin: 0; padding: 0 } ul, ol, p { margin: 0; padding: 0 }
''') ''') % (self.body_font_family, self.body_font_size)
if ef:
prefix = ef + '\n' + prefix
ans = [] ans = []
for (cls, css) in sorted(self.classes.itervalues(), key=lambda x:x[0]): for (cls, css) in sorted(self.classes.itervalues(), key=lambda x:x[0]):

View File

@ -14,9 +14,10 @@ from lxml.html.builder import (
HTML, HEAD, TITLE, BODY, LINK, META, P, SPAN, BR) HTML, HEAD, TITLE, BODY, LINK, META, P, SPAN, BR)
from calibre.ebooks.docx.container import DOCX, fromstring from calibre.ebooks.docx.container import DOCX, fromstring
from calibre.ebooks.docx.names import XPath, is_tag, barename, XML, STYLES, NUMBERING from calibre.ebooks.docx.names import XPath, is_tag, XML, STYLES, NUMBERING, FONTS
from calibre.ebooks.docx.styles import Styles, inherit from calibre.ebooks.docx.styles import Styles, inherit
from calibre.ebooks.docx.numbering import Numbering from calibre.ebooks.docx.numbering import Numbering
from calibre.ebooks.docx.fonts import Fonts
from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1 from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1
class Text: class Text:
@ -63,16 +64,14 @@ class Convert(object):
doc = self.docx.document doc = self.docx.document
relationships_by_id, relationships_by_type = self.docx.document_relationships relationships_by_id, relationships_by_type = self.docx.document_relationships
self.read_styles(relationships_by_type) self.read_styles(relationships_by_type)
for top_level in XPath('/w:document/w:body/*')(doc): self.layers = OrderedDict()
if is_tag(top_level, 'w:p'): for wp in XPath('//w:p')(doc):
p = self.convert_p(top_level) p = self.convert_p(wp)
self.body.append(p) self.body.append(p)
elif is_tag(top_level, 'w:tbl'): # TODO: tables <w:tbl> child of <w:body> (nested tables?)
pass # TODO: tables # TODO: Last section properties <w:sectPr> child of <w:body>
elif is_tag(top_level, 'w:sectPr'):
pass # TODO: Last section properties self.styles.cascade(self.layers)
else:
self.log.debug('Unknown top-level tag: %s, ignoring' % barename(top_level.tag))
numbered = [] numbered = []
for html_obj, obj in self.object_map.iteritems(): for html_obj, obj in self.object_map.iteritems():
@ -116,7 +115,18 @@ class Convert(object):
nname = get_name(NUMBERING, 'numbering.xml') nname = get_name(NUMBERING, 'numbering.xml')
sname = get_name(STYLES, 'styles.xml') sname = get_name(STYLES, 'styles.xml')
fname = get_name(FONTS, 'fontTable.xml')
numbering = self.numbering = Numbering() numbering = self.numbering = Numbering()
fonts = self.fonts = Fonts()
if fname is not None:
embed_relationships = self.docx.get_relationships(fname)[0]
try:
raw = self.docx.read(fname)
except KeyError:
self.log.warn('Fonts table %s does not exist' % fname)
else:
fonts(fromstring(raw), embed_relationships, self.docx, self.dest_dir)
if sname is not None: if sname is not None:
try: try:
@ -124,7 +134,7 @@ class Convert(object):
except KeyError: except KeyError:
self.log.warn('Styles %s do not exist' % sname) self.log.warn('Styles %s do not exist' % sname)
else: else:
self.styles(fromstring(raw)) self.styles(fromstring(raw), fonts)
if nname is not None: if nname is not None:
try: try:
@ -140,7 +150,7 @@ class Convert(object):
raw = html.tostring(self.html, encoding='utf-8', doctype='<!DOCTYPE html>') raw = html.tostring(self.html, encoding='utf-8', doctype='<!DOCTYPE html>')
with open(os.path.join(self.dest_dir, 'index.html'), 'wb') as f: with open(os.path.join(self.dest_dir, 'index.html'), 'wb') as f:
f.write(raw) f.write(raw)
css = self.styles.generate_css() css = self.styles.generate_css(self.dest_dir, self.docx)
if css: if css:
with open(os.path.join(self.dest_dir, 'docx.css'), 'wb') as f: with open(os.path.join(self.dest_dir, 'docx.css'), 'wb') as f:
f.write(css.encode('utf-8')) f.write(css.encode('utf-8'))
@ -149,9 +159,11 @@ class Convert(object):
dest = P() dest = P()
self.object_map[dest] = p self.object_map[dest] = p
style = self.styles.resolve_paragraph(p) style = self.styles.resolve_paragraph(p)
self.layers[p] = []
for run in XPath('descendant::w:r')(p): for run in XPath('descendant::w:r')(p):
span = self.convert_run(run) span = self.convert_run(run)
dest.append(span) dest.append(span)
self.layers[p].append(run)
m = re.match(r'heading\s+(\d+)$', style.style_name or '', re.IGNORECASE) m = re.match(r'heading\s+(\d+)$', style.style_name or '', re.IGNORECASE)
if m is not None: if m is not None:
@ -177,12 +189,9 @@ class Convert(object):
spans = [] spans = []
bs = {} bs = {}
for span, style in border_run: for span, style in border_run:
c = style.css style.get_border_css(bs)
style.clear_border_css()
spans.append(span) spans.append(span)
for x in ('width', 'color', 'style'):
val = c.pop('border-%s' % x, None)
if val is not None:
bs['border-%s' % x] = val
if bs: if bs:
cls = self.styles.register(bs, 'text_border') cls = self.styles.register(bs, 'text_border')
wrapper = self.wrap_elems(spans, SPAN()) wrapper = self.wrap_elems(spans, SPAN())

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
import re import re
from urlparse import urlparse from urlparse import urlparse
from collections import deque from collections import deque, Counter
from functools import partial from functools import partial
from lxml import etree from lxml import etree
@ -29,7 +29,8 @@ class TOC(object):
def __init__(self, title=None, dest=None, frag=None): def __init__(self, title=None, dest=None, frag=None):
self.title, self.dest, self.frag = title, dest, frag self.title, self.dest, self.frag = title, dest, frag
self.dest_exists = self.dest_error = None self.dest_exists = self.dest_error = None
if self.title: self.title = self.title.strip() if self.title:
self.title = self.title.strip()
self.parent = None self.parent = None
self.children = [] self.children = []
@ -326,11 +327,13 @@ def create_ncx(toc, to_href, btitle, lang, uid):
navmap = etree.SubElement(ncx, NCX('navMap')) navmap = etree.SubElement(ncx, NCX('navMap'))
spat = re.compile(r'\s+') spat = re.compile(r'\s+')
def process_node(xml_parent, toc_parent, play_order=0): play_order = Counter()
def process_node(xml_parent, toc_parent):
for child in toc_parent: for child in toc_parent:
play_order += 1 play_order['c'] += 1
point = etree.SubElement(xml_parent, NCX('navPoint'), id=uuid_id(), point = etree.SubElement(xml_parent, NCX('navPoint'), id=uuid_id(),
playOrder=str(play_order)) playOrder=str(play_order['c']))
label = etree.SubElement(point, NCX('navLabel')) label = etree.SubElement(point, NCX('navLabel'))
title = child.title title = child.title
if title: if title:
@ -341,7 +344,7 @@ def create_ncx(toc, to_href, btitle, lang, uid):
if child.frag: if child.frag:
href += '#'+child.frag href += '#'+child.frag
etree.SubElement(point, NCX('content'), src=href) etree.SubElement(point, NCX('content'), src=href)
process_node(point, child, play_order) process_node(point, child)
process_node(navmap, toc) process_node(navmap, toc)
return ncx return ncx

View File

@ -3,17 +3,21 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__license__ = 'GPL v3' __license__ = 'GPL v3'
import json import json, os, traceback
from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, QFont, from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QSyntaxHighlighter, QFont,
QRegExp, QApplication, QTextCharFormat, QColor, QCursor) QRegExp, QApplication, QTextCharFormat, QColor, QCursor,
QIcon, QSize)
from calibre.gui2 import error_dialog from calibre import sanitize_file_name_unicode
from calibre.constants import config_dir
from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog
from calibre.utils.formatter_functions import formatter_functions from calibre.utils.formatter_functions import formatter_functions
from calibre.utils.icu import sort_key
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.formatter import SafeFormat from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.library.coloring import (displayable_columns) from calibre.library.coloring import (displayable_columns, color_row_key)
from calibre.gui2 import error_dialog, choose_files, pixmap_to_data
class ParenPosition: class ParenPosition:
@ -198,25 +202,56 @@ class TemplateHighlighter(QSyntaxHighlighter):
class TemplateDialog(QDialog, Ui_TemplateDialog): class TemplateDialog(QDialog, Ui_TemplateDialog):
def __init__(self, parent, text, mi=None, fm=None, color_field=None): def __init__(self, parent, text, mi=None, fm=None, color_field=None,
icon_field_key=None, icon_rule_kind=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
Ui_TemplateDialog.__init__(self) Ui_TemplateDialog.__init__(self)
self.setupUi(self) self.setupUi(self)
self.coloring = color_field is not None self.coloring = color_field is not None
self.iconing = icon_field_key is not None
cols = []
if fm is not None:
for key in sorted(displayable_columns(fm),
key=lambda(k): sort_key(fm[k]['name']) if k != color_row_key else 0):
if key == color_row_key and not self.coloring:
continue
from calibre.gui2.preferences.coloring import all_columns_string
name = all_columns_string if key == color_row_key else fm[key]['name']
if name:
cols.append((name, key))
self.color_layout.setVisible(False)
self.icon_layout.setVisible(False)
if self.coloring: if self.coloring:
cols = sorted([k for k in displayable_columns(fm)]) self.color_layout.setVisible(True)
self.colored_field.addItems(cols) for n1, k1 in cols:
self.colored_field.setCurrentIndex(self.colored_field.findText(color_field)) self.colored_field.addItem(n1, k1)
self.colored_field.setCurrentIndex(self.colored_field.findData(color_field))
colors = QColor.colorNames() colors = QColor.colorNames()
colors.sort() colors.sort()
self.color_name.addItems(colors) self.color_name.addItems(colors)
else: elif self.iconing:
self.colored_field.setVisible(False) self.icon_layout.setVisible(True)
self.colored_field_label.setVisible(False) for n1, k1 in cols:
self.color_chooser_label.setVisible(False) self.icon_field.addItem(n1, k1)
self.color_name.setVisible(False) self.icon_file_names = []
self.color_copy_button.setVisible(False) d = os.path.join(config_dir, 'cc_icons')
if os.path.exists(d):
for icon_file in os.listdir(d):
icon_file = icu_lower(icon_file)
if os.path.exists(os.path.join(d, icon_file)):
if icon_file.endswith('.png'):
self.icon_file_names.append(icon_file)
self.icon_file_names.sort(key=sort_key)
self.update_filename_box()
self.icon_with_text.setChecked(True)
if icon_rule_kind == 'icon_only':
self.icon_without_text.setChecked(True)
self.icon_field.setCurrentIndex(self.icon_field.findData(icon_field_key))
if mi: if mi:
self.mi = mi self.mi = mi
else: else:
@ -248,6 +283,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel'))
self.color_copy_button.clicked.connect(self.color_to_clipboard) self.color_copy_button.clicked.connect(self.color_to_clipboard)
self.filename_button.clicked.connect(self.filename_button_clicked)
self.icon_copy_button.clicked.connect(self.icon_to_clipboard)
try: try:
with open(P('template-functions.json'), 'rb') as f: with open(P('template-functions.json'), 'rb') as f:
@ -276,11 +313,55 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
'<a href="http://manual.calibre-ebook.com/template_ref.html">' '<a href="http://manual.calibre-ebook.com/template_ref.html">'
'%s</a>'%tt) '%s</a>'%tt)
def filename_button_clicked(self):
try:
path = choose_files(self, 'choose_category_icon',
_('Select Icon'), filters=[
('Images', ['png', 'gif', 'jpg', 'jpeg'])],
all_files=False, select_only_single_file=True)
if path:
icon_path = path[0]
icon_name = sanitize_file_name_unicode(
os.path.splitext(
os.path.basename(icon_path))[0]+'.png')
if icon_name not in self.icon_file_names:
self.icon_file_names.append(icon_name)
self.update_filename_box()
try:
p = QIcon(icon_path).pixmap(QSize(128, 128))
d = os.path.join(config_dir, 'cc_icons')
if not os.path.exists(os.path.join(d, icon_name)):
if not os.path.exists(d):
os.makedirs(d)
with open(os.path.join(d, icon_name), 'wb') as f:
f.write(pixmap_to_data(p, format='PNG'))
except:
traceback.print_exc()
self.icon_files.setCurrentIndex(self.icon_files.findText(icon_name))
self.icon_files.adjustSize()
except:
traceback.print_exc()
return
def update_filename_box(self):
self.icon_files.clear()
self.icon_file_names.sort(key=sort_key)
self.icon_files.addItem('')
self.icon_files.addItems(self.icon_file_names)
for i,filename in enumerate(self.icon_file_names):
icon = QIcon(os.path.join(config_dir, 'cc_icons', filename))
self.icon_files.setItemIcon(i+1, icon)
def color_to_clipboard(self): def color_to_clipboard(self):
app = QApplication.instance() app = QApplication.instance()
c = app.clipboard() c = app.clipboard()
c.setText(unicode(self.color_name.currentText())) c.setText(unicode(self.color_name.currentText()))
def icon_to_clipboard(self):
app = QApplication.instance()
c = app.clipboard()
c.setText(unicode(self.icon_files.currentText()))
def textbox_changed(self): def textbox_changed(self):
cur_text = unicode(self.textbox.toPlainText()) cur_text = unicode(self.textbox.toPlainText())
if self.last_text != cur_text: if self.last_text != cur_text:
@ -324,5 +405,14 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
_('The template box cannot be empty'), show=True) _('The template box cannot be empty'), show=True)
return return
self.rule = (unicode(self.colored_field.currentText()), txt) self.rule = (unicode(self.colored_field.itemData(
self.colored_field.currentIndex()).toString()), txt)
elif self.iconing:
rt = 'icon' if self.icon_with_text.isChecked() else 'icon_only'
self.rule = (rt,
unicode(self.icon_field.itemData(
self.icon_field.currentIndex()).toString()),
txt)
else:
self.rule = ('', txt)
QDialog.accept(self) QDialog.accept(self)

View File

@ -21,6 +21,7 @@
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QWidget" name="color_layout">
<layout class="QGridLayout"> <layout class="QGridLayout">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="colored_field_label"> <widget class="QLabel" name="colored_field_label">
@ -62,6 +63,97 @@
</widget> </widget>
</item> </item>
</layout> </layout>
</widget>
</item>
<item>
<widget class="QWidget" name="icon_layout">
<layout class="QGridLayout">
<item row="0" column="0" colspan="2">
<widget class="QGroupBox">
<property name="title">
<string>Kind</string>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="QRadioButton" name="icon_without_text">
<property name="text">
<string>icon with no text</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="icon_with_text">
<property name="text">
<string>icon with text</string>
</property>
</widget>
</item>
</layout>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="icon_chooser_label">
<property name="text">
<string>Apply the icon to column:</string>
</property>
<property name="buddy">
<cstring>icon_field</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="icon_field">
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="image_chooser_label">
<property name="text">
<string>Copy an icon file name to the clipboard:</string>
</property>
<property name="buddy">
<cstring>color_name</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QWidget">
<layout class="QHBoxLayout">
<item>
<widget class="QComboBox" name="icon_files">
</widget>
</item>
<item>
<widget class="QToolButton" name="icon_copy_button">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/edit-copy.png</normaloff>:/images/edit-copy.png</iconset>
</property>
<property name="toolTip">
<string>Copy the selected icon file name to the clipboard</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="filename_button">
<property name="text">
<string>Add icon</string>
</property>
<property name="toolTip">
<string>Add an icon file to the set of choices</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item> </item>
<item> <item>
<widget class="QPlainTextEdit" name="textbox"/> <widget class="QPlainTextEdit" name="textbox"/>

View File

@ -636,10 +636,20 @@ class RulesModel(QAbstractListModel): # {{{
def rule_to_html(self, kind, col, rule): def rule_to_html(self, kind, col, rule):
if not isinstance(rule, Rule): if not isinstance(rule, Rule):
if kind == 'color':
return _(''' return _('''
<p>Advanced Rule for column <b>%(col)s</b>: <p>Advanced Rule for column <b>%(col)s</b>:
<pre>%(rule)s</pre> <pre>%(rule)s</pre>
''')%dict(col=col, rule=prepare_string_for_xml(rule)) ''')%dict(col=col, rule=prepare_string_for_xml(rule))
else:
return _('''
<p>Advanced Rule: set <b>%(typ)s</b> for column <b>%(col)s</b>:
<pre>%(rule)s</pre>
''')%dict(col=col,
typ=icon_rule_kinds[0][0]
if kind == icon_rule_kinds[0][1] else icon_rule_kinds[1][0],
rule=prepare_string_for_xml(rule))
conditions = [self.condition_to_html(c) for c in rule.conditions] conditions = [self.condition_to_html(c) for c in rule.conditions]
trans_kind = 'not found' trans_kind = 'not found'
@ -761,7 +771,7 @@ class EditRules(QWidget): # {{{
' what icon to use. Click the Add Rule button below' ' what icon to use. Click the Add Rule button below'
' to get started.<p>You can <b>change an existing rule</b> by' ' to get started.<p>You can <b>change an existing rule</b> by'
' double clicking it.')) ' double clicking it.'))
self.add_advanced_button.setVisible(False) # self.add_advanced_button.setVisible(False)
def add_rule(self): def add_rule(self):
d = RuleEditor(self.model.fm, self.pref_name) d = RuleEditor(self.model.fm, self.pref_name)
@ -774,6 +784,7 @@ class EditRules(QWidget): # {{{
self.changed.emit() self.changed.emit()
def add_advanced(self): def add_advanced(self):
if self.pref_name == 'column_color_rules':
td = TemplateDialog(self, '', mi=self.mi, fm=self.fm, color_field='') td = TemplateDialog(self, '', mi=self.mi, fm=self.fm, color_field='')
if td.exec_() == td.Accepted: if td.exec_() == td.Accepted:
col, r = td.rule col, r = td.rule
@ -781,6 +792,15 @@ class EditRules(QWidget): # {{{
idx = self.model.add_rule('color', col, r) idx = self.model.add_rule('color', col, r)
self.rules_view.scrollTo(idx) self.rules_view.scrollTo(idx)
self.changed.emit() self.changed.emit()
else:
td = TemplateDialog(self, '', mi=self.mi, fm=self.fm, icon_field_key='')
if td.exec_() == td.Accepted:
print(td.rule)
typ, col, r = td.rule
if typ and r and col:
idx = self.model.add_rule(typ, col, r)
self.rules_view.scrollTo(idx)
self.changed.emit()
def edit_rule(self, index): def edit_rule(self, index):
try: try:
@ -790,8 +810,12 @@ class EditRules(QWidget): # {{{
if isinstance(rule, Rule): if isinstance(rule, Rule):
d = RuleEditor(self.model.fm, self.pref_name) d = RuleEditor(self.model.fm, self.pref_name)
d.apply_rule(kind, col, rule) d.apply_rule(kind, col, rule)
else: elif self.pref_name == 'column_color_rules':
d = TemplateDialog(self, rule, mi=self.mi, fm=self.fm, color_field=col) d = TemplateDialog(self, rule, mi=self.mi, fm=self.fm, color_field=col)
else:
d = TemplateDialog(self, rule, mi=self.mi, fm=self.fm, icon_field_key=col,
icon_rule_kind=kind)
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted:
if len(d.rule) == 2: # Convert template dialog rules to a triple if len(d.rule) == 2: # Convert template dialog rules to a triple
d.rule = ('color', d.rule[0], d.rule[1]) d.rule = ('color', d.rule[0], d.rule[1])

View File

@ -13,13 +13,82 @@ from threading import Thread
from calibre import walk, prints, as_unicode from calibre import walk, prints, as_unicode
from calibre.constants import (config_dir, iswindows, isosx, plugins, DEBUG, from calibre.constants import (config_dir, iswindows, isosx, plugins, DEBUG,
isworker) isworker, filesystem_encoding)
from calibre.utils.fonts.metadata import FontMetadata, UnsupportedFont from calibre.utils.fonts.metadata import FontMetadata, UnsupportedFont
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
class NoFonts(ValueError): class NoFonts(ValueError):
pass pass
def default_font_dirs():
return [
'/opt/share/fonts',
'/usr/share/fonts',
'/usr/local/share/fonts',
os.path.expanduser('~/.local/share/fonts'),
os.path.expanduser('~/.fonts')
]
def fc_list():
import ctypes
from ctypes.util import find_library
lib = find_library('fontconfig')
if lib is None:
return default_font_dirs()
try:
lib = ctypes.CDLL(lib)
except:
return default_font_dirs()
prototype = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)
try:
get_font_dirs = prototype(('FcConfigGetFontDirs', lib))
except (AttributeError):
return default_font_dirs()
prototype = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.c_void_p)
try:
next_dir = prototype(('FcStrListNext', lib))
except (AttributeError):
return default_font_dirs()
prototype = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
try:
end = prototype(('FcStrListDone', lib))
except (AttributeError):
return default_font_dirs()
str_list = get_font_dirs(ctypes.c_void_p())
if not str_list:
return default_font_dirs()
ans = []
while True:
d = next_dir(str_list)
if not d:
break
if d:
try:
ans.append(d.decode(filesystem_encoding))
except ValueError:
return default_font_dirs
end(str_list)
if len(ans) < 3:
return default_font_dirs()
parents = []
for f in ans:
found = False
for p in parents:
if f.startswith(p):
found = True
break
if not found:
parents.append(f)
return parents
def font_dirs(): def font_dirs():
if iswindows: if iswindows:
winutil, err = plugins['winutil'] winutil, err = plugins['winutil']
@ -35,12 +104,7 @@ def font_dirs():
os.path.expanduser('~/.fonts'), os.path.expanduser('~/.fonts'),
os.path.expanduser('~/Library/Fonts'), os.path.expanduser('~/Library/Fonts'),
] ]
return [ return fc_list()
'/opt/share/fonts',
'/usr/share/fonts',
'/usr/local/share/fonts',
os.path.expanduser('~/.fonts')
]
class Scanner(Thread): class Scanner(Thread):
@ -133,7 +197,8 @@ class Scanner(Thread):
for family in self.find_font_families(): for family in self.find_font_families():
faces = filter(filter_faces, self.fonts_for_family(family)) faces = filter(filter_faces, self.fonts_for_family(family))
if not faces: continue if not faces:
continue
generic_family = panose_to_css_generic_family(faces[0]['panose']) generic_family = panose_to_css_generic_family(faces[0]['panose'])
if generic_family in allowed_families or generic_family == preferred_families[0]: if generic_family in allowed_families or generic_family == preferred_families[0]:
return (family, faces) return (family, faces)
@ -233,7 +298,8 @@ class Scanner(Thread):
def build_families(self): def build_families(self):
families = defaultdict(list) families = defaultdict(list)
for f in self.cached_fonts.itervalues(): for f in self.cached_fonts.itervalues():
if not f: continue if not f:
continue
lf = icu_lower(f['font-family'] or '') lf = icu_lower(f['font-family'] or '')
if lf: if lf:
families[lf].append(f) families[lf].append(f)

View File

@ -661,6 +661,17 @@ icu_set_default_encoding(PyObject *self, PyObject *args) {
} }
// }}} // }}}
// set_default_encoding {{{
static PyObject *
icu_set_filesystem_encoding(PyObject *self, PyObject *args) {
char *encoding;
if (!PyArg_ParseTuple(args, "s:setfilesystemencoding", &encoding))
return NULL;
Py_FileSystemDefaultEncoding = strdup(encoding);
Py_RETURN_NONE;
}
// }}}
// set_default_encoding {{{ // set_default_encoding {{{
static PyObject * static PyObject *
icu_get_available_transliterators(PyObject *self, PyObject *args) { icu_get_available_transliterators(PyObject *self, PyObject *args) {
@ -707,6 +718,10 @@ static PyMethodDef icu_methods[] = {
"set_default_encoding(encoding) -> Set the default encoding for the python unicode implementation." "set_default_encoding(encoding) -> Set the default encoding for the python unicode implementation."
}, },
{"set_filesystem_encoding", icu_set_filesystem_encoding, METH_VARARGS,
"set_filesystem_encoding(encoding) -> Set the filesystem encoding for python."
},
{"get_available_transliterators", icu_get_available_transliterators, METH_VARARGS, {"get_available_transliterators", icu_get_available_transliterators, METH_VARARGS,
"get_available_transliterators() -> Return list of available transliterators. This list is rather limited on OS X." "get_available_transliterators() -> Return list of available transliterators. This list is rather limited on OS X."
}, },

View File

@ -163,11 +163,22 @@ load_collator()
_icu_not_ok = _icu is None or _collator is None _icu_not_ok = _icu is None or _collator is None
try: try:
if sys.getdefaultencoding().lower() == 'ascii': senc = sys.getdefaultencoding()
if not senc or senc.lower() == 'ascii':
_icu.set_default_encoding('utf-8') _icu.set_default_encoding('utf-8')
del senc
except: except:
pass pass
try:
fenc = sys.getfilesystemencoding()
if not fenc or fenc.lower() == 'ascii':
_icu.set_filesystem_encoding('utf-8')
del fenc
except:
pass
# }}} # }}}
################# The string functions ######################################## ################# The string functions ########################################