mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add code to get metrics from fonts, convert QRawFont to an Sfnt and fix curve drawing
This commit is contained in:
parent
391a58f9e9
commit
5e9a943cc0
@ -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()
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
86
src/calibre/utils/fonts/sfnt/metrics.py
Normal file
86
src/calibre/utils/fonts/sfnt/metrics.py
Normal 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]))
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user