mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
PDF Output: Implement embedding of TrueType fonts (without subsetting or ToUnicode maps)
This commit is contained in:
parent
8a0417c72b
commit
ea50efb4f5
@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import codecs, zlib
|
import codecs, zlib
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from struct import pack
|
||||||
|
|
||||||
EOL = b'\n'
|
EOL = b'\n'
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ def serialize(o, stream):
|
|||||||
o.pdf_serialize(stream)
|
o.pdf_serialize(stream)
|
||||||
elif isinstance(o, bool):
|
elif isinstance(o, bool):
|
||||||
stream.write(b'true' if o else b'false')
|
stream.write(b'true' if o else b'false')
|
||||||
elif isinstance(o, (int, float)):
|
elif isinstance(o, (int, long, float)):
|
||||||
stream.write(type(u'')(o).encode('ascii'))
|
stream.write(type(u'')(o).encode('ascii'))
|
||||||
elif o is None:
|
elif o is None:
|
||||||
stream.write(b'null')
|
stream.write(b'null')
|
||||||
@ -78,14 +79,30 @@ class String(unicode):
|
|||||||
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
|
raw = codecs.BOM_UTF16_BE + s.encode('utf-16-be')
|
||||||
stream.write(b'('+raw+b')')
|
stream.write(b'('+raw+b')')
|
||||||
|
|
||||||
|
class GlyphIndex(object):
|
||||||
|
|
||||||
|
def __init__(self, code, compress):
|
||||||
|
self.code = code
|
||||||
|
self.compress = compress
|
||||||
|
|
||||||
|
def pdf_serialize(self, stream):
|
||||||
|
if self.compress:
|
||||||
|
stream.write(pack(b'>sHs', b'(', self.code, b')'))
|
||||||
|
else:
|
||||||
|
byts = bytearray(pack(b'>H', self.code))
|
||||||
|
stream.write('<%s>'%''.join(map(
|
||||||
|
lambda x: bytes(hex(x)[2:]).rjust(2, b'0'), byts)))
|
||||||
|
|
||||||
class Dictionary(dict):
|
class Dictionary(dict):
|
||||||
|
|
||||||
def pdf_serialize(self, stream):
|
def pdf_serialize(self, stream):
|
||||||
stream.write(b'<<' + EOL)
|
stream.write(b'<<' + EOL)
|
||||||
for k, v in self.iteritems():
|
sorted_keys = sorted(self.iterkeys(),
|
||||||
|
key=lambda x:((' ' if x == 'Type' else '')+x))
|
||||||
|
for k in sorted_keys:
|
||||||
serialize(Name(k), stream)
|
serialize(Name(k), stream)
|
||||||
stream.write(b' ')
|
stream.write(b' ')
|
||||||
serialize(v, stream)
|
serialize(self[k], stream)
|
||||||
stream.write(EOL)
|
stream.write(EOL)
|
||||||
stream.write(b'>>' + EOL)
|
stream.write(b'>>' + EOL)
|
||||||
|
|
||||||
@ -116,6 +133,9 @@ class Stream(BytesIO):
|
|||||||
BytesIO.__init__(self)
|
BytesIO.__init__(self)
|
||||||
self.compress = compress
|
self.compress = compress
|
||||||
|
|
||||||
|
def add_extra_keys(self, d):
|
||||||
|
pass
|
||||||
|
|
||||||
def pdf_serialize(self, stream):
|
def pdf_serialize(self, stream):
|
||||||
raw = self.getvalue()
|
raw = self.getvalue()
|
||||||
dl = len(raw)
|
dl = len(raw)
|
||||||
@ -125,6 +145,7 @@ class Stream(BytesIO):
|
|||||||
raw = zlib.compress(raw)
|
raw = zlib.compress(raw)
|
||||||
|
|
||||||
d = InlineDictionary({'Length':len(raw), 'DL':dl})
|
d = InlineDictionary({'Length':len(raw), 'DL':dl})
|
||||||
|
self.add_extra_keys(d)
|
||||||
if filters:
|
if filters:
|
||||||
d['Filter'] = filters
|
d['Filter'] = filters
|
||||||
serialize(d, stream)
|
serialize(d, stream)
|
||||||
|
@ -13,10 +13,10 @@ from collections import namedtuple
|
|||||||
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, QRawFont)
|
QTransform, QPainterPath, QTextOption, QTextLayout)
|
||||||
|
|
||||||
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)
|
||||||
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.container import Sfnt
|
||||||
from calibre.utils.fonts.sfnt.metrics import FontMetrics
|
from calibre.utils.fonts.sfnt.metrics import FontMetrics
|
||||||
@ -236,6 +236,9 @@ class PdfEngine(QPaintEngine):
|
|||||||
self.xscale, self.yscale = sx, sy
|
self.xscale, self.yscale = sx, sy
|
||||||
self.graphics_state = GraphicsState()
|
self.graphics_state = GraphicsState()
|
||||||
self.errors = []
|
self.errors = []
|
||||||
|
self.text_option = QTextOption()
|
||||||
|
self.text_option.setWrapMode(QTextOption.NoWrap)
|
||||||
|
self.fonts = {}
|
||||||
|
|
||||||
def init_page(self):
|
def init_page(self):
|
||||||
self.pdf.transform(self.pdf_system)
|
self.pdf.transform(self.pdf_system)
|
||||||
@ -353,44 +356,41 @@ class PdfEngine(QPaintEngine):
|
|||||||
|
|
||||||
@store_error
|
@store_error
|
||||||
def drawTextItem(self, point, text_item):
|
def drawTextItem(self, point, text_item):
|
||||||
# super(PdfEngine, self).drawTextItem(point+QPoint(0, 300), text_item)
|
# super(PdfEngine, self).drawTextItem(point+QPoint(0, 0), text_item)
|
||||||
f = text_item.font()
|
text = type(u'')(text_item.text()).replace('\n', ' ')
|
||||||
px, pt = f.pixelSize(), f.pointSizeF()
|
tl = QTextLayout(text, text_item.font(), self.paintDevice())
|
||||||
if px == -1:
|
self.text_option.setTextDirection(Qt.RightToLeft if
|
||||||
sz = pt/self.yscale
|
text_item.renderFlags() & text_item.RightToLeft else Qt.LeftToRight)
|
||||||
else:
|
tl.setTextOption(self.text_option)
|
||||||
sz = px
|
tl.setPosition(point)
|
||||||
|
tl.beginLayout()
|
||||||
|
line = tl.createLine()
|
||||||
|
if not line.isValid():
|
||||||
|
tl.endLayout()
|
||||||
|
return
|
||||||
|
line.setLineWidth(int(1e12))
|
||||||
|
tl.endLayout()
|
||||||
|
for run in tl.glyphRuns():
|
||||||
|
rf = run.rawFont()
|
||||||
|
name = hash(bytes(rf.fontTable('name')))
|
||||||
|
if name not in self.fonts:
|
||||||
|
self.fonts[name] = FontMetrics(Sfnt(rf))
|
||||||
|
metrics = self.fonts[name]
|
||||||
|
indices = run.glyphIndexes()
|
||||||
|
glyphs = []
|
||||||
|
pdf_pos = point
|
||||||
|
first_baseline = None
|
||||||
|
for i, pos in enumerate(run.positions()):
|
||||||
|
if first_baseline is None:
|
||||||
|
first_baseline = pos.y()
|
||||||
|
glyph_pos = point + pos
|
||||||
|
delta = glyph_pos - pdf_pos
|
||||||
|
glyphs.append((delta.x(), pos.y()-first_baseline, indices[i]))
|
||||||
|
pdf_pos = glyph_pos
|
||||||
|
|
||||||
|
self.pdf.draw_glyph_run([1, 0, 0, -1, point.x(),
|
||||||
|
point.y()], rf.pixelSize(), metrics, glyphs)
|
||||||
|
|
||||||
r = QRawFont.fromFont(f)
|
|
||||||
metrics = FontMetrics(Sfnt(r))
|
|
||||||
to = Text()
|
|
||||||
to.size = sz
|
|
||||||
to.set_transform(1, 0, 0, -1, point.x(), point.y())
|
|
||||||
stretch = f.stretch()
|
|
||||||
if stretch != 100:
|
|
||||||
to.horizontal_scale = stretch
|
|
||||||
ws = f.wordSpacing()
|
|
||||||
if ws != 0:
|
|
||||||
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: 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)
|
|
||||||
|
|
||||||
@store_error
|
@store_error
|
||||||
def drawPolygon(self, points, mode):
|
def drawPolygon(self, points, mode):
|
||||||
@ -460,40 +460,40 @@ if __name__ == '__main__':
|
|||||||
xmax, ymax = p.viewport().width(), p.viewport().height()
|
xmax, ymax = p.viewport().width(), p.viewport().height()
|
||||||
try:
|
try:
|
||||||
p.drawRect(0, 0, xmax, ymax)
|
p.drawRect(0, 0, xmax, ymax)
|
||||||
p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax),
|
# p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax),
|
||||||
QPoint(0, ymax), QPoint(0, 0))
|
# QPoint(0, ymax), QPoint(0, 0))
|
||||||
pp = QPainterPath()
|
# pp = QPainterPath()
|
||||||
pp.addRect(0, 0, xmax, ymax)
|
# pp.addRect(0, 0, xmax, ymax)
|
||||||
p.drawPath(pp)
|
# p.drawPath(pp)
|
||||||
p.save()
|
# p.save()
|
||||||
for i in xrange(3):
|
# for i in xrange(3):
|
||||||
col = [0, 0, 0, 200]
|
# col = [0, 0, 0, 200]
|
||||||
col[i] = 255
|
# col[i] = 255
|
||||||
p.setOpacity(0.3)
|
# p.setOpacity(0.3)
|
||||||
p.setBrush(QBrush(QColor(*col)))
|
# p.setBrush(QBrush(QColor(*col)))
|
||||||
p.drawRect(0, 0, xmax/10, xmax/10)
|
# p.drawRect(0, 0, xmax/10, xmax/10)
|
||||||
p.translate(xmax/10, xmax/10)
|
# p.translate(xmax/10, xmax/10)
|
||||||
p.scale(1, 1.5)
|
# p.scale(1, 1.5)
|
||||||
p.restore()
|
# p.restore()
|
||||||
|
|
||||||
p.save()
|
# p.save()
|
||||||
p.drawLine(0, 0, 5000, 0)
|
# p.drawLine(0, 0, 5000, 0)
|
||||||
p.rotate(45)
|
# p.rotate(45)
|
||||||
p.drawLine(0, 0, 5000, 0)
|
# p.drawLine(0, 0, 5000, 0)
|
||||||
p.restore()
|
# p.restore()
|
||||||
|
|
||||||
f = p.font()
|
f = p.font()
|
||||||
f.setPointSize(48)
|
f.setPointSize(24)
|
||||||
f.setLetterSpacing(f.PercentageSpacing, 200)
|
# f.setLetterSpacing(f.PercentageSpacing, 200)
|
||||||
# f.setUnderline(True)
|
# f.setUnderline(True)
|
||||||
# f.setOverline(True)
|
# f.setOverline(True)
|
||||||
# f.setStrikeOut(True)
|
# f.setStrikeOut(True)
|
||||||
f.setFamily('Times New Roman')
|
f.setFamily('Calibri')
|
||||||
p.setFont(f)
|
p.setFont(f)
|
||||||
# p.scale(2, 2)
|
# p.scale(2, 2)
|
||||||
# p.rotate(45)
|
# p.rotate(45)
|
||||||
p.setPen(QColor(0, 0, 255))
|
# p.setPen(QColor(0, 0, 255))
|
||||||
p.drawText(QPoint(100, 300), 'Some text')
|
p.drawText(QPoint(100, 300), 'Some text ū --- Д AV')
|
||||||
finally:
|
finally:
|
||||||
p.end()
|
p.end()
|
||||||
if dev.engine.errors:
|
if dev.engine.errors:
|
||||||
|
@ -7,7 +7,13 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from calibre.ebooks.pdf.render.common import (
|
import re
|
||||||
|
from itertools import izip, groupby
|
||||||
|
from operator import itemgetter
|
||||||
|
from collections import Counter
|
||||||
|
from future_builtins import map
|
||||||
|
|
||||||
|
from calibre.ebooks.pdf.render.common import (Array, String, Stream,
|
||||||
Dictionary, Name)
|
Dictionary, Name)
|
||||||
|
|
||||||
STANDARD_FONTS = {
|
STANDARD_FONTS = {
|
||||||
@ -16,11 +22,129 @@ STANDARD_FONTS = {
|
|||||||
'Helvetica-Oblique', 'Courier-Oblique', 'Times-BoldItalic',
|
'Helvetica-Oblique', 'Courier-Oblique', 'Times-BoldItalic',
|
||||||
'Helvetica-BoldOblique', 'Courier-BoldOblique', }
|
'Helvetica-BoldOblique', 'Courier-BoldOblique', }
|
||||||
|
|
||||||
|
'''
|
||||||
|
Notes
|
||||||
|
=======
|
||||||
|
|
||||||
|
We must use Type 0 CID keyed fonts to represent unicode text.
|
||||||
|
|
||||||
|
For TrueType
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The mapping from the text strings to glyph ids is defined by two things:
|
||||||
|
|
||||||
|
The /Encoding key of the Type-0 font dictionary
|
||||||
|
The /CIDToGIDMap key of the descendant font dictionary (for TrueType fonts)
|
||||||
|
|
||||||
|
We set /Encoding to /Identity-H and /CIDToGIDMap to /Identity. This means that
|
||||||
|
text strings are interpreted as a sequence of two-byte numbers, high order byte
|
||||||
|
first. Each number gets mapped to a glyph id equal to itself by the
|
||||||
|
/CIDToGIDMap.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
class FontStream(Stream):
|
||||||
|
|
||||||
|
def __init__(self, is_otf):
|
||||||
|
Stream.__init__(self)
|
||||||
|
self.is_otf = is_otf
|
||||||
|
|
||||||
|
def add_extra_keys(self, d):
|
||||||
|
d['Length1'] = d['DL']
|
||||||
|
if self.is_otf:
|
||||||
|
d['Subtype'] = Name('OpenType')
|
||||||
|
|
||||||
|
class Font(object):
|
||||||
|
|
||||||
|
def __init__(self, metrics, num, objects):
|
||||||
|
self.metrics = metrics
|
||||||
|
self.subset_tag = bytes(re.sub('.', lambda m: chr(int(m.group())+ord('A')),
|
||||||
|
oct(num))).rjust(6, b'A').decode('ascii')
|
||||||
|
self.font_stream = FontStream(metrics.is_otf)
|
||||||
|
self.font_descriptor = Dictionary({
|
||||||
|
'Type': Name('FontDescriptor'),
|
||||||
|
'FontName': Name(metrics.postscript_name),
|
||||||
|
'Flags': 0b100, # Symbolic font
|
||||||
|
'FontBBox': Array(metrics.pdf_bbox),
|
||||||
|
'ItalicAngle': metrics.post.italic_angle,
|
||||||
|
'Ascent': metrics.pdf_ascent,
|
||||||
|
'Descent': metrics.pdf_descent,
|
||||||
|
'CapHeight': metrics.pdf_capheight,
|
||||||
|
'AvgWidth': metrics.pdf_avg_width,
|
||||||
|
'StemV': metrics.pdf_stemv,
|
||||||
|
})
|
||||||
|
self.descendant_font = Dictionary({
|
||||||
|
'Type':Name('Font'),
|
||||||
|
'Subtype':Name('CIDFontType' + ('0' if metrics.is_otf else '2')),
|
||||||
|
'BaseFont': self.font_descriptor['FontName'],
|
||||||
|
'FontDescriptor':objects.add(self.font_descriptor),
|
||||||
|
'CIDSystemInfo':Dictionary({
|
||||||
|
'Registry':String('Adobe'),
|
||||||
|
'Ordering':String('Identity'),
|
||||||
|
'Supplement':0,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if not metrics.is_otf:
|
||||||
|
self.descendant_font['CIDToGIDMap'] = Name('Identity')
|
||||||
|
|
||||||
|
self.font_dict = Dictionary({
|
||||||
|
'Type':Name('Font'),
|
||||||
|
'Subtype':Name('Type0'),
|
||||||
|
'Encoding':Name('Identity-H'),
|
||||||
|
'BaseFont':self.descendant_font['BaseFont'],
|
||||||
|
'DescendantFonts':Array([objects.add(self.descendant_font)]),
|
||||||
|
})
|
||||||
|
|
||||||
|
self.used_glyphs = set()
|
||||||
|
|
||||||
|
def embed(self, objects):
|
||||||
|
# TODO: Subsetting and OpenType
|
||||||
|
self.font_descriptor['FontFile2'] = objects.add(self.font_stream)
|
||||||
|
self.write_widths(objects)
|
||||||
|
self.metrics.os2.zero_fstype()
|
||||||
|
self.metrics.sfnt(self.font_stream)
|
||||||
|
|
||||||
|
def write_widths(self, objects):
|
||||||
|
glyphs = sorted(self.used_glyphs|{0})
|
||||||
|
widths = {g:self.metrics.pdf_scale(w) for g, w in izip(glyphs,
|
||||||
|
self.metrics.glyph_widths(glyphs))}
|
||||||
|
counter = Counter()
|
||||||
|
for g, w in widths.iteritems():
|
||||||
|
counter[w] += 1
|
||||||
|
most_common = counter.most_common(1)[0][0]
|
||||||
|
self.descendant_font['DW'] = most_common
|
||||||
|
widths = {g:w for g, w in widths.iteritems() if w != most_common}
|
||||||
|
|
||||||
|
groups = Array()
|
||||||
|
for k, g in groupby(enumerate(widths.iterkeys()), lambda (i,x):i-x):
|
||||||
|
group = list(map(itemgetter(1), g))
|
||||||
|
gwidths = [widths[g] for g in group]
|
||||||
|
if len(set(gwidths)) == 1 and len(group) > 1:
|
||||||
|
w = (min(group), max(group), gwidths[0])
|
||||||
|
else:
|
||||||
|
w = (min(group), Array(gwidths))
|
||||||
|
groups.extend(w)
|
||||||
|
self.descendant_font['W'] = objects.add(groups)
|
||||||
|
|
||||||
|
|
||||||
class FontManager(object):
|
class FontManager(object):
|
||||||
|
|
||||||
def __init__(self, objects):
|
def __init__(self, objects):
|
||||||
self.objects = objects
|
self.objects = objects
|
||||||
self.std_map = {}
|
self.std_map = {}
|
||||||
|
self.font_map = {}
|
||||||
|
self.fonts = []
|
||||||
|
|
||||||
|
def add_font(self, font_metrics, glyph_ids):
|
||||||
|
if font_metrics not in self.font_map:
|
||||||
|
self.fonts.append(Font(font_metrics, len(self.fonts),
|
||||||
|
self.objects))
|
||||||
|
d = self.objects.add(self.fonts[-1].font_dict)
|
||||||
|
self.font_map[font_metrics] = (d, self.fonts[-1])
|
||||||
|
|
||||||
|
fontref, font = self.font_map[font_metrics]
|
||||||
|
font.used_glyphs |= glyph_ids
|
||||||
|
return fontref
|
||||||
|
|
||||||
def add_standard_font(self, name):
|
def add_standard_font(self, name):
|
||||||
if name not in STANDARD_FONTS:
|
if name not in STANDARD_FONTS:
|
||||||
@ -33,3 +157,7 @@ class FontManager(object):
|
|||||||
}))
|
}))
|
||||||
return self.std_map[name]
|
return self.std_map[name]
|
||||||
|
|
||||||
|
def embed_fonts(self):
|
||||||
|
for font in self.fonts:
|
||||||
|
font.embed(self.objects)
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ from collections import namedtuple
|
|||||||
|
|
||||||
from calibre.constants import (__appname__, __version__)
|
from calibre.constants import (__appname__, __version__)
|
||||||
from calibre.ebooks.pdf.render.common import (
|
from calibre.ebooks.pdf.render.common import (
|
||||||
Reference, EOL, serialize, Stream, Dictionary, String, Name, Array)
|
Reference, EOL, serialize, Stream, Dictionary, String, Name, Array,
|
||||||
|
GlyphIndex)
|
||||||
from calibre.ebooks.pdf.render.fonts import FontManager
|
from calibre.ebooks.pdf.render.fonts import FontManager
|
||||||
|
|
||||||
PDFVER = b'%PDF-1.6'
|
PDFVER = b'%PDF-1.6'
|
||||||
@ -357,9 +358,24 @@ class PDFStream(object):
|
|||||||
name = self.current_page.add_font(fontref)
|
name = self.current_page.add_font(fontref)
|
||||||
text_object.pdf_serialize(self.current_page, name)
|
text_object.pdf_serialize(self.current_page, name)
|
||||||
|
|
||||||
|
def draw_glyph_run(self, transform, size, font_metrics, glyphs):
|
||||||
|
glyph_ids = {x[-1] for x in glyphs}
|
||||||
|
fontref = self.font_manager.add_font(font_metrics, glyph_ids)
|
||||||
|
name = self.current_page.add_font(fontref)
|
||||||
|
self.current_page.write(b'BT ')
|
||||||
|
serialize(Name(name), self.current_page)
|
||||||
|
self.current_page.write(' %g Tf '%size)
|
||||||
|
self.current_page.write('%s Tm '%' '.join(map(type(u''), transform)))
|
||||||
|
for x, y, glyph_id in glyphs:
|
||||||
|
self.current_page.write('%g %g Td '%(x, y))
|
||||||
|
serialize(GlyphIndex(glyph_id, self.compress), self.current_page)
|
||||||
|
self.current_page.write(' Tj ')
|
||||||
|
self.current_page.write_line(b' ET')
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
if self.current_page.getvalue():
|
if self.current_page.getvalue():
|
||||||
self.end_page()
|
self.end_page()
|
||||||
|
self.font_manager.embed_fonts()
|
||||||
inforef = self.objects.add(self.info)
|
inforef = self.objects.add(self.info)
|
||||||
self.objects.pdf_serialize(self.stream)
|
self.objects.pdf_serialize(self.stream)
|
||||||
self.write_line()
|
self.write_line()
|
||||||
|
@ -66,6 +66,8 @@ class Sfnt(object):
|
|||||||
if table:
|
if table:
|
||||||
self.tables[table_tag] = self.TABLE_MAP.get(
|
self.tables[table_tag] = self.TABLE_MAP.get(
|
||||||
table_tag, UnknownTable)(table)
|
table_tag, UnknownTable)(table)
|
||||||
|
self.sfnt_version = (b'\0\x01\0\0' if b'glyf' in self.tables
|
||||||
|
else b'OTTO')
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.tables[key]
|
return self.tables[key]
|
||||||
@ -102,8 +104,8 @@ class Sfnt(object):
|
|||||||
ans[tag] = len(self[tag])
|
ans[tag] = len(self[tag])
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self, stream=None):
|
||||||
stream = BytesIO()
|
stream = BytesIO() if stream is None else stream
|
||||||
|
|
||||||
def spack(*args):
|
def spack(*args):
|
||||||
stream.write(pack(*args))
|
stream.write(pack(*args))
|
||||||
|
@ -8,7 +8,7 @@ __copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from itertools import izip
|
from itertools import izip
|
||||||
from struct import unpack_from, pack
|
from struct import unpack_from, pack, calcsize
|
||||||
|
|
||||||
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
|
from calibre.utils.fonts.sfnt.errors import UnsupportedFont
|
||||||
@ -32,10 +32,10 @@ class HeadTable(UnknownTable):
|
|||||||
'units_per_em' , 'H',
|
'units_per_em' , 'H',
|
||||||
'_created' , 'q',
|
'_created' , 'q',
|
||||||
'_modified' , 'q',
|
'_modified' , 'q',
|
||||||
'x_min' , 'H',
|
'x_min' , 'h',
|
||||||
'y_min' , 'H',
|
'y_min' , 'h',
|
||||||
'x_max' , 'H',
|
'x_max' , 'h',
|
||||||
'y_max' , 'H',
|
'y_max' , 'h',
|
||||||
'mac_style' , 'H',
|
'mac_style' , 'H',
|
||||||
'lowest_rec_ppem' , 'H',
|
'lowest_rec_ppem' , 'H',
|
||||||
'font_direction_hint' , 'h',
|
'font_direction_hint' , 'h',
|
||||||
@ -99,21 +99,58 @@ class HorizontalHeader(UnknownTable):
|
|||||||
|
|
||||||
class OS2Table(UnknownTable):
|
class OS2Table(UnknownTable):
|
||||||
|
|
||||||
version_number = FixedProperty('_version')
|
|
||||||
|
|
||||||
def read_data(self):
|
def read_data(self):
|
||||||
if hasattr(self, 'char_width'): return
|
if hasattr(self, 'char_width'): return
|
||||||
from calibre.utils.fonts.utils import get_font_characteristics
|
ver, = unpack_from(b'>H', self.raw)
|
||||||
vals = get_font_characteristics(self.raw, raw_is_table=True,
|
field_types = [
|
||||||
return_all=True)
|
'version' , 'H',
|
||||||
for i, attr in enumerate((
|
'average_char_width', 'h',
|
||||||
'_version', 'char_width', 'weight', 'width', 'fs_type',
|
'weight_class', 'H',
|
||||||
'subscript_x_size', 'subscript_y_size', 'subscript_x_offset',
|
'width_class', 'H',
|
||||||
'subscript_y_offset', 'superscript_x_size', 'superscript_y_size',
|
'fs_type', 'H',
|
||||||
'superscript_x_offset', 'superscript_y_offset', 'strikeout_size',
|
'subscript_x_size', 'h',
|
||||||
'strikeout_position', 'family_class', 'panose', 'selection',
|
'subscript_y_size', 'h',
|
||||||
'is_italic', 'is_bold', 'is_regular')):
|
'subscript_x_offset', 'h',
|
||||||
setattr(self, attr, vals[i])
|
'subscript_y_offset', 'h',
|
||||||
|
'superscript_x_size', 'h',
|
||||||
|
'superscript_y_size', 'h',
|
||||||
|
'superscript_x_offset', 'h',
|
||||||
|
'superscript_y_offset', 'h',
|
||||||
|
'strikeout_size', 'h',
|
||||||
|
'strikeout_position', 'h',
|
||||||
|
'family_class', 'h',
|
||||||
|
'panose', '10s',
|
||||||
|
'ranges', '16s',
|
||||||
|
'vendor_id', '4s',
|
||||||
|
'selection', 'H',
|
||||||
|
'first_char_index', 'H',
|
||||||
|
'last_char_index', 'H',
|
||||||
|
'typo_ascender', 'h',
|
||||||
|
'typo_descender', 'h',
|
||||||
|
'typo_line_gap', 'h',
|
||||||
|
'win_ascent', 'H',
|
||||||
|
'win_descent', 'H',
|
||||||
|
]
|
||||||
|
if ver > 1:
|
||||||
|
field_types += [
|
||||||
|
'code_page_range', '8s',
|
||||||
|
'x_height', 'h',
|
||||||
|
'cap_height', 'h',
|
||||||
|
'default_char', 'H',
|
||||||
|
'break_char', 'H',
|
||||||
|
'max_context', '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)
|
||||||
|
|
||||||
|
def zero_fstype(self):
|
||||||
|
prefix = calcsize(b'>HhHH')
|
||||||
|
self.raw = self.raw[:prefix] + b'\0\0' + self.raw[prefix+2:]
|
||||||
|
self.fs_type = 0
|
||||||
|
|
||||||
class PostTable(UnknownTable):
|
class PostTable(UnknownTable):
|
||||||
|
|
||||||
|
@ -21,25 +21,42 @@ class FontMetrics(object):
|
|||||||
def __init__(self, sfnt):
|
def __init__(self, sfnt):
|
||||||
self.sfnt = sfnt
|
self.sfnt = sfnt
|
||||||
|
|
||||||
|
self.head = self.sfnt[b'head']
|
||||||
hhea = self.sfnt[b'hhea']
|
hhea = self.sfnt[b'hhea']
|
||||||
hhea.read_data(self.sfnt[b'hmtx'])
|
hhea.read_data(self.sfnt[b'hmtx'])
|
||||||
self.ascent = hhea.ascender
|
self.ascent = hhea.ascender
|
||||||
self.descent = hhea.descender
|
self.descent = hhea.descender
|
||||||
|
self.bbox = ( self.head.x_min, self.head.y_min, self.head.x_max,
|
||||||
|
self.head.y_max )
|
||||||
self._advance_widths = hhea.advance_widths
|
self._advance_widths = hhea.advance_widths
|
||||||
self.cmap = self.sfnt[b'cmap']
|
self.cmap = self.sfnt[b'cmap']
|
||||||
self.head = self.sfnt[b'head']
|
|
||||||
self.units_per_em = self.head.units_per_em
|
self.units_per_em = self.head.units_per_em
|
||||||
self.os2 = self.sfnt[b'OS/2']
|
self.os2 = self.sfnt[b'OS/2']
|
||||||
self.os2.read_data()
|
self.os2.read_data()
|
||||||
self.post = self.sfnt[b'post']
|
self.post = self.sfnt[b'post']
|
||||||
self.post.read_data()
|
self.post.read_data()
|
||||||
self.names = get_all_font_names(self.sfnt[b'name'].raw, raw_is_table=True)
|
self.names = get_all_font_names(self.sfnt[b'name'].raw, raw_is_table=True)
|
||||||
|
self.is_otf = 'CFF ' in self.sfnt.tables
|
||||||
|
self._sig = hash(self.sfnt[b'name'].raw)
|
||||||
|
|
||||||
|
# Metrics for embedding in PDF
|
||||||
|
pdf_scale = self.pdf_scale = lambda x:int(round(x*1000./self.units_per_em))
|
||||||
|
self.pdf_ascent, self.pdf_descent = map(pdf_scale,
|
||||||
|
(self.os2.typo_ascender, self.os2.typo_descender))
|
||||||
|
self.pdf_bbox = tuple(map(pdf_scale, self.bbox))
|
||||||
|
self.pdf_capheight = pdf_scale(getattr(self.os2, 'cap_height',
|
||||||
|
self.os2.typo_ascender))
|
||||||
|
self.pdf_avg_width = pdf_scale(self.os2.average_char_width)
|
||||||
|
self.pdf_stemv = 50 + int((self.os2.weight_class / 65.0) ** 2)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self._sig
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def postscript_name(self):
|
def postscript_name(self):
|
||||||
if 'postscript_name' in self.names:
|
if 'postscript_name' in self.names:
|
||||||
return self.names['postscript_name']
|
return self.names['postscript_name'].replace(' ', '-')
|
||||||
return self.names['full_name'].replace(' ', '')
|
return self.names['full_name'].replace(' ', '-')
|
||||||
|
|
||||||
def underline_thickness(self, pixel_size=12.0):
|
def underline_thickness(self, pixel_size=12.0):
|
||||||
'Thickness for lines (in pixels) at the specified size'
|
'Thickness for lines (in pixels) at the specified size'
|
||||||
@ -74,10 +91,14 @@ class FontMetrics(object):
|
|||||||
chars = tuple(map(ord, string))
|
chars = tuple(map(ord, string))
|
||||||
cmap = self.cmap.get_character_map(chars)
|
cmap = self.cmap.get_character_map(chars)
|
||||||
glyph_ids = (cmap[c] for c in chars)
|
glyph_ids = (cmap[c] for c in chars)
|
||||||
last = len(self._advance_widths)
|
|
||||||
pixel_size_x = stretch * pixel_size
|
pixel_size_x = stretch * pixel_size
|
||||||
xscale = pixel_size_x / self.units_per_em
|
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)
|
return tuple(i*xscale for i in self.glyph_widths(glyph_ids))
|
||||||
|
|
||||||
|
def glyph_widths(self, glyph_ids):
|
||||||
|
last = len(self._advance_widths)
|
||||||
|
return tuple(self._advance_widths[i if i < last else -1] for i in
|
||||||
|
glyph_ids)
|
||||||
|
|
||||||
def width(self, string, pixel_size=12.0, stretch=1.0):
|
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'
|
'The width of the string at the specified pixel size and stretch, in pixels'
|
||||||
@ -86,9 +107,15 @@ class FontMetrics(object):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
from calibre.utils.fonts.sfnt.container import Sfnt
|
from calibre.utils.fonts.sfnt.container import Sfnt
|
||||||
with open(sys.argv[-2], 'rb') as f:
|
with open(sys.argv[-1], 'rb') as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
sfnt = Sfnt(raw)
|
sfnt = Sfnt(raw)
|
||||||
m = FontMetrics(sfnt)
|
m = FontMetrics(sfnt)
|
||||||
print (m.advance_widths(sys.argv[-1]))
|
print ('Ascent:', m.pdf_ascent)
|
||||||
|
print ('Descent:', m.pdf_descent)
|
||||||
|
print ('PDF BBox:', m.pdf_bbox)
|
||||||
|
print ('CapHeight:', m.pdf_capheight)
|
||||||
|
print ('AvgWidth:', m.pdf_avg_width)
|
||||||
|
print ('ItalicAngle', m.post.italic_angle)
|
||||||
|
print ('StemV', m.pdf_stemv)
|
||||||
|
|
||||||
|
@ -64,11 +64,8 @@ def get_font_characteristics(raw, raw_is_table=False, return_all=False):
|
|||||||
offset = struct.calcsize(common_fields)
|
offset = struct.calcsize(common_fields)
|
||||||
panose = struct.unpack_from(b'>10B', os2_table, offset)
|
panose = struct.unpack_from(b'>10B', os2_table, offset)
|
||||||
offset += 10
|
offset += 10
|
||||||
(range1,) = struct.unpack_from(b'>L', os2_table, offset)
|
(range1, range2, range3, range4) = struct.unpack_from(b'>4L', os2_table, offset)
|
||||||
offset += struct.calcsize(b'>L')
|
offset += struct.calcsize(b'>4L')
|
||||||
if version > 0:
|
|
||||||
range2, range3, range4 = struct.unpack_from(b'>3L', os2_table, offset)
|
|
||||||
offset += struct.calcsize(b'>3L')
|
|
||||||
vendor_id = os2_table[offset:offset+4]
|
vendor_id = os2_table[offset:offset+4]
|
||||||
vendor_id
|
vendor_id
|
||||||
offset += 4
|
offset += 4
|
||||||
|
Loading…
x
Reference in New Issue
Block a user