161 lines
5.9 KiB
Python
161 lines
5.9 KiB
Python
from fontTools.misc.roundTools import otRound
|
|
from fontTools import ttLib
|
|
from fontTools.misc.textTools import safeEval
|
|
from . import DefaultTable
|
|
import sys
|
|
import struct
|
|
import array
|
|
import logging
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class table__h_m_t_x(DefaultTable.DefaultTable):
|
|
"""Horizontal Metrics table
|
|
|
|
The ``hmtx`` table contains per-glyph metrics for the glyphs in a
|
|
``glyf``, ``CFF ``, or ``CFF2`` table, as needed for horizontal text
|
|
layout.
|
|
|
|
See also https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx
|
|
"""
|
|
|
|
headerTag = "hhea"
|
|
advanceName = "width"
|
|
sideBearingName = "lsb"
|
|
numberOfMetricsName = "numberOfHMetrics"
|
|
longMetricFormat = "Hh"
|
|
|
|
def decompile(self, data, ttFont):
|
|
numGlyphs = ttFont["maxp"].numGlyphs
|
|
headerTable = ttFont.get(self.headerTag)
|
|
if headerTable is not None:
|
|
numberOfMetrics = int(getattr(headerTable, self.numberOfMetricsName))
|
|
else:
|
|
numberOfMetrics = numGlyphs
|
|
if numberOfMetrics > numGlyphs:
|
|
log.warning(
|
|
"The %s.%s exceeds the maxp.numGlyphs"
|
|
% (self.headerTag, self.numberOfMetricsName)
|
|
)
|
|
numberOfMetrics = numGlyphs
|
|
if len(data) < 4 * numberOfMetrics:
|
|
raise ttLib.TTLibError("not enough '%s' table data" % self.tableTag)
|
|
# Note: advanceWidth is unsigned, but some font editors might
|
|
# read/write as signed. We can't be sure whether it was a mistake
|
|
# or not, so we read as unsigned but also issue a warning...
|
|
metricsFmt = ">" + self.longMetricFormat * numberOfMetrics
|
|
metrics = struct.unpack(metricsFmt, data[: 4 * numberOfMetrics])
|
|
data = data[4 * numberOfMetrics :]
|
|
numberOfSideBearings = numGlyphs - numberOfMetrics
|
|
sideBearings = array.array("h", data[: 2 * numberOfSideBearings])
|
|
data = data[2 * numberOfSideBearings :]
|
|
|
|
if sys.byteorder != "big":
|
|
sideBearings.byteswap()
|
|
if data:
|
|
log.warning("too much '%s' table data" % self.tableTag)
|
|
self.metrics = {}
|
|
glyphOrder = ttFont.getGlyphOrder()
|
|
for i in range(numberOfMetrics):
|
|
glyphName = glyphOrder[i]
|
|
advanceWidth, lsb = metrics[i * 2 : i * 2 + 2]
|
|
if advanceWidth > 32767:
|
|
log.warning(
|
|
"Glyph %r has a huge advance %s (%d); is it intentional or "
|
|
"an (invalid) negative value?",
|
|
glyphName,
|
|
self.advanceName,
|
|
advanceWidth,
|
|
)
|
|
self.metrics[glyphName] = (advanceWidth, lsb)
|
|
lastAdvance = metrics[-2]
|
|
for i in range(numberOfSideBearings):
|
|
glyphName = glyphOrder[i + numberOfMetrics]
|
|
self.metrics[glyphName] = (lastAdvance, sideBearings[i])
|
|
|
|
def compile(self, ttFont):
|
|
metrics = []
|
|
hasNegativeAdvances = False
|
|
for glyphName in ttFont.getGlyphOrder():
|
|
advanceWidth, sideBearing = self.metrics[glyphName]
|
|
if advanceWidth < 0:
|
|
log.error(
|
|
"Glyph %r has negative advance %s" % (glyphName, self.advanceName)
|
|
)
|
|
hasNegativeAdvances = True
|
|
metrics.append([advanceWidth, sideBearing])
|
|
|
|
headerTable = ttFont.get(self.headerTag)
|
|
if headerTable is not None:
|
|
lastAdvance = metrics[-1][0]
|
|
lastIndex = len(metrics)
|
|
while metrics[lastIndex - 2][0] == lastAdvance:
|
|
lastIndex -= 1
|
|
if lastIndex <= 1:
|
|
# all advances are equal
|
|
lastIndex = 1
|
|
break
|
|
additionalMetrics = metrics[lastIndex:]
|
|
additionalMetrics = [otRound(sb) for _, sb in additionalMetrics]
|
|
metrics = metrics[:lastIndex]
|
|
numberOfMetrics = len(metrics)
|
|
setattr(headerTable, self.numberOfMetricsName, numberOfMetrics)
|
|
else:
|
|
# no hhea/vhea, can't store numberOfMetrics; assume == numGlyphs
|
|
numberOfMetrics = ttFont["maxp"].numGlyphs
|
|
additionalMetrics = []
|
|
|
|
allMetrics = []
|
|
for advance, sb in metrics:
|
|
allMetrics.extend([otRound(advance), otRound(sb)])
|
|
metricsFmt = ">" + self.longMetricFormat * numberOfMetrics
|
|
try:
|
|
data = struct.pack(metricsFmt, *allMetrics)
|
|
except struct.error as e:
|
|
if "out of range" in str(e) and hasNegativeAdvances:
|
|
raise ttLib.TTLibError(
|
|
"'%s' table can't contain negative advance %ss"
|
|
% (self.tableTag, self.advanceName)
|
|
)
|
|
else:
|
|
raise
|
|
additionalMetrics = array.array("h", additionalMetrics)
|
|
if sys.byteorder != "big":
|
|
additionalMetrics.byteswap()
|
|
data = data + additionalMetrics.tobytes()
|
|
return data
|
|
|
|
def toXML(self, writer, ttFont):
|
|
names = sorted(self.metrics.keys())
|
|
for glyphName in names:
|
|
advance, sb = self.metrics[glyphName]
|
|
writer.simpletag(
|
|
"mtx",
|
|
[
|
|
("name", glyphName),
|
|
(self.advanceName, advance),
|
|
(self.sideBearingName, sb),
|
|
],
|
|
)
|
|
writer.newline()
|
|
|
|
def fromXML(self, name, attrs, content, ttFont):
|
|
if not hasattr(self, "metrics"):
|
|
self.metrics = {}
|
|
if name == "mtx":
|
|
self.metrics[attrs["name"]] = (
|
|
safeEval(attrs[self.advanceName]),
|
|
safeEval(attrs[self.sideBearingName]),
|
|
)
|
|
|
|
def __delitem__(self, glyphName):
|
|
del self.metrics[glyphName]
|
|
|
|
def __getitem__(self, glyphName):
|
|
return self.metrics[glyphName]
|
|
|
|
def __setitem__(self, glyphName, advance_sb_pair):
|
|
self.metrics[glyphName] = tuple(advance_sb_pair)
|