Merge from trunk

This commit is contained in:
Charles Haley 2012-12-25 09:11:09 +01:00
commit 8e3ba79234
13 changed files with 603 additions and 326 deletions

View File

@ -6,7 +6,6 @@ www.nsfwcorp.com
''' '''
import urllib import urllib
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class NotSafeForWork(BasicNewsRecipe): class NotSafeForWork(BasicNewsRecipe):
@ -21,8 +20,9 @@ class NotSafeForWork(BasicNewsRecipe):
needs_subscription = True needs_subscription = True
auto_cleanup = False auto_cleanup = False
INDEX = 'https://www.nsfwcorp.com' INDEX = 'https://www.nsfwcorp.com'
LOGIN = INDEX + '/login' LOGIN = INDEX + '/login/target/'
use_embedded_content = False SETTINGS = INDEX + '/settings/'
use_embedded_content = True
language = 'en' language = 'en'
publication_type = 'magazine' publication_type = 'magazine'
masthead_url = 'http://assets.nsfwcorp.com/media/headers/nsfw_banner.jpg' masthead_url = 'http://assets.nsfwcorp.com/media/headers/nsfw_banner.jpg'
@ -46,15 +46,6 @@ class NotSafeForWork(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
remove_tags_before = dict(attrs={'id':'fromToLine'})
remove_tags_after = dict(attrs={'id':'unlockButtonDiv'})
remove_tags=[
dict(name=['meta', 'link', 'iframe', 'embed', 'object'])
,dict(name='a', attrs={'class':'switchToDeskNotes'})
,dict(attrs={'id':'unlockButtonDiv'})
]
remove_attributes = ['lang']
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open(self.LOGIN) br.open(self.LOGIN)
@ -65,30 +56,12 @@ class NotSafeForWork(BasicNewsRecipe):
br.open(self.LOGIN, data) br.open(self.LOGIN, data)
return br return br
def parse_index(self): def get_feeds(self):
articles = [] self.feeds = []
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.SETTINGS)
dispatches = soup.find(attrs={'id':'dispatches'}) for item in soup.findAll('input', attrs={'type':'text'}):
if dispatches: if item.has_key('value') and item['value'].startswith('http://www.nsfwcorp.com/feed/'):
for item in dispatches.findAll('h3'): self.feeds.append(item['value'])
description = u'' return self.feeds
title_link = item.find('span', attrs={'class':'dispatchTitle'}) return self.feeds
description_link = item.find('span', attrs={'class':'dispatchSubtitle'})
feed_link = item.find('a', href=True)
if feed_link:
url = self.INDEX + feed_link['href']
title = self.tag_to_string(title_link)
description = self.tag_to_string(description_link)
date = strftime(self.timefmt)
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
return [('Dispatches', articles)]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -1,27 +1,27 @@
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class PajamasMedia(BasicNewsRecipe): class PajamasMedia(BasicNewsRecipe):
title = u'Pajamas Media' title = u'Pajamas Media'
description = u'Provides exclusive news and opinion for forty countries.' description = u'Provides exclusive news and opinion for forty countries.'
language = 'en' language = 'en'
__author__ = 'Krittika Goyal' __author__ = 'Krittika Goyal'
oldest_article = 1 #days oldest_article = 2 #days
max_articles_per_feed = 25 max_articles_per_feed = 25
recursions = 1 recursions = 1
match_regexps = [r'http://pajamasmedia.com/blog/.*/2/$'] match_regexps = [r'http://pajamasmedia.com/blog/.*/2/$']
#encoding = 'latin1' #encoding = 'latin1'
remove_stylesheets = True remove_stylesheets = True
#remove_tags_before = dict(name='h1', attrs={'class':'heading'}) auto_cleanup = True
remove_tags_after = dict(name='div', attrs={'class':'paged-nav'}) ##remove_tags_before = dict(name='h1', attrs={'class':'heading'})
remove_tags = [ #remove_tags_after = dict(name='div', attrs={'class':'paged-nav'})
dict(name='iframe'), #remove_tags = [
dict(name='div', attrs={'class':['pages']}), #dict(name='iframe'),
#dict(name='div', attrs={'id':['bookmark']}), #dict(name='div', attrs={'class':['pages']}),
#dict(name='span', attrs={'class':['related_link', 'slideshowcontrols']}), ##dict(name='div', attrs={'id':['bookmark']}),
#dict(name='ul', attrs={'class':'articleTools'}), ##dict(name='span', attrs={'class':['related_link', 'slideshowcontrols']}),
] ##dict(name='ul', attrs={'class':'articleTools'}),
#]
feeds = [ feeds = [
('pajamas Media', ('pajamas Media',
@ -29,20 +29,20 @@ class PajamasMedia(BasicNewsRecipe):
] ]
def preprocess_html(self, soup): #def preprocess_html(self, soup):
story = soup.find(name='div', attrs={'id':'innerpage-content'}) #story = soup.find(name='div', attrs={'id':'innerpage-content'})
#td = heading.findParent(name='td') ##td = heading.findParent(name='td')
#td.extract() ##td.extract()
soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>') #soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
body = soup.find(name='body') #body = soup.find(name='body')
body.insert(0, story) #body.insert(0, story)
return soup #return soup
def postprocess_html(self, soup, first): #def postprocess_html(self, soup, first):
if not first: #if not first:
h = soup.find(attrs={'class':'innerpage-header'}) #h = soup.find(attrs={'class':'innerpage-header'})
if h: h.extract() #if h: h.extract()
auth = soup.find(attrs={'class':'author'}) #auth = soup.find(attrs={'class':'author'})
if auth: auth.extract() #if auth: auth.extract()
return soup #return soup

View File

@ -14,50 +14,32 @@ import os
from calibre.customize.conversion import OutputFormatPlugin, \ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.constants import iswindows
UNITS = [ UNITS = ['millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot',
'millimeter', 'cicero', 'devicepixel']
'point',
'inch' ,
'pica' ,
'didot',
'cicero',
'devicepixel',
]
PAPER_SIZES = ['b2', PAPER_SIZES = ['b2', 'b4', 'b5', 'b6', 'b0', 'b1', 'letter', 'b3', 'a3', 'a1',
'a9', 'a0', 'legal', 'a6', 'a2', 'a5', 'a4']
'executive',
'tabloid',
'b4',
'b5',
'b6',
'b7',
'b0',
'b1',
'letter',
'b3',
'a7',
'a8',
'b8',
'b9',
'a3',
'a1',
'folio',
'c5e',
'dle',
'a0',
'ledger',
'legal',
'a6',
'a2',
'b10',
'a5',
'comm10e',
'a4']
ORIENTATIONS = ['portrait', 'landscape'] class PDFMetadata(object): # {{{
def __init__(self, oeb_metadata=None):
from calibre import force_unicode
from calibre.ebooks.metadata import authors_to_string
self.title = _(u'Unknown')
self.author = _(u'Unknown')
self.tags = u''
if oeb_metadata != None:
if len(oeb_metadata.title) >= 1:
self.title = oeb_metadata.title[0].value
if len(oeb_metadata.creator) >= 1:
self.author = authors_to_string([x.value for x in oeb_metadata.creator])
if oeb_metadata.subject:
self.tags = u', '.join(map(unicode, oeb_metadata.subject))
self.title = force_unicode(self.title)
self.author = force_unicode(self.author)
# }}}
class PDFOutput(OutputFormatPlugin): class PDFOutput(OutputFormatPlugin):
@ -66,9 +48,14 @@ class PDFOutput(OutputFormatPlugin):
file_type = 'pdf' file_type = 'pdf'
options = set([ options = set([
OptionRecommendation(name='override_profile_size', recommended_value=False,
help=_('Normally, the PDF page size is set by the output profile'
' chosen under page options. This option will cause the '
' page size settings under PDF Output to override the '
' size specified by the output profile.')),
OptionRecommendation(name='unit', recommended_value='inch', OptionRecommendation(name='unit', recommended_value='inch',
level=OptionRecommendation.LOW, short_switch='u', choices=UNITS, level=OptionRecommendation.LOW, short_switch='u', choices=UNITS,
help=_('The unit of measure. Default is inch. Choices ' help=_('The unit of measure for page sizes. Default is inch. Choices '
'are %s ' 'are %s '
'Note: This does not override the unit for margins!') % UNITS), 'Note: This does not override the unit for margins!') % UNITS),
OptionRecommendation(name='paper_size', recommended_value='letter', OptionRecommendation(name='paper_size', recommended_value='letter',
@ -80,10 +67,6 @@ class PDFOutput(OutputFormatPlugin):
help=_('Custom size of the document. Use the form widthxheight ' help=_('Custom size of the document. Use the form widthxheight '
'EG. `123x321` to specify the width and height. ' 'EG. `123x321` to specify the width and height. '
'This overrides any specified paper-size.')), 'This overrides any specified paper-size.')),
OptionRecommendation(name='orientation', recommended_value='portrait',
level=OptionRecommendation.LOW, choices=ORIENTATIONS,
help=_('The orientation of the page. Default is portrait. Choices '
'are %s') % ORIENTATIONS),
OptionRecommendation(name='preserve_cover_aspect_ratio', OptionRecommendation(name='preserve_cover_aspect_ratio',
recommended_value=False, recommended_value=False,
help=_('Preserve the aspect ratio of the cover, instead' help=_('Preserve the aspect ratio of the cover, instead'
@ -108,6 +91,11 @@ class PDFOutput(OutputFormatPlugin):
OptionRecommendation(name='pdf_mono_font_size', OptionRecommendation(name='pdf_mono_font_size',
recommended_value=16, help=_( recommended_value=16, help=_(
'The default font size for monospaced text')), 'The default font size for monospaced text')),
OptionRecommendation(name='uncompressed_pdf',
recommended_value=False, help=_(
'Generate an uncompressed PDF (useful for debugging)')),
OptionRecommendation(name='old_pdf_engine', recommended_value=False,
help=_('Use the old, less capable engine to generate the PDF')),
]) ])
def convert(self, oeb_book, output_path, input_plugin, opts, log): def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -200,33 +188,18 @@ class PDFOutput(OutputFormatPlugin):
if k in family_map: if k in family_map:
val[i].value = family_map[k] val[i].value = family_map[k]
def remove_font_specification(self):
# Qt produces image based pdfs on windows when non-generic fonts are specified
# This might change in Qt WebKit 2.3+ you will have to test.
for item in self.oeb.manifest:
if not hasattr(item.data, 'cssRules'): continue
for i, rule in enumerate(item.data.cssRules):
if rule.type != rule.STYLE_RULE: continue
ff = rule.style.getProperty('font-family')
if ff is None: continue
val = ff.propertyValue
for i in xrange(val.length):
k = icu_lower(val[i].value)
if k not in {'serif', 'sans', 'sans-serif', 'sansserif',
'monospace', 'cursive', 'fantasy'}:
val[i].value = ''
def convert_text(self, oeb_book): def convert_text(self, oeb_book):
from calibre.ebooks.pdf.writer import PDFWriter if self.opts.old_pdf_engine:
from calibre.ebooks.pdf.writer import PDFWriter
PDFWriter
else:
from calibre.ebooks.pdf.render.from_html import PDFWriter
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
self.log.debug('Serializing oeb input to disk for processing...') self.log.debug('Serializing oeb input to disk for processing...')
self.get_cover_data() self.get_cover_data()
if iswindows: self.handle_embedded_fonts()
self.remove_font_specification()
else:
self.handle_embedded_fonts()
with TemporaryDirectory('_pdf_out') as oeb_dir: with TemporaryDirectory('_pdf_out') as oeb_dir:
from calibre.customize.ui import plugin_for_output_format from calibre.customize.ui import plugin_for_output_format
@ -240,9 +213,9 @@ class PDFOutput(OutputFormatPlugin):
'toc', None)) 'toc', None))
def write(self, Writer, items, toc): def write(self, Writer, items, toc):
from calibre.ebooks.pdf.writer import PDFMetadata
writer = Writer(self.opts, self.log, cover_data=self.cover_data, writer = Writer(self.opts, self.log, cover_data=self.cover_data,
toc=toc) toc=toc)
writer.report_progress = self.report_progress
close = False close = False
if not hasattr(self.output_path, 'write'): if not hasattr(self.output_path, 'write'):

View File

@ -18,6 +18,8 @@ inch = 72.0
cm = inch / 2.54 cm = inch / 2.54
mm = cm * 0.1 mm = cm * 0.1
pica = 12.0 pica = 12.0
didot = 0.375 * mm
cicero = 12 * didot
_W, _H = (21*cm, 29.7*cm) _W, _H = (21*cm, 29.7*cm)
@ -41,6 +43,10 @@ B3 = (_BH*2, _BW)
B2 = (_BW*2, _BH*2) B2 = (_BW*2, _BH*2)
B1 = (_BH*4, _BW*2) B1 = (_BH*4, _BW*2)
B0 = (_BW*4, _BH*4) B0 = (_BW*4, _BH*4)
PAPER_SIZES = {k:globals()[k.upper()] for k in ('a0 a1 a2 a3 a4 a5 a6 b0 b1 b2'
' b3 b4 b5 b6 letter legal').split()}
# }}} # }}}
# Basic PDF datatypes {{{ # Basic PDF datatypes {{{
@ -79,19 +85,12 @@ class String(unicode):
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be') raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
stream.write(b'('+raw+b')') stream.write(b'('+raw+b')')
class GlyphIndex(object): class GlyphIndex(int):
def __init__(self, code, compress):
self.code = code
self.compress = compress
def pdf_serialize(self, stream): def pdf_serialize(self, stream):
if self.compress: byts = bytearray(pack(b'>H', self))
stream.write(pack(b'>sHs', b'(', self.code, b')')) stream.write('<%s>'%''.join(map(
else: lambda x: bytes(hex(x)[2:]).rjust(2, b'0'), byts)))
byts = bytearray(pack(b'>H', self.code))
stream.write('<%s>'%''.join(map(
lambda x: bytes(hex(int(x))[2:]).rjust(2, b'0'), byts)))
class Dictionary(dict): class Dictionary(dict):

View File

@ -16,15 +16,11 @@ from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPainterPath, QTextOption, QTextLayout, QTransform, QPainterPath, QTextOption, QTextLayout,
QImage, QByteArray, QBuffer, qRgba) QImage, QByteArray, QBuffer, qRgba)
from calibre.constants import DEBUG
from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path) from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path)
from calibre.ebooks.pdf.render.common import inch, A4 from calibre.ebooks.pdf.render.common import inch, A4
from calibre.utils.fonts.sfnt.container import Sfnt from calibre.utils.fonts.sfnt.container import Sfnt
from calibre.utils.fonts.sfnt.metrics import FontMetrics from calibre.utils.fonts.sfnt.metrics import FontMetrics
XDPI = 1200
YDPI = 1200
Point = namedtuple('Point', 'x y') Point = namedtuple('Point', 'x y')
ColorState = namedtuple('ColorState', 'color opacity do') ColorState = namedtuple('ColorState', 'color opacity do')
@ -35,7 +31,8 @@ def store_error(func):
try: try:
func(self, *args, **kwargs) func(self, *args, **kwargs)
except: except:
self.errors.append(traceback.format_exc()) self.errors_occurred = True
self.errors(traceback.format_exc())
return errh return errh
@ -43,7 +40,7 @@ class GraphicsState(object): # {{{
def __init__(self): def __init__(self):
self.ops = {} self.ops = {}
self.current_state = self.initial_state = { self.initial_state = {
'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False), 'fill': ColorState(Color(0., 0., 0., 1.), 1.0, False),
'transform': QTransform(), 'transform': QTransform(),
'dash': [], 'dash': [],
@ -53,9 +50,10 @@ class GraphicsState(object): # {{{
'line_join': 'miter', 'line_join': 'miter',
'clip': (Qt.NoClip, QPainterPath()), 'clip': (Qt.NoClip, QPainterPath()),
} }
self.current_state = self.initial_state.copy()
def reset(self): def reset(self):
self.current_state = self.initial_state self.current_state = self.initial_state.copy()
def update_color_state(self, which, color=None, opacity=None, def update_color_state(self, which, color=None, opacity=None,
brush_style=None, pen_style=None): brush_style=None, pen_style=None):
@ -78,7 +76,6 @@ class GraphicsState(object): # {{{
self.ops[which] = n self.ops[which] = n
def read(self, state): def read(self, state):
self.ops = {}
flags = state.state() flags = state.state()
if flags & QPaintEngine.DirtyTransform: if flags & QPaintEngine.DirtyTransform:
@ -110,15 +107,12 @@ class GraphicsState(object): # {{{
self.update_color_state('fill', opacity=state.opacity()) self.update_color_state('fill', opacity=state.opacity())
self.update_color_state('stroke', opacity=state.opacity()) self.update_color_state('stroke', opacity=state.opacity())
if flags & QPaintEngine.DirtyClipPath: if flags & QPaintEngine.DirtyClipPath or flags & QPaintEngine.DirtyClipRegion:
self.ops['clip'] = (state.clipOperation(), state.clipPath()) self.ops['clip'] = True
elif flags & QPaintEngine.DirtyClipRegion:
path = QPainterPath()
for rect in state.clipRegion().rects():
path.addRect(rect)
self.ops['clip'] = (state.clipOperation(), path)
def __call__(self, engine): def __call__(self, engine):
if not self.ops:
return
pdf = engine.pdf pdf = engine.pdf
ops = self.ops ops = self.ops
current_transform = self.current_state['transform'] current_transform = self.current_state['transform']
@ -128,58 +122,34 @@ class GraphicsState(object): # {{{
if reset_stack: if reset_stack:
pdf.restore_stack() pdf.restore_stack()
pdf.save_stack() pdf.save_stack()
# We apply clip before transform as the clip may have to be merged with
# the previous clip path so it is easiest to work with clips that are
# pre-transformed
prev_op, prev_clip_path = self.current_state['clip']
if 'clip' in ops:
op, path = ops['clip']
self.current_state['clip'] = (op, path)
transform = ops.get('transform', QTransform())
if not transform.isIdentity() and path is not None:
# Pre transform the clip path
path = current_transform.map(path)
self.current_state['clip'] = (op, path)
if op == Qt.ReplaceClip:
pass
elif op == Qt.IntersectClip:
if prev_op != Qt.NoClip:
self.current_state['clip'] = (op, path.intersected(prev_clip_path))
elif op == Qt.UniteClip:
if prev_clip_path is not None:
path.addPath(prev_clip_path)
else:
self.current_state['clip'] = (Qt.NoClip, QPainterPath())
op, path = self.current_state['clip']
if op != Qt.NoClip:
engine.add_clip(path)
elif reset_stack and prev_op != Qt.NoClip:
# Re-apply the previous clip path since no clipping operation was
# specified
engine.add_clip(prev_clip_path)
if reset_stack:
# Since we have reset the stack we need to re-apply all previous # Since we have reset the stack we need to re-apply all previous
# operations, that are different from the default value (clip is # operations, that are different from the default value (clip is
# handled separately). # handled separately).
for op in set(self.current_state) - (set(ops)|{'clip'}): for op in set(self.initial_state) - {'clip'}:
if self.current_state[op] != self.initial_state[op]: if op in ops: # These will be applied below
self.current_state[op] = self.initial_state[op]
elif self.current_state[op] != self.initial_state[op]:
self.apply(op, self.current_state[op], engine, pdf) self.apply(op, self.current_state[op], engine, pdf)
# Now apply the new operations # Now apply the new operations
for op, val in ops.iteritems(): for op, val in ops.iteritems():
if op != 'clip': if op != 'clip' and self.current_state[op] != val:
self.apply(op, val, engine, pdf) self.apply(op, val, engine, pdf)
self.current_state[op] = val self.current_state[op] = val
if 'clip' in ops:
# Get the current clip
path = engine.painter().clipPath()
if not path.isEmpty():
engine.add_clip(path)
self.ops = {}
def apply(self, op, val, engine, pdf): def apply(self, op, val, engine, pdf):
getattr(self, 'apply_'+op)(val, engine, pdf) getattr(self, 'apply_'+op)(val, engine, pdf)
def apply_transform(self, val, engine, pdf): def apply_transform(self, val, engine, pdf):
engine.qt_system = val if not val.isIdentity():
pdf.transform(val) pdf.transform(val)
def apply_stroke(self, val, engine, pdf): def apply_stroke(self, val, engine, pdf):
self.apply_color_state('stroke', val, engine, pdf) self.apply_color_state('stroke', val, engine, pdf)
@ -215,9 +185,11 @@ class Font(FontMetrics):
class PdfEngine(QPaintEngine): class PdfEngine(QPaintEngine):
def __init__(self, file_object, page_width, page_height, left_margin, def __init__(self, file_object, page_width, page_height, left_margin,
top_margin, right_margin, bottom_margin, width, height): top_margin, right_margin, bottom_margin, width, height,
errors=print, debug=print, compress=True):
QPaintEngine.__init__(self, self.features) QPaintEngine.__init__(self, self.features)
self.file_object = file_object self.file_object = file_object
self.compress = compress
self.page_height, self.page_width = page_height, page_width self.page_height, self.page_width = page_height, page_width
self.left_margin, self.top_margin = left_margin, top_margin self.left_margin, self.top_margin = left_margin, top_margin
self.right_margin, self.bottom_margin = right_margin, bottom_margin self.right_margin, self.bottom_margin = right_margin, bottom_margin
@ -236,29 +208,32 @@ class PdfEngine(QPaintEngine):
self.bottom_margin) / self.pixel_height self.bottom_margin) / self.pixel_height
self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy)
self.qt_system = QTransform()
self.do_stroke = True self.do_stroke = True
self.do_fill = False self.do_fill = False
self.scale = sqrt(sy**2 + sx**2) self.scale = sqrt(sy**2 + sx**2)
self.xscale, self.yscale = sx, sy self.xscale, self.yscale = sx, sy
self.graphics_state = GraphicsState() self.graphics_state = GraphicsState()
self.errors, self.debug = [], [] self.errors_occurred = False
self.errors, self.debug = errors, debug
self.text_option = QTextOption() self.text_option = QTextOption()
self.text_option.setWrapMode(QTextOption.NoWrap) self.text_option.setWrapMode(QTextOption.NoWrap)
self.fonts = {} self.fonts = {}
i = QImage(1, 1, QImage.Format_ARGB32) i = QImage(1, 1, QImage.Format_ARGB32)
i.fill(qRgba(0, 0, 0, 255)) i.fill(qRgba(0, 0, 0, 255))
self.alpha_bit = i.constBits().asstring(4).find(b'\xff') self.alpha_bit = i.constBits().asstring(4).find(b'\xff')
self.current_page_num = 1
self.current_page_inited = False
def init_page(self): def init_page(self):
self.pdf.transform(self.pdf_system) self.pdf.transform(self.pdf_system)
self.pdf.set_rgb_colorspace() self.pdf.set_rgb_colorspace()
width = self.painter.pen().widthF() if self.isActive() else 0 width = self.painter().pen().widthF() if self.isActive() else 0
self.pdf.set_line_width(width) self.pdf.set_line_width(width)
self.do_stroke = True self.do_stroke = True
self.do_fill = False self.do_fill = False
self.graphics_state.reset() self.graphics_state.reset()
self.pdf.save_stack() self.pdf.save_stack()
self.current_page_inited = True
@property @property
def features(self): def features(self):
@ -268,25 +243,26 @@ class PdfEngine(QPaintEngine):
QPaintEngine.PrimitiveTransform) QPaintEngine.PrimitiveTransform)
def begin(self, device): def begin(self, device):
try: if not hasattr(self, 'pdf'):
self.pdf = PDFStream(self.file_object, (self.page_width, try:
self.page_height), self.pdf = PDFStream(self.file_object, (self.page_width,
compress=not DEBUG) self.page_height),
self.init_page() compress=self.compress)
except: except:
self.errors.append(traceback.format_exc()) self.errors.append(traceback.format_exc())
return False return False
return True return True
def end_page(self, start_new=True): def end_page(self):
self.pdf.restore_stack() if self.current_page_inited:
self.pdf.end_page() self.pdf.restore_stack()
if start_new: self.pdf.end_page()
self.init_page() self.current_page_inited = False
self.current_page_num += 1
def end(self): def end(self):
try: try:
self.end_page(start_new=False) self.end_page()
self.pdf.end() self.pdf.end()
except: except:
self.errors.append(traceback.format_exc()) self.errors.append(traceback.format_exc())
@ -300,6 +276,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPixmap(self, rect, pixmap, source_rect): def drawPixmap(self, rect, pixmap, source_rect):
self.graphics_state(self)
source_rect = source_rect.toRect() source_rect = source_rect.toRect()
pixmap = (pixmap if source_rect == pixmap.rect() else pixmap = (pixmap if source_rect == pixmap.rect() else
pixmap.copy(source_rect)) pixmap.copy(source_rect))
@ -311,6 +288,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor): def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
self.graphics_state(self)
source_rect = source_rect.toRect() source_rect = source_rect.toRect()
image = (image if source_rect == image.rect() else image = (image if source_rect == image.rect() else
image.copy(source_rect)) image.copy(source_rect))
@ -386,7 +364,6 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def updateState(self, state): def updateState(self, state):
self.graphics_state.read(state) self.graphics_state.read(state)
self.graphics_state(self)
def convert_path(self, path): def convert_path(self, path):
p = Path() p = Path()
@ -414,6 +391,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPath(self, path): def drawPath(self, path):
self.graphics_state(self)
p = self.convert_path(path) p = self.convert_path(path)
fill_rule = {Qt.OddEvenFill:'evenodd', fill_rule = {Qt.OddEvenFill:'evenodd',
Qt.WindingFill:'winding'}[path.fillRule()] Qt.WindingFill:'winding'}[path.fillRule()]
@ -428,6 +406,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPoints(self, points): def drawPoints(self, points):
self.graphics_state(self)
p = Path() p = Path()
for point in points: for point in points:
p.move_to(point.x(), point.y()) p.move_to(point.x(), point.y())
@ -436,6 +415,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawRects(self, rects): def drawRects(self, rects):
self.graphics_state(self)
for rect in rects: for rect in rects:
bl = rect.topLeft() bl = rect.topLeft()
self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
@ -488,7 +468,7 @@ class PdfEngine(QPaintEngine):
glyph_map[g[0]] = string glyph_map[g[0]] = string
break break
if not found: if not found:
self.debug.append( self.debug(
'Failed to find glyph->unicode mapping for text: %s'%text) 'Failed to find glyph->unicode mapping for text: %s'%text)
break break
ipos += 1 ipos += 1
@ -498,7 +478,8 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawTextItem(self, point, text_item): def drawTextItem(self, point, text_item):
# super(PdfEngine, self).drawTextItem(point+QPoint(0, 0), text_item) # super(PdfEngine, self).drawTextItem(point, text_item)
self.graphics_state(self)
text = type(u'')(text_item.text()).replace('\n', ' ') text = type(u'')(text_item.text()).replace('\n', ' ')
text = unicodedata.normalize('NFKC', text) text = unicodedata.normalize('NFKC', text)
tl = self.get_text_layout(text_item, text) tl = self.get_text_layout(text_item, text)
@ -535,6 +516,7 @@ class PdfEngine(QPaintEngine):
@store_error @store_error
def drawPolygon(self, points, mode): def drawPolygon(self, points, mode):
self.graphics_state(self)
if not points: return if not points: return
p = Path() p = Path()
p.move_to(points[0].x(), points[0].y()) p.move_to(points[0].x(), points[0].y())
@ -546,6 +528,9 @@ class PdfEngine(QPaintEngine):
self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule, self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule,
fill=(mode in (self.OddEvenMode, self.WindingMode, self.ConvexMode))) fill=(mode in (self.OddEvenMode, self.WindingMode, self.ConvexMode)))
def set_metadata(self, *args, **kwargs):
self.pdf.set_metadata(*args, **kwargs)
def __enter__(self): def __enter__(self):
self.pdf.save_stack() self.pdf.save_stack()
self.saved_ps = (self.do_stroke, self.do_fill) self.saved_ps = (self.do_stroke, self.do_fill)
@ -558,23 +543,26 @@ class PdfDevice(QPaintDevice): # {{{
def __init__(self, file_object, page_size=A4, left_margin=inch, def __init__(self, file_object, page_size=A4, left_margin=inch,
top_margin=inch, right_margin=inch, bottom_margin=inch): top_margin=inch, right_margin=inch, bottom_margin=inch,
xdpi=1200, ydpi=1200, errors=print, debug=print, compress=True):
QPaintDevice.__init__(self) QPaintDevice.__init__(self)
self.xdpi, self.ydpi = xdpi, ydpi
self.page_width, self.page_height = page_size self.page_width, self.page_height = page_size
self.body_width = self.page_width - left_margin - right_margin self.body_width = self.page_width - left_margin - right_margin
self.body_height = self.page_height - top_margin - bottom_margin self.body_height = self.page_height - top_margin - bottom_margin
self.engine = PdfEngine(file_object, self.page_width, self.page_height, self.engine = PdfEngine(file_object, self.page_width, self.page_height,
left_margin, top_margin, right_margin, left_margin, top_margin, right_margin,
bottom_margin, self.width(), self.height()) bottom_margin, self.width(), self.height(),
errors=errors, debug=debug, compress=compress)
def paintEngine(self): def paintEngine(self):
return self.engine return self.engine
def metric(self, m): def metric(self, m):
if m in (self.PdmDpiX, self.PdmPhysicalDpiX): if m in (self.PdmDpiX, self.PdmPhysicalDpiX):
return XDPI return self.xdpi
if m in (self.PdmDpiY, self.PdmPhysicalDpiY): if m in (self.PdmDpiY, self.PdmPhysicalDpiY):
return YDPI return self.ydpi
if m == self.PdmDepth: if m == self.PdmDepth:
return 32 return 32
if m == self.PdmNumColors: if m == self.PdmNumColors:
@ -584,10 +572,32 @@ class PdfDevice(QPaintDevice): # {{{
if m == self.PdmHeightMM: if m == self.PdmHeightMM:
return int(round(self.body_height * 0.35277777777778)) return int(round(self.body_height * 0.35277777777778))
if m == self.PdmWidth: if m == self.PdmWidth:
return int(round(self.body_width * XDPI / 72.0)) return int(round(self.body_width * self.xdpi / 72.0))
if m == self.PdmHeight: if m == self.PdmHeight:
return int(round(self.body_height * YDPI / 72.0)) return int(round(self.body_height * self.ydpi / 72.0))
return 0 return 0
def end_page(self):
self.engine.end_page()
def init_page(self):
self.engine.init_page()
@property
def current_page_num(self):
return self.engine.current_page_num
@property
def errors_occurred(self):
return self.engine.errors_occurred
def to_px(self, pt, vertical=True):
return pt * (self.height()/self.page_height if vertical else
self.width()/self.page_width)
def set_metadata(self, *args, **kwargs):
self.engine.set_metadata(*args, **kwargs)
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
@ -596,8 +606,9 @@ if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
p = QPainter() p = QPainter()
with open('/tmp/painter.pdf', 'wb') as f: with open('/tmp/painter.pdf', 'wb') as f:
dev = PdfDevice(f) dev = PdfDevice(f, compress=False)
p.begin(dev) p.begin(dev)
dev.init_page()
xmax, ymax = p.viewport().width(), p.viewport().height() xmax, ymax = p.viewport().width(), p.viewport().height()
try: try:
p.drawRect(0, 0, xmax, ymax) p.drawRect(0, 0, xmax, ymax)
@ -606,21 +617,21 @@ if __name__ == '__main__':
# pp = QPainterPath() # pp = QPainterPath()
# pp.addRect(0, 0, xmax, ymax) # pp.addRect(0, 0, xmax, ymax)
# p.drawPath(pp) # p.drawPath(pp)
p.save() # p.save()
for i in xrange(3): # for i in xrange(3):
col = [0, 0, 0, 200] # col = [0, 0, 0, 200]
col[i] = 255 # col[i] = 255
p.setOpacity(0.3) # p.setOpacity(0.3)
p.setBrush(QBrush(QColor(*col))) # p.setBrush(QBrush(QColor(*col)))
p.drawRect(0, 0, xmax/10, xmax/10) # p.drawRect(0, 0, xmax/10, xmax/10)
p.translate(xmax/10, xmax/10) # p.translate(xmax/10, xmax/10)
p.scale(1, 1.5) # p.scale(1, 1.5)
p.restore() # p.restore()
# p.scale(2, 2) # # p.scale(2, 2)
# p.rotate(45) # # p.rotate(45)
p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png'))) # p.drawPixmap(0, 0, 2048, 2048, QPixmap(I('library.png')))
p.drawRect(0, 0, 2048, 2048) # p.drawRect(0, 0, 2048, 2048)
# p.save() # p.save()
# p.drawLine(0, 0, 5000, 0) # p.drawLine(0, 0, 5000, 0)
@ -628,23 +639,20 @@ if __name__ == '__main__':
# p.drawLine(0, 0, 5000, 0) # p.drawLine(0, 0, 5000, 0)
# p.restore() # p.restore()
# f = p.font() f = p.font()
# f.setPointSize(24) f.setPointSize(20)
# f.setLetterSpacing(f.PercentageSpacing, 200) # f.setLetterSpacing(f.PercentageSpacing, 200)
# f.setUnderline(True) # f.setUnderline(True)
# f.setOverline(True) # f.setOverline(True)
# f.setStrikeOut(True) # f.setStrikeOut(True)
# f.setFamily('Calibri') f.setFamily('DejaVu Sans')
# p.setFont(f) p.setFont(f)
# p.setPen(QColor(0, 0, 255)) # p.setPen(QColor(0, 0, 255))
# p.scale(2, 2) # p.scale(2, 2)
# p.rotate(45) # p.rotate(45)
# p.drawText(QPoint(100, 300), 'Some text ū --- Д AV ff ff') p.drawText(QPoint(0, 300), 'Some—text not Bys ū --- Д AV ff ff')
finally: finally:
p.end() p.end()
for line in dev.engine.debug: if dev.engine.errors_occurred:
print (line)
if dev.engine.errors:
for err in dev.engine.errors: print (err)
raise SystemExit(1) raise SystemExit(1)

View File

@ -0,0 +1,286 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json, os
from future_builtins import map
from math import floor
from PyQt4.Qt import (QObject, QPainter, Qt, QSize, QString, QTimer,
pyqtProperty, QEventLoop, QPixmap, QRect)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
from calibre import fit_image
from calibre.ebooks.oeb.display.webview import load_html
from calibre.ebooks.pdf.render.engine import PdfDevice
from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero,
didot, PAPER_SIZES)
from calibre.ebooks.pdf.outline_writer import Outline
def get_page_size(opts, for_comic=False): # {{{
use_profile = not (opts.override_profile_size or
opts.output_profile.short_name == 'default')
if use_profile:
w = (opts.output_profile.comic_screen_size[0] if for_comic else
opts.output_profile.width)
h = (opts.output_profile.comic_screen_size[1] if for_comic else
opts.output_profile.height)
dpi = opts.output_profile.dpi
factor = 72.0 / dpi
page_size = (factor * w, factor * h)
else:
page_size = None
if opts.custom_size != None:
width, sep, height = opts.custom_size.partition('x')
if height:
try:
width = float(width)
height = float(height)
except:
pass
else:
if opts.unit == 'devicepixel':
factor = 72.0 / opts.output_profile.dpi
else:
{'point':1.0, 'inch':inch, 'cicero':cicero,
'didot':didot, 'pica':pica, 'millimeter':mm,
'centimeter':cm}[opts.unit]
page_size = (factor*width, factor*height)
if page_size is None:
page_size = PAPER_SIZES[opts.paper_size]
return page_size
# }}}
class Page(QWebPage): # {{{
def __init__(self, opts, log):
self.log = log
QWebPage.__init__(self)
settings = self.settings()
settings.setFontSize(QWebSettings.DefaultFontSize,
opts.pdf_default_font_size)
settings.setFontSize(QWebSettings.DefaultFixedFontSize,
opts.pdf_mono_font_size)
settings.setFontSize(QWebSettings.MinimumLogicalFontSize, 8)
settings.setFontSize(QWebSettings.MinimumFontSize, 8)
std = {'serif':opts.pdf_serif_family, 'sans':opts.pdf_sans_family,
'mono':opts.pdf_mono_family}.get(opts.pdf_standard_font,
opts.pdf_serif_family)
if std:
settings.setFontFamily(QWebSettings.StandardFont, std)
if opts.pdf_serif_family:
settings.setFontFamily(QWebSettings.SerifFont, opts.pdf_serif_family)
if opts.pdf_sans_family:
settings.setFontFamily(QWebSettings.SansSerifFont,
opts.pdf_sans_family)
if opts.pdf_mono_family:
settings.setFontFamily(QWebSettings.FixedFont, opts.pdf_mono_family)
def javaScriptConsoleMessage(self, msg, lineno, msgid):
self.log.debug(u'JS:', unicode(msg))
def javaScriptAlert(self, frame, msg):
self.log(unicode(msg))
# }}}
def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True):
if preserve_aspect_ratio:
aspect_ratio = float(p.width())/p.height()
nw, nh = page_rect.width(), page_rect.height()
if aspect_ratio > 1:
nh = int(page_rect.width()/aspect_ratio)
else: # Width is smaller than height
nw = page_rect.height()*aspect_ratio
__, nnw, nnh = fit_image(nw, nh, page_rect.width(),
page_rect.height())
dx = int((page_rect.width() - nnw)/2.)
dy = int((page_rect.height() - nnh)/2.)
page_rect.moveTo(dx, dy)
page_rect.setHeight(nnh)
page_rect.setWidth(nnw)
painter.drawPixmap(page_rect, p, p.rect())
class PDFWriter(QObject):
def _pass_json_value_getter(self):
val = json.dumps(self.bridge_value)
return QString(val)
def _pass_json_value_setter(self, value):
self.bridge_value = json.loads(unicode(value))
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
fset=_pass_json_value_setter)
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt')
QObject.__init__(self)
self.logger = self.log = log
self.opts = opts
self.cover_data = cover_data
self.paged_js = None
self.toc = toc
self.loop = QEventLoop()
self.view = QWebView()
self.page = Page(opts, self.log)
self.view.setPage(self.page)
self.view.setRenderHints(QPainter.Antialiasing|
QPainter.TextAntialiasing|QPainter.SmoothPixmapTransform)
self.view.loadFinished.connect(self.render_html,
type=Qt.QueuedConnection)
for x in (Qt.Horizontal, Qt.Vertical):
self.view.page().mainFrame().setScrollBarPolicy(x,
Qt.ScrollBarAlwaysOff)
self.report_progress = lambda x, y: x
def dump(self, items, out_stream, pdf_metadata):
opts = self.opts
self.outline = Outline(self.toc, items)
page_size = get_page_size(self.opts)
xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY()
ml, mr = opts.margin_left, opts.margin_right
margin_side = min(ml, mr)
ml, mr = ml - margin_side, mr - margin_side
self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml,
top_margin=0, right_margin=mr, bottom_margin=0,
xdpi=xdpi, ydpi=ydpi, errors=self.log.error,
debug=self.log.debug, compress=not
opts.uncompressed_pdf)
self.page.setViewportSize(QSize(self.doc.width(), self.doc.height()))
self.render_queue = items
self.total_items = len(items)
# TODO: Test margins
mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom))
ms = self.doc.to_px(margin_side, vertical=False)
self.margin_top, self.margin_size, self.margin_bottom = map(
lambda x:int(floor(x)), (mt, ms, mb))
self.painter = QPainter(self.doc)
self.doc.set_metadata(title=pdf_metadata.title,
author=pdf_metadata.author,
tags=pdf_metadata.tags)
self.painter.save()
try:
if self.cover_data is not None:
p = QPixmap()
p.loadFromData(self.cover_data)
if not p.isNull():
draw_image_page(QRect(0, 0, self.doc.width(), self.doc.height()),
self.painter, p,
preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio)
self.doc.end_page()
finally:
self.painter.restore()
QTimer.singleShot(0, self.render_book)
self.loop.exec_()
# TODO: Outline and links
self.painter.end()
if self.doc.errors_occurred:
raise Exception('PDF Output failed, see log for details')
def render_book(self):
if self.doc.errors_occurred:
return self.loop.exit(1)
try:
if not self.render_queue:
self.loop.exit()
else:
self.render_next()
except:
self.logger.exception('Rendering failed')
self.loop.exit(1)
def render_next(self):
item = unicode(self.render_queue.pop(0))
self.logger.debug('Processing %s...' % item)
self.current_item = item
load_html(item, self.view)
def render_html(self, ok):
if ok:
try:
self.do_paged_render()
except:
self.log.exception('Rendering failed')
self.loop.exit(1)
else:
# The document is so corrupt that we can't render the page.
self.logger.error('Document cannot be rendered.')
self.loop.exit(1)
return
done = self.total_items - len(self.render_queue)
self.report_progress(done/self.total_items,
_('Rendered %s'%os.path.basename(self.current_item)))
self.render_book()
@property
def current_page_num(self):
return self.doc.current_page_num
def do_paged_render(self):
if self.paged_js is None:
from calibre.utils.resources import compiled_coffeescript
self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils')
self.paged_js += compiled_coffeescript('ebooks.oeb.display.indexing')
self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged')
self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self)
evaljs = self.view.page().mainFrame().evaluateJavaScript
evaljs(self.paged_js)
evaljs('''
py_bridge.__defineGetter__('value', function() {
return JSON.parse(this._pass_json_value);
});
py_bridge.__defineSetter__('value', function(val) {
this._pass_json_value = JSON.stringify(val);
});
document.body.style.backgroundColor = "white";
paged_display.set_geometry(1, %d, %d, %d);
paged_display.layout();
paged_display.fit_images();
'''%(self.margin_top, self.margin_size, self.margin_bottom))
mf = self.view.page().mainFrame()
start_page = self.current_page_num
dx = 0
while True:
self.doc.init_page()
self.painter.save()
mf.render(self.painter)
self.painter.restore()
nsl = evaljs('paged_display.next_screen_location()').toInt()
self.doc.end_page()
if not nsl[1] or nsl[0] <= 0:
break
dx = nsl[0]
evaljs('window.scrollTo(%d, 0)'%dx)
if self.doc.errors_occurred:
break
self.bridge_value = tuple(self.outline.anchor_map[self.current_item])
evaljs('py_bridge.value = book_indexing.anchor_positions(py_bridge.value)')
amap = self.bridge_value
if not isinstance(amap, dict):
amap = {} # Some javascript error occurred
self.outline.set_pos(self.current_item, None, start_page, 0)
for anchor, x in amap.iteritems():
pagenum, ypos = x
self.outline.set_pos(self.current_item, anchor, start_page + pagenum, ypos)

View File

@ -303,6 +303,14 @@ class PDFStream(object):
def catalog(self): def catalog(self):
return self.objects[1] return self.objects[1]
def set_metadata(self, title=None, author=None, tags=None):
if title:
self.info['Title'] = String(title)
if author:
self.info['Author'] = String(author)
if tags:
self.info['Keywords'] = String(tags)
def write_line(self, byts=b''): def write_line(self, byts=b''):
byts = byts if isinstance(byts, bytes) else byts.encode('ascii') byts = byts if isinstance(byts, bytes) else byts.encode('ascii')
self.stream.write(byts + EOL) self.stream.write(byts + EOL)
@ -409,7 +417,7 @@ class PDFStream(object):
self.current_page.write('%s Tm '%' '.join(map(type(u''), transform))) self.current_page.write('%s Tm '%' '.join(map(type(u''), transform)))
for x, y, glyph_id in glyphs: for x, y, glyph_id in glyphs:
self.current_page.write('%g %g Td '%(x, y)) self.current_page.write('%g %g Td '%(x, y))
serialize(GlyphIndex(glyph_id, self.compress), self.current_page) serialize(GlyphIndex(glyph_id), self.current_page)
self.current_page.write(' Tj ') self.current_page.write(' Tj ')
self.current_page.write_line(b' ET') self.current_page.write_line(b' ET')

View File

@ -9,18 +9,16 @@ Write content to PDF.
''' '''
import os, shutil, json import os, shutil, json
from future_builtins import map
from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter, from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter,
QPixmap, QTimer, pyqtProperty, QString, QSize) QPixmap, QTimer, pyqtProperty, QString, QSize)
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.ebooks.pdf.pageoptions import (unit, paper_size, orientation) from calibre.ebooks.pdf.pageoptions import (unit, paper_size)
from calibre.ebooks.pdf.outline_writer import Outline from calibre.ebooks.pdf.outline_writer import Outline
from calibre.ebooks.metadata import authors_to_string
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import (__appname__, __version__, fit_image, isosx, force_unicode) from calibre import (__appname__, __version__, fit_image, isosx)
from calibre.ebooks.oeb.display.webview import load_html from calibre.ebooks.oeb.display.webview import load_html
def get_custom_size(opts): def get_custom_size(opts):
@ -72,7 +70,6 @@ def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{
else: else:
printer.setPageMargins(opts.margin_left, opts.margin_top, printer.setPageMargins(opts.margin_left, opts.margin_top,
opts.margin_right, opts.margin_bottom, QPrinter.Point) opts.margin_right, opts.margin_bottom, QPrinter.Point)
printer.setOrientation(orientation(opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFormat(QPrinter.PdfFormat)
printer.setFullPage(for_comic) printer.setFullPage(for_comic)
if output_file_name: if output_file_name:
@ -103,24 +100,6 @@ def draw_image_page(printer, painter, p, preserve_aspect_ratio=True):
painter.drawPixmap(page_rect, p, p.rect()) painter.drawPixmap(page_rect, p, p.rect())
class PDFMetadata(object): # {{{
def __init__(self, oeb_metadata=None):
self.title = _(u'Unknown')
self.author = _(u'Unknown')
self.tags = u''
if oeb_metadata != None:
if len(oeb_metadata.title) >= 1:
self.title = oeb_metadata.title[0].value
if len(oeb_metadata.creator) >= 1:
self.author = authors_to_string([x.value for x in oeb_metadata.creator])
if oeb_metadata.subject:
self.tags = u', '.join(map(unicode, oeb_metadata.subject))
self.title = force_unicode(self.title)
self.author = force_unicode(self.author)
# }}}
class Page(QWebPage): # {{{ class Page(QWebPage): # {{{
def __init__(self, opts, log): def __init__(self, opts, log):

View File

@ -151,7 +151,7 @@ class AddAction(InterfaceAction):
Add an empty book item to the library. This does not import any formats Add an empty book item to the library. This does not import any formats
from a book file. from a book file.
''' '''
author = None author = series = None
index = self.gui.library_view.currentIndex() index = self.gui.library_view.currentIndex()
if index.isValid(): if index.isValid():
raw = index.model().db.authors(index.row()) raw = index.model().db.authors(index.row())
@ -159,16 +159,27 @@ class AddAction(InterfaceAction):
authors = [a.strip().replace('|', ',') for a in raw.split(',')] authors = [a.strip().replace('|', ',') for a in raw.split(',')]
if authors: if authors:
author = authors[0] author = authors[0]
dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author) series = index.model().db.series(index.row())
dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db,
author, series)
if dlg.exec_() == dlg.Accepted: if dlg.exec_() == dlg.Accepted:
num = dlg.qty_to_add num = dlg.qty_to_add
series = dlg.selected_series
db = self.gui.library_view.model().db
ids = []
for x in xrange(num): for x in xrange(num):
mi = MetaInformation(_('Unknown'), dlg.selected_authors) mi = MetaInformation(_('Unknown'), dlg.selected_authors)
self.gui.library_view.model().db.import_book(mi, []) if series:
mi.series = series
mi.series_index = db.get_next_series_num_for(series)
ids.append(db.import_book(mi, []))
self.gui.library_view.model().books_added(num) self.gui.library_view.model().books_added(num)
if hasattr(self.gui, 'db_images'): if hasattr(self.gui, 'db_images'):
self.gui.db_images.reset() self.gui.db_images.reset()
self.gui.tags_view.recount() self.gui.tags_view.recount()
if ids:
ids.reverse()
self.gui.library_view.select_rows(ids)
def add_isbns(self, books, add_tags=[]): def add_isbns(self, books, add_tags=[]):
self.isbn_books = list(books) self.isbn_books = list(books)

View File

@ -18,16 +18,17 @@ class PluginWidget(Widget, Ui_Form):
ICON = I('mimetypes/pdf.png') ICON = I('mimetypes/pdf.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, ['paper_size', 'custom_size', Widget.__init__(self, parent, [
'orientation', 'preserve_cover_aspect_ratio', 'pdf_serif_family', 'override_profile_size', 'paper_size', 'custom_size',
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font', 'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
'pdf_default_font_size', 'pdf_mono_font_size']) 'pdf_default_font_size', 'pdf_mono_font_size'])
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
for x in get_option('paper_size').option.choices: for x in get_option('paper_size').option.choices:
self.opt_paper_size.addItem(x) self.opt_paper_size.addItem(x)
for x in get_option('orientation').option.choices: for x in get_option('unit').option.choices:
self.opt_orientation.addItem(x) self.opt_unit.addItem(x)
for x in get_option('pdf_standard_font').option.choices: for x in get_option('pdf_standard_font').option.choices:
self.opt_pdf_standard_font.addItem(x) self.opt_pdf_standard_font.addItem(x)

View File

@ -14,7 +14,27 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QFormLayout" name="formLayout"> <layout class="QFormLayout" name="formLayout">
<item row="1" column="0"> <property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_10">
<property name="text">
<string>&lt;b&gt;Note:&lt;/b&gt; The paper size settings below only take effect if you enable the &quot;Override&quot; checkbox below. Otherwise the size from the output profile will be used.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_override_profile_size">
<property name="text">
<string>&amp;Override paper size set in output profile</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>&amp;Paper Size:</string> <string>&amp;Paper Size:</string>
@ -24,21 +44,8 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_paper_size"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Orientation:</string>
</property>
<property name="buddy">
<cstring>opt_orientation</cstring>
</property>
</widget>
</item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="opt_orientation"/> <widget class="QComboBox" name="opt_paper_size"/>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
@ -51,7 +58,24 @@
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="QLineEdit" name="opt_custom_size"/> <layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="opt_custom_size"/>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>&amp;Unit:</string>
</property>
<property name="buddy">
<cstring>opt_unit</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_unit"/>
</item>
</layout>
</item> </item>
<item row="4" column="0" colspan="2"> <item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio"> <widget class="QCheckBox" name="opt_preserve_cover_aspect_ratio">
@ -60,19 +84,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>213</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
@ -159,15 +170,18 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0" colspan="2"> <item row="11" column="0">
<widget class="QLabel" name="label_10"> <spacer name="verticalSpacer">
<property name="text"> <property name="orientation">
<string>&lt;b&gt;Note:&lt;/b&gt; The paper size settings below only take effect if you have set the output profile to the default output profile. Otherwise the output profile will override these settings.</string> <enum>Qt::Vertical</enum>
</property> </property>
<property name="wordWrap"> <property name="sizeHint" stdset="0">
<bool>true</bool> <size>
<width>20</width>
<height>213</height>
</size>
</property> </property>
</widget> </spacer>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -12,7 +12,7 @@ from calibre.utils.config import tweaks
class AddEmptyBookDialog(QDialog): class AddEmptyBookDialog(QDialog):
def __init__(self, parent, db, author): def __init__(self, parent, db, author, series=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.db = db self.db = db
@ -45,6 +45,22 @@ class AddEmptyBookDialog(QDialog):
self.clear_button.clicked.connect(self.reset_author) self.clear_button.clicked.connect(self.reset_author)
self._layout.addWidget(self.clear_button, 3, 1, 1, 1) self._layout.addWidget(self.clear_button, 3, 1, 1, 1)
self.series_label = QLabel(_('Set the series of the new books to:'))
self._layout.addWidget(self.series_label, 4, 0, 1, 2)
self.series_combo = EditWithComplete(self)
self.authors_combo.setSizeAdjustPolicy(
self.authors_combo.AdjustToMinimumContentsLengthWithIcon)
self.series_combo.setEditable(True)
self._layout.addWidget(self.series_combo, 5, 0, 1, 1)
self.initialize_series(db, series)
self.sclear_button = QToolButton(self)
self.sclear_button.setIcon(QIcon(I('trash.png')))
self.sclear_button.setToolTip(_('Reset series'))
self.sclear_button.clicked.connect(self.reset_series)
self._layout.addWidget(self.sclear_button, 5, 1, 1, 1)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept) button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject) button_box.rejected.connect(self.reject)
@ -54,6 +70,9 @@ class AddEmptyBookDialog(QDialog):
def reset_author(self, *args): def reset_author(self, *args):
self.authors_combo.setEditText(_('Unknown')) self.authors_combo.setEditText(_('Unknown'))
def reset_series(self):
self.series_combo.setEditText('')
def initialize_authors(self, db, author): def initialize_authors(self, db, author):
au = author au = author
if not au: if not au:
@ -65,6 +84,11 @@ class AddEmptyBookDialog(QDialog):
self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator']) self.authors_combo.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors_combo.update_items_cache(db.all_author_names()) self.authors_combo.update_items_cache(db.all_author_names())
def initialize_series(self, db, series):
self.series_combo.show_initial_value(series or '')
self.series_combo.update_items_cache(db.all_series_names())
self.series_combo.set_separator(None)
@property @property
def qty_to_add(self): def qty_to_add(self):
return self.qty_spinbox.value() return self.qty_spinbox.value()
@ -73,6 +97,10 @@ class AddEmptyBookDialog(QDialog):
def selected_authors(self): def selected_authors(self):
return string_to_authors(unicode(self.authors_combo.text())) return string_to_authors(unicode(self.authors_combo.text()))
@property
def selected_series(self):
return unicode(self.series_combo.text())
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
d = AddEmptyBookDialog() d = AddEmptyBookDialog()

View File

@ -52,10 +52,9 @@ class ColumnColor(object):
self.mi = None self.mi = None
def __call__(self, id_, key, fmt, db, formatter, color_cache, colors): def __call__(self, id_, key, fmt, db, formatter, color_cache, colors):
if id_ in color_cache: if id_ in color_cache and key in color_cache[id_]:
if key in color_cache[id_]: self.mi = None
self.mi = None return color_cache[id_][key]
return color_cache[id_][key]
try: try:
if self.mi is None: if self.mi is None:
self.mi = db.get_metadata(id_, index_is_id=True) self.mi = db.get_metadata(id_, index_is_id=True)
@ -763,9 +762,8 @@ class BooksModel(QAbstractTableModel): # {{{
self.column_color.mi = None self.column_color.mi = None
if self.color_row_fmt_cache is None: if self.color_row_fmt_cache is None:
d = dict(self.db.prefs['column_color_rules']) self.color_row_fmt_cache = tuple(fmt for key, fmt in
self.color_row_fmt_cache = d.get(color_row_key, '') self.db.prefs['column_color_rules'] if key == color_row_key)
for k, fmt in self.db.prefs['column_color_rules']: for k, fmt in self.db.prefs['column_color_rules']:
if k == key: if k == key:
@ -789,10 +787,9 @@ class BooksModel(QAbstractTableModel): # {{{
except: except:
pass pass
if self.color_row_fmt_cache: for fmt in self.color_row_fmt_cache:
key = color_row_key ccol = self.column_color(id_, color_row_key, fmt, self.db,
ccol = self.column_color(id_, key, self.color_row_fmt_cache, self.formatter, self.color_cache, self.colors)
self.db, self.formatter, self.color_cache, self.colors)
if ccol is not None: if ccol is not None:
return ccol return ccol