Add code to get metrics from fonts, convert QRawFont to an Sfnt and fix curve drawing

This commit is contained in:
Kovid Goyal 2012-12-17 09:22:37 +05:30
parent 391a58f9e9
commit 5e9a943cc0
6 changed files with 251 additions and 72 deletions

View File

@ -10,15 +10,16 @@ __docformat__ = 'restructuredtext en'
import sys, traceback import sys, traceback
from math import sqrt from math import sqrt
from collections import namedtuple from collections import namedtuple
from future_builtins import map
from functools import wraps from functools import wraps
from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter,
QTransform, QPainterPath, QFontMetricsF) QTransform, QPainterPath, QRawFont)
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path, Text) from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path, Text)
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.metrics import FontMetrics
XDPI = 1200 XDPI = 1200
YDPI = 1200 YDPI = 1200
@ -309,11 +310,16 @@ class PdfEngine(QPaintEngine):
elif elem.isLineTo(): elif elem.isLineTo():
p.line_to(*em) p.line_to(*em)
elif elem.isCurveTo(): elif elem.isCurveTo():
added = False
if path.elementCount() > i+1: if path.elementCount() > i+1:
c1, c2 = map(lambda j:( c1, c2 = path.elementAt(i), path.elementAt(i+1)
path.elementAt(j).x, path.elementAt(j).y), (i, i+1)) if (c1.type == path.CurveToDataElement and c2.type ==
i += 2 path.CurveToDataElement):
p.curve_to(*(c1 + c2 + em)) 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 return p
@store_error @store_error
@ -355,15 +361,8 @@ class PdfEngine(QPaintEngine):
else: else:
sz = px sz = px
q = self.qt_system r = QRawFont.fromFont(f)
if not q.isIdentity() and q.type() > q.TxShear: metrics = FontMetrics(Sfnt(r))
# 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
to = Text() to = Text()
to.size = sz to.size = sz
to.set_transform(1, 0, 0, -1, point.x(), point.y()) to.set_transform(1, 0, 0, -1, point.x(), point.y())
@ -375,43 +374,24 @@ class PdfEngine(QPaintEngine):
to.word_spacing = ws to.word_spacing = ws
spacing = f.letterSpacing() spacing = f.letterSpacing()
st = f.letterSpacingType() st = f.letterSpacingType()
text = type(u'')(text_item.text())
if st == f.AbsoluteSpacing and spacing != 0: if st == f.AbsoluteSpacing and spacing != 0:
to.char_space = spacing/self.scale to.char_space = spacing/self.scale
if st == f.PercentageSpacing and spacing not in {100, 0}: if st == f.PercentageSpacing and spacing not in {100, 0}:
# TODO: Implement this with the TJ operator # TODO: Figure out why the results from uncommenting the super
avg_char_width = QFontMetricsF(f).averageCharWidth() # class call above differ. The advance widths are the same as those
to.char_space = (spacing - 100) * avg_char_width / 100 # reported by QRawfont, so presumably, Qt use some other
text = type(u'')(text_item.text()) # 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 to.text = text
with self: with self:
self.graphics_state.apply_fill(self.graphics_state.current_state['stroke'], self.graphics_state.apply_fill(self.graphics_state.current_state['stroke'],
self, self.pdf) self, self.pdf)
self.pdf.draw_text(to) 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 @store_error
def drawPolygon(self, points, mode): def drawPolygon(self, points, mode):
if not points: return if not points: return
@ -419,8 +399,7 @@ class PdfEngine(QPaintEngine):
p.move_to(points[0].x(), points[0].y()) p.move_to(points[0].x(), points[0].y())
for point in points[1:]: for point in points[1:]:
p.line_to(point.x(), point.y()) p.line_to(point.x(), point.y())
if points[-1] != points[0]: p.close()
p.line_to(points[0].x(), points[0].y())
fill_rule = {self.OddEvenMode:'evenodd', fill_rule = {self.OddEvenMode:'evenodd',
self.WindingMode:'winding'}.get(mode, 'evenodd') self.WindingMode:'winding'}.get(mode, 'evenodd')
self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule, self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule,
@ -504,13 +483,16 @@ if __name__ == '__main__':
p.restore() p.restore()
f = p.font() f = p.font()
f.setPointSize(24) f.setPointSize(48)
f.setUnderline(True) f.setLetterSpacing(f.PercentageSpacing, 200)
# f.setUnderline(True)
# f.setOverline(True)
# f.setStrikeOut(True)
f.setFamily('Times New Roman') f.setFamily('Times New Roman')
p.setFont(f) p.setFont(f)
# p.scale(2, 2) # p.scale(2, 2)
p.rotate(45) # p.rotate(45)
p.setPen(QColor(0, 255, 0)) p.setPen(QColor(0, 0, 255))
p.drawText(QPoint(100, 300), 'Some text') p.drawText(QPoint(100, 300), 'Some text')
finally: finally:
p.end() p.end()

View File

@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en'
import hashlib import hashlib
from future_builtins import map from future_builtins import map
from itertools import izip
from collections import namedtuple from collections import namedtuple
from calibre.constants import (__appname__, __version__) from calibre.constants import (__appname__, __version__)
@ -137,6 +138,9 @@ class Path(object):
def curve_to(self, x1, y1, x2, y2, x, y): def curve_to(self, x1, y1, x2, y2, x, y):
self.ops.append((x1, y1, x2, y2, x, y, 'c')) self.ops.append((x1, y1, x2, y2, x, y, 'c'))
def close(self):
self.ops.append(('h',))
class Text(object): class Text(object):
def __init__(self): def __init__(self):
@ -146,6 +150,7 @@ class Text(object):
self.horizontal_scale = self.default_horizontal_scale = 100 self.horizontal_scale = self.default_horizontal_scale = 100
self.word_spacing = self.default_word_spacing = 0 self.word_spacing = self.default_word_spacing = 0
self.char_space = self.default_char_space = 0 self.char_space = self.default_char_space = 0
self.glyph_adjust = self.default_glyph_adjust = None
self.size = 12 self.size = 12
self.text = '' self.text = ''
@ -170,8 +175,17 @@ class Text(object):
if self.char_space != self.default_char_space: if self.char_space != self.default_char_space:
stream.write('%g Tc '%self.char_space) stream.write('%g Tc '%self.char_space)
stream.write_line() stream.write_line()
serialize(String(self.text), stream) if self.glyph_adjust is self.default_glyph_adjust:
stream.write(' Tj ') 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') stream.write_line('ET')

View File

@ -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 import align_block, UnknownTable, max_power_of_two
from calibre.utils.fonts.sfnt.errors import UnsupportedFont 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.maxp import MaxpTable
from calibre.utils.fonts.sfnt.loca import LocaTable from calibre.utils.fonts.sfnt.loca import LocaTable
from calibre.utils.fonts.sfnt.glyf import GlyfTable from calibre.utils.fonts.sfnt.glyf import GlyfTable
@ -29,26 +30,42 @@ from calibre.utils.fonts.sfnt.cff.table import CFFTable
class Sfnt(object): class Sfnt(object):
def __init__(self, raw): TABLE_MAP = {
self.sfnt_version = raw[:4] b'head' : HeadTable,
if self.sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO', b'true', b'hhea' : HorizontalHeader,
b'type1'}: b'maxp' : MaxpTable,
raise UnsupportedFont('Font has unknown sfnt version: %r'%self.sfnt_version) b'loca' : LocaTable,
self.read_tables(raw) 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 = {} self.tables = {}
for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw): if isinstance(raw_or_qrawfont, bytes):
self.tables[table_tag] = { raw = raw_or_qrawfont
b'head' : HeadTable, self.sfnt_version = raw[:4]
b'maxp' : MaxpTable, if self.sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO', b'true',
b'loca' : LocaTable, b'type1'}:
b'glyf' : GlyfTable, raise UnsupportedFont('Font has unknown sfnt version: %r'%self.sfnt_version)
b'cmap' : CmapTable, for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw):
b'CFF ' : CFFTable, self.tables[table_tag] = self.TABLE_MAP.get(
b'kern' : KernTable, table_tag, UnknownTable)(table)
b'GSUB' : GSUBTable, else:
}.get(table_tag, UnknownTable)(table) 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): def __getitem__(self, key):
return self.tables[key] return self.tables[key]
@ -140,7 +157,8 @@ def test_roundtrip(ff=None):
if data[:12] != rd[:12]: if data[:12] != rd[:12]:
raise ValueError('Roundtripping failed, font header not the same') raise ValueError('Roundtripping failed, font header not the same')
if len(data) != len(rd): 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__': if __name__ == '__main__':
import sys import sys

View File

@ -11,6 +11,7 @@ from itertools import izip
from struct import unpack_from, pack from struct import unpack_from, pack
from calibre.utils.fonts.sfnt import UnknownTable, DateTimeProperty, FixedProperty from calibre.utils.fonts.sfnt import UnknownTable, DateTimeProperty, FixedProperty
from calibre.utils.fonts.sfnt.errors import UnsupportedFont
class HeadTable(UnknownTable): class HeadTable(UnknownTable):
@ -52,4 +53,75 @@ class HeadTable(UnknownTable):
vals = [getattr(self, f) for f in self._fields] vals = [getattr(self, f) for f in self._fields]
self.raw = pack(self._fmt, *vals) 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)

View File

@ -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 <kovid at kovidgoyal.net>'
__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]))

View File

@ -41,7 +41,7 @@ def get_table(raw, name):
return table, table_index, table_offset, table_checksum return table, table_index, table_offset, table_checksum
return None, None, None, None 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, Return (weight, is_italic, is_bold, is_regular, fs_type, panose, width,
is_oblique, is_wws). These 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_regular = (selection & (1 << 6)) != 0
is_wws = (selection & (1 << 8)) != 0 is_wws = (selection & (1 << 8)) != 0
is_oblique = (selection & (1 << 9)) != 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 return weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws, version
def panose_to_css_generic_family(panose): def panose_to_css_generic_family(panose):