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
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))
c1, c2 = path.elementAt(i), path.elementAt(i+1)
if (c1.type == path.CurveToDataElement and c2.type ==
path.CurveToDataElement):
i += 2
p.curve_to(*(c1 + c2 + em))
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()

View File

@ -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()
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')

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.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,18 +30,9 @@ 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)
def read_tables(self, raw):
self.tables = {}
for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw):
self.tables[table_tag] = {
TABLE_MAP = {
b'head' : HeadTable,
b'hhea' : HorizontalHeader,
b'maxp' : MaxpTable,
b'loca' : LocaTable,
b'glyf' : GlyfTable,
@ -48,7 +40,32 @@ class Sfnt(object):
b'CFF ' : CFFTable,
b'kern' : KernTable,
b'GSUB' : GSUBTable,
}.get(table_tag, UnknownTable)(table)
b'OS/2' : OS2Table,
b'post' : PostTable,
}
def __init__(self, raw_or_qrawfont):
self.tables = {}
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

View File

@ -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)

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 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):