diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index 2336dab85c..5751f5fa01 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -10,15 +10,16 @@ __docformat__ = 'restructuredtext en' 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, QFontMetricsF) + QTransform, QPainterPath, QRawFont) from calibre.constants import DEBUG from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path, Text) from calibre.ebooks.pdf.render.common import inch, A4 +from calibre.utils.fonts.sfnt.container import Sfnt +from calibre.utils.fonts.sfnt.metrics import FontMetrics XDPI = 1200 YDPI = 1200 @@ -309,11 +310,16 @@ class PdfEngine(QPaintEngine): elif elem.isLineTo(): p.line_to(*em) elif elem.isCurveTo(): + added = False if path.elementCount() > i+1: - c1, c2 = map(lambda j:( - path.elementAt(j).x, path.elementAt(j).y), (i, i+1)) - i += 2 - p.curve_to(*(c1 + c2 + em)) + c1, c2 = path.elementAt(i), path.elementAt(i+1) + if (c1.type == path.CurveToDataElement and c2.type == + path.CurveToDataElement): + i += 2 + p.curve_to(em[0], em[1], c1.x, c1.y, c2.x, c2.y) + added = True + if not added: + raise ValueError('Invalid curve to operation') return p @store_error @@ -355,15 +361,8 @@ class PdfEngine(QPaintEngine): else: sz = px - q = self.qt_system - if not q.isIdentity() and q.type() > q.TxShear: - # We cant map this transform to a PDF text transform operator - f, s = self.do_fill, self.do_stroke - self.do_fill, self.do_stroke = True, False - super(PdfEngine, self).drawTextItem(point, text_item) - self.do_fill, self.do_stroke = f, s - return - + r = QRawFont.fromFont(f) + metrics = FontMetrics(Sfnt(r)) to = Text() to.size = sz to.set_transform(1, 0, 0, -1, point.x(), point.y()) @@ -375,43 +374,24 @@ class PdfEngine(QPaintEngine): to.word_spacing = ws spacing = f.letterSpacing() st = f.letterSpacingType() + text = type(u'')(text_item.text()) if st == f.AbsoluteSpacing and spacing != 0: 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()) + # TODO: Figure out why the results from uncommenting the super + # class call above differ. The advance widths are the same as those + # reported by QRawfont, so presumably, Qt use some other + # algorithm, I can't be bothered to track it down. This behavior is + # correct as per the Qt docs' description of PercentageSpacing + widths = [w*-1 for w in metrics.advance_widths(text, + sz, f.stretch()/100.)] + to.glyph_adjust = ((spacing-100)/100., widths) 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'): - m = QFontMetricsF(f) - tw = m.width(text) - p = Path() - if kind == 'underline': - dy = m.underlinePos() - elif kind == 'overline': - dy = -m.overlinePos() - elif kind == 'strikeout': - 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() - if f.overline(): - draw_line('overline') - if f.strikeOut(): - draw_line('strikeout') - @store_error def drawPolygon(self, points, mode): if not points: return @@ -419,8 +399,7 @@ class PdfEngine(QPaintEngine): p.move_to(points[0].x(), points[0].y()) for point in points[1:]: p.line_to(point.x(), point.y()) - if points[-1] != points[0]: - p.line_to(points[0].x(), points[0].y()) + p.close() fill_rule = {self.OddEvenMode:'evenodd', self.WindingMode:'winding'}.get(mode, 'evenodd') self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule, @@ -504,13 +483,16 @@ if __name__ == '__main__': p.restore() f = p.font() - f.setPointSize(24) - f.setUnderline(True) + f.setPointSize(48) + f.setLetterSpacing(f.PercentageSpacing, 200) + # f.setUnderline(True) + # f.setOverline(True) + # f.setStrikeOut(True) f.setFamily('Times New Roman') p.setFont(f) # p.scale(2, 2) - p.rotate(45) - p.setPen(QColor(0, 255, 0)) + # p.rotate(45) + p.setPen(QColor(0, 0, 255)) p.drawText(QPoint(100, 300), 'Some text') finally: p.end() diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index 8d0b40eb49..a5ae944356 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' import hashlib from future_builtins import map +from itertools import izip from collections import namedtuple from calibre.constants import (__appname__, __version__) @@ -137,6 +138,9 @@ class Path(object): def curve_to(self, x1, y1, x2, y2, x, y): self.ops.append((x1, y1, x2, y2, x, y, 'c')) + def close(self): + self.ops.append(('h',)) + class Text(object): def __init__(self): @@ -146,6 +150,7 @@ class Text(object): 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.glyph_adjust = self.default_glyph_adjust = None self.size = 12 self.text = '' @@ -170,8 +175,17 @@ class Text(object): 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 ') + if self.glyph_adjust is self.default_glyph_adjust: + serialize(String(self.text), stream) + stream.write(' Tj ') + else: + chars = Array() + frac, widths = self.glyph_adjust + for c, width in izip(self.text, widths): + chars.append(String(c)) + chars.append(int(width * frac)) + serialize(chars, stream) + stream.write(' TJ ') stream.write_line('ET') diff --git a/src/calibre/utils/fonts/sfnt/container.py b/src/calibre/utils/fonts/sfnt/container.py index 92246fe1a9..92eb814337 100644 --- a/src/calibre/utils/fonts/sfnt/container.py +++ b/src/calibre/utils/fonts/sfnt/container.py @@ -16,7 +16,8 @@ from calibre.utils.fonts.utils import (get_tables, checksum_of_block, from calibre.utils.fonts.sfnt import align_block, UnknownTable, max_power_of_two from calibre.utils.fonts.sfnt.errors import UnsupportedFont -from calibre.utils.fonts.sfnt.head import HeadTable +from calibre.utils.fonts.sfnt.head import (HeadTable, HorizontalHeader, + OS2Table, PostTable) from calibre.utils.fonts.sfnt.maxp import MaxpTable from calibre.utils.fonts.sfnt.loca import LocaTable from calibre.utils.fonts.sfnt.glyf import GlyfTable @@ -29,26 +30,42 @@ from calibre.utils.fonts.sfnt.cff.table import CFFTable class Sfnt(object): - def __init__(self, raw): - self.sfnt_version = raw[:4] - if self.sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO', b'true', - b'type1'}: - raise UnsupportedFont('Font has unknown sfnt version: %r'%self.sfnt_version) - self.read_tables(raw) + TABLE_MAP = { + b'head' : HeadTable, + b'hhea' : HorizontalHeader, + b'maxp' : MaxpTable, + b'loca' : LocaTable, + b'glyf' : GlyfTable, + b'cmap' : CmapTable, + b'CFF ' : CFFTable, + b'kern' : KernTable, + b'GSUB' : GSUBTable, + b'OS/2' : OS2Table, + b'post' : PostTable, + } - def read_tables(self, raw): + def __init__(self, raw_or_qrawfont): self.tables = {} - for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw): - self.tables[table_tag] = { - b'head' : HeadTable, - b'maxp' : MaxpTable, - b'loca' : LocaTable, - b'glyf' : GlyfTable, - b'cmap' : CmapTable, - b'CFF ' : CFFTable, - b'kern' : KernTable, - b'GSUB' : GSUBTable, - }.get(table_tag, UnknownTable)(table) + if isinstance(raw_or_qrawfont, bytes): + raw = raw_or_qrawfont + self.sfnt_version = raw[:4] + if self.sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO', b'true', + b'type1'}: + raise UnsupportedFont('Font has unknown sfnt version: %r'%self.sfnt_version) + for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw): + self.tables[table_tag] = self.TABLE_MAP.get( + table_tag, UnknownTable)(table) + else: + for table_tag in { + b'cmap', b'hhea', b'head', b'hmtx', b'maxp', b'name', b'OS/2', + b'post', b'cvt ', b'fpgm', b'glyf', b'loca', b'prep', b'CFF ', + b'VORG', b'EBDT', b'EBLC', b'EBSC', b'BASE', b'GSUB', b'GPOS', + b'GDEF', b'JSTF', b'gasp', b'hdmx', b'kern', b'LTSH', b'PCLT', + b'VDMX', b'vhea', b'vmtx', b'MATH'}: + table = bytes(raw_or_qrawfont.fontTable(table_tag)) + if table: + self.tables[table_tag] = self.TABLE_MAP.get( + table_tag, UnknownTable)(table) def __getitem__(self, key): return self.tables[key] @@ -140,7 +157,8 @@ def test_roundtrip(ff=None): if data[:12] != rd[:12]: raise ValueError('Roundtripping failed, font header not the same') if len(data) != len(rd): - raise ValueError('Roundtripping failed, size different') + raise ValueError('Roundtripping failed, size different (%d vs. %d)'% + (len(data), len(rd))) if __name__ == '__main__': import sys diff --git a/src/calibre/utils/fonts/sfnt/head.py b/src/calibre/utils/fonts/sfnt/head.py index 584d685cc5..1a1a919860 100644 --- a/src/calibre/utils/fonts/sfnt/head.py +++ b/src/calibre/utils/fonts/sfnt/head.py @@ -11,6 +11,7 @@ from itertools import izip from struct import unpack_from, pack from calibre.utils.fonts.sfnt import UnknownTable, DateTimeProperty, FixedProperty +from calibre.utils.fonts.sfnt.errors import UnsupportedFont class HeadTable(UnknownTable): @@ -52,4 +53,75 @@ class HeadTable(UnknownTable): vals = [getattr(self, f) for f in self._fields] self.raw = pack(self._fmt, *vals) +class HorizontalHeader(UnknownTable): + + version_number = FixedProperty('_version_number') + + def read_data(self, hmtx): + if hasattr(self, 'ascender'): return + field_types = ( + '_version_number' , 'l', + 'ascender', 'h', + 'descender', 'h', + 'line_gap', 'h', + 'advance_width_max', 'H', + 'min_left_size_bearing', 'h', + 'min_right_side_bearing', 'h', + 'x_max_extent', 'h', + 'caret_slope_rise', 'h', + 'caret_slop_run', 'h', + 'caret_offset', 'h', + 'r1', 'h', + 'r2', 'h', + 'r3', 'h', + 'r4', 'h', + 'metric_data_format', 'h', + 'number_of_h_metrics', 'H', + ) + + self._fmt = ('>%s'%(''.join(field_types[1::2]))).encode('ascii') + self._fields = field_types[0::2] + + for f, val in izip(self._fields, unpack_from(self._fmt, self.raw)): + setattr(self, f, val) + + raw = hmtx.raw + num = self.number_of_h_metrics + if len(raw) < 4*num: + raise UnsupportedFont('The hmtx table has insufficient data') + long_hor_metric = raw[:4*num] + fmt = '>%dH'%(2*num) + entries = unpack_from(fmt.encode('ascii'), long_hor_metric) + self.advance_widths = entries[0::2] + fmt = '>%dh'%(2*num) + entries = unpack_from(fmt.encode('ascii'), long_hor_metric) + self.left_side_bearings = entries[1::2] + +class OS2Table(UnknownTable): + + version_number = FixedProperty('_version') + + def read_data(self): + if hasattr(self, 'char_width'): return + from calibre.utils.fonts.utils import get_font_characteristics + vals = get_font_characteristics(self.raw, raw_is_table=True, + return_all=True) + for i, attr in enumerate(( + '_version', 'char_width', 'weight', 'width', 'fs_type', + 'subscript_x_size', 'subscript_y_size', 'subscript_x_offset', + 'subscript_y_offset', 'superscript_x_size', 'superscript_y_size', + 'superscript_x_offset', 'superscript_y_offset', 'strikeout_size', + 'strikeout_position', 'family_class', 'panose', 'selection', + 'is_italic', 'is_bold', 'is_regular')): + setattr(self, attr, vals[i]) + +class PostTable(UnknownTable): + + version_number = FixedProperty('_version') + italic_angle = FixedProperty('_italic_angle') + + def read_data(self): + if hasattr(self, 'underline_position'): return + (self._version, self._italic_angle, self.underline_position, + self.underline_thickness) = unpack_from(b'>llhh', self.raw) diff --git a/src/calibre/utils/fonts/sfnt/metrics.py b/src/calibre/utils/fonts/sfnt/metrics.py new file mode 100644 index 0000000000..4cbe5dba68 --- /dev/null +++ b/src/calibre/utils/fonts/sfnt/metrics.py @@ -0,0 +1,86 @@ +#!/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 ' +__docformat__ = 'restructuredtext en' + +from future_builtins import map + +class FontMetrics(object): + + ''' + Get various metrics for the specified sfnt. All the metrics are returned in + units of pixels. To calculate a metric you have to specify the font size + (in pixels) and the horizontal stretch factor (between 0.0 and 1.0). + ''' + + def __init__(self, sfnt): + self.sfnt = sfnt + + hhea = self.sfnt[b'hhea'] + hhea.read_data(self.sfnt[b'hmtx']) + self.ascent = hhea.ascender + self.descent = hhea.descender + self._advance_widths = hhea.advance_widths + self.cmap = self.sfnt[b'cmap'] + self.head = self.sfnt[b'head'] + self.units_per_em = self.head.units_per_em + self.os2 = self.sfnt[b'OS/2'] + self.os2.read_data() + self.post = self.sfnt[b'post'] + self.post.read_data() + + def underline_thickness(self, pixel_size=12.0): + 'Thickness for lines (in pixels) at the specified size' + yscale = pixel_size / self.units_per_em + return self.post.underline_thickness * yscale + + def underline_position(self, pixel_size=12.0): + yscale = pixel_size / self.units_per_em + return self.post.underline_position * yscale + + def overline_position(self, pixel_size=12.0): + yscale = pixel_size / self.units_per_em + return (self.ascent + 2) * yscale + + def strikeout_size(self, pixel_size=12.0): + 'The width of the strikeout line, in pixels' + yscale = pixel_size / self.units_per_em + return yscale * self.os2.strikeout_size + + def strikeout_position(self, pixel_size=12.0): + 'The displacement from the baseline to top of the strikeout line, in pixels' + yscale = pixel_size / self.units_per_em + return yscale * self.os2.strikeout_position + + def advance_widths(self, string, pixel_size=12.0, stretch=1.0): + ''' + Return the advance widths (in pixels) for all glyphs corresponding to + the characters in string at the specified pixel_size and stretch factor. + ''' + if not isinstance(string, type(u'')): + raise ValueError('Must supply a unicode object') + chars = tuple(map(ord, string)) + cmap = self.cmap.get_character_map(chars) + glyph_ids = (cmap[c] for c in chars) + last = len(self._advance_widths) + pixel_size_x = stretch * pixel_size + xscale = pixel_size_x / self.units_per_em + return tuple(self._advance_widths[i if i < last else -1]*xscale for i in glyph_ids) + + def width(self, string, pixel_size=12.0, stretch=1.0): + 'The width of the string at the specified pixel size and stretch, in pixels' + return sum(self.advance_widths(string, pixel_size, stretch)) + +if __name__ == '__main__': + import sys + from calibre.utils.fonts.sfnt.container import Sfnt + with open(sys.argv[-2], 'rb') as f: + raw = f.read() + sfnt = Sfnt(raw) + m = FontMetrics(sfnt) + print (m.advance_widths(sys.argv[-1])) + diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index 8aeaccc16a..25793723e5 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -41,7 +41,7 @@ def get_table(raw, name): return table, table_index, table_offset, table_checksum return None, None, None, None -def get_font_characteristics(raw, raw_is_table=False): +def get_font_characteristics(raw, raw_is_table=False, return_all=False): ''' Return (weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws). These @@ -79,6 +79,13 @@ def get_font_characteristics(raw, raw_is_table=False): is_regular = (selection & (1 << 6)) != 0 is_wws = (selection & (1 << 8)) != 0 is_oblique = (selection & (1 << 9)) != 0 + if return_all: + return (version, char_width, weight, width, fs_type, subscript_x_size, + subscript_y_size, subscript_x_offset, subscript_y_offset, + superscript_x_size, superscript_y_size, superscript_x_offset, + superscript_y_offset, strikeout_size, strikeout_position, + family_class, panose, selection, is_italic, is_bold, is_regular) + return weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws, version def panose_to_css_generic_family(panose):