This commit is contained in:
GRiker 2013-05-13 20:09:05 -06:00
commit a6b14254e2
27 changed files with 566 additions and 156 deletions

View File

@ -1,32 +1,37 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
import re import re
class NetMagazineRecipe (BasicNewsRecipe): class dotnetMagazine (BasicNewsRecipe):
__author__ = u'Marc Busqué <marc@lamarciana.com>' __author__ = u'Bonni Salles'
__url__ = 'http://www.lamarciana.com' __version__ = '1.0'
__version__ = '1.0' __license__ = 'GPL v3'
__license__ = 'GPL v3' __copyright__ = u'2013, Bonni Salles'
__copyright__ = u'2012, Marc Busqué <marc@lamarciana.com>' title = '.net magazine'
title = u'.net magazine' oldest_article = 7
description = u'net is the worlds best-selling magazine for web designers and developers, featuring tutorials from leading agencies, interviews with the webs biggest names, and agenda-setting features on the hottest issues affecting the internet today.' no_stylesheets = True
language = 'en' encoding = 'utf8'
tags = 'web development, software' use_embedded_content = False
oldest_article = 7 language = 'en'
remove_empty_feeds = True remove_empty_feeds = True
no_stylesheets = True extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} '
cover_url = u'http://media.netmagazine.futurecdn.net/sites/all/themes/netmag/logo.png' cover_url = u'http://media.netmagazine.futurecdn.net/sites/all/themes/netmag/logo.png'
keep_only_tags = [
dict(name='article', attrs={'class': re.compile('^node.*$', re.IGNORECASE)}) remove_tags_after = dict(name='footer', id=lambda x:not x)
] remove_tags_before = dict(name='header', id=lambda x:not x)
remove_tags = [
dict(name='span', attrs={'class': 'comment-count'}), remove_tags = [
dict(name='div', attrs={'class': 'item-list share-links'}), dict(name='div', attrs={'class': 'item-list'}),
dict(name='footer'), dict(name='h4', attrs={'class': 'std-hdr'}),
] dict(name='div', attrs={'class': 'item-list share-links'}), #removes share links
remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style'] dict(name=['script', 'noscript']),
extra_css = 'img {max-width: 100%; display: block; margin: auto;} .captioned-image div {text-align: center; font-style: italic;}' dict(name='div', attrs={'id': 'comments-form'}), #comment these out if you want the comments to show
dict(name='div', attrs={'id': re.compile('advertorial_block_($|| )')}),
dict(name='div', attrs={'id': 'right-col'}),
dict(name='div', attrs={'id': 'comments'}), #comment these out if you want the comments to show
dict(name='div', attrs={'class': 'item-list related-content'}),
feeds = [
(u'.net', u'http://feeds.feedburner.com/net/topstories'),
] ]
feeds = [
(u'net', u'http://feeds.feedburner.com/net/topstories')
]

75
recipes/nrc_next.recipe Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# Based on veezh's original recipe, Kovid Goyal's New York Times recipe and Snaabs nrc Handelsblad recipe
__license__ = 'GPL v3'
__copyright__ = '2013, Niels Giesen'
'''
www.nrc.nl
'''
import os, zipfile
import time
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ptempfile import PersistentTemporaryFile
class NRCNext(BasicNewsRecipe):
title = u'nrc•next'
description = u'De ePaper-versie van nrc•next'
language = 'nl'
lang = 'nl-NL'
needs_subscription = True
__author__ = 'Niels Giesen'
conversion_options = {
'no_default_epub_cover' : True
}
def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None:
br.open('http://login.nrc.nl/login')
br.select_form(nr=0)
br['username'] = self.username
br['password'] = self.password
br.submit()
return br
def build_index(self):
today = time.strftime("%Y%m%d")
domain = "http://digitaleeditie.nrc.nl"
url = domain + "/digitaleeditie/helekrant/epub/nn_" + today + ".epub"
#print url
try:
br = self.get_browser()
f = br.open(url)
except:
self.report_progress(0,_('Kan niet inloggen om editie te downloaden'))
raise ValueError('Krant van vandaag nog niet beschikbaar')
tmp = PersistentTemporaryFile(suffix='.epub')
self.report_progress(0,_('downloading epub'))
tmp.write(f.read())
f.close()
br.close()
if zipfile.is_zipfile(tmp):
try:
zfile = zipfile.ZipFile(tmp.name, 'r')
zfile.extractall(self.output_dir)
self.report_progress(0,_('extracting epub'))
except zipfile.BadZipfile:
self.report_progress(0,_('BadZip error, continuing'))
tmp.close()
index = os.path.join(self.output_dir, 'metadata.opf')
self.report_progress(1,_('epub downloaded and extracted'))
return index

View File

@ -32,7 +32,7 @@ defaults.
# Set the use_series_auto_increment_tweak_when_importing tweak to True to # Set the use_series_auto_increment_tweak_when_importing tweak to True to
# use the above values when importing/adding books. If this tweak is set to # use the above values when importing/adding books. If this tweak is set to
# False (the default) then the series number will be set to 1 if it is not # False (the default) then the series number will be set to 1 if it is not
# explicitly set to during the import. If set to True, then the # explicitly set during the import. If set to True, then the
# series index will be set according to the series_index_auto_increment setting. # series index will be set according to the series_index_auto_increment setting.
# Note that the use_series_auto_increment_tweak_when_importing tweak is used # Note that the use_series_auto_increment_tweak_when_importing tweak is used
# only when a value is not provided during import. If the importing regular # only when a value is not provided during import. If the importing regular

View File

@ -1548,12 +1548,13 @@ class StoreNextoStore(StoreBase):
class StoreNookUKStore(StoreBase): class StoreNookUKStore(StoreBase):
name = 'Nook UK' name = 'Nook UK'
author = 'John Schember' author = 'Charles Haley'
description = u'Barnes & Noble S.à r.l, a subsidiary of Barnes & Noble, Inc., a leading retailer of content, digital media and educational products, is proud to bring the award-winning NOOK® reading experience and a leading digital bookstore to the UK.' # noqa description = u'Barnes & Noble S.A.R.L, a subsidiary of Barnes & Noble, Inc., a leading retailer of content, digital media and educational products, is proud to bring the award-winning NOOK reading experience and a leading digital bookstore to the UK.' # noqa
actual_plugin = 'calibre.gui2.store.stores.nook_uk_plugin:NookUKStore' actual_plugin = 'calibre.gui2.store.stores.nook_uk_plugin:NookUKStore'
headquarters = 'UK' headquarters = 'UK'
formats = ['NOOK'] formats = ['NOOK']
affiliate = True
class StoreOpenBooksStore(StoreBase): class StoreOpenBooksStore(StoreBase):
name = 'Open Books' name = 'Open Books'

View File

@ -107,6 +107,12 @@ class DevicePlugin(Plugin):
#: :meth:`set_user_blacklisted_devices` #: :meth:`set_user_blacklisted_devices`
ASK_TO_ALLOW_CONNECT = False ASK_TO_ALLOW_CONNECT = False
#: Set this to a dictionary of the form {'title':title, 'msg':msg, 'det_msg':detailed_msg} to have calibre popup
#: a message to the user after some callbacks are run (currently only upload_books).
#: Be careful to not spam the user with too many messages. This variable is checked after *every* callback,
#: so only set it when you really need to.
user_feedback_after_callback = None
@classmethod @classmethod
def get_gui_name(cls): def get_gui_name(cls):
if hasattr(cls, 'gui_name'): if hasattr(cls, 'gui_name'):
@ -157,16 +163,15 @@ class DevicePlugin(Plugin):
if (vid in device_id or vidd in device_id) and \ if (vid in device_id or vidd in device_id) and \
(pid in device_id or pidd in device_id) and \ (pid in device_id or pidd in device_id) and \
self.test_bcd_windows(device_id, bcd): self.test_bcd_windows(device_id, bcd):
if debug: if debug:
self.print_usb_device_info(device_id) self.print_usb_device_info(device_id)
if only_presence or self.can_handle_windows(device_id, debug=debug): if only_presence or self.can_handle_windows(device_id, debug=debug):
try: try:
bcd = int(device_id.rpartition( bcd = int(device_id.rpartition(
'rev_')[-1].replace(':', 'a'), 16) 'rev_')[-1].replace(':', 'a'), 16)
except: except:
bcd = None bcd = None
return True, (vendor_id, product_id, bcd, None, return True, (vendor_id, product_id, bcd, None, None, None)
None, None)
return False, None return False, None
def test_bcd(self, bcdDevice, bcd): def test_bcd(self, bcdDevice, bcd):
@ -638,7 +643,6 @@ class DevicePlugin(Plugin):
''' '''
device_prefs.set_overrides() device_prefs.set_overrides()
# Dynamic control interface. # Dynamic control interface.
# The following methods are probably called on the GUI thread. Any driver # The following methods are probably called on the GUI thread. Any driver
# that implements these methods must take pains to be thread safe, because # that implements these methods must take pains to be thread safe, because

View File

@ -77,7 +77,7 @@ class Plumber(object):
def __init__(self, input, output, log, report_progress=DummyReporter(), def __init__(self, input, output, log, report_progress=DummyReporter(),
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False, dummy=False, merge_plugin_recs=True, abort_after_input_dump=False,
override_input_metadata=False): override_input_metadata=False, for_regex_wizard=False):
''' '''
:param input: Path to input file. :param input: Path to input file.
:param output: Path to output file/directory :param output: Path to output file/directory
@ -87,6 +87,7 @@ class Plumber(object):
if isbytestring(output): if isbytestring(output):
output = output.decode(filesystem_encoding) output = output.decode(filesystem_encoding)
self.original_input_arg = input self.original_input_arg = input
self.for_regex_wizard = for_regex_wizard
self.input = os.path.abspath(input) self.input = os.path.abspath(input)
self.output = os.path.abspath(output) self.output = os.path.abspath(output)
self.log = log self.log = log
@ -123,7 +124,7 @@ OptionRecommendation(name='input_profile',
'conversion system information on how to interpret ' 'conversion system information on how to interpret '
'various information in the input document. For ' 'various information in the input document. For '
'example resolution dependent lengths (i.e. lengths in ' 'example resolution dependent lengths (i.e. lengths in '
'pixels). Choices are:')+\ 'pixels). Choices are:')+
', '.join([x.short_name for x in input_profiles()]) ', '.join([x.short_name for x in input_profiles()])
), ),
@ -135,7 +136,7 @@ OptionRecommendation(name='output_profile',
'created document for the specified device. In some cases, ' 'created document for the specified device. In some cases, '
'an output profile is required to produce documents that ' 'an output profile is required to produce documents that '
'will work on a device. For example EPUB on the SONY reader. ' 'will work on a device. For example EPUB on the SONY reader. '
'Choices are:') + \ 'Choices are:') +
', '.join([x.short_name for x in output_profiles()]) ', '.join([x.short_name for x in output_profiles()])
), ),
@ -490,7 +491,7 @@ OptionRecommendation(name='asciiize',
'cases where there are multiple representations of a character ' 'cases where there are multiple representations of a character '
'(characters shared by Chinese and Japanese for instance) the ' '(characters shared by Chinese and Japanese for instance) the '
'representation based on the current calibre interface language will be ' 'representation based on the current calibre interface language will be '
'used.')%\ 'used.')%
u'\u041c\u0438\u0445\u0430\u0438\u043b ' u'\u041c\u0438\u0445\u0430\u0438\u043b '
u'\u0413\u043e\u0440\u0431\u0430\u0447\u0451\u0432' u'\u0413\u043e\u0440\u0431\u0430\u0447\u0451\u0432'
) )
@ -711,7 +712,6 @@ OptionRecommendation(name='search_replace',
self.input_fmt = input_fmt self.input_fmt = input_fmt
self.output_fmt = output_fmt self.output_fmt = output_fmt
self.all_format_options = set() self.all_format_options = set()
self.input_options = set() self.input_options = set()
self.output_options = set() self.output_options = set()
@ -775,7 +775,7 @@ OptionRecommendation(name='search_replace',
if not html_files: if not html_files:
raise ValueError(_('Could not find an ebook inside the archive')) 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 = [(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.sort(cmp=lambda x, y: cmp(x[1], y[1]))
html_files = [f[0] for f in html_files] html_files = [f[0] for f in html_files]
for q in ('toc', 'index'): for q in ('toc', 'index'):
for f in html_files: for f in html_files:
@ -783,8 +783,6 @@ OptionRecommendation(name='search_replace',
return f, os.path.splitext(f)[1].lower()[1:] return f, os.path.splitext(f)[1].lower()[1:]
return html_files[-1], os.path.splitext(html_files[-1])[1].lower()[1:] return html_files[-1], os.path.splitext(html_files[-1])[1].lower()[1:]
def get_option_by_name(self, name): def get_option_by_name(self, name):
for group in (self.input_options, self.pipeline_options, for group in (self.input_options, self.pipeline_options,
self.output_options, self.all_format_options): self.output_options, self.all_format_options):
@ -956,7 +954,6 @@ OptionRecommendation(name='search_replace',
self.log.info('Input debug saved to:', out_dir) self.log.info('Input debug saved to:', out_dir)
def run(self): def run(self):
''' '''
Run the conversion pipeline Run the conversion pipeline
@ -965,10 +962,12 @@ OptionRecommendation(name='search_replace',
self.setup_options() self.setup_options()
if self.opts.verbose: if self.opts.verbose:
self.log.filter_level = self.log.DEBUG self.log.filter_level = self.log.DEBUG
if self.for_regex_wizard and hasattr(self.opts, 'no_process'):
self.opts.no_process = True
self.flush() self.flush()
import cssutils, logging import cssutils, logging
cssutils.log.setLevel(logging.WARN) cssutils.log.setLevel(logging.WARN)
get_types_map() # Ensure the mimetypes module is intialized get_types_map() # Ensure the mimetypes module is intialized
if self.opts.debug_pipeline is not None: if self.opts.debug_pipeline is not None:
self.opts.verbose = max(self.opts.verbose, 4) self.opts.verbose = max(self.opts.verbose, 4)
@ -1003,6 +1002,8 @@ OptionRecommendation(name='search_replace',
self.ui_reporter(0.01, _('Converting input to HTML...')) self.ui_reporter(0.01, _('Converting input to HTML...'))
ir = CompositeProgressReporter(0.01, 0.34, self.ui_reporter) ir = CompositeProgressReporter(0.01, 0.34, self.ui_reporter)
self.input_plugin.report_progress = ir self.input_plugin.report_progress = ir
if self.for_regex_wizard:
self.input_plugin.for_viewer = True
with self.input_plugin: with self.input_plugin:
self.oeb = self.input_plugin(stream, self.opts, self.oeb = self.input_plugin(stream, self.opts,
self.input_fmt, self.log, self.input_fmt, self.log,
@ -1014,8 +1015,12 @@ OptionRecommendation(name='search_replace',
if self.input_fmt in ('recipe', 'downloaded_recipe'): if self.input_fmt in ('recipe', 'downloaded_recipe'):
self.opts_to_mi(self.user_metadata) self.opts_to_mi(self.user_metadata)
if not hasattr(self.oeb, 'manifest'): if not hasattr(self.oeb, 'manifest'):
self.oeb = create_oebbook(self.log, self.oeb, self.opts, self.oeb = create_oebbook(
encoding=self.input_plugin.output_encoding) self.log, self.oeb, self.opts,
encoding=self.input_plugin.output_encoding,
for_regex_wizard=self.for_regex_wizard)
if self.for_regex_wizard:
return
self.input_plugin.postprocess_book(self.oeb, self.opts, self.log) self.input_plugin.postprocess_book(self.oeb, self.opts, self.log)
self.opts.is_image_collection = self.input_plugin.is_image_collection self.opts.is_image_collection = self.input_plugin.is_image_collection
pr = CompositeProgressReporter(0.34, 0.67, self.ui_reporter) pr = CompositeProgressReporter(0.34, 0.67, self.ui_reporter)
@ -1081,7 +1086,6 @@ OptionRecommendation(name='search_replace',
self.dump_oeb(self.oeb, out_dir) self.dump_oeb(self.oeb, out_dir)
self.log('Structured HTML written to:', out_dir) self.log('Structured HTML written to:', out_dir)
if self.opts.extra_css and os.path.exists(self.opts.extra_css): if self.opts.extra_css and os.path.exists(self.opts.extra_css):
self.opts.extra_css = open(self.opts.extra_css, 'rb').read() self.opts.extra_css = open(self.opts.extra_css, 'rb').read()
@ -1161,13 +1165,20 @@ OptionRecommendation(name='search_replace',
self.log(self.output_fmt.upper(), 'output written to', self.output) self.log(self.output_fmt.upper(), 'output written to', self.output)
self.flush() self.flush()
# This has to be global as create_oebbook can be called from other locations
# (for example in the html input plugin)
regex_wizard_callback = None
def set_regex_wizard_callback(f):
global regex_wizard_callback
regex_wizard_callback = f
def create_oebbook(log, path_or_stream, opts, reader=None, def create_oebbook(log, path_or_stream, opts, reader=None,
encoding='utf-8', populate=True): encoding='utf-8', populate=True, for_regex_wizard=False):
''' '''
Create an OEBBook. Create an OEBBook.
''' '''
from calibre.ebooks.oeb.base import OEBBook from calibre.ebooks.oeb.base import OEBBook
html_preprocessor = HTMLPreProcessor(log, opts) html_preprocessor = HTMLPreProcessor(log, opts, regex_wizard_callback=regex_wizard_callback)
if not encoding: if not encoding:
encoding = None encoding = None
oeb = OEBBook(log, html_preprocessor, oeb = OEBBook(log, html_preprocessor,
@ -1182,3 +1193,4 @@ def create_oebbook(log, path_or_stream, opts, reader=None,
reader()(oeb, path_or_stream) reader()(oeb, path_or_stream)
return oeb return oeb

View File

@ -14,7 +14,7 @@ SVG_NS = 'http://www.w3.org/2000/svg'
XLINK_NS = 'http://www.w3.org/1999/xlink' XLINK_NS = 'http://www.w3.org/1999/xlink'
convert_entities = functools.partial(entity_to_unicode, convert_entities = functools.partial(entity_to_unicode,
result_exceptions = { result_exceptions={
u'<' : '&lt;', u'<' : '&lt;',
u'>' : '&gt;', u'>' : '&gt;',
u"'" : '&apos;', u"'" : '&apos;',
@ -144,9 +144,9 @@ class DocAnalysis(object):
percent is the percentage of lines that should be in a single bucket to return true percent is the percentage of lines that should be in a single bucket to return true
The majority of the lines will exist in 1-2 buckets in typical docs with hard line breaks The majority of the lines will exist in 1-2 buckets in typical docs with hard line breaks
''' '''
minLineLength=20 # Ignore lines under 20 chars (typical of spaces) minLineLength=20 # Ignore lines under 20 chars (typical of spaces)
maxLineLength=1900 # Discard larger than this to stay in range maxLineLength=1900 # Discard larger than this to stay in range
buckets=20 # Each line is divided into a bucket based on length buckets=20 # Each line is divided into a bucket based on length
#print "there are "+str(len(lines))+" lines" #print "there are "+str(len(lines))+" lines"
#max = 0 #max = 0
@ -156,7 +156,7 @@ class DocAnalysis(object):
# max = l # max = l
#print "max line found is "+str(max) #print "max line found is "+str(max)
# Build the line length histogram # Build the line length histogram
hRaw = [ 0 for i in range(0,buckets) ] hRaw = [0 for i in range(0,buckets)]
for line in self.lines: for line in self.lines:
l = len(line) l = len(line)
if l > minLineLength and l < maxLineLength: if l > minLineLength and l < maxLineLength:
@ -167,7 +167,7 @@ class DocAnalysis(object):
# Normalize the histogram into percents # Normalize the histogram into percents
totalLines = len(self.lines) totalLines = len(self.lines)
if totalLines > 0: if totalLines > 0:
h = [ float(count)/totalLines for count in hRaw ] h = [float(count)/totalLines for count in hRaw]
else: else:
h = [] h = []
#print "\nhRaw histogram lengths are: "+str(hRaw) #print "\nhRaw histogram lengths are: "+str(hRaw)
@ -200,7 +200,7 @@ class Dehyphenator(object):
# Add common suffixes to the regex below to increase the likelihood of a match - # Add common suffixes to the regex below to increase the likelihood of a match -
# don't add suffixes which are also complete words, such as 'able' or 'sex' # don't add suffixes which are also complete words, such as 'able' or 'sex'
# only remove if it's not already the point of hyphenation # only remove if it's not already the point of hyphenation
self.suffix_string = "((ed)?ly|'?e?s||a?(t|s)?ion(s|al(ly)?)?|ings?|er|(i)?ous|(i|a)ty|(it)?ies|ive|gence|istic(ally)?|(e|a)nce|m?ents?|ism|ated|(e|u)ct(ed)?|ed|(i|ed)?ness|(e|a)ncy|ble|ier|al|ex|ian)$" self.suffix_string = "((ed)?ly|'?e?s||a?(t|s)?ion(s|al(ly)?)?|ings?|er|(i)?ous|(i|a)ty|(it)?ies|ive|gence|istic(ally)?|(e|a)nce|m?ents?|ism|ated|(e|u)ct(ed)?|ed|(i|ed)?ness|(e|a)ncy|ble|ier|al|ex|ian)$" # noqa
self.suffixes = re.compile(r"^%s" % self.suffix_string, re.IGNORECASE) self.suffixes = re.compile(r"^%s" % self.suffix_string, re.IGNORECASE)
self.removesuffixes = re.compile(r"%s" % self.suffix_string, re.IGNORECASE) self.removesuffixes = re.compile(r"%s" % self.suffix_string, re.IGNORECASE)
# remove prefixes if the prefix was not already the point of hyphenation # remove prefixes if the prefix was not already the point of hyphenation
@ -265,19 +265,18 @@ class Dehyphenator(object):
self.html = html self.html = html
self.format = format self.format = format
if format == 'html': if format == 'html':
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|)\s*(?=<)(?P<wraptags>(</span>)?\s*(</[iubp]>\s*){1,2}(?P<up2threeblanks><(p|div)[^>]*>\s*(<p[^>]*>\s*</p>\s*)?</(p|div)>\s+){0,3}\s*(<[iubp][^>]*>\s*){1,2}(<span[^>]*>)?)\s*(?P<secondpart>[\w\d]+)' % length) intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|)\s*(?=<)(?P<wraptags>(</span>)?\s*(</[iubp]>\s*){1,2}(?P<up2threeblanks><(p|div)[^>]*>\s*(<p[^>]*>\s*</p>\s*)?</(p|div)>\s+){0,3}\s*(<[iubp][^>]*>\s*){1,2}(<span[^>]*>)?)\s*(?P<secondpart>[\w\d]+)' % length) # noqa
elif format == 'pdf': elif format == 'pdf':
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|)\s*(?P<wraptags><p>|</[iub]>\s*<p>\s*<[iub]>)\s*(?P<secondpart>[\w\d]+)'% length) intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|)\s*(?P<wraptags><p>|</[iub]>\s*<p>\s*<[iub]>)\s*(?P<secondpart>[\w\d]+)'% length)
elif format == 'txt': elif format == 'txt':
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|)(\u0020|\u0009)*(?P<wraptags>(\n(\u0020|\u0009)*)+)(?P<secondpart>[\w\d]+)'% length) intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|)(\u0020|\u0009)*(?P<wraptags>(\n(\u0020|\u0009)*)+)(?P<secondpart>[\w\d]+)'% length) # noqa
elif format == 'individual_words': elif format == 'individual_words':
intextmatch = re.compile(u'(?!<)(?P<firstpart>[^\W\-]+)(-|)\s*(?P<secondpart>\w+)(?![^<]*?>)') intextmatch = re.compile(u'(?!<)(?P<firstpart>[^\W\-]+)(-|)\s*(?P<secondpart>\w+)(?![^<]*?>)')
elif format == 'html_cleanup': elif format == 'html_cleanup':
intextmatch = re.compile(u'(?P<firstpart>[^\W\-]+)(-|)\s*(?=<)(?P<wraptags></span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*(?P<secondpart>[\w\d]+)') intextmatch = re.compile(u'(?P<firstpart>[^\W\-]+)(-|)\s*(?=<)(?P<wraptags></span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*(?P<secondpart>[\w\d]+)') # noqa
elif format == 'txt_cleanup': elif format == 'txt_cleanup':
intextmatch = re.compile(u'(?P<firstpart>[^\W\-]+)(-|)(?P<wraptags>\s+)(?P<secondpart>[\w\d]+)') intextmatch = re.compile(u'(?P<firstpart>[^\W\-]+)(-|)(?P<wraptags>\s+)(?P<secondpart>[\w\d]+)')
html = intextmatch.sub(self.dehyphenate, html) html = intextmatch.sub(self.dehyphenate, html)
return html return html
@ -498,9 +497,11 @@ class HTMLPreProcessor(object):
(re.compile('<span[^><]*?id=subtitle[^><]*?>(.*?)</span>', re.IGNORECASE|re.DOTALL), (re.compile('<span[^><]*?id=subtitle[^><]*?>(.*?)</span>', re.IGNORECASE|re.DOTALL),
lambda match : '<h3 class="subtitle">%s</h3>'%(match.group(1),)), lambda match : '<h3 class="subtitle">%s</h3>'%(match.group(1),)),
] ]
def __init__(self, log=None, extra_opts=None): def __init__(self, log=None, extra_opts=None, regex_wizard_callback=None):
self.log = log self.log = log
self.extra_opts = extra_opts self.extra_opts = extra_opts
self.regex_wizard_callback = regex_wizard_callback
self.current_href = None
def is_baen(self, src): def is_baen(self, src):
return re.compile(r'<meta\s+name="Publisher"\s+content=".*?Baen.*?"', return re.compile(r'<meta\s+name="Publisher"\s+content=".*?Baen.*?"',
@ -581,12 +582,15 @@ class HTMLPreProcessor(object):
end_rules.append((re.compile(u'(?<=.{%i}[–—])\s*<p>\s*(?=[[a-z\d])' % length), lambda match: '')) end_rules.append((re.compile(u'(?<=.{%i}[–—])\s*<p>\s*(?=[[a-z\d])' % length), lambda match: ''))
end_rules.append( end_rules.append(
# Un wrap using punctuation # Un wrap using punctuation
(re.compile(u'(?<=.{%i}([a-zäëïöüàèìòùáćéíĺóŕńśúýâêîôûçąężıãõñæøþðßěľščťžňďřů,:)\IA\u00DF]|(?<!\&\w{4});))\s*(?P<ital></(i|b|u)>)?\s*(</p>\s*<p>\s*)+\s*(?=(<(i|b|u)>)?\s*[\w\d$(])' % length, re.UNICODE), wrap_lines), (re.compile(u'(?<=.{%i}([a-zäëïöüàèìòùáćéíĺóŕńśúýâêîôûçąężıãõñæøþðßěľščťžňďřů,:)\IA\u00DF]|(?<!\&\w{4});))\s*(?P<ital></(i|b|u)>)?\s*(</p>\s*<p>\s*)+\s*(?=(<(i|b|u)>)?\s*[\w\d$(])' % length, re.UNICODE), wrap_lines), # noqa
) )
for rule in self.PREPROCESS + start_rules: for rule in self.PREPROCESS + start_rules:
html = rule[0].sub(rule[1], html) html = rule[0].sub(rule[1], html)
if self.regex_wizard_callback is not None:
self.regex_wizard_callback(self.current_href, html)
if get_preprocess_html: if get_preprocess_html:
return html return html

View File

@ -175,6 +175,20 @@ def read_shd(parent, dest):
if val: if val:
ans = simple_color(val, auto='transparent') ans = simple_color(val, auto='transparent')
setattr(dest, 'background_color', ans) setattr(dest, 'background_color', ans)
def read_numbering(parent, dest):
lvl = num_id = None
for np in XPath('./w:numPr')(parent):
for ilvl in XPath('./w:ilvl[@w:val]')(np):
try:
lvl = int(get(ilvl, 'w:val'))
except (ValueError, TypeError):
pass
for num in XPath('./w:numId[@w:val]')(np):
num_id = get(num, 'w:val')
val = (num_id, lvl) if num_id is not None or lvl is not None else inherit
setattr(dest, 'numbering', val)
# }}} # }}}
class ParagraphStyle(object): class ParagraphStyle(object):
@ -194,6 +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',
) )
def __init__(self, pPr=None): def __init__(self, pPr=None):
@ -210,7 +225,7 @@ class ParagraphStyle(object):
): ):
setattr(self, p, binary_property(pPr, p)) setattr(self, p, binary_property(pPr, p))
for x in ('border', 'indent', 'justification', 'spacing', 'direction', 'shd'): for x in ('border', 'indent', 'justification', 'spacing', 'direction', 'shd', 'numbering'):
f = globals()['read_%s' % x] f = globals()['read_%s' % x]
f(pPr, self) f(pPr, self)

View File

@ -0,0 +1,37 @@
#!/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 sys, os, shutil
from lxml import etree
from calibre import walk
from calibre.utils.zipfile import ZipFile
def dump(path):
dest = os.path.splitext(os.path.basename(path))[0]
dest += '_extracted'
if os.path.exists(dest):
shutil.rmtree(dest)
with ZipFile(path) as zf:
zf.extractall(dest)
for f in walk(dest):
if f.endswith('.xml'):
with open(f, 'r+b') as stream:
raw = stream.read()
root = etree.fromstring(raw)
stream.seek(0)
stream.truncate()
stream.write(etree.tostring(root, pretty_print=True, encoding='utf-8', xml_declaration=True))
print (path, 'dumped to', dest)
if __name__ == '__main__':
dump(sys.argv[-1])

View File

@ -45,8 +45,13 @@ namespaces = {
'dcterms': 'http://purl.org/dc/terms/' 'dcterms': 'http://purl.org/dc/terms/'
} }
xpath_cache = {}
def XPath(expr): def XPath(expr):
return X(expr, namespaces=namespaces) ans = xpath_cache.get(expr, None)
if ans is None:
xpath_cache[expr] = ans = X(expr, namespaces=namespaces)
return ans
def is_tag(x, q): def is_tag(x, q):
tag = getattr(x, 'tag', x) tag = getattr(x, 'tag', x)

View File

@ -6,6 +6,11 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from collections import Counter
from lxml.html.builder import OL, UL, SPAN
from calibre.ebooks.docx.block_styles import ParagraphStyle from calibre.ebooks.docx.block_styles import ParagraphStyle
from calibre.ebooks.docx.char_styles import RunStyle from calibre.ebooks.docx.char_styles import RunStyle
from calibre.ebooks.docx.names import XPath, get from calibre.ebooks.docx.names import XPath, get
@ -33,10 +38,26 @@ class Level(object):
self.fmt = 'decimal' self.fmt = 'decimal'
self.para_link = None self.para_link = None
self.paragraph_style = self.character_style = None self.paragraph_style = self.character_style = None
self.is_numbered = False
self.num_template = None
if lvl is not None: if lvl is not None:
self.read_from_xml(lvl) self.read_from_xml(lvl)
def copy(self):
ans = Level()
for x in ('restart', 'start', 'fmt', 'para_link', 'paragraph_style', 'character_style', 'is_numbered', 'num_template'):
setattr(ans, x, getattr(self, x))
return ans
def format_template(self, counter, ilvl):
def sub(m):
x = int(m.group(1)) - 1
if x > ilvl or x not in counter:
return ''
return '%d' % (counter[x] - (0 if x == ilvl else 1))
return re.sub(r'%(\d+)', sub, self.num_template).rstrip() + '\xa0'
def read_from_xml(self, lvl, override=False): def read_from_xml(self, lvl, override=False):
for lr in XPath('./w:lvlRestart[@w:val]')(lvl): for lr in XPath('./w:lvlRestart[@w:val]')(lvl):
try: try:
@ -57,9 +78,13 @@ class Level(object):
for lr in XPath('./w:numFmt[@w:val]')(lvl): for lr in XPath('./w:numFmt[@w:val]')(lvl):
val = get(lr, 'w:val') val = get(lr, 'w:val')
if val == 'bullet': if val == 'bullet':
self.is_numbered = False
self.fmt = {'\uf0a7':'square', 'o':'circle'}.get(lt, 'disc') self.fmt = {'\uf0a7':'square', 'o':'circle'}.get(lt, 'disc')
else: else:
self.is_numbered = True
self.fmt = STYLE_MAP.get(val, 'decimal') self.fmt = STYLE_MAP.get(val, 'decimal')
if lt and re.match(r'%\d+\.$', lt) is None:
self.num_template = lt
for lr in XPath('./w:pStyle[@w:val]')(lvl): for lr in XPath('./w:pStyle[@w:val]')(lvl):
self.para_link = get(lr, 'w:val') self.para_link = get(lr, 'w:val')
@ -78,12 +103,6 @@ class Level(object):
else: else:
self.character_style.update(ps) self.character_style.update(ps)
def copy(self):
ans = Level()
for x in ('restart', 'start', 'fmt', 'para_link', 'paragraph_style', 'character_style'):
setattr(ans, x, getattr(self, x))
return ans
class NumberingDefinition(object): class NumberingDefinition(object):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -107,6 +126,7 @@ class Numbering(object):
def __init__(self): def __init__(self):
self.definitions = {} self.definitions = {}
self.instances = {} self.instances = {}
self.counters = {}
def __call__(self, root, styles): def __call__(self, root, styles):
' Read all numbering style definitions ' ' Read all numbering style definitions '
@ -131,6 +151,7 @@ class Numbering(object):
if alvl is None: if alvl is None:
alvl = Level() alvl = Level()
alvl.read_from_xml(lvl, override=True) alvl.read_from_xml(lvl, override=True)
return nd
next_pass = {} next_pass = {}
for n in XPath('./w:num[@w:numId]')(root): for n in XPath('./w:num[@w:numId]')(root):
@ -154,3 +175,114 @@ class Numbering(object):
if d is not None: if d is not None:
self.instances[num_id] = create_instance(n, d) self.instances[num_id] = create_instance(n, d)
for num_id, d in self.instances.iteritems():
self.counters[num_id] = Counter({lvl:d.levels[lvl].start for lvl in d.levels})
def get_pstyle(self, num_id, style_id):
d = self.instances.get(num_id, None)
if d is not None:
for ilvl, lvl in d.levels.iteritems():
if lvl.para_link == style_id:
return ilvl
def get_para_style(self, num_id, lvl):
d = self.instances.get(num_id, None)
if d is not None:
lvl = d.levels.get(lvl, None)
return getattr(lvl, 'paragraph_style', None)
def update_counter(self, counter, levelnum, levels):
counter[levelnum] += 1
for ilvl, lvl in levels.iteritems():
restart = lvl.restart
if (restart is None and ilvl == levelnum + 1) or restart == levelnum + 1:
counter[ilvl] = lvl.start
def apply_markup(self, items, body, styles, object_map):
for p, num_id, ilvl in items:
d = self.instances.get(num_id, None)
if d is not None:
lvl = d.levels.get(ilvl, None)
if lvl is not None:
counter = self.counters[num_id]
p.tag = 'li'
p.set('value', '%s' % counter[ilvl])
p.set('list-lvl', str(ilvl))
p.set('list-id', num_id)
if lvl.num_template is not None:
val = lvl.format_template(counter, ilvl)
p.set('list-template', val)
self.update_counter(counter, ilvl, d.levels)
def commit(current_run):
if not current_run:
return
start = current_run[0]
parent = start.getparent()
idx = parent.index(start)
d = self.instances[start.get('list-id')]
ilvl = int(start.get('list-lvl'))
lvl = d.levels[ilvl]
lvlid = start.get('list-id') + start.get('list-lvl')
wrap = (OL if lvl.is_numbered else UL)('\n\t')
has_template = 'list-template' in start.attrib
if has_template:
wrap.set('lvlid', lvlid)
else:
wrap.set('class', styles.register({'list-style-type': lvl.fmt}, 'list'))
parent.insert(idx, wrap)
last_val = None
for child in current_run:
wrap.append(child)
child.tail = '\n\t'
if has_template:
span = SPAN()
span.text = child.text
child.text = None
for gc in child:
span.append(gc)
child.append(span)
span = SPAN(child.get('list-template'))
child.insert(0, span)
for attr in ('list-lvl', 'list-id', 'list-template'):
child.attrib.pop(attr, None)
val = int(child.get('value'))
if last_val == val - 1 or wrap.tag == 'ul':
child.attrib.pop('value')
last_val = val
current_run[-1].tail = '\n'
del current_run[:]
parents = set()
for child in body.iterdescendants('li'):
parents.add(child.getparent())
for parent in parents:
current_run = []
for child in parent:
if child.tag == 'li':
if current_run:
last = current_run[-1]
if (last.get('list-id') , last.get('list-lvl')) != (child.get('list-id'), child.get('list-lvl')):
commit(current_run)
current_run.append(child)
else:
commit(current_run)
commit(current_run)
for wrap in body.xpath('//ol[@lvlid]'):
wrap.attrib.pop('lvlid')
wrap.tag = 'div'
for i, li in enumerate(wrap.iterchildren('li')):
li.tag = 'div'
li.attrib.pop('value', None)
li.set('style', 'display:table-row')
obj = object_map[li]
bs = styles.para_cache[obj]
if i == 0:
wrap.set('style', 'display:table; margin-left: %s' % (bs.css.get('margin-left', 0)))
bs.css.pop('margin-left', None)
for child in li:
child.set('style', 'display:table-cell')

View File

@ -198,8 +198,19 @@ class Styles(object):
if default_para.character_style is not None: if default_para.character_style is not None:
self.para_char_cache[p] = default_para.character_style self.para_char_cache[p] = default_para.character_style
is_numbering = direct_formatting.numbering is not inherit
if is_numbering:
num_id, lvl = direct_formatting.numbering
if num_id is not None:
p.set('calibre_num_id', '%s:%s' % (lvl, num_id))
if num_id is not None and lvl is not None:
ps = self.numbering.get_para_style(num_id, lvl)
if ps is not None:
parent_styles.append(ps)
for attr in ans.all_properties: for attr in ans.all_properties:
setattr(ans, attr, self.para_val(parent_styles, direct_formatting, attr)) if not (is_numbering and attr == 'text_indent'): # skip text-indent for lists
setattr(ans, attr, self.para_val(parent_styles, direct_formatting, attr))
return ans return ans
def resolve_run(self, r): def resolve_run(self, r):
@ -244,10 +255,20 @@ class Styles(object):
return self.resolve_run(obj) return self.resolve_run(obj)
def resolve_numbering(self, numbering): def resolve_numbering(self, numbering):
pass # TODO: Implement this # When a numPr element appears inside a paragraph style, the lvl info
# must be discarder and pStyle used instead.
self.numbering = numbering
for style in self:
ps = style.paragraph_style
if ps is not None and ps.numbering is not inherit:
lvl = numbering.get_pstyle(ps.numbering[0], style.style_id)
if lvl is None:
ps.numbering = inherit
else:
ps.numbering = (ps.numbering[0], lvl)
def register(self, css, prefix): def register(self, css, prefix):
h = hash(tuple(css.iteritems())) h = hash(frozenset(css.iteritems()))
ans, _ = self.classes.get(h, (None, None)) ans, _ = self.classes.get(h, (None, None))
if ans is None: if ans is None:
self.counter[prefix] += 1 self.counter[prefix] += 1
@ -266,13 +287,15 @@ class Styles(object):
self.register(css, 'text') self.register(css, 'text')
def class_name(self, css): def class_name(self, css):
h = hash(tuple(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):
prefix = textwrap.dedent( prefix = textwrap.dedent(
'''\ '''\
p { margin: 0; padding: 0; text-indent: 1.5em } p { text-indent: 1.5em }
ul, ol, p { margin: 0; padding: 0 }
''') ''')
ans = [] ans = []

View File

@ -7,6 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, re import sys, os, re
from collections import OrderedDict
from lxml import html from lxml import html
from lxml.html.builder import ( from lxml.html.builder import (
@ -36,7 +37,7 @@ class Convert(object):
self.mi = self.docx.metadata self.mi = self.docx.metadata
self.body = BODY() self.body = BODY()
self.styles = Styles() self.styles = Styles()
self.object_map = {} self.object_map = OrderedDict()
self.html = HTML( self.html = HTML(
HEAD( HEAD(
META(charset='utf-8'), META(charset='utf-8'),
@ -72,6 +73,19 @@ class Convert(object):
pass # TODO: Last section properties pass # TODO: Last section properties
else: else:
self.log.debug('Unknown top-level tag: %s, ignoring' % barename(top_level.tag)) self.log.debug('Unknown top-level tag: %s, ignoring' % barename(top_level.tag))
numbered = []
for html_obj, obj in self.object_map.iteritems():
raw = obj.get('calibre_num_id', None)
if raw is not None:
lvl, num_id = raw.partition(':')[0::2]
try:
lvl = int(lvl)
except (TypeError, ValueError):
lvl = 0
numbered.append((html_obj, num_id, lvl))
self.numbering.apply_markup(numbered, self.body, self.styles, self.object_map)
if len(self.body) > 0: if len(self.body) > 0:
self.body.text = '\n\t' self.body.text = '\n\t'
for child in self.body: for child in self.body:
@ -102,7 +116,7 @@ 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')
numbering = Numbering() numbering = self.numbering = Numbering()
if sname is not None: if sname is not None:
try: try:
@ -133,6 +147,7 @@ class Convert(object):
def convert_p(self, p): def convert_p(self, p):
dest = P() dest = P()
self.object_map[dest] = p
style = self.styles.resolve_paragraph(p) style = self.styles.resolve_paragraph(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)
@ -173,7 +188,6 @@ class Convert(object):
wrapper = self.wrap_elems(spans, SPAN()) wrapper = self.wrap_elems(spans, SPAN())
wrapper.set('class', cls) wrapper.set('class', cls)
self.object_map[dest] = p
return dest return dest
def wrap_elems(self, elems, wrapper): def wrap_elems(self, elems, wrapper):
@ -188,7 +202,7 @@ class Convert(object):
def convert_run(self, run): def convert_run(self, run):
ans = SPAN() ans = SPAN()
ans.run = run self.object_map[ans] = run
text = Text(ans, 'text', []) text = Text(ans, 'text', [])
for child in run: for child in run:
@ -224,7 +238,6 @@ class Convert(object):
ans.tag = 'sub' if style.vert_align == 'subscript' else 'sup' ans.tag = 'sub' if style.vert_align == 'subscript' else 'sup'
if style.lang is not inherit: if style.lang is not inherit:
ans.lang = style.lang ans.lang = style.lang
self.object_map[ans] = run
return ans return ans
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -163,7 +163,8 @@ class MOBIFile(object):
ext = 'dat' ext = 'dat'
prefix = 'binary' prefix = 'binary'
suffix = '' suffix = ''
if sig in {b'HUFF', b'CDIC', b'INDX'}: continue if sig in {b'HUFF', b'CDIC', b'INDX'}:
continue
# TODO: Ignore CNCX records as well # TODO: Ignore CNCX records as well
if sig == b'FONT': if sig == b'FONT':
font = read_font_record(rec.raw) font = read_font_record(rec.raw)
@ -196,7 +197,6 @@ class MOBIFile(object):
vals = list(index)[:-1] + [None, None, None, None] vals = list(index)[:-1] + [None, None, None, None]
entry_map.append(Entry(*(vals[:12]))) entry_map.append(Entry(*(vals[:12])))
indexing_data = collect_indexing_data(entry_map, list(map(len, indexing_data = collect_indexing_data(entry_map, list(map(len,
self.text_records))) self.text_records)))
self.indexing_data = [DOC + '\n' +textwrap.dedent('''\ self.indexing_data = [DOC + '\n' +textwrap.dedent('''\

View File

@ -16,7 +16,8 @@ from calibre.ebooks.oeb.transforms.flatcss import KeyMapper
from calibre.utils.magick.draw import identify_data from calibre.utils.magick.draw import identify_data
MBP_NS = 'http://mobipocket.com/ns/mbp' MBP_NS = 'http://mobipocket.com/ns/mbp'
def MBP(name): return '{%s}%s' % (MBP_NS, name) def MBP(name):
return '{%s}%s' % (MBP_NS, name)
MOBI_NSMAP = {None: XHTML_NS, 'mbp': MBP_NS} MOBI_NSMAP = {None: XHTML_NS, 'mbp': MBP_NS}
@ -413,7 +414,7 @@ class MobiMLizer(object):
# img sizes in units other than px # img sizes in units other than px
# See #7520 for test case # See #7520 for test case
try: try:
pixs = int(round(float(value) / \ pixs = int(round(float(value) /
(72./self.profile.dpi))) (72./self.profile.dpi)))
except: except:
continue continue
@ -488,8 +489,6 @@ class MobiMLizer(object):
if elem.text: if elem.text:
if istate.preserve: if istate.preserve:
text = elem.text text = elem.text
elif len(elem) > 0 and isspace(elem.text):
text = None
else: else:
text = COLLAPSE.sub(' ', elem.text) text = COLLAPSE.sub(' ', elem.text)
valign = style['vertical-align'] valign = style['vertical-align']

View File

@ -181,9 +181,9 @@ class BookHeader(object):
self.codec = 'cp1252' if not user_encoding else user_encoding self.codec = 'cp1252' if not user_encoding else user_encoding
log.warn('Unknown codepage %d. Assuming %s' % (self.codepage, log.warn('Unknown codepage %d. Assuming %s' % (self.codepage,
self.codec)) self.codec))
# Some KF8 files have header length == 256 (generated by kindlegen # Some KF8 files have header length == 264 (generated by kindlegen
# 2.7?). See https://bugs.launchpad.net/bugs/1067310 # 2.9?). See https://bugs.launchpad.net/bugs/1179144
max_header_length = 0x100 max_header_length = 0x108
if (ident == 'TEXTREAD' or self.length < 0xE4 or if (ident == 'TEXTREAD' or self.length < 0xE4 or
self.length > max_header_length or self.length > max_header_length or

View File

@ -112,7 +112,7 @@ def update_flow_links(mobi8_reader, resource_map, log):
url_css_index_pattern = re.compile(r'''kindle:flow:([0-9|A-V]+)\?mime=text/css[^\)]*''', re.IGNORECASE) url_css_index_pattern = re.compile(r'''kindle:flow:([0-9|A-V]+)\?mime=text/css[^\)]*''', re.IGNORECASE)
for flow in mr.flows: for flow in mr.flows:
if flow is None: # 0th flow is None if flow is None: # 0th flow is None
flows.append(flow) flows.append(flow)
continue continue
@ -330,7 +330,7 @@ def expand_mobi8_markup(mobi8_reader, resource_map, log):
mobi8_reader.flows = flows mobi8_reader.flows = flows
# write out the parts and file flows # write out the parts and file flows
os.mkdir('text') # directory containing all parts os.mkdir('text') # directory containing all parts
spine = [] spine = []
for i, part in enumerate(parts): for i, part in enumerate(parts):
pi = mobi8_reader.partinfo[i] pi = mobi8_reader.partinfo[i]

View File

@ -871,6 +871,7 @@ class Manifest(object):
orig_data = data orig_data = data
fname = urlunquote(self.href) fname = urlunquote(self.href)
self.oeb.log.debug('Parsing', fname, '...') self.oeb.log.debug('Parsing', fname, '...')
self.oeb.html_preprocessor.current_href = self.href
try: try:
data = parse_html(data, log=self.oeb.log, data = parse_html(data, log=self.oeb.log,
decoder=self.oeb.decode, decoder=self.oeb.decode,
@ -1312,9 +1313,9 @@ class Guide(object):
('notes', __('Notes')), ('notes', __('Notes')),
('preface', __('Preface')), ('preface', __('Preface')),
('text', __('Main Text'))] ('text', __('Main Text'))]
TYPES = set(t for t, _ in _TYPES_TITLES) TYPES = set(t for t, _ in _TYPES_TITLES) # noqa
TITLES = dict(_TYPES_TITLES) TITLES = dict(_TYPES_TITLES)
ORDER = dict((t, i) for i, (t, _) in enumerate(_TYPES_TITLES)) ORDER = dict((t, i) for i, (t, _) in enumerate(_TYPES_TITLES)) # noqa
def __init__(self, oeb, type, title, href): def __init__(self, oeb, type, title, href):
self.oeb = oeb self.oeb = oeb

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, re import sys, os, re
from calibre.customize.ui import available_input_formats from calibre.customize.ui import available_input_formats
@ -26,17 +26,18 @@ def EbookIterator(*args, **kwargs):
from calibre.ebooks.oeb.iterator.book import EbookIterator from calibre.ebooks.oeb.iterator.book import EbookIterator
return EbookIterator(*args, **kwargs) return EbookIterator(*args, **kwargs)
def get_preprocess_html(path_to_ebook, output): def get_preprocess_html(path_to_ebook, output=None):
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ebooks.conversion.plumber import set_regex_wizard_callback, Plumber
iterator = EbookIterator(path_to_ebook) from calibre.utils.logging import DevNull
iterator.__enter__(only_input_plugin=True, run_char_count=False, from calibre.ptempfile import TemporaryDirectory
read_anchor_map=False) raw = {}
preprocessor = HTMLPreProcessor(None, False) set_regex_wizard_callback(raw.__setitem__)
with open(output, 'wb') as out: with TemporaryDirectory('_regex_wiz') as tdir:
for path in iterator.spine: pl = Plumber(path_to_ebook, os.path.join(tdir, 'a.epub'), DevNull(), for_regex_wizard=True)
with open(path, 'rb') as f: pl.run()
html = f.read().decode('utf-8', 'replace') items = [raw[item.href] for item in pl.oeb.spine if item.href in raw]
html = preprocessor(html, get_preprocess_html=True)
with (sys.stdout if output is None else open(output, 'wb')) as out:
for html in items:
out.write(html.encode('utf-8')) out.write(html.encode('utf-8'))
out.write(b'\n\n' + b'-'*80 + b'\n\n') out.write(b'\n\n' + b'-'*80 + b'\n\n')

View File

@ -25,7 +25,7 @@ from calibre.ebooks.oeb.transforms.cover import CoverManager
from calibre.ebooks.oeb.iterator.spine import (SpineItem, create_indexing_data) from calibre.ebooks.oeb.iterator.spine import (SpineItem, create_indexing_data)
from calibre.ebooks.oeb.iterator.bookmarks import BookmarksMixin from calibre.ebooks.oeb.iterator.bookmarks import BookmarksMixin
TITLEPAGE = CoverManager.SVG_TEMPLATE.decode('utf-8').replace(\ TITLEPAGE = CoverManager.SVG_TEMPLATE.decode('utf-8').replace(
'__ar__', 'none').replace('__viewbox__', '0 0 600 800' '__ar__', 'none').replace('__viewbox__', '0 0 600 800'
).replace('__width__', '600').replace('__height__', '800') ).replace('__width__', '600').replace('__height__', '800')

View File

@ -44,8 +44,10 @@ META_XP = XPath('/h:html/h:head/h:meta[@http-equiv="Content-Type"]')
def merge_multiple_html_heads_and_bodies(root, log=None): def merge_multiple_html_heads_and_bodies(root, log=None):
heads, bodies = xpath(root, '//h:head'), xpath(root, '//h:body') heads, bodies = xpath(root, '//h:head'), xpath(root, '//h:body')
if not (len(heads) > 1 or len(bodies) > 1): return root if not (len(heads) > 1 or len(bodies) > 1):
for child in root: root.remove(child) return root
for child in root:
root.remove(child)
head = root.makeelement(XHTML('head')) head = root.makeelement(XHTML('head'))
body = root.makeelement(XHTML('body')) body = root.makeelement(XHTML('body'))
for h in heads: for h in heads:
@ -88,7 +90,7 @@ def html5_parse(data, max_nesting_depth=100):
# Check that the asinine HTML 5 algorithm did not result in a tree with # Check that the asinine HTML 5 algorithm did not result in a tree with
# insane nesting depths # insane nesting depths
for x in data.iterdescendants(): for x in data.iterdescendants():
if isinstance(x.tag, basestring) and len(x) is 0: # Leaf node if isinstance(x.tag, basestring) and len(x) is 0: # Leaf node
depth = node_depth(x) depth = node_depth(x)
if depth > max_nesting_depth: if depth > max_nesting_depth:
raise ValueError('html5lib resulted in a tree with nesting' raise ValueError('html5lib resulted in a tree with nesting'
@ -228,7 +230,7 @@ def parse_html(data, log=None, decoder=None, preprocessor=None,
if idx > -1: if idx > -1:
pre = data[:idx] pre = data[:idx]
data = data[idx:] data = data[idx:]
if '<!DOCTYPE' in pre: # Handle user defined entities if '<!DOCTYPE' in pre: # Handle user defined entities
user_entities = {} user_entities = {}
for match in re.finditer(r'<!ENTITY\s+(\S+)\s+([^>]+)', pre): for match in re.finditer(r'<!ENTITY\s+(\S+)\s+([^>]+)', pre):
val = match.group(2) val = match.group(2)
@ -368,8 +370,7 @@ def parse_html(data, log=None, decoder=None, preprocessor=None,
meta.getparent().remove(meta) meta.getparent().remove(meta)
meta = etree.SubElement(head, XHTML('meta'), meta = etree.SubElement(head, XHTML('meta'),
attrib={'http-equiv': 'Content-Type'}) attrib={'http-equiv': 'Content-Type'})
meta.set('content', 'text/html; charset=utf-8') # Ensure content is second meta.set('content', 'text/html; charset=utf-8') # Ensure content is second attribute
# attribute
# Ensure has a <body/> # Ensure has a <body/>
if not xpath(data, '/h:html/h:body'): if not xpath(data, '/h:html/h:body'):

View File

@ -45,11 +45,15 @@ class Links(object):
href, page, rect = link href, page, rect = link
p, frag = href.partition('#')[0::2] p, frag = href.partition('#')[0::2]
try: try:
link = ((path, p, frag or None), self.pdf.get_pageref(page).obj, Array(rect)) pref = self.pdf.get_pageref(page).obj
except IndexError: except IndexError:
self.log.warn('Unable to find page for link: %r, ignoring it' % link) try:
continue pref = self.pdf.get_pageref(page-1).obj
self.links.append(link) except IndexError:
self.pdf.debug('Unable to find page for link: %r, ignoring it' % link)
continue
self.pdf.debug('The link %s points to non-existent page, moving it one page back' % href)
self.links.append(((path, p, frag or None), pref, Array(rect)))
def add_links(self): def add_links(self):
for link in self.links: for link in self.links:

View File

@ -122,7 +122,8 @@ def device_name_for_plugboards(device_class):
class DeviceManager(Thread): # {{{ class DeviceManager(Thread): # {{{
def __init__(self, connected_slot, job_manager, open_feedback_slot, def __init__(self, connected_slot, job_manager, open_feedback_slot,
open_feedback_msg, allow_connect_slot, sleep_time=2): open_feedback_msg, allow_connect_slot,
after_callback_feedback_slot, sleep_time=2):
''' '''
:sleep_time: Time to sleep between device probes in secs :sleep_time: Time to sleep between device probes in secs
''' '''
@ -150,6 +151,7 @@ class DeviceManager(Thread): # {{{
self.ejected_devices = set([]) self.ejected_devices = set([])
self.mount_connection_requests = Queue.Queue(0) self.mount_connection_requests = Queue.Queue(0)
self.open_feedback_slot = open_feedback_slot self.open_feedback_slot = open_feedback_slot
self.after_callback_feedback_slot = after_callback_feedback_slot
self.open_feedback_msg = open_feedback_msg self.open_feedback_msg = open_feedback_msg
self._device_information = None self._device_information = None
self.current_library_uuid = None self.current_library_uuid = None
@ -392,6 +394,10 @@ class DeviceManager(Thread): # {{{
self.device.set_progress_reporter(job.report_progress) self.device.set_progress_reporter(job.report_progress)
self.current_job.run() self.current_job.run()
self.current_job = None self.current_job = None
feedback = getattr(self.device, 'user_feedback_after_callback', None)
if feedback is not None:
self.device.user_feedback_after_callback = None
self.after_callback_feedback_slot(feedback)
else: else:
break break
if do_sleep: if do_sleep:
@ -850,7 +856,7 @@ class DeviceMixin(object): # {{{
self.device_manager = DeviceManager(FunctionDispatcher(self.device_detected), self.device_manager = DeviceManager(FunctionDispatcher(self.device_detected),
self.job_manager, Dispatcher(self.status_bar.show_message), self.job_manager, Dispatcher(self.status_bar.show_message),
Dispatcher(self.show_open_feedback), Dispatcher(self.show_open_feedback),
FunctionDispatcher(self.allow_connect)) FunctionDispatcher(self.allow_connect), Dispatcher(self.after_callback_feedback))
self.device_manager.start() self.device_manager.start()
self.device_manager.devices_initialized.wait() self.device_manager.devices_initialized.wait()
if tweaks['auto_connect_to_folder']: if tweaks['auto_connect_to_folder']:
@ -862,6 +868,10 @@ class DeviceMixin(object): # {{{
name, show_copy_button=False, name, show_copy_button=False,
override_icon=QIcon(icon)) override_icon=QIcon(icon))
def after_callback_feedback(self, feedback):
title, msg, det_msg = feedback
info_dialog(self, feedback['title'], feedback['msg'], det_msg=feedback['det_msg']).show()
def debug_detection(self, done): def debug_detection(self, done):
self.debug_detection_callback = weakref.ref(done) self.debug_detection_callback = weakref.ref(done)
self.device_manager.debug_detection(FunctionDispatcher(self.debug_detection_done)) self.device_manager.debug_detection(FunctionDispatcher(self.debug_detection_done))
@ -1116,7 +1126,7 @@ class DeviceMixin(object): # {{{
return return
dm = self.iactions['Remove Books'].delete_memory dm = self.iactions['Remove Books'].delete_memory
if dm.has_key(job): if job in dm:
paths, model = dm.pop(job) paths, model = dm.pop(job)
self.device_manager.remove_books_from_metadata(paths, self.device_manager.remove_books_from_metadata(paths,
self.booklists()) self.booklists())
@ -1141,7 +1151,7 @@ class DeviceMixin(object): # {{{
def dispatch_sync_event(self, dest, delete, specific): def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows() rows = self.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0: if not rows or len(rows) == 0:
error_dialog(self, _('No books'), _('No books')+' '+\ error_dialog(self, _('No books'), _('No books')+' '+
_('selected to send')).exec_() _('selected to send')).exec_()
return return
@ -1160,7 +1170,7 @@ class DeviceMixin(object): # {{{
if fmts: if fmts:
for f in fmts.split(','): for f in fmts.split(','):
f = f.lower() f = f.lower()
if format_count.has_key(f): if f in format_count:
format_count[f] += 1 format_count[f] += 1
else: else:
format_count[f] = 1 format_count[f] = 1

View File

@ -149,6 +149,9 @@ class Quickview(QDialog, Ui_Quickview):
key = self.view.model().column_map[self.current_column] key = self.view.model().column_map[self.current_column]
book_id = self.view.model().id(bv_row) book_id = self.view.model().id(bv_row)
if self.current_book_id == book_id and self.current_key == key:
return
# Only show items for categories # Only show items for categories
if not self.db.field_metadata[key]['is_category']: if not self.db.field_metadata[key]['is_category']:
if self.current_key is None: if self.current_key is None:
@ -203,8 +206,7 @@ class Quickview(QDialog, Ui_Quickview):
sv = selected_item sv = selected_item
sv = sv.replace('"', r'\"') sv = sv.replace('"', r'\"')
self.last_search = self.current_key+':"=' + sv + '"' self.last_search = self.current_key+':"=' + sv + '"'
books = self.db.search_getting_ids(self.last_search, books = self.db.search(self.last_search, return_matches=True)
self.db.data.search_restriction)
self.books_table.setRowCount(len(books)) self.books_table.setRowCount(len(books))
self.books_label.setText(_('Books with selected item "{0}": {1}'). self.books_label.setText(_('Books with selected item "{0}": {1}').

View File

@ -10,9 +10,9 @@ from functools import partial
from future_builtins import map from future_builtins import map
from collections import OrderedDict from collections import OrderedDict
from PyQt4.Qt import (QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, from PyQt4.Qt import (QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, QFont,
QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, QStyle,
QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect) QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect, QHeaderView, QStyleOptionHeader)
from calibre.gui2.library.delegates import (RatingDelegate, PubDateDelegate, from calibre.gui2.library.delegates import (RatingDelegate, PubDateDelegate,
TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate,
@ -25,7 +25,55 @@ from calibre.gui2.library import DEFAULT_SORT
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding
from calibre import force_unicode from calibre import force_unicode
class PreserveViewState(object): # {{{ class HeaderView(QHeaderView): # {{{
def __init__(self, *args):
QHeaderView.__init__(self, *args)
self.hover = -1
self.current_font = QFont(self.font())
self.current_font.setBold(True)
self.current_font.setItalic(True)
def event(self, e):
if e.type() in (e.HoverMove, e.HoverEnter):
self.hover = self.logicalIndexAt(e.pos())
elif e.type() in (e.Leave, e.HoverLeave):
self.hover = -1
return QHeaderView.event(self, e)
def paintSection(self, painter, rect, logical_index):
opt = QStyleOptionHeader()
self.initStyleOption(opt)
opt.rect = rect
opt.section = logical_index
opt.orientation = self.orientation()
opt.textAlignment = Qt.AlignHCenter | Qt.AlignVCenter
model = self.parent().model()
opt.text = model.headerData(logical_index, opt.orientation, Qt.DisplayRole).toString()
if self.isSortIndicatorShown() and self.sortIndicatorSection() == logical_index:
opt.sortIndicator = QStyleOptionHeader.SortDown if self.sortIndicatorOrder() == Qt.AscendingOrder else QStyleOptionHeader.SortUp
opt.text = opt.fontMetrics.elidedText(opt.text, Qt.ElideRight, rect.width() - 4)
if self.isEnabled():
opt.state |= QStyle.State_Enabled
if self.window().isActiveWindow():
opt.state |= QStyle.State_Active
if self.hover == logical_index:
opt.state |= QStyle.State_MouseOver
sm = self.selectionModel()
if opt.orientation == Qt.Vertical:
if sm.isRowSelected(logical_index, QModelIndex()):
opt.state |= QStyle.State_Sunken
painter.save()
if (
(opt.orientation == Qt.Horizontal and sm.currentIndex().column() == logical_index) or
(opt.orientation == Qt.Vertical and sm.currentIndex().row() == logical_index)):
painter.setFont(self.current_font)
self.style().drawControl(QStyle.CE_Header, opt, painter, self)
painter.restore()
# }}}
class PreserveViewState(object): # {{{
''' '''
Save the set of selected books at enter time. If at exit time there are no Save the set of selected books at enter time. If at exit time there are no
@ -72,13 +120,14 @@ class PreserveViewState(object): # {{{
return {x:getattr(self, x) for x in ('selected_ids', 'current_id', return {x:getattr(self, x) for x in ('selected_ids', 'current_id',
'vscroll', 'hscroll')} 'vscroll', 'hscroll')}
def fset(self, state): def fset(self, state):
for k, v in state.iteritems(): setattr(self, k, v) for k, v in state.iteritems():
setattr(self, k, v)
self.__exit__() self.__exit__()
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
# }}} # }}}
class BooksView(QTableView): # {{{ class BooksView(QTableView): # {{{
files_dropped = pyqtSignal(object) files_dropped = pyqtSignal(object)
add_column_signal = pyqtSignal() add_column_signal = pyqtSignal()
@ -90,6 +139,7 @@ class BooksView(QTableView): # {{{
def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True): def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
self.setProperty('highlight_current_item', 150)
self.row_sizing_done = False self.row_sizing_done = False
if not tweaks['horizontal_scrolling_per_column']: if not tweaks['horizontal_scrolling_per_column']:
@ -152,12 +202,16 @@ class BooksView(QTableView): # {{{
# {{{ Column Header setup # {{{ Column Header setup
self.can_add_columns = True self.can_add_columns = True
self.was_restored = False self.was_restored = False
self.column_header = self.horizontalHeader() self.column_header = HeaderView(Qt.Horizontal, self)
self.setHorizontalHeader(self.column_header)
self.column_header.setMovable(True) self.column_header.setMovable(True)
self.column_header.setClickable(True)
self.column_header.sectionMoved.connect(self.save_state) self.column_header.sectionMoved.connect(self.save_state)
self.column_header.setContextMenuPolicy(Qt.CustomContextMenu) self.column_header.setContextMenuPolicy(Qt.CustomContextMenu)
self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu) self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu)
self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection) self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection)
self.row_header = HeaderView(Qt.Vertical, self)
self.setVerticalHeader(self.row_header)
# }}} # }}}
self._model.database_changed.connect(self.database_changed) self._model.database_changed.connect(self.database_changed)
@ -235,7 +289,7 @@ class BooksView(QTableView): # {{{
ac.setCheckable(True) ac.setCheckable(True)
ac.setChecked(True) ac.setChecked(True)
if col not in ('ondevice', 'inlibrary') and \ if col not in ('ondevice', 'inlibrary') and \
(not self.model().is_custom_column(col) or \ (not self.model().is_custom_column(col) or
self.model().custom_columns[col]['datatype'] not in ('bool', self.model().custom_columns[col]['datatype'] not in ('bool',
)): )):
m = self.column_header_context_menu.addMenu( m = self.column_header_context_menu.addMenu(
@ -277,7 +331,6 @@ class BooksView(QTableView): # {{{
partial(self.column_header_context_handler, partial(self.column_header_context_handler,
action='show', column=col)) action='show', column=col))
self.column_header_context_menu.addSeparator() self.column_header_context_menu.addSeparator()
self.column_header_context_menu.addAction( self.column_header_context_menu.addAction(
_('Shrink column if it is too wide to fit'), _('Shrink column if it is too wide to fit'),
@ -366,7 +419,7 @@ class BooksView(QTableView): # {{{
h = self.column_header h = self.column_header
cm = self.column_map cm = self.column_map
state = {} state = {}
state['hidden_columns'] = [cm[i] for i in range(h.count()) state['hidden_columns'] = [cm[i] for i in range(h.count())
if h.isSectionHidden(i) and cm[i] != 'ondevice'] if h.isSectionHidden(i) and cm[i] != 'ondevice']
state['last_modified_injected'] = True state['last_modified_injected'] = True
state['languages_injected'] = True state['languages_injected'] = True
@ -514,7 +567,6 @@ class BooksView(QTableView): # {{{
db.prefs[name] = ans db.prefs[name] = ans
return ans return ans
def restore_state(self): def restore_state(self):
old_state = self.get_old_state() old_state = self.get_old_state()
if old_state is None: if old_state is None:
@ -837,7 +889,8 @@ class BooksView(QTableView): # {{{
ids = frozenset(ids) ids = frozenset(ids)
m = self.model() m = self.model()
for row in xrange(m.rowCount(QModelIndex())): for row in xrange(m.rowCount(QModelIndex())):
if len(row_map) >= len(ids): break if len(row_map) >= len(ids):
break
c = m.id(row) c = m.id(row)
if c in ids: if c in ids:
row_map[c] = row row_map[c] = row
@ -897,7 +950,8 @@ class BooksView(QTableView): # {{{
pass pass
return None return None
def fset(self, val): def fset(self, val):
if val is None: return if val is None:
return
m = self.model() m = self.model()
for row in xrange(m.rowCount(QModelIndex())): for row in xrange(m.rowCount(QModelIndex())):
if m.id(row) == val: if m.id(row) == val:
@ -919,7 +973,8 @@ class BooksView(QTableView): # {{{
column = ci.column() column = ci.column()
for i in xrange(ci.row()+1, self.row_count()): for i in xrange(ci.row()+1, self.row_count()):
if i in selected_rows: continue if i in selected_rows:
continue
try: try:
return self.model().id(self.model().index(i, column)) return self.model().id(self.model().index(i, column))
except: except:
@ -927,7 +982,8 @@ class BooksView(QTableView): # {{{
# No unselected rows after the current row, look before # No unselected rows after the current row, look before
for i in xrange(ci.row()-1, -1, -1): for i in xrange(ci.row()-1, -1, -1):
if i in selected_rows: continue if i in selected_rows:
continue
try: try:
return self.model().id(self.model().index(i, column)) return self.model().id(self.model().index(i, column))
except: except:
@ -975,7 +1031,7 @@ class BooksView(QTableView): # {{{
# }}} # }}}
class DeviceBooksView(BooksView): # {{{ class DeviceBooksView(BooksView): # {{{
def __init__(self, parent): def __init__(self, parent):
BooksView.__init__(self, parent, DeviceBooksModel, BooksView.__init__(self, parent, DeviceBooksModel,

View File

@ -964,6 +964,7 @@ Style::Style()
itsAnimateStep(0), itsAnimateStep(0),
itsTitlebarHeight(0), itsTitlebarHeight(0),
calibre_icon_map(QHash<int,QString>()), calibre_icon_map(QHash<int,QString>()),
calibre_item_view_focus(0),
is_kde_session(0), is_kde_session(0),
itsPos(-1, -1), itsPos(-1, -1),
itsHoverWidget(0L), itsHoverWidget(0L),
@ -3696,6 +3697,9 @@ bool Style::event(QEvent *event) {
++i; ++i;
} }
return true; return true;
} else if (e->propertyName() == QString("calibre_item_view_focus")) {
calibre_item_view_focus = property("calibre_item_view_focus").toInt();
return true;
} }
} }
return BASE_STYLE::event(event); return BASE_STYLE::event(event);
@ -4784,11 +4788,7 @@ void Style::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
if(widget && ::qobject_cast<const QGroupBox *>(widget)) if(widget && ::qobject_cast<const QGroupBox *>(widget))
r2.adjust(0, 2, 0, 0); r2.adjust(0, 2, 0, 0);
// Added by Kovid so that the highlight does not cover the text if(calibre_item_view_focus || FOCUS_STANDARD==opts.focus) // Changed by Kovid, as the underline focus does not work well in item views
if(widget && ::qobject_cast<const QListView *>(widget))
r2.adjust(0, 0, 0, 2);
if(FOCUS_STANDARD==opts.focus)
{ {
// Taken from QWindowsStyle... // Taken from QWindowsStyle...
painter->save(); painter->save();
@ -4803,10 +4803,11 @@ void Style::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
painter->setBrush(QBrush(patternCol, Qt::Dense4Pattern)); painter->setBrush(QBrush(patternCol, Qt::Dense4Pattern));
painter->setBrushOrigin(r.topLeft()); painter->setBrushOrigin(r.topLeft());
painter->setPen(Qt::NoPen); painter->setPen(Qt::NoPen);
painter->drawRect(r.left(), r.top(), r.width(), 1); // Top int fwidth = (calibre_item_view_focus > 1) ? 2 : 1;
painter->drawRect(r.left(), r.bottom(), r.width(), 1); // Bottom painter->drawRect(r.left(), r.top(), r.width(), fwidth); // Top
painter->drawRect(r.left(), r.top(), 1, r.height()); // Left painter->drawRect(r.left(), r.bottom(), r.width(), fwidth); // Bottom
painter->drawRect(r.right(), r.top(), 1, r.height()); // Right painter->drawRect(r.left(), r.top(), fwidth, r.height()); // Left
painter->drawRect(r.right(), r.top(), fwidth, r.height()); // Right
painter->restore(); painter->restore();
} }
else else
@ -5249,6 +5250,14 @@ void Style::drawPrimitive(PrimitiveElement element, const QStyleOption *option,
QColor color(hasCustomBackground && hasSolidBackground QColor color(hasCustomBackground && hasSolidBackground
? v4Opt->backgroundBrush.color() ? v4Opt->backgroundBrush.color()
: palette.color(cg, QPalette::Highlight)); : palette.color(cg, QPalette::Highlight));
if (state & State_HasFocus && widget && widget->property("highlight_current_item").toBool()) {
// Added by Kovid to highlight the current cell in the book list
if (color.lightness() > 128)
color = color.darker(widget->property("highlight_current_item").toInt());
else
color = color.lighter();
}
bool square((opts.square&SQUARE_LISTVIEW_SELECTION) && bool square((opts.square&SQUARE_LISTVIEW_SELECTION) &&
(/*(!widget && r.height()<=40 && r.width()>=48) || */ (/*(!widget && r.height()<=40 && r.width()>=48) || */
(widget && !widget->inherits("KFilePlacesView") && (widget && !widget->inherits("KFilePlacesView") &&

View File

@ -355,6 +355,7 @@ class Style : public QCommonStyle
mutable QList<int> itsMdiButtons[2]; // 0=left, 1=right mutable QList<int> itsMdiButtons[2]; // 0=left, 1=right
mutable int itsTitlebarHeight; mutable int itsTitlebarHeight;
QHash<int,QString> calibre_icon_map; QHash<int,QString> calibre_icon_map;
int calibre_item_view_focus;
bool is_kde_session; bool is_kde_session;
// Required for Q3Header hover... // Required for Q3Header hover...