Fix clipping, basic text output (no font embedding) and better error handling

This commit is contained in:
Kovid Goyal 2012-12-16 13:27:38 +05:30
parent 2f254bf0e0
commit 5eca5a7b5a
4 changed files with 323 additions and 176 deletions

View File

@ -0,0 +1,152 @@
#!/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 codecs, zlib
from io import BytesIO
EOL = b'\n'
# Sizes {{{
inch = 72.0
cm = inch / 2.54
mm = cm * 0.1
pica = 12.0
_W, _H = (21*cm, 29.7*cm)
A6 = (_W*.5, _H*.5)
A5 = (_H*.5, _W)
A4 = (_W, _H)
A3 = (_H, _W*2)
A2 = (_W*2, _H*2)
A1 = (_H*2, _W*4)
A0 = (_W*4, _H*4)
LETTER = (8.5*inch, 11*inch)
LEGAL = (8.5*inch, 14*inch)
ELEVENSEVENTEEN = (11*inch, 17*inch)
_BW, _BH = (25*cm, 35.3*cm)
B6 = (_BW*.5, _BH*.5)
B5 = (_BH*.5, _BW)
B4 = (_BW, _BH)
B3 = (_BH*2, _BW)
B2 = (_BW*2, _BH*2)
B1 = (_BH*4, _BW*2)
B0 = (_BW*4, _BH*4)
# }}}
# Basic PDF datatypes {{{
def serialize(o, stream):
if hasattr(o, 'pdf_serialize'):
o.pdf_serialize(stream)
elif isinstance(o, bool):
stream.write(b'true' if o else b'false')
elif isinstance(o, (int, float)):
stream.write(type(u'')(o).encode('ascii'))
elif o is None:
stream.write(b'null')
else:
raise ValueError('Unknown object: %r'%o)
class Name(unicode):
def pdf_serialize(self, stream):
raw = self.encode('ascii')
if len(raw) > 126:
raise ValueError('Name too long: %r'%self)
buf = [x if 33 < ord(x) < 126 and x != b'#' else b'#'+hex(ord(x)) for x
in raw]
stream.write(b'/'+b''.join(buf))
class String(unicode):
def pdf_serialize(self, stream):
s = self.replace('\\', '\\\\').replace('(', r'\(').replace(')', r'\)')
try:
raw = s.encode('latin1')
if raw.startswith(codecs.BOM_UTF16_BE):
raise UnicodeEncodeError('')
except UnicodeEncodeError:
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
stream.write(b'('+raw+b')')
class Dictionary(dict):
def pdf_serialize(self, stream):
stream.write(b'<<' + EOL)
for k, v in self.iteritems():
serialize(Name(k), stream)
stream.write(b' ')
serialize(v, stream)
stream.write(EOL)
stream.write(b'>>' + EOL)
class InlineDictionary(Dictionary):
def pdf_serialize(self, stream):
stream.write(b'<< ')
for k, v in self.iteritems():
serialize(Name(k), stream)
stream.write(b' ')
serialize(v, stream)
stream.write(b' ')
stream.write(b'>>')
class Array(list):
def pdf_serialize(self, stream):
stream.write(b'[')
for i, o in enumerate(self):
if i != 0:
stream.write(b' ')
serialize(o, stream)
stream.write(b']')
class Stream(BytesIO):
def __init__(self, compress=False):
BytesIO.__init__(self)
self.compress = compress
def pdf_serialize(self, stream):
raw = self.getvalue()
dl = len(raw)
filters = Array()
if self.compress:
filters.append(Name('FlateDecode'))
raw = zlib.compress(raw)
d = InlineDictionary({'Length':len(raw), 'DL':dl})
if filters:
d['Filter'] = filters
serialize(d, stream)
stream.write(EOL+b'stream'+EOL)
stream.write(raw)
stream.write(EOL+b'endstream'+EOL)
def write_line(self, raw=b''):
self.write(raw if isinstance(raw, bytes) else raw.encode('ascii'))
self.write(EOL)
def write(self, raw):
super(Stream, self).write(raw if isinstance(raw, bytes) else
raw.encode('ascii'))
class Reference(object):
def __init__(self, num, obj):
self.num, self.obj = num, obj
def pdf_serialize(self, stream):
raw = '%d 0 R'%self.num
stream.write(raw.encode('ascii'))
# }}}

View File

@ -11,13 +11,14 @@ import sys, traceback
from math import sqrt
from collections import namedtuple
from future_builtins import map
from functools import wraps
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPainterPath)
QTransform, QPainterPath, QFontMetricsF)
from calibre.constants import DEBUG
from calibre.ebooks.pdf.render.serialize import (Color, inch, A4, PDFStream,
Path)
from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path, Text)
from calibre.ebooks.pdf.render.common import inch, A4
XDPI = 1200
YDPI = 1200
@ -25,6 +26,17 @@ YDPI = 1200
Point = namedtuple('Point', 'x y')
ColorState = namedtuple('ColorState', 'color opacity do')
def store_error(func):
@wraps(func)
def errh(self, *args, **kwargs):
try:
func(self, *args, **kwargs)
except:
self.errors.append(traceback.format_exc())
return errh
class GraphicsState(object): # {{{
def __init__(self):
@ -156,6 +168,7 @@ class GraphicsState(object): # {{{
# Now apply the new operations
for op, val in ops.iteritems():
if op != 'clip':
self.apply(op, val, engine, pdf)
self.current_state[op] = val
@ -219,8 +232,9 @@ class PdfEngine(QPaintEngine):
self.do_stroke = True
self.do_fill = False
self.scale = sqrt(sy**2 + sx**2)
self.yscale = sy
self.xscale, self.yscale = sx, sy
self.graphics_state = GraphicsState()
self.errors = []
def init_page(self):
self.pdf.transform(self.pdf_system)
@ -246,7 +260,7 @@ class PdfEngine(QPaintEngine):
compress=not DEBUG)
self.init_page()
except:
traceback.print_exc()
self.errors.append(traceback.format_exc())
return False
return True
@ -261,7 +275,7 @@ class PdfEngine(QPaintEngine):
self.end_page(start_new=False)
self.pdf.end()
except:
traceback.print_exc()
self.errors.append(traceback.format_exc())
return False
finally:
self.pdf = self.file_object = None
@ -270,12 +284,15 @@ class PdfEngine(QPaintEngine):
def type(self):
return QPaintEngine.Pdf
@store_error
def drawPixmap(self, rect, pixmap, source_rect):
print ('TODO: drawPixmap() currently unimplemented')
@store_error
def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
print ('TODO: drawImage() currently unimplemented')
@store_error
def updateState(self, state):
self.graphics_state.read(state)
self.graphics_state(self)
@ -299,6 +316,7 @@ class PdfEngine(QPaintEngine):
p.curve_to(*(c1 + c2 + em))
return p
@store_error
def drawPath(self, path):
p = self.convert_path(path)
fill_rule = {Qt.OddEvenFill:'evenodd',
@ -312,6 +330,7 @@ class PdfEngine(QPaintEngine):
Qt.WindingFill:'winding'}[path.fillRule()]
self.pdf.add_clip(p, fill_rule=fill_rule)
@store_error
def drawPoints(self, points):
p = Path()
for point in points:
@ -319,14 +338,16 @@ class PdfEngine(QPaintEngine):
p.line_to(point.x(), point.y() + 0.001)
self.pdf.draw_path(p, stroke=self.do_stroke, fill=False)
@store_error
def drawRects(self, rects):
for rect in rects:
bl = rect.topLeft()
self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
stroke=self.do_stroke, fill=self.do_fill)
@store_error
def drawTextItem(self, point, text_item):
# super(PdfEngine, self).drawTextItem(point, text_item)
# super(PdfEngine, self).drawTextItem(point+QPoint(0, 300), text_item)
f = text_item.font()
px, pt = f.pixelSize(), f.pointSizeF()
if px == -1:
@ -343,37 +364,46 @@ class PdfEngine(QPaintEngine):
self.do_fill, self.do_stroke = f, s
return
to = self.canvas.beginText()
# set_transform(QTransform(1, 0, 0, -1, point.x(), point.y()), to.setTextTransform)
fontname = 'Times-Roman'
to.setFont(fontname, sz) # TODO: Embed font
to = Text()
to.size = sz
to.set_transform(1, 0, 0, -1, point.x(), point.y())
stretch = f.stretch()
if stretch != 100:
to.setHorizontalScale(stretch)
to.horizontal_scale = stretch
ws = f.wordSpacing()
if ws != 0:
to.setWordSpacing(self.map_dx(ws))
to.word_spacing = ws
spacing = f.letterSpacing()
st = f.letterSpacingType()
if st == f.AbsoluteSpacing and spacing != 0:
to.setCharSpace(spacing)
# TODO: Handle percentage letter spacing
to.char_space = spacing/self.scale
if st == f.PercentageSpacing and spacing not in {100, 0}:
# TODO: Implement this with the TJ operator
avg_char_width = QFontMetricsF(f).averageCharWidth()
to.char_space = (spacing - 100) * avg_char_width / 100
text = type(u'')(text_item.text())
to.textOut(text)
# TODO: handle colors
self.canvas.drawText(to)
to.text = text
with self:
self.graphics_state.apply_fill(self.graphics_state.current_state['stroke'],
self, self.pdf)
self.pdf.draw_text(to)
def draw_line(kind='underline'):
tw = self.canvas.stringWidth(text, fontname, sz)
p = self.canvas.beginPath()
m = QFontMetricsF(f)
tw = m.width(text)
p = Path()
if kind == 'underline':
dy = -text_item.descent()
dy = m.underlinePos()
elif kind == 'overline':
dy = text_item.ascent()
dy = -m.overlinePos()
elif kind == 'strikeout':
dy = text_item.ascent()/2
p.moveTo(point.x, point.y+dy)
p.lineTo(point.x+tw, point.y+dy)
dy = -m.strikeOutPos()
p.move_to(point.x(), point.y()+dy)
p.line_to(point.x()+tw, point.y()+dy)
with self:
self.graphics_state.apply_line_width(m.lineWidth(),
self, self.pdf)
self.pdf.draw_path(p, stroke=True, fill=False)
if f.underline():
draw_line()
@ -382,6 +412,7 @@ class PdfEngine(QPaintEngine):
if f.strikeOut():
draw_line('strikeout')
@store_error
def drawPolygon(self, points, mode):
if not points: return
p = Path()
@ -397,8 +428,10 @@ class PdfEngine(QPaintEngine):
def __enter__(self):
self.pdf.save_stack()
self.saved_ps = (self.do_stroke, self.do_fill)
def __exit__(self, *args):
self.do_stroke, self.do_fill = self.saved_ps
self.pdf.restore_stack()
class PdfDevice(QPaintDevice): # {{{
@ -470,13 +503,18 @@ if __name__ == '__main__':
p.drawLine(0, 0, 5000, 0)
p.restore()
# f = p.font()
# f.setPointSize(24)
# f.setFamily('Times New Roman')
# p.setFont(f)
# # p.scale(2, 2)
# p.rotate(45)
# p.drawText(QPoint(100, 300), 'Some text')
f = p.font()
f.setPointSize(24)
f.setUnderline(True)
f.setFamily('Times New Roman')
p.setFont(f)
# p.scale(2, 2)
p.rotate(45)
p.setPen(QColor(0, 255, 0))
p.drawText(QPoint(100, 300), 'Some text')
finally:
p.end()
if dev.engine.errors:
for err in dev.engine.errors: print (err)
raise SystemExit(1)

View File

@ -0,0 +1,35 @@
#!/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'
from calibre.ebooks.pdf.render.common import (
Dictionary, Name)
STANDARD_FONTS = {
'Times-Roman', 'Helvetica', 'Courier', 'Symbol', 'Times-Bold',
'Helvetica-Bold', 'Courier-Bold', 'ZapfDingbats', 'Times-Italic',
'Helvetica-Oblique', 'Courier-Oblique', 'Times-BoldItalic',
'Helvetica-BoldOblique', 'Courier-BoldOblique', }
class FontManager(object):
def __init__(self, objects):
self.objects = objects
self.std_map = {}
def add_standard_font(self, name):
if name not in STANDARD_FONTS:
raise ValueError('%s is not a standard font'%name)
if name not in self.std_map:
self.std_map[name] = self.objects.add(Dictionary({
'Type':Name('Font'),
'Subtype':Name('Type1'),
'BaseFont':Name(name)
}))
return self.std_map[name]

View File

@ -7,156 +7,19 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import codecs, zlib, hashlib
from io import BytesIO
import hashlib
from future_builtins import map
from collections import namedtuple
from calibre.constants import (__appname__, __version__)
from calibre.ebooks.pdf.render.common import (
Reference, EOL, serialize, Stream, Dictionary, String, Name, Array)
from calibre.ebooks.pdf.render.fonts import FontManager
PDFVER = b'%PDF-1.6'
EOL = b'\n'
Color = namedtuple('Color', 'red green blue opacity')
# Sizes {{{
inch = 72.0
cm = inch / 2.54
mm = cm * 0.1
pica = 12.0
_W, _H = (21*cm, 29.7*cm)
A6 = (_W*.5, _H*.5)
A5 = (_H*.5, _W)
A4 = (_W, _H)
A3 = (_H, _W*2)
A2 = (_W*2, _H*2)
A1 = (_H*2, _W*4)
A0 = (_W*4, _H*4)
LETTER = (8.5*inch, 11*inch)
LEGAL = (8.5*inch, 14*inch)
ELEVENSEVENTEEN = (11*inch, 17*inch)
_BW, _BH = (25*cm, 35.3*cm)
B6 = (_BW*.5, _BH*.5)
B5 = (_BH*.5, _BW)
B4 = (_BW, _BH)
B3 = (_BH*2, _BW)
B2 = (_BW*2, _BH*2)
B1 = (_BH*4, _BW*2)
B0 = (_BW*4, _BH*4)
# }}}
# Basic PDF datatypes {{{
def serialize(o, stream):
if hasattr(o, 'pdf_serialize'):
o.pdf_serialize(stream)
elif isinstance(o, bool):
stream.write(b'true' if o else b'false')
elif isinstance(o, (int, float)):
stream.write(type(u'')(o).encode('ascii'))
elif o is None:
stream.write(b'null')
else:
raise ValueError('Unknown object: %r'%o)
class Name(unicode):
def pdf_serialize(self, stream):
raw = self.encode('ascii')
if len(raw) > 126:
raise ValueError('Name too long: %r'%self)
buf = [x if 33 < ord(x) < 126 and x != b'#' else b'#'+hex(ord(x)) for x
in raw]
stream.write(b'/'+b''.join(buf))
class String(unicode):
def pdf_serialize(self, stream):
s = self.replace('\\', '\\\\').replace('(', r'\(').replace(')', r'\)')
try:
raw = s.encode('latin1')
if raw.startswith(codecs.BOM_UTF16_BE):
raise UnicodeEncodeError('')
except UnicodeEncodeError:
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
stream.write(b'('+raw+b')')
class Dictionary(dict):
def pdf_serialize(self, stream):
stream.write(b'<<' + EOL)
for k, v in self.iteritems():
serialize(Name(k), stream)
stream.write(b' ')
serialize(v, stream)
stream.write(EOL)
stream.write(b'>>' + EOL)
class InlineDictionary(Dictionary):
def pdf_serialize(self, stream):
stream.write(b'<< ')
for k, v in self.iteritems():
serialize(Name(k), stream)
stream.write(b' ')
serialize(v, stream)
stream.write(b' ')
stream.write(b'>>')
class Array(list):
def pdf_serialize(self, stream):
stream.write(b'[')
for i, o in enumerate(self):
if i != 0:
stream.write(b' ')
serialize(o, stream)
stream.write(b']')
class Stream(BytesIO):
def __init__(self, compress=False):
BytesIO.__init__(self)
self.compress = compress
def pdf_serialize(self, stream):
raw = self.getvalue()
dl = len(raw)
filters = Array()
if self.compress:
filters.append(Name('FlateDecode'))
raw = zlib.compress(raw)
d = InlineDictionary({'Length':len(raw), 'DL':dl})
if filters:
d['Filter'] = filters
serialize(d, stream)
stream.write(EOL+b'stream'+EOL)
stream.write(raw)
stream.write(EOL+b'endstream'+EOL)
def write_line(self, raw=b''):
self.write(raw if isinstance(raw, bytes) else raw.encode('ascii'))
self.write(EOL)
def write(self, raw):
super(Stream, self).write(raw if isinstance(raw, bytes) else
raw.encode('ascii'))
class Reference(object):
def __init__(self, num, obj):
self.num, self.obj = num, obj
def pdf_serialize(self, stream):
raw = '%d 0 R'%self.num
stream.write(raw.encode('ascii'))
# }}}
class IndirectObjects(object):
def __init__(self):
@ -222,6 +85,7 @@ class Page(Stream):
'Parent': parentref,
})
self.opacities = {}
self.fonts = {}
def set_opacity(self, opref):
if opref not in self.opacities:
@ -230,6 +94,11 @@ class Page(Stream):
serialize(Name(name), self)
self.write(b' gs ')
def add_font(self, fontref):
if fontref not in self.fonts:
self.fonts[fontref] = 'F%d'%len(self.fonts)
return self.fonts[fontref]
def add_resources(self):
r = Dictionary()
if self.opacities:
@ -237,6 +106,11 @@ class Page(Stream):
for opref, name in self.opacities.iteritems():
extgs[name] = opref
r['ExtGState'] = extgs
if self.fonts:
fonts = Dictionary()
for ref, name in self.fonts.iteritems():
fonts[name] = ref
r['Font'] = fonts
if r:
self.page_dict['Resources'] = r
@ -263,6 +137,44 @@ class Path(object):
def curve_to(self, x1, y1, x2, y2, x, y):
self.ops.append((x1, y1, x2, y2, x, y, 'c'))
class Text(object):
def __init__(self):
self.transform = self.default_transform = [1, 0, 0, 1, 0, 0]
self.font_name = 'Times-Roman'
self.font_path = None
self.horizontal_scale = self.default_horizontal_scale = 100
self.word_spacing = self.default_word_spacing = 0
self.char_space = self.default_char_space = 0
self.size = 12
self.text = ''
def set_transform(self, *args):
if len(args) == 1:
m = args[0]
vals = [m.m11(), m.m12(), m.m21(), m.m22(), m.dx(), m.dy()]
else:
vals = args
self.transform = vals
def pdf_serialize(self, stream, font_name):
if not self.text: return
stream.write_line('BT ')
serialize(Name(font_name), stream)
stream.write(' %g Tf '%self.size)
stream.write(' '.join(map(type(u''), self.transform)) + ' Tm ')
if self.horizontal_scale != self.default_horizontal_scale:
stream.write('%g Tz '%self.horizontal_scale)
if self.word_spacing != self.default_word_spacing:
stream.write('%g Tw '%self.word_spacing)
if self.char_space != self.default_char_space:
stream.write('%g Tc '%self.char_space)
stream.write_line()
serialize(String(self.text), stream)
stream.write(' Tj ')
stream.write_line('ET')
class Catalog(Dictionary):
def __init__(self, pagetree):
@ -325,6 +237,7 @@ class PDFStream(object):
self.info = Dictionary({'Creator':String(creator),
'Producer':String(creator)})
self.stroke_opacities, self.fill_opacities = {}, {}
self.font_manager = FontManager(self.objects)
@property
def page_tree(self):
@ -377,8 +290,9 @@ class PDFStream(object):
def add_clip(self, path, fill_rule='winding'):
if not path.ops: return
self.write_path(path)
op = 'W' if fill_rule == 'winding' else 'W*'
self.current_page.write(op + ' ' + 'n')
self.current_page.write_line(op + ' ' + 'n')
def set_dash(self, array, phase=0):
array = Array(array)
@ -421,6 +335,14 @@ class PDFStream(object):
self.page_tree.obj.add_page(pageref)
self.current_page = Page(self.page_tree, compress=self.compress)
def draw_text(self, text_object):
if text_object.font_path is None:
fontref = self.font_manager.add_standard_font(text_object.font_name)
else:
raise NotImplementedError()
name = self.current_page.add_font(fontref)
text_object.pdf_serialize(self.current_page, name)
def end(self):
if self.current_page.getvalue():
self.end_page()