1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683 |
- """_g_l_y_f.py -- Converter classes for the 'glyf' table."""
- from collections import namedtuple
- from fontTools.misc import sstruct
- from fontTools import ttLib
- from fontTools import version
- from fontTools.misc.transform import DecomposedTransform
- from fontTools.misc.textTools import tostr, safeEval, pad
- from fontTools.misc.arrayTools import updateBounds, pointInRect
- from fontTools.misc.bezierTools import calcQuadraticBounds
- from fontTools.misc.fixedTools import (
- fixedToFloat as fi2fl,
- floatToFixed as fl2fi,
- floatToFixedToStr as fl2str,
- strToFixedToFloat as str2fl,
- )
- from fontTools.misc.roundTools import noRound, otRound
- from fontTools.misc.vector import Vector
- from numbers import Number
- from . import DefaultTable
- from . import ttProgram
- import sys
- import struct
- import array
- import logging
- import math
- import os
- from fontTools.misc import xmlWriter
- from fontTools.misc.filenames import userNameToFileName
- from fontTools.misc.loggingTools import deprecateFunction
- from enum import IntFlag
- from functools import partial
- from types import SimpleNamespace
- from typing import Set
- log = logging.getLogger(__name__)
- # We compute the version the same as is computed in ttlib/__init__
- # so that we can write 'ttLibVersion' attribute of the glyf TTX files
- # when glyf is written to separate files.
- version = ".".join(version.split(".")[:2])
- #
- # The Apple and MS rasterizers behave differently for
- # scaled composite components: one does scale first and then translate
- # and the other does it vice versa. MS defined some flags to indicate
- # the difference, but it seems nobody actually _sets_ those flags.
- #
- # Funny thing: Apple seems to _only_ do their thing in the
- # WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE
- # (eg. Charcoal)...
- #
- SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple
- class table__g_l_y_f(DefaultTable.DefaultTable):
- """Glyph Data Table
- This class represents the `glyf <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf>`_
- table, which contains outlines for glyphs in TrueType format. In many cases,
- it is easier to access and manipulate glyph outlines through the ``GlyphSet``
- object returned from :py:meth:`fontTools.ttLib.ttFont.getGlyphSet`::
- >> from fontTools.pens.boundsPen import BoundsPen
- >> glyphset = font.getGlyphSet()
- >> bp = BoundsPen(glyphset)
- >> glyphset["A"].draw(bp)
- >> bp.bounds
- (19, 0, 633, 716)
- However, this class can be used for low-level access to the ``glyf`` table data.
- Objects of this class support dictionary-like access, mapping glyph names to
- :py:class:`Glyph` objects::
- >> glyf = font["glyf"]
- >> len(glyf["Aacute"].components)
- 2
- Note that when adding glyphs to the font via low-level access to the ``glyf``
- table, the new glyphs must also be added to the ``hmtx``/``vmtx`` table::
- >> font["glyf"]["divisionslash"] = Glyph()
- >> font["hmtx"]["divisionslash"] = (640, 0)
- """
- dependencies = ["fvar"]
- # this attribute controls the amount of padding applied to glyph data upon compile.
- # Glyph lenghts are aligned to multiples of the specified value.
- # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means
- # no padding, except for when padding would allow to use short loca offsets.
- padding = 1
- def decompile(self, data, ttFont):
- self.axisTags = (
- [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
- )
- loca = ttFont["loca"]
- pos = int(loca[0])
- nextPos = 0
- noname = 0
- self.glyphs = {}
- self.glyphOrder = glyphOrder = ttFont.getGlyphOrder()
- self._reverseGlyphOrder = {}
- for i in range(0, len(loca) - 1):
- try:
- glyphName = glyphOrder[i]
- except IndexError:
- noname = noname + 1
- glyphName = "ttxautoglyph%s" % i
- nextPos = int(loca[i + 1])
- glyphdata = data[pos:nextPos]
- if len(glyphdata) != (nextPos - pos):
- raise ttLib.TTLibError("not enough 'glyf' table data")
- glyph = Glyph(glyphdata)
- self.glyphs[glyphName] = glyph
- pos = nextPos
- if len(data) - nextPos >= 4:
- log.warning(
- "too much 'glyf' table data: expected %d, received %d bytes",
- nextPos,
- len(data),
- )
- if noname:
- log.warning("%s glyphs have no name", noname)
- if ttFont.lazy is False: # Be lazy for None and True
- self.ensureDecompiled()
- def ensureDecompiled(self, recurse=False):
- # The recurse argument is unused, but part of the signature of
- # ensureDecompiled across the library.
- for glyph in self.glyphs.values():
- glyph.expand(self)
- def compile(self, ttFont):
- self.axisTags = (
- [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
- )
- if not hasattr(self, "glyphOrder"):
- self.glyphOrder = ttFont.getGlyphOrder()
- padding = self.padding
- assert padding in (0, 1, 2, 4)
- locations = []
- currentLocation = 0
- dataList = []
- recalcBBoxes = ttFont.recalcBBoxes
- boundsDone = set()
- for glyphName in self.glyphOrder:
- glyph = self.glyphs[glyphName]
- glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone)
- if padding > 1:
- glyphData = pad(glyphData, size=padding)
- locations.append(currentLocation)
- currentLocation = currentLocation + len(glyphData)
- dataList.append(glyphData)
- locations.append(currentLocation)
- if padding == 1 and currentLocation < 0x20000:
- # See if we can pad any odd-lengthed glyphs to allow loca
- # table to use the short offsets.
- indices = [
- i for i, glyphData in enumerate(dataList) if len(glyphData) % 2 == 1
- ]
- if indices and currentLocation + len(indices) < 0x20000:
- # It fits. Do it.
- for i in indices:
- dataList[i] += b"\0"
- currentLocation = 0
- for i, glyphData in enumerate(dataList):
- locations[i] = currentLocation
- currentLocation += len(glyphData)
- locations[len(dataList)] = currentLocation
- data = b"".join(dataList)
- if "loca" in ttFont:
- ttFont["loca"].set(locations)
- if "maxp" in ttFont:
- ttFont["maxp"].numGlyphs = len(self.glyphs)
- if not data:
- # As a special case when all glyph in the font are empty, add a zero byte
- # to the table, so that OTS doesn’t reject it, and to make the table work
- # on Windows as well.
- # See https://github.com/khaledhosny/ots/issues/52
- data = b"\0"
- return data
- def toXML(self, writer, ttFont, splitGlyphs=False):
- notice = (
- "The xMin, yMin, xMax and yMax values\n"
- "will be recalculated by the compiler."
- )
- glyphNames = ttFont.getGlyphNames()
- if not splitGlyphs:
- writer.newline()
- writer.comment(notice)
- writer.newline()
- writer.newline()
- numGlyphs = len(glyphNames)
- if splitGlyphs:
- path, ext = os.path.splitext(writer.file.name)
- existingGlyphFiles = set()
- for glyphName in glyphNames:
- glyph = self.get(glyphName)
- if glyph is None:
- log.warning("glyph '%s' does not exist in glyf table", glyphName)
- continue
- if glyph.numberOfContours:
- if splitGlyphs:
- glyphPath = userNameToFileName(
- tostr(glyphName, "utf-8"),
- existingGlyphFiles,
- prefix=path + ".",
- suffix=ext,
- )
- existingGlyphFiles.add(glyphPath.lower())
- glyphWriter = xmlWriter.XMLWriter(
- glyphPath,
- idlefunc=writer.idlefunc,
- newlinestr=writer.newlinestr,
- )
- glyphWriter.begintag("ttFont", ttLibVersion=version)
- glyphWriter.newline()
- glyphWriter.begintag("glyf")
- glyphWriter.newline()
- glyphWriter.comment(notice)
- glyphWriter.newline()
- writer.simpletag("TTGlyph", src=os.path.basename(glyphPath))
- else:
- glyphWriter = writer
- glyphWriter.begintag(
- "TTGlyph",
- [
- ("name", glyphName),
- ("xMin", glyph.xMin),
- ("yMin", glyph.yMin),
- ("xMax", glyph.xMax),
- ("yMax", glyph.yMax),
- ],
- )
- glyphWriter.newline()
- glyph.toXML(glyphWriter, ttFont)
- glyphWriter.endtag("TTGlyph")
- glyphWriter.newline()
- if splitGlyphs:
- glyphWriter.endtag("glyf")
- glyphWriter.newline()
- glyphWriter.endtag("ttFont")
- glyphWriter.newline()
- glyphWriter.close()
- else:
- writer.simpletag("TTGlyph", name=glyphName)
- writer.comment("contains no outline data")
- if not splitGlyphs:
- writer.newline()
- writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- if name != "TTGlyph":
- return
- if not hasattr(self, "glyphs"):
- self.glyphs = {}
- if not hasattr(self, "glyphOrder"):
- self.glyphOrder = ttFont.getGlyphOrder()
- glyphName = attrs["name"]
- log.debug("unpacking glyph '%s'", glyphName)
- glyph = Glyph()
- for attr in ["xMin", "yMin", "xMax", "yMax"]:
- setattr(glyph, attr, safeEval(attrs.get(attr, "0")))
- self.glyphs[glyphName] = glyph
- for element in content:
- if not isinstance(element, tuple):
- continue
- name, attrs, content = element
- glyph.fromXML(name, attrs, content, ttFont)
- if not ttFont.recalcBBoxes:
- glyph.compact(self, 0)
- def setGlyphOrder(self, glyphOrder):
- """Sets the glyph order
- Args:
- glyphOrder ([str]): List of glyph names in order.
- """
- self.glyphOrder = glyphOrder
- self._reverseGlyphOrder = {}
- def getGlyphName(self, glyphID):
- """Returns the name for the glyph with the given ID.
- Raises a ``KeyError`` if the glyph name is not found in the font.
- """
- return self.glyphOrder[glyphID]
- def _buildReverseGlyphOrderDict(self):
- self._reverseGlyphOrder = d = {}
- for glyphID, glyphName in enumerate(self.glyphOrder):
- d[glyphName] = glyphID
- def getGlyphID(self, glyphName):
- """Returns the ID of the glyph with the given name.
- Raises a ``ValueError`` if the glyph is not found in the font.
- """
- glyphOrder = self.glyphOrder
- id = getattr(self, "_reverseGlyphOrder", {}).get(glyphName)
- if id is None or id >= len(glyphOrder) or glyphOrder[id] != glyphName:
- self._buildReverseGlyphOrderDict()
- id = self._reverseGlyphOrder.get(glyphName)
- if id is None:
- raise ValueError(glyphName)
- return id
- def removeHinting(self):
- """Removes TrueType hints from all glyphs in the glyphset.
- See :py:meth:`Glyph.removeHinting`.
- """
- for glyph in self.glyphs.values():
- glyph.removeHinting()
- def keys(self):
- return self.glyphs.keys()
- def has_key(self, glyphName):
- return glyphName in self.glyphs
- __contains__ = has_key
- def get(self, glyphName, default=None):
- glyph = self.glyphs.get(glyphName, default)
- if glyph is not None:
- glyph.expand(self)
- return glyph
- def __getitem__(self, glyphName):
- glyph = self.glyphs[glyphName]
- glyph.expand(self)
- return glyph
- def __setitem__(self, glyphName, glyph):
- self.glyphs[glyphName] = glyph
- if glyphName not in self.glyphOrder:
- self.glyphOrder.append(glyphName)
- def __delitem__(self, glyphName):
- del self.glyphs[glyphName]
- self.glyphOrder.remove(glyphName)
- def __len__(self):
- assert len(self.glyphOrder) == len(self.glyphs)
- return len(self.glyphs)
- def _getPhantomPoints(self, glyphName, hMetrics, vMetrics=None):
- """Compute the four "phantom points" for the given glyph from its bounding box
- and the horizontal and vertical advance widths and sidebearings stored in the
- ttFont's "hmtx" and "vmtx" tables.
- 'hMetrics' should be ttFont['hmtx'].metrics.
- 'vMetrics' should be ttFont['vmtx'].metrics if there is "vmtx" or None otherwise.
- If there is no vMetrics passed in, vertical phantom points are set to the zero coordinate.
- https://docs.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#phantoms
- """
- glyph = self[glyphName]
- if not hasattr(glyph, "xMin"):
- glyph.recalcBounds(self)
- horizontalAdvanceWidth, leftSideBearing = hMetrics[glyphName]
- leftSideX = glyph.xMin - leftSideBearing
- rightSideX = leftSideX + horizontalAdvanceWidth
- if vMetrics:
- verticalAdvanceWidth, topSideBearing = vMetrics[glyphName]
- topSideY = topSideBearing + glyph.yMax
- bottomSideY = topSideY - verticalAdvanceWidth
- else:
- bottomSideY = topSideY = 0
- return [
- (leftSideX, 0),
- (rightSideX, 0),
- (0, topSideY),
- (0, bottomSideY),
- ]
- def _getCoordinatesAndControls(
- self, glyphName, hMetrics, vMetrics=None, *, round=otRound
- ):
- """Return glyph coordinates and controls as expected by "gvar" table.
- The coordinates includes four "phantom points" for the glyph metrics,
- as mandated by the "gvar" spec.
- The glyph controls is a namedtuple with the following attributes:
- - numberOfContours: -1 for composite glyphs.
- - endPts: list of indices of end points for each contour in simple
- glyphs, or component indices in composite glyphs (used for IUP
- optimization).
- - flags: array of contour point flags for simple glyphs (None for
- composite glyphs).
- - components: list of base glyph names (str) for each component in
- composite glyphs (None for simple glyphs).
- The "hMetrics" and vMetrics are used to compute the "phantom points" (see
- the "_getPhantomPoints" method).
- Return None if the requested glyphName is not present.
- """
- glyph = self.get(glyphName)
- if glyph is None:
- return None
- if glyph.isComposite():
- coords = GlyphCoordinates(
- [(getattr(c, "x", 0), getattr(c, "y", 0)) for c in glyph.components]
- )
- controls = _GlyphControls(
- numberOfContours=glyph.numberOfContours,
- endPts=list(range(len(glyph.components))),
- flags=None,
- components=[
- (c.glyphName, getattr(c, "transform", None))
- for c in glyph.components
- ],
- )
- elif glyph.isVarComposite():
- coords = []
- controls = []
- for component in glyph.components:
- (
- componentCoords,
- componentControls,
- ) = component.getCoordinatesAndControls()
- coords.extend(componentCoords)
- controls.extend(componentControls)
- coords = GlyphCoordinates(coords)
- controls = _GlyphControls(
- numberOfContours=glyph.numberOfContours,
- endPts=list(range(len(coords))),
- flags=None,
- components=[
- (c.glyphName, getattr(c, "flags", None)) for c in glyph.components
- ],
- )
- else:
- coords, endPts, flags = glyph.getCoordinates(self)
- coords = coords.copy()
- controls = _GlyphControls(
- numberOfContours=glyph.numberOfContours,
- endPts=endPts,
- flags=flags,
- components=None,
- )
- # Add phantom points for (left, right, top, bottom) positions.
- phantomPoints = self._getPhantomPoints(glyphName, hMetrics, vMetrics)
- coords.extend(phantomPoints)
- coords.toInt(round=round)
- return coords, controls
- def _setCoordinates(self, glyphName, coord, hMetrics, vMetrics=None):
- """Set coordinates and metrics for the given glyph.
- "coord" is an array of GlyphCoordinates which must include the "phantom
- points" as the last four coordinates.
- Both the horizontal/vertical advances and left/top sidebearings in "hmtx"
- and "vmtx" tables (if any) are updated from four phantom points and
- the glyph's bounding boxes.
- The "hMetrics" and vMetrics are used to propagate "phantom points"
- into "hmtx" and "vmtx" tables if desired. (see the "_getPhantomPoints"
- method).
- """
- glyph = self[glyphName]
- # Handle phantom points for (left, right, top, bottom) positions.
- assert len(coord) >= 4
- leftSideX = coord[-4][0]
- rightSideX = coord[-3][0]
- topSideY = coord[-2][1]
- bottomSideY = coord[-1][1]
- coord = coord[:-4]
- if glyph.isComposite():
- assert len(coord) == len(glyph.components)
- for p, comp in zip(coord, glyph.components):
- if hasattr(comp, "x"):
- comp.x, comp.y = p
- elif glyph.isVarComposite():
- for comp in glyph.components:
- coord = comp.setCoordinates(coord)
- assert not coord
- elif glyph.numberOfContours == 0:
- assert len(coord) == 0
- else:
- assert len(coord) == len(glyph.coordinates)
- glyph.coordinates = GlyphCoordinates(coord)
- glyph.recalcBounds(self, boundsDone=set())
- horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
- if horizontalAdvanceWidth < 0:
- # unlikely, but it can happen, see:
- # https://github.com/fonttools/fonttools/pull/1198
- horizontalAdvanceWidth = 0
- leftSideBearing = otRound(glyph.xMin - leftSideX)
- hMetrics[glyphName] = horizontalAdvanceWidth, leftSideBearing
- if vMetrics is not None:
- verticalAdvanceWidth = otRound(topSideY - bottomSideY)
- if verticalAdvanceWidth < 0: # unlikely but do the same as horizontal
- verticalAdvanceWidth = 0
- topSideBearing = otRound(topSideY - glyph.yMax)
- vMetrics[glyphName] = verticalAdvanceWidth, topSideBearing
- # Deprecated
- def _synthesizeVMetrics(self, glyphName, ttFont, defaultVerticalOrigin):
- """This method is wrong and deprecated.
- For rationale see:
- https://github.com/fonttools/fonttools/pull/2266/files#r613569473
- """
- vMetrics = getattr(ttFont.get("vmtx"), "metrics", None)
- if vMetrics is None:
- verticalAdvanceWidth = ttFont["head"].unitsPerEm
- topSideY = getattr(ttFont.get("hhea"), "ascent", None)
- if topSideY is None:
- if defaultVerticalOrigin is not None:
- topSideY = defaultVerticalOrigin
- else:
- topSideY = verticalAdvanceWidth
- glyph = self[glyphName]
- glyph.recalcBounds(self)
- topSideBearing = otRound(topSideY - glyph.yMax)
- vMetrics = {glyphName: (verticalAdvanceWidth, topSideBearing)}
- return vMetrics
- @deprecateFunction("use '_getPhantomPoints' instead", category=DeprecationWarning)
- def getPhantomPoints(self, glyphName, ttFont, defaultVerticalOrigin=None):
- """Old public name for self._getPhantomPoints().
- See: https://github.com/fonttools/fonttools/pull/2266"""
- hMetrics = ttFont["hmtx"].metrics
- vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
- return self._getPhantomPoints(glyphName, hMetrics, vMetrics)
- @deprecateFunction(
- "use '_getCoordinatesAndControls' instead", category=DeprecationWarning
- )
- def getCoordinatesAndControls(self, glyphName, ttFont, defaultVerticalOrigin=None):
- """Old public name for self._getCoordinatesAndControls().
- See: https://github.com/fonttools/fonttools/pull/2266"""
- hMetrics = ttFont["hmtx"].metrics
- vMetrics = self._synthesizeVMetrics(glyphName, ttFont, defaultVerticalOrigin)
- return self._getCoordinatesAndControls(glyphName, hMetrics, vMetrics)
- @deprecateFunction("use '_setCoordinates' instead", category=DeprecationWarning)
- def setCoordinates(self, glyphName, ttFont):
- """Old public name for self._setCoordinates().
- See: https://github.com/fonttools/fonttools/pull/2266"""
- hMetrics = ttFont["hmtx"].metrics
- vMetrics = getattr(ttFont.get("vmtx"), "metrics", None)
- self._setCoordinates(glyphName, hMetrics, vMetrics)
- _GlyphControls = namedtuple(
- "_GlyphControls", "numberOfContours endPts flags components"
- )
- glyphHeaderFormat = """
- > # big endian
- numberOfContours: h
- xMin: h
- yMin: h
- xMax: h
- yMax: h
- """
- # flags
- flagOnCurve = 0x01
- flagXShort = 0x02
- flagYShort = 0x04
- flagRepeat = 0x08
- flagXsame = 0x10
- flagYsame = 0x20
- flagOverlapSimple = 0x40
- flagCubic = 0x80
- # These flags are kept for XML output after decompiling the coordinates
- keepFlags = flagOnCurve + flagOverlapSimple + flagCubic
- _flagSignBytes = {
- 0: 2,
- flagXsame: 0,
- flagXShort | flagXsame: +1,
- flagXShort: -1,
- flagYsame: 0,
- flagYShort | flagYsame: +1,
- flagYShort: -1,
- }
- def flagBest(x, y, onCurve):
- """For a given x,y delta pair, returns the flag that packs this pair
- most efficiently, as well as the number of byte cost of such flag."""
- flag = flagOnCurve if onCurve else 0
- cost = 0
- # do x
- if x == 0:
- flag = flag | flagXsame
- elif -255 <= x <= 255:
- flag = flag | flagXShort
- if x > 0:
- flag = flag | flagXsame
- cost += 1
- else:
- cost += 2
- # do y
- if y == 0:
- flag = flag | flagYsame
- elif -255 <= y <= 255:
- flag = flag | flagYShort
- if y > 0:
- flag = flag | flagYsame
- cost += 1
- else:
- cost += 2
- return flag, cost
- def flagFits(newFlag, oldFlag, mask):
- newBytes = _flagSignBytes[newFlag & mask]
- oldBytes = _flagSignBytes[oldFlag & mask]
- return newBytes == oldBytes or abs(newBytes) > abs(oldBytes)
- def flagSupports(newFlag, oldFlag):
- return (
- (oldFlag & flagOnCurve) == (newFlag & flagOnCurve)
- and flagFits(newFlag, oldFlag, flagXsame | flagXShort)
- and flagFits(newFlag, oldFlag, flagYsame | flagYShort)
- )
- def flagEncodeCoord(flag, mask, coord, coordBytes):
- byteCount = _flagSignBytes[flag & mask]
- if byteCount == 1:
- coordBytes.append(coord)
- elif byteCount == -1:
- coordBytes.append(-coord)
- elif byteCount == 2:
- coordBytes.extend(struct.pack(">h", coord))
- def flagEncodeCoords(flag, x, y, xBytes, yBytes):
- flagEncodeCoord(flag, flagXsame | flagXShort, x, xBytes)
- flagEncodeCoord(flag, flagYsame | flagYShort, y, yBytes)
- ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes
- ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points
- ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true
- WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0
- NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!)
- MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one
- WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy
- WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11
- WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow
- USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph
- OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts
- SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple)
- UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS)
- CompositeMaxpValues = namedtuple(
- "CompositeMaxpValues", ["nPoints", "nContours", "maxComponentDepth"]
- )
- class Glyph(object):
- """This class represents an individual TrueType glyph.
- TrueType glyph objects come in two flavours: simple and composite. Simple
- glyph objects contain contours, represented via the ``.coordinates``,
- ``.flags``, ``.numberOfContours``, and ``.endPtsOfContours`` attributes;
- composite glyphs contain components, available through the ``.components``
- attributes.
- Because the ``.coordinates`` attribute (and other simple glyph attributes mentioned
- above) is only set on simple glyphs and the ``.components`` attribute is only
- set on composite glyphs, it is necessary to use the :py:meth:`isComposite`
- method to test whether a glyph is simple or composite before attempting to
- access its data.
- For a composite glyph, the components can also be accessed via array-like access::
- >> assert(font["glyf"]["Aacute"].isComposite())
- >> font["glyf"]["Aacute"][0]
- <fontTools.ttLib.tables._g_l_y_f.GlyphComponent at 0x1027b2ee0>
- """
- def __init__(self, data=b""):
- if not data:
- # empty char
- self.numberOfContours = 0
- return
- self.data = data
- def compact(self, glyfTable, recalcBBoxes=True):
- data = self.compile(glyfTable, recalcBBoxes)
- self.__dict__.clear()
- self.data = data
- def expand(self, glyfTable):
- if not hasattr(self, "data"):
- # already unpacked
- return
- if not self.data:
- # empty char
- del self.data
- self.numberOfContours = 0
- return
- dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self)
- del self.data
- # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in
- # some glyphs; decompileCoordinates assumes that there's at least
- # one, so short-circuit here.
- if self.numberOfContours == 0:
- return
- if self.isComposite():
- self.decompileComponents(data, glyfTable)
- elif self.isVarComposite():
- self.decompileVarComponents(data, glyfTable)
- else:
- self.decompileCoordinates(data)
- def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None):
- if hasattr(self, "data"):
- if recalcBBoxes:
- # must unpack glyph in order to recalculate bounding box
- self.expand(glyfTable)
- else:
- return self.data
- if self.numberOfContours == 0:
- return b""
- if recalcBBoxes:
- self.recalcBounds(glyfTable, boundsDone=boundsDone)
- data = sstruct.pack(glyphHeaderFormat, self)
- if self.isComposite():
- data = data + self.compileComponents(glyfTable)
- elif self.isVarComposite():
- data = data + self.compileVarComponents(glyfTable)
- else:
- data = data + self.compileCoordinates()
- return data
- def toXML(self, writer, ttFont):
- if self.isComposite():
- for compo in self.components:
- compo.toXML(writer, ttFont)
- haveInstructions = hasattr(self, "program")
- elif self.isVarComposite():
- for compo in self.components:
- compo.toXML(writer, ttFont)
- haveInstructions = False
- else:
- last = 0
- for i in range(self.numberOfContours):
- writer.begintag("contour")
- writer.newline()
- for j in range(last, self.endPtsOfContours[i] + 1):
- attrs = [
- ("x", self.coordinates[j][0]),
- ("y", self.coordinates[j][1]),
- ("on", self.flags[j] & flagOnCurve),
- ]
- if self.flags[j] & flagOverlapSimple:
- # Apple's rasterizer uses flagOverlapSimple in the first contour/first pt to flag glyphs that contain overlapping contours
- attrs.append(("overlap", 1))
- if self.flags[j] & flagCubic:
- attrs.append(("cubic", 1))
- writer.simpletag("pt", attrs)
- writer.newline()
- last = self.endPtsOfContours[i] + 1
- writer.endtag("contour")
- writer.newline()
- haveInstructions = self.numberOfContours > 0
- if haveInstructions:
- if self.program:
- writer.begintag("instructions")
- writer.newline()
- self.program.toXML(writer, ttFont)
- writer.endtag("instructions")
- else:
- writer.simpletag("instructions")
- writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- if name == "contour":
- if self.numberOfContours < 0:
- raise ttLib.TTLibError("can't mix composites and contours in glyph")
- self.numberOfContours = self.numberOfContours + 1
- coordinates = GlyphCoordinates()
- flags = bytearray()
- for element in content:
- if not isinstance(element, tuple):
- continue
- name, attrs, content = element
- if name != "pt":
- continue # ignore anything but "pt"
- coordinates.append((safeEval(attrs["x"]), safeEval(attrs["y"])))
- flag = bool(safeEval(attrs["on"]))
- if "overlap" in attrs and bool(safeEval(attrs["overlap"])):
- flag |= flagOverlapSimple
- if "cubic" in attrs and bool(safeEval(attrs["cubic"])):
- flag |= flagCubic
- flags.append(flag)
- if not hasattr(self, "coordinates"):
- self.coordinates = coordinates
- self.flags = flags
- self.endPtsOfContours = [len(coordinates) - 1]
- else:
- self.coordinates.extend(coordinates)
- self.flags.extend(flags)
- self.endPtsOfContours.append(len(self.coordinates) - 1)
- elif name == "component":
- if self.numberOfContours > 0:
- raise ttLib.TTLibError("can't mix composites and contours in glyph")
- self.numberOfContours = -1
- if not hasattr(self, "components"):
- self.components = []
- component = GlyphComponent()
- self.components.append(component)
- component.fromXML(name, attrs, content, ttFont)
- elif name == "varComponent":
- if self.numberOfContours > 0:
- raise ttLib.TTLibError("can't mix composites and contours in glyph")
- self.numberOfContours = -2
- if not hasattr(self, "components"):
- self.components = []
- component = GlyphVarComponent()
- self.components.append(component)
- component.fromXML(name, attrs, content, ttFont)
- elif name == "instructions":
- self.program = ttProgram.Program()
- for element in content:
- if not isinstance(element, tuple):
- continue
- name, attrs, content = element
- self.program.fromXML(name, attrs, content, ttFont)
- def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1):
- assert self.isComposite() or self.isVarComposite()
- nContours = 0
- nPoints = 0
- initialMaxComponentDepth = maxComponentDepth
- for compo in self.components:
- baseGlyph = glyfTable[compo.glyphName]
- if baseGlyph.numberOfContours == 0:
- continue
- elif baseGlyph.numberOfContours > 0:
- nP, nC = baseGlyph.getMaxpValues()
- else:
- nP, nC, componentDepth = baseGlyph.getCompositeMaxpValues(
- glyfTable, initialMaxComponentDepth + 1
- )
- maxComponentDepth = max(maxComponentDepth, componentDepth)
- nPoints = nPoints + nP
- nContours = nContours + nC
- return CompositeMaxpValues(nPoints, nContours, maxComponentDepth)
- def getMaxpValues(self):
- assert self.numberOfContours > 0
- return len(self.coordinates), len(self.endPtsOfContours)
- def decompileComponents(self, data, glyfTable):
- self.components = []
- more = 1
- haveInstructions = 0
- while more:
- component = GlyphComponent()
- more, haveInstr, data = component.decompile(data, glyfTable)
- haveInstructions = haveInstructions | haveInstr
- self.components.append(component)
- if haveInstructions:
- (numInstructions,) = struct.unpack(">h", data[:2])
- data = data[2:]
- self.program = ttProgram.Program()
- self.program.fromBytecode(data[:numInstructions])
- data = data[numInstructions:]
- if len(data) >= 4:
- log.warning(
- "too much glyph data at the end of composite glyph: %d excess bytes",
- len(data),
- )
- def decompileVarComponents(self, data, glyfTable):
- self.components = []
- while len(data) >= GlyphVarComponent.MIN_SIZE:
- component = GlyphVarComponent()
- data = component.decompile(data, glyfTable)
- self.components.append(component)
- def decompileCoordinates(self, data):
- endPtsOfContours = array.array("H")
- endPtsOfContours.frombytes(data[: 2 * self.numberOfContours])
- if sys.byteorder != "big":
- endPtsOfContours.byteswap()
- self.endPtsOfContours = endPtsOfContours.tolist()
- pos = 2 * self.numberOfContours
- (instructionLength,) = struct.unpack(">h", data[pos : pos + 2])
- self.program = ttProgram.Program()
- self.program.fromBytecode(data[pos + 2 : pos + 2 + instructionLength])
- pos += 2 + instructionLength
- nCoordinates = self.endPtsOfContours[-1] + 1
- flags, xCoordinates, yCoordinates = self.decompileCoordinatesRaw(
- nCoordinates, data, pos
- )
- # fill in repetitions and apply signs
- self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates)
- xIndex = 0
- yIndex = 0
- for i in range(nCoordinates):
- flag = flags[i]
- # x coordinate
- if flag & flagXShort:
- if flag & flagXsame:
- x = xCoordinates[xIndex]
- else:
- x = -xCoordinates[xIndex]
- xIndex = xIndex + 1
- elif flag & flagXsame:
- x = 0
- else:
- x = xCoordinates[xIndex]
- xIndex = xIndex + 1
- # y coordinate
- if flag & flagYShort:
- if flag & flagYsame:
- y = yCoordinates[yIndex]
- else:
- y = -yCoordinates[yIndex]
- yIndex = yIndex + 1
- elif flag & flagYsame:
- y = 0
- else:
- y = yCoordinates[yIndex]
- yIndex = yIndex + 1
- coordinates[i] = (x, y)
- assert xIndex == len(xCoordinates)
- assert yIndex == len(yCoordinates)
- coordinates.relativeToAbsolute()
- # discard all flags except "keepFlags"
- for i in range(len(flags)):
- flags[i] &= keepFlags
- self.flags = flags
- def decompileCoordinatesRaw(self, nCoordinates, data, pos=0):
- # unpack flags and prepare unpacking of coordinates
- flags = bytearray(nCoordinates)
- # Warning: deep Python trickery going on. We use the struct module to unpack
- # the coordinates. We build a format string based on the flags, so we can
- # unpack the coordinates in one struct.unpack() call.
- xFormat = ">" # big endian
- yFormat = ">" # big endian
- j = 0
- while True:
- flag = data[pos]
- pos += 1
- repeat = 1
- if flag & flagRepeat:
- repeat = data[pos] + 1
- pos += 1
- for k in range(repeat):
- if flag & flagXShort:
- xFormat = xFormat + "B"
- elif not (flag & flagXsame):
- xFormat = xFormat + "h"
- if flag & flagYShort:
- yFormat = yFormat + "B"
- elif not (flag & flagYsame):
- yFormat = yFormat + "h"
- flags[j] = flag
- j = j + 1
- if j >= nCoordinates:
- break
- assert j == nCoordinates, "bad glyph flags"
- # unpack raw coordinates, krrrrrr-tching!
- xDataLen = struct.calcsize(xFormat)
- yDataLen = struct.calcsize(yFormat)
- if len(data) - pos - (xDataLen + yDataLen) >= 4:
- log.warning(
- "too much glyph data: %d excess bytes",
- len(data) - pos - (xDataLen + yDataLen),
- )
- xCoordinates = struct.unpack(xFormat, data[pos : pos + xDataLen])
- yCoordinates = struct.unpack(
- yFormat, data[pos + xDataLen : pos + xDataLen + yDataLen]
- )
- return flags, xCoordinates, yCoordinates
- def compileComponents(self, glyfTable):
- data = b""
- lastcomponent = len(self.components) - 1
- more = 1
- haveInstructions = 0
- for i in range(len(self.components)):
- if i == lastcomponent:
- haveInstructions = hasattr(self, "program")
- more = 0
- compo = self.components[i]
- data = data + compo.compile(more, haveInstructions, glyfTable)
- if haveInstructions:
- instructions = self.program.getBytecode()
- data = data + struct.pack(">h", len(instructions)) + instructions
- return data
- def compileVarComponents(self, glyfTable):
- return b"".join(c.compile(glyfTable) for c in self.components)
- def compileCoordinates(self):
- assert len(self.coordinates) == len(self.flags)
- data = []
- endPtsOfContours = array.array("H", self.endPtsOfContours)
- if sys.byteorder != "big":
- endPtsOfContours.byteswap()
- data.append(endPtsOfContours.tobytes())
- instructions = self.program.getBytecode()
- data.append(struct.pack(">h", len(instructions)))
- data.append(instructions)
- deltas = self.coordinates.copy()
- deltas.toInt()
- deltas.absoluteToRelative()
- # TODO(behdad): Add a configuration option for this?
- deltas = self.compileDeltasGreedy(self.flags, deltas)
- # deltas = self.compileDeltasOptimal(self.flags, deltas)
- data.extend(deltas)
- return b"".join(data)
- def compileDeltasGreedy(self, flags, deltas):
- # Implements greedy algorithm for packing coordinate deltas:
- # uses shortest representation one coordinate at a time.
- compressedFlags = bytearray()
- compressedXs = bytearray()
- compressedYs = bytearray()
- lastflag = None
- repeat = 0
- for flag, (x, y) in zip(flags, deltas):
- # Oh, the horrors of TrueType
- # do x
- if x == 0:
- flag = flag | flagXsame
- elif -255 <= x <= 255:
- flag = flag | flagXShort
- if x > 0:
- flag = flag | flagXsame
- else:
- x = -x
- compressedXs.append(x)
- else:
- compressedXs.extend(struct.pack(">h", x))
- # do y
- if y == 0:
- flag = flag | flagYsame
- elif -255 <= y <= 255:
- flag = flag | flagYShort
- if y > 0:
- flag = flag | flagYsame
- else:
- y = -y
- compressedYs.append(y)
- else:
- compressedYs.extend(struct.pack(">h", y))
- # handle repeating flags
- if flag == lastflag and repeat != 255:
- repeat = repeat + 1
- if repeat == 1:
- compressedFlags.append(flag)
- else:
- compressedFlags[-2] = flag | flagRepeat
- compressedFlags[-1] = repeat
- else:
- repeat = 0
- compressedFlags.append(flag)
- lastflag = flag
- return (compressedFlags, compressedXs, compressedYs)
- def compileDeltasOptimal(self, flags, deltas):
- # Implements optimal, dynaic-programming, algorithm for packing coordinate
- # deltas. The savings are negligible :(.
- candidates = []
- bestTuple = None
- bestCost = 0
- repeat = 0
- for flag, (x, y) in zip(flags, deltas):
- # Oh, the horrors of TrueType
- flag, coordBytes = flagBest(x, y, flag)
- bestCost += 1 + coordBytes
- newCandidates = [
- (bestCost, bestTuple, flag, coordBytes),
- (bestCost + 1, bestTuple, (flag | flagRepeat), coordBytes),
- ]
- for lastCost, lastTuple, lastFlag, coordBytes in candidates:
- if (
- lastCost + coordBytes <= bestCost + 1
- and (lastFlag & flagRepeat)
- and (lastFlag < 0xFF00)
- and flagSupports(lastFlag, flag)
- ):
- if (lastFlag & 0xFF) == (
- flag | flagRepeat
- ) and lastCost == bestCost + 1:
- continue
- newCandidates.append(
- (lastCost + coordBytes, lastTuple, lastFlag + 256, coordBytes)
- )
- candidates = newCandidates
- bestTuple = min(candidates, key=lambda t: t[0])
- bestCost = bestTuple[0]
- flags = []
- while bestTuple:
- cost, bestTuple, flag, coordBytes = bestTuple
- flags.append(flag)
- flags.reverse()
- compressedFlags = bytearray()
- compressedXs = bytearray()
- compressedYs = bytearray()
- coords = iter(deltas)
- ff = []
- for flag in flags:
- repeatCount, flag = flag >> 8, flag & 0xFF
- compressedFlags.append(flag)
- if flag & flagRepeat:
- assert repeatCount > 0
- compressedFlags.append(repeatCount)
- else:
- assert repeatCount == 0
- for i in range(1 + repeatCount):
- x, y = next(coords)
- flagEncodeCoords(flag, x, y, compressedXs, compressedYs)
- ff.append(flag)
- try:
- next(coords)
- raise Exception("internal error")
- except StopIteration:
- pass
- return (compressedFlags, compressedXs, compressedYs)
- def recalcBounds(self, glyfTable, *, boundsDone=None):
- """Recalculates the bounds of the glyph.
- Each glyph object stores its bounding box in the
- ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
- recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
- must be provided to resolve component bounds.
- """
- if self.isComposite() and self.tryRecalcBoundsComposite(
- glyfTable, boundsDone=boundsDone
- ):
- return
- try:
- coords, endPts, flags = self.getCoordinates(glyfTable)
- self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds()
- except NotImplementedError:
- pass
- def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None):
- """Try recalculating the bounds of a composite glyph that has
- certain constrained properties. Namely, none of the components
- have a transform other than an integer translate, and none
- uses the anchor points.
- Each glyph object stores its bounding box in the
- ``xMin``/``yMin``/``xMax``/``yMax`` attributes. These bounds must be
- recomputed when the ``coordinates`` change. The ``table__g_l_y_f`` bounds
- must be provided to resolve component bounds.
- Return True if bounds were calculated, False otherwise.
- """
- for compo in self.components:
- if hasattr(compo, "firstPt") or hasattr(compo, "transform"):
- return False
- if not float(compo.x).is_integer() or not float(compo.y).is_integer():
- return False
- # All components are untransformed and have an integer x/y translate
- bounds = None
- for compo in self.components:
- glyphName = compo.glyphName
- g = glyfTable[glyphName]
- if boundsDone is None or glyphName not in boundsDone:
- g.recalcBounds(glyfTable, boundsDone=boundsDone)
- if boundsDone is not None:
- boundsDone.add(glyphName)
- # empty components shouldn't update the bounds of the parent glyph
- if g.numberOfContours == 0:
- continue
- x, y = compo.x, compo.y
- bounds = updateBounds(bounds, (g.xMin + x, g.yMin + y))
- bounds = updateBounds(bounds, (g.xMax + x, g.yMax + y))
- if bounds is None:
- bounds = (0, 0, 0, 0)
- self.xMin, self.yMin, self.xMax, self.yMax = bounds
- return True
- def isComposite(self):
- """Test whether a glyph has components"""
- if hasattr(self, "data"):
- return struct.unpack(">h", self.data[:2])[0] == -1 if self.data else False
- else:
- return self.numberOfContours == -1
- def isVarComposite(self):
- """Test whether a glyph has variable components"""
- if hasattr(self, "data"):
- return struct.unpack(">h", self.data[:2])[0] == -2 if self.data else False
- else:
- return self.numberOfContours == -2
- def getCoordinates(self, glyfTable):
- """Return the coordinates, end points and flags
- This method returns three values: A :py:class:`GlyphCoordinates` object,
- a list of the indexes of the final points of each contour (allowing you
- to split up the coordinates list into contours) and a list of flags.
- On simple glyphs, this method returns information from the glyph's own
- contours; on composite glyphs, it "flattens" all components recursively
- to return a list of coordinates representing all the components involved
- in the glyph.
- To interpret the flags for each point, see the "Simple Glyph Flags"
- section of the `glyf table specification <https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description>`.
- """
- if self.numberOfContours > 0:
- return self.coordinates, self.endPtsOfContours, self.flags
- elif self.isComposite():
- # it's a composite
- allCoords = GlyphCoordinates()
- allFlags = bytearray()
- allEndPts = []
- for compo in self.components:
- g = glyfTable[compo.glyphName]
- try:
- coordinates, endPts, flags = g.getCoordinates(glyfTable)
- except RecursionError:
- raise ttLib.TTLibError(
- "glyph '%s' contains a recursive component reference"
- % compo.glyphName
- )
- coordinates = GlyphCoordinates(coordinates)
- if hasattr(compo, "firstPt"):
- # component uses two reference points: we apply the transform _before_
- # computing the offset between the points
- if hasattr(compo, "transform"):
- coordinates.transform(compo.transform)
- x1, y1 = allCoords[compo.firstPt]
- x2, y2 = coordinates[compo.secondPt]
- move = x1 - x2, y1 - y2
- coordinates.translate(move)
- else:
- # component uses XY offsets
- move = compo.x, compo.y
- if not hasattr(compo, "transform"):
- coordinates.translate(move)
- else:
- apple_way = compo.flags & SCALED_COMPONENT_OFFSET
- ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
- assert not (apple_way and ms_way)
- if not (apple_way or ms_way):
- scale_component_offset = (
- SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file
- )
- else:
- scale_component_offset = apple_way
- if scale_component_offset:
- # the Apple way: first move, then scale (ie. scale the component offset)
- coordinates.translate(move)
- coordinates.transform(compo.transform)
- else:
- # the MS way: first scale, then move
- coordinates.transform(compo.transform)
- coordinates.translate(move)
- offset = len(allCoords)
- allEndPts.extend(e + offset for e in endPts)
- allCoords.extend(coordinates)
- allFlags.extend(flags)
- return allCoords, allEndPts, allFlags
- elif self.isVarComposite():
- raise NotImplementedError("use TTGlyphSet to draw VarComposite glyphs")
- else:
- return GlyphCoordinates(), [], bytearray()
- def getComponentNames(self, glyfTable):
- """Returns a list of names of component glyphs used in this glyph
- This method can be used on simple glyphs (in which case it returns an
- empty list) or composite glyphs.
- """
- if hasattr(self, "data") and self.isVarComposite():
- # TODO(VarComposite) Add implementation without expanding glyph
- self.expand(glyfTable)
- if not hasattr(self, "data"):
- if self.isComposite() or self.isVarComposite():
- return [c.glyphName for c in self.components]
- else:
- return []
- # Extract components without expanding glyph
- if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0:
- return [] # Not composite
- data = self.data
- i = 10
- components = []
- more = 1
- while more:
- flags, glyphID = struct.unpack(">HH", data[i : i + 4])
- i += 4
- flags = int(flags)
- components.append(glyfTable.getGlyphName(int(glyphID)))
- if flags & ARG_1_AND_2_ARE_WORDS:
- i += 4
- else:
- i += 2
- if flags & WE_HAVE_A_SCALE:
- i += 2
- elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
- i += 4
- elif flags & WE_HAVE_A_TWO_BY_TWO:
- i += 8
- more = flags & MORE_COMPONENTS
- return components
- def trim(self, remove_hinting=False):
- """Remove padding and, if requested, hinting, from a glyph.
- This works on both expanded and compacted glyphs, without
- expanding it."""
- if not hasattr(self, "data"):
- if remove_hinting:
- if self.isComposite():
- if hasattr(self, "program"):
- del self.program
- elif self.isVarComposite():
- pass # Doesn't have hinting
- else:
- self.program = ttProgram.Program()
- self.program.fromBytecode([])
- # No padding to trim.
- return
- if not self.data:
- return
- numContours = struct.unpack(">h", self.data[:2])[0]
- data = bytearray(self.data)
- i = 10
- if numContours >= 0:
- i += 2 * numContours # endPtsOfContours
- nCoordinates = ((data[i - 2] << 8) | data[i - 1]) + 1
- instructionLen = (data[i] << 8) | data[i + 1]
- if remove_hinting:
- # Zero instruction length
- data[i] = data[i + 1] = 0
- i += 2
- if instructionLen:
- # Splice it out
- data = data[:i] + data[i + instructionLen :]
- instructionLen = 0
- else:
- i += 2 + instructionLen
- coordBytes = 0
- j = 0
- while True:
- flag = data[i]
- i = i + 1
- repeat = 1
- if flag & flagRepeat:
- repeat = data[i] + 1
- i = i + 1
- xBytes = yBytes = 0
- if flag & flagXShort:
- xBytes = 1
- elif not (flag & flagXsame):
- xBytes = 2
- if flag & flagYShort:
- yBytes = 1
- elif not (flag & flagYsame):
- yBytes = 2
- coordBytes += (xBytes + yBytes) * repeat
- j += repeat
- if j >= nCoordinates:
- break
- assert j == nCoordinates, "bad glyph flags"
- i += coordBytes
- # Remove padding
- data = data[:i]
- elif self.isComposite():
- more = 1
- we_have_instructions = False
- while more:
- flags = (data[i] << 8) | data[i + 1]
- if remove_hinting:
- flags &= ~WE_HAVE_INSTRUCTIONS
- if flags & WE_HAVE_INSTRUCTIONS:
- we_have_instructions = True
- data[i + 0] = flags >> 8
- data[i + 1] = flags & 0xFF
- i += 4
- flags = int(flags)
- if flags & ARG_1_AND_2_ARE_WORDS:
- i += 4
- else:
- i += 2
- if flags & WE_HAVE_A_SCALE:
- i += 2
- elif flags & WE_HAVE_AN_X_AND_Y_SCALE:
- i += 4
- elif flags & WE_HAVE_A_TWO_BY_TWO:
- i += 8
- more = flags & MORE_COMPONENTS
- if we_have_instructions:
- instructionLen = (data[i] << 8) | data[i + 1]
- i += 2 + instructionLen
- # Remove padding
- data = data[:i]
- elif self.isVarComposite():
- i = 0
- MIN_SIZE = GlyphVarComponent.MIN_SIZE
- while len(data[i : i + MIN_SIZE]) >= MIN_SIZE:
- size = GlyphVarComponent.getSize(data[i : i + MIN_SIZE])
- i += size
- data = data[:i]
- self.data = data
- def removeHinting(self):
- """Removes TrueType hinting instructions from the glyph."""
- self.trim(remove_hinting=True)
- def draw(self, pen, glyfTable, offset=0):
- """Draws the glyph using the supplied pen object.
- Arguments:
- pen: An object conforming to the pen protocol.
- glyfTable: A :py:class:`table__g_l_y_f` object, to resolve components.
- offset (int): A horizontal offset. If provided, all coordinates are
- translated by this offset.
- """
- if self.isComposite():
- for component in self.components:
- glyphName, transform = component.getComponentInfo()
- pen.addComponent(glyphName, transform)
- return
- coordinates, endPts, flags = self.getCoordinates(glyfTable)
- if offset:
- coordinates = coordinates.copy()
- coordinates.translate((offset, 0))
- start = 0
- maybeInt = lambda v: int(v) if v == int(v) else v
- for end in endPts:
- end = end + 1
- contour = coordinates[start:end]
- cFlags = [flagOnCurve & f for f in flags[start:end]]
- cuFlags = [flagCubic & f for f in flags[start:end]]
- start = end
- if 1 not in cFlags:
- assert all(cuFlags) or not any(cuFlags)
- cubic = all(cuFlags)
- if cubic:
- count = len(contour)
- assert count % 2 == 0, "Odd number of cubic off-curves undefined"
- l = contour[-1]
- f = contour[0]
- p0 = (maybeInt((l[0] + f[0]) * 0.5), maybeInt((l[1] + f[1]) * 0.5))
- pen.moveTo(p0)
- for i in range(0, count, 2):
- p1 = contour[i]
- p2 = contour[i + 1]
- p4 = contour[i + 2 if i + 2 < count else 0]
- p3 = (
- maybeInt((p2[0] + p4[0]) * 0.5),
- maybeInt((p2[1] + p4[1]) * 0.5),
- )
- pen.curveTo(p1, p2, p3)
- else:
- # There is not a single on-curve point on the curve,
- # use pen.qCurveTo's special case by specifying None
- # as the on-curve point.
- contour.append(None)
- pen.qCurveTo(*contour)
- else:
- # Shuffle the points so that the contour is guaranteed
- # to *end* in an on-curve point, which we'll use for
- # the moveTo.
- firstOnCurve = cFlags.index(1) + 1
- contour = contour[firstOnCurve:] + contour[:firstOnCurve]
- cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve]
- cuFlags = cuFlags[firstOnCurve:] + cuFlags[:firstOnCurve]
- pen.moveTo(contour[-1])
- while contour:
- nextOnCurve = cFlags.index(1) + 1
- if nextOnCurve == 1:
- # Skip a final lineTo(), as it is implied by
- # pen.closePath()
- if len(contour) > 1:
- pen.lineTo(contour[0])
- else:
- cubicFlags = [f for f in cuFlags[: nextOnCurve - 1]]
- assert all(cubicFlags) or not any(cubicFlags)
- cubic = any(cubicFlags)
- if cubic:
- assert all(
- cubicFlags
- ), "Mixed cubic and quadratic segment undefined"
- count = nextOnCurve
- assert (
- count >= 3
- ), "At least two cubic off-curve points required"
- assert (
- count - 1
- ) % 2 == 0, "Odd number of cubic off-curves undefined"
- for i in range(0, count - 3, 2):
- p1 = contour[i]
- p2 = contour[i + 1]
- p4 = contour[i + 2]
- p3 = (
- maybeInt((p2[0] + p4[0]) * 0.5),
- maybeInt((p2[1] + p4[1]) * 0.5),
- )
- lastOnCurve = p3
- pen.curveTo(p1, p2, p3)
- pen.curveTo(*contour[count - 3 : count])
- else:
- pen.qCurveTo(*contour[:nextOnCurve])
- contour = contour[nextOnCurve:]
- cFlags = cFlags[nextOnCurve:]
- cuFlags = cuFlags[nextOnCurve:]
- pen.closePath()
- def drawPoints(self, pen, glyfTable, offset=0):
- """Draw the glyph using the supplied pointPen. As opposed to Glyph.draw(),
- this will not change the point indices.
- """
- if self.isComposite():
- for component in self.components:
- glyphName, transform = component.getComponentInfo()
- pen.addComponent(glyphName, transform)
- return
- coordinates, endPts, flags = self.getCoordinates(glyfTable)
- if offset:
- coordinates = coordinates.copy()
- coordinates.translate((offset, 0))
- start = 0
- for end in endPts:
- end = end + 1
- contour = coordinates[start:end]
- cFlags = flags[start:end]
- start = end
- pen.beginPath()
- # Start with the appropriate segment type based on the final segment
- if cFlags[-1] & flagOnCurve:
- segmentType = "line"
- elif cFlags[-1] & flagCubic:
- segmentType = "curve"
- else:
- segmentType = "qcurve"
- for i, pt in enumerate(contour):
- if cFlags[i] & flagOnCurve:
- pen.addPoint(pt, segmentType=segmentType)
- segmentType = "line"
- else:
- pen.addPoint(pt)
- segmentType = "curve" if cFlags[i] & flagCubic else "qcurve"
- pen.endPath()
- def __eq__(self, other):
- if type(self) != type(other):
- return NotImplemented
- return self.__dict__ == other.__dict__
- def __ne__(self, other):
- result = self.__eq__(other)
- return result if result is NotImplemented else not result
- # Vector.__round__ uses the built-in (Banker's) `round` but we want
- # to use otRound below
- _roundv = partial(Vector.__round__, round=otRound)
- def _is_mid_point(p0: tuple, p1: tuple, p2: tuple) -> bool:
- # True if p1 is in the middle of p0 and p2, either before or after rounding
- p0 = Vector(p0)
- p1 = Vector(p1)
- p2 = Vector(p2)
- return ((p0 + p2) * 0.5).isclose(p1) or _roundv(p0) + _roundv(p2) == _roundv(p1) * 2
- def dropImpliedOnCurvePoints(*interpolatable_glyphs: Glyph) -> Set[int]:
- """Drop impliable on-curve points from the (simple) glyph or glyphs.
- In TrueType glyf outlines, on-curve points can be implied when they are located at
- the midpoint of the line connecting two consecutive off-curve points.
- If more than one glyphs are passed, these are assumed to be interpolatable masters
- of the same glyph impliable, and thus only the on-curve points that are impliable
- for all of them will actually be implied.
- Composite glyphs or empty glyphs are skipped, only simple glyphs with 1 or more
- contours are considered.
- The input glyph(s) is/are modified in-place.
- Args:
- interpolatable_glyphs: The glyph or glyphs to modify in-place.
- Returns:
- The set of point indices that were dropped if any.
- Raises:
- ValueError if simple glyphs are not in fact interpolatable because they have
- different point flags or number of contours.
- Reference:
- https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
- """
- staticAttributes = SimpleNamespace(
- numberOfContours=None, flags=None, endPtsOfContours=None
- )
- drop = None
- simple_glyphs = []
- for i, glyph in enumerate(interpolatable_glyphs):
- if glyph.numberOfContours < 1:
- # ignore composite or empty glyphs
- continue
- for attr in staticAttributes.__dict__:
- expected = getattr(staticAttributes, attr)
- found = getattr(glyph, attr)
- if expected is None:
- setattr(staticAttributes, attr, found)
- elif expected != found:
- raise ValueError(
- f"Incompatible {attr} for glyph at master index {i}: "
- f"expected {expected}, found {found}"
- )
- may_drop = set()
- start = 0
- coords = glyph.coordinates
- flags = staticAttributes.flags
- endPtsOfContours = staticAttributes.endPtsOfContours
- for last in endPtsOfContours:
- for i in range(start, last + 1):
- if not (flags[i] & flagOnCurve):
- continue
- prv = i - 1 if i > start else last
- nxt = i + 1 if i < last else start
- if (flags[prv] & flagOnCurve) or flags[prv] != flags[nxt]:
- continue
- # we may drop the ith on-curve if halfway between previous/next off-curves
- if not _is_mid_point(coords[prv], coords[i], coords[nxt]):
- continue
- may_drop.add(i)
- start = last + 1
- # we only want to drop if ALL interpolatable glyphs have the same implied oncurves
- if drop is None:
- drop = may_drop
- else:
- drop.intersection_update(may_drop)
- simple_glyphs.append(glyph)
- if drop:
- # Do the actual dropping
- flags = staticAttributes.flags
- assert flags is not None
- newFlags = array.array(
- "B", (flags[i] for i in range(len(flags)) if i not in drop)
- )
- endPts = staticAttributes.endPtsOfContours
- assert endPts is not None
- newEndPts = []
- i = 0
- delta = 0
- for d in sorted(drop):
- while d > endPts[i]:
- newEndPts.append(endPts[i] - delta)
- i += 1
- delta += 1
- while i < len(endPts):
- newEndPts.append(endPts[i] - delta)
- i += 1
- for glyph in simple_glyphs:
- coords = glyph.coordinates
- glyph.coordinates = GlyphCoordinates(
- coords[i] for i in range(len(coords)) if i not in drop
- )
- glyph.flags = newFlags
- glyph.endPtsOfContours = newEndPts
- return drop if drop is not None else set()
- class GlyphComponent(object):
- """Represents a component within a composite glyph.
- The component is represented internally with four attributes: ``glyphName``,
- ``x``, ``y`` and ``transform``. If there is no "two-by-two" matrix (i.e
- no scaling, reflection, or rotation; only translation), the ``transform``
- attribute is not present.
- """
- # The above documentation is not *completely* true, but is *true enough* because
- # the rare firstPt/lastPt attributes are not totally supported and nobody seems to
- # mind - see below.
- def __init__(self):
- pass
- def getComponentInfo(self):
- """Return information about the component
- This method returns a tuple of two values: the glyph name of the component's
- base glyph, and a transformation matrix. As opposed to accessing the attributes
- directly, ``getComponentInfo`` always returns a six-element tuple of the
- component's transformation matrix, even when the two-by-two ``.transform``
- matrix is not present.
- """
- # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement
- # something equivalent in fontTools.objects.glyph (I'd rather not
- # convert it to an absolute offset, since it is valuable information).
- # This method will now raise "AttributeError: x" on glyphs that use
- # this TT feature.
- if hasattr(self, "transform"):
- [[xx, xy], [yx, yy]] = self.transform
- trans = (xx, xy, yx, yy, self.x, self.y)
- else:
- trans = (1, 0, 0, 1, self.x, self.y)
- return self.glyphName, trans
- def decompile(self, data, glyfTable):
- flags, glyphID = struct.unpack(">HH", data[:4])
- self.flags = int(flags)
- glyphID = int(glyphID)
- self.glyphName = glyfTable.getGlyphName(int(glyphID))
- data = data[4:]
- if self.flags & ARG_1_AND_2_ARE_WORDS:
- if self.flags & ARGS_ARE_XY_VALUES:
- self.x, self.y = struct.unpack(">hh", data[:4])
- else:
- x, y = struct.unpack(">HH", data[:4])
- self.firstPt, self.secondPt = int(x), int(y)
- data = data[4:]
- else:
- if self.flags & ARGS_ARE_XY_VALUES:
- self.x, self.y = struct.unpack(">bb", data[:2])
- else:
- x, y = struct.unpack(">BB", data[:2])
- self.firstPt, self.secondPt = int(x), int(y)
- data = data[2:]
- if self.flags & WE_HAVE_A_SCALE:
- (scale,) = struct.unpack(">h", data[:2])
- self.transform = [
- [fi2fl(scale, 14), 0],
- [0, fi2fl(scale, 14)],
- ] # fixed 2.14
- data = data[2:]
- elif self.flags & WE_HAVE_AN_X_AND_Y_SCALE:
- xscale, yscale = struct.unpack(">hh", data[:4])
- self.transform = [
- [fi2fl(xscale, 14), 0],
- [0, fi2fl(yscale, 14)],
- ] # fixed 2.14
- data = data[4:]
- elif self.flags & WE_HAVE_A_TWO_BY_TWO:
- (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8])
- self.transform = [
- [fi2fl(xscale, 14), fi2fl(scale01, 14)],
- [fi2fl(scale10, 14), fi2fl(yscale, 14)],
- ] # fixed 2.14
- data = data[8:]
- more = self.flags & MORE_COMPONENTS
- haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS
- self.flags = self.flags & (
- ROUND_XY_TO_GRID
- | USE_MY_METRICS
- | SCALED_COMPONENT_OFFSET
- | UNSCALED_COMPONENT_OFFSET
- | NON_OVERLAPPING
- | OVERLAP_COMPOUND
- )
- return more, haveInstructions, data
- def compile(self, more, haveInstructions, glyfTable):
- data = b""
- # reset all flags we will calculate ourselves
- flags = self.flags & (
- ROUND_XY_TO_GRID
- | USE_MY_METRICS
- | SCALED_COMPONENT_OFFSET
- | UNSCALED_COMPONENT_OFFSET
- | NON_OVERLAPPING
- | OVERLAP_COMPOUND
- )
- if more:
- flags = flags | MORE_COMPONENTS
- if haveInstructions:
- flags = flags | WE_HAVE_INSTRUCTIONS
- if hasattr(self, "firstPt"):
- if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255):
- data = data + struct.pack(">BB", self.firstPt, self.secondPt)
- else:
- data = data + struct.pack(">HH", self.firstPt, self.secondPt)
- flags = flags | ARG_1_AND_2_ARE_WORDS
- else:
- x = otRound(self.x)
- y = otRound(self.y)
- flags = flags | ARGS_ARE_XY_VALUES
- if (-128 <= x <= 127) and (-128 <= y <= 127):
- data = data + struct.pack(">bb", x, y)
- else:
- data = data + struct.pack(">hh", x, y)
- flags = flags | ARG_1_AND_2_ARE_WORDS
- if hasattr(self, "transform"):
- transform = [[fl2fi(x, 14) for x in row] for row in self.transform]
- if transform[0][1] or transform[1][0]:
- flags = flags | WE_HAVE_A_TWO_BY_TWO
- data = data + struct.pack(
- ">hhhh",
- transform[0][0],
- transform[0][1],
- transform[1][0],
- transform[1][1],
- )
- elif transform[0][0] != transform[1][1]:
- flags = flags | WE_HAVE_AN_X_AND_Y_SCALE
- data = data + struct.pack(">hh", transform[0][0], transform[1][1])
- else:
- flags = flags | WE_HAVE_A_SCALE
- data = data + struct.pack(">h", transform[0][0])
- glyphID = glyfTable.getGlyphID(self.glyphName)
- return struct.pack(">HH", flags, glyphID) + data
- def toXML(self, writer, ttFont):
- attrs = [("glyphName", self.glyphName)]
- if not hasattr(self, "firstPt"):
- attrs = attrs + [("x", self.x), ("y", self.y)]
- else:
- attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)]
- if hasattr(self, "transform"):
- transform = self.transform
- if transform[0][1] or transform[1][0]:
- attrs = attrs + [
- ("scalex", fl2str(transform[0][0], 14)),
- ("scale01", fl2str(transform[0][1], 14)),
- ("scale10", fl2str(transform[1][0], 14)),
- ("scaley", fl2str(transform[1][1], 14)),
- ]
- elif transform[0][0] != transform[1][1]:
- attrs = attrs + [
- ("scalex", fl2str(transform[0][0], 14)),
- ("scaley", fl2str(transform[1][1], 14)),
- ]
- else:
- attrs = attrs + [("scale", fl2str(transform[0][0], 14))]
- attrs = attrs + [("flags", hex(self.flags))]
- writer.simpletag("component", attrs)
- writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- self.glyphName = attrs["glyphName"]
- if "firstPt" in attrs:
- self.firstPt = safeEval(attrs["firstPt"])
- self.secondPt = safeEval(attrs["secondPt"])
- else:
- self.x = safeEval(attrs["x"])
- self.y = safeEval(attrs["y"])
- if "scale01" in attrs:
- scalex = str2fl(attrs["scalex"], 14)
- scale01 = str2fl(attrs["scale01"], 14)
- scale10 = str2fl(attrs["scale10"], 14)
- scaley = str2fl(attrs["scaley"], 14)
- self.transform = [[scalex, scale01], [scale10, scaley]]
- elif "scalex" in attrs:
- scalex = str2fl(attrs["scalex"], 14)
- scaley = str2fl(attrs["scaley"], 14)
- self.transform = [[scalex, 0], [0, scaley]]
- elif "scale" in attrs:
- scale = str2fl(attrs["scale"], 14)
- self.transform = [[scale, 0], [0, scale]]
- self.flags = safeEval(attrs["flags"])
- def __eq__(self, other):
- if type(self) != type(other):
- return NotImplemented
- return self.__dict__ == other.__dict__
- def __ne__(self, other):
- result = self.__eq__(other)
- return result if result is NotImplemented else not result
- #
- # Variable Composite glyphs
- # https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
- #
- class VarComponentFlags(IntFlag):
- USE_MY_METRICS = 0x0001
- AXIS_INDICES_ARE_SHORT = 0x0002
- UNIFORM_SCALE = 0x0004
- HAVE_TRANSLATE_X = 0x0008
- HAVE_TRANSLATE_Y = 0x0010
- HAVE_ROTATION = 0x0020
- HAVE_SCALE_X = 0x0040
- HAVE_SCALE_Y = 0x0080
- HAVE_SKEW_X = 0x0100
- HAVE_SKEW_Y = 0x0200
- HAVE_TCENTER_X = 0x0400
- HAVE_TCENTER_Y = 0x0800
- GID_IS_24BIT = 0x1000
- AXES_HAVE_VARIATION = 0x2000
- RESET_UNSPECIFIED_AXES = 0x4000
- VarComponentTransformMappingValues = namedtuple(
- "VarComponentTransformMappingValues",
- ["flag", "fractionalBits", "scale", "defaultValue"],
- )
- VAR_COMPONENT_TRANSFORM_MAPPING = {
- "translateX": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
- ),
- "translateY": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
- ),
- "rotation": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_ROTATION, 12, 180, 0
- ),
- "scaleX": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_SCALE_X, 10, 1, 1
- ),
- "scaleY": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1
- ),
- "skewX": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_SKEW_X, 12, -180, 0
- ),
- "skewY": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0
- ),
- "tCenterX": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0
- ),
- "tCenterY": VarComponentTransformMappingValues(
- VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0
- ),
- }
- class GlyphVarComponent(object):
- MIN_SIZE = 5
- def __init__(self):
- self.location = {}
- self.transform = DecomposedTransform()
- @staticmethod
- def getSize(data):
- size = 5
- flags = struct.unpack(">H", data[:2])[0]
- numAxes = int(data[2])
- if flags & VarComponentFlags.GID_IS_24BIT:
- size += 1
- size += numAxes
- if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
- size += 2 * numAxes
- else:
- axisIndices = array.array("B", data[:numAxes])
- size += numAxes
- for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
- if flags & mapping_values.flag:
- size += 2
- return size
- def decompile(self, data, glyfTable):
- flags = struct.unpack(">H", data[:2])[0]
- self.flags = int(flags)
- data = data[2:]
- numAxes = int(data[0])
- data = data[1:]
- if flags & VarComponentFlags.GID_IS_24BIT:
- glyphID = int(struct.unpack(">L", b"\0" + data[:3])[0])
- data = data[3:]
- flags ^= VarComponentFlags.GID_IS_24BIT
- else:
- glyphID = int(struct.unpack(">H", data[:2])[0])
- data = data[2:]
- self.glyphName = glyfTable.getGlyphName(int(glyphID))
- if flags & VarComponentFlags.AXIS_INDICES_ARE_SHORT:
- axisIndices = array.array("H", data[: 2 * numAxes])
- if sys.byteorder != "big":
- axisIndices.byteswap()
- data = data[2 * numAxes :]
- flags ^= VarComponentFlags.AXIS_INDICES_ARE_SHORT
- else:
- axisIndices = array.array("B", data[:numAxes])
- data = data[numAxes:]
- assert len(axisIndices) == numAxes
- axisIndices = list(axisIndices)
- axisValues = array.array("h", data[: 2 * numAxes])
- if sys.byteorder != "big":
- axisValues.byteswap()
- data = data[2 * numAxes :]
- assert len(axisValues) == numAxes
- axisValues = [fi2fl(v, 14) for v in axisValues]
- self.location = {
- glyfTable.axisTags[i]: v for i, v in zip(axisIndices, axisValues)
- }
- def read_transform_component(data, values):
- if flags & values.flag:
- return (
- data[2:],
- fi2fl(struct.unpack(">h", data[:2])[0], values.fractionalBits)
- * values.scale,
- )
- else:
- return data, values.defaultValue
- for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
- data, value = read_transform_component(data, mapping_values)
- setattr(self.transform, attr_name, value)
- if flags & VarComponentFlags.UNIFORM_SCALE:
- if flags & VarComponentFlags.HAVE_SCALE_X and not (
- flags & VarComponentFlags.HAVE_SCALE_Y
- ):
- self.transform.scaleY = self.transform.scaleX
- flags |= VarComponentFlags.HAVE_SCALE_Y
- flags ^= VarComponentFlags.UNIFORM_SCALE
- return data
- def compile(self, glyfTable):
- data = b""
- if not hasattr(self, "flags"):
- flags = 0
- # Calculate optimal transform component flags
- for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
- value = getattr(self.transform, attr_name)
- if fl2fi(value / mapping.scale, mapping.fractionalBits) != fl2fi(
- mapping.defaultValue / mapping.scale, mapping.fractionalBits
- ):
- flags |= mapping.flag
- else:
- flags = self.flags
- if (
- flags & VarComponentFlags.HAVE_SCALE_X
- and flags & VarComponentFlags.HAVE_SCALE_Y
- and fl2fi(self.transform.scaleX, 10) == fl2fi(self.transform.scaleY, 10)
- ):
- flags |= VarComponentFlags.UNIFORM_SCALE
- flags ^= VarComponentFlags.HAVE_SCALE_Y
- numAxes = len(self.location)
- data = data + struct.pack(">B", numAxes)
- glyphID = glyfTable.getGlyphID(self.glyphName)
- if glyphID > 65535:
- flags |= VarComponentFlags.GID_IS_24BIT
- data = data + struct.pack(">L", glyphID)[1:]
- else:
- data = data + struct.pack(">H", glyphID)
- axisIndices = [glyfTable.axisTags.index(tag) for tag in self.location.keys()]
- if all(a <= 255 for a in axisIndices):
- axisIndices = array.array("B", axisIndices)
- else:
- axisIndices = array.array("H", axisIndices)
- if sys.byteorder != "big":
- axisIndices.byteswap()
- flags |= VarComponentFlags.AXIS_INDICES_ARE_SHORT
- data = data + bytes(axisIndices)
- axisValues = self.location.values()
- axisValues = array.array("h", (fl2fi(v, 14) for v in axisValues))
- if sys.byteorder != "big":
- axisValues.byteswap()
- data = data + bytes(axisValues)
- def write_transform_component(data, value, values):
- if flags & values.flag:
- return data + struct.pack(
- ">h", fl2fi(value / values.scale, values.fractionalBits)
- )
- else:
- return data
- for attr_name, mapping_values in VAR_COMPONENT_TRANSFORM_MAPPING.items():
- value = getattr(self.transform, attr_name)
- data = write_transform_component(data, value, mapping_values)
- return struct.pack(">H", flags) + data
- def toXML(self, writer, ttFont):
- attrs = [("glyphName", self.glyphName)]
- if hasattr(self, "flags"):
- attrs = attrs + [("flags", hex(self.flags))]
- for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
- v = getattr(self.transform, attr_name)
- if v != mapping.defaultValue:
- attrs.append((attr_name, fl2str(v, mapping.fractionalBits)))
- writer.begintag("varComponent", attrs)
- writer.newline()
- writer.begintag("location")
- writer.newline()
- for tag, v in self.location.items():
- writer.simpletag("axis", [("tag", tag), ("value", fl2str(v, 14))])
- writer.newline()
- writer.endtag("location")
- writer.newline()
- writer.endtag("varComponent")
- writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- self.glyphName = attrs["glyphName"]
- if "flags" in attrs:
- self.flags = safeEval(attrs["flags"])
- for attr_name, mapping in VAR_COMPONENT_TRANSFORM_MAPPING.items():
- if attr_name not in attrs:
- continue
- v = str2fl(safeEval(attrs[attr_name]), mapping.fractionalBits)
- setattr(self.transform, attr_name, v)
- for c in content:
- if not isinstance(c, tuple):
- continue
- name, attrs, content = c
- if name != "location":
- continue
- for c in content:
- if not isinstance(c, tuple):
- continue
- name, attrs, content = c
- assert name == "axis"
- assert not content
- self.location[attrs["tag"]] = str2fl(safeEval(attrs["value"]), 14)
- def getPointCount(self):
- assert hasattr(self, "flags"), "VarComponent with variations must have flags"
- count = 0
- if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
- count += len(self.location)
- if self.flags & (
- VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
- ):
- count += 1
- if self.flags & VarComponentFlags.HAVE_ROTATION:
- count += 1
- if self.flags & (
- VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
- ):
- count += 1
- if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
- count += 1
- if self.flags & (
- VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
- ):
- count += 1
- return count
- def getCoordinatesAndControls(self):
- coords = []
- controls = []
- if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
- for tag, v in self.location.items():
- controls.append(tag)
- coords.append((fl2fi(v, 14), 0))
- if self.flags & (
- VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
- ):
- controls.append("translate")
- coords.append((self.transform.translateX, self.transform.translateY))
- if self.flags & VarComponentFlags.HAVE_ROTATION:
- controls.append("rotation")
- coords.append((fl2fi(self.transform.rotation / 180, 12), 0))
- if self.flags & (
- VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
- ):
- controls.append("scale")
- coords.append(
- (fl2fi(self.transform.scaleX, 10), fl2fi(self.transform.scaleY, 10))
- )
- if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
- controls.append("skew")
- coords.append(
- (
- fl2fi(self.transform.skewX / -180, 12),
- fl2fi(self.transform.skewY / 180, 12),
- )
- )
- if self.flags & (
- VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
- ):
- controls.append("tCenter")
- coords.append((self.transform.tCenterX, self.transform.tCenterY))
- return coords, controls
- def setCoordinates(self, coords):
- i = 0
- if self.flags & VarComponentFlags.AXES_HAVE_VARIATION:
- newLocation = {}
- for tag in self.location:
- newLocation[tag] = fi2fl(coords[i][0], 14)
- i += 1
- self.location = newLocation
- self.transform = DecomposedTransform()
- if self.flags & (
- VarComponentFlags.HAVE_TRANSLATE_X | VarComponentFlags.HAVE_TRANSLATE_Y
- ):
- self.transform.translateX, self.transform.translateY = coords[i]
- i += 1
- if self.flags & VarComponentFlags.HAVE_ROTATION:
- self.transform.rotation = fi2fl(coords[i][0], 12) * 180
- i += 1
- if self.flags & (
- VarComponentFlags.HAVE_SCALE_X | VarComponentFlags.HAVE_SCALE_Y
- ):
- self.transform.scaleX, self.transform.scaleY = fi2fl(
- coords[i][0], 10
- ), fi2fl(coords[i][1], 10)
- i += 1
- if self.flags & (VarComponentFlags.HAVE_SKEW_X | VarComponentFlags.HAVE_SKEW_Y):
- self.transform.skewX, self.transform.skewY = (
- fi2fl(coords[i][0], 12) * -180,
- fi2fl(coords[i][1], 12) * 180,
- )
- i += 1
- if self.flags & (
- VarComponentFlags.HAVE_TCENTER_X | VarComponentFlags.HAVE_TCENTER_Y
- ):
- self.transform.tCenterX, self.transform.tCenterY = coords[i]
- i += 1
- return coords[i:]
- def __eq__(self, other):
- if type(self) != type(other):
- return NotImplemented
- return self.__dict__ == other.__dict__
- def __ne__(self, other):
- result = self.__eq__(other)
- return result if result is NotImplemented else not result
- class GlyphCoordinates(object):
- """A list of glyph coordinates.
- Unlike an ordinary list, this is a numpy-like matrix object which supports
- matrix addition, scalar multiplication and other operations described below.
- """
- def __init__(self, iterable=[]):
- self._a = array.array("d")
- self.extend(iterable)
- @property
- def array(self):
- """Returns the underlying array of coordinates"""
- return self._a
- @staticmethod
- def zeros(count):
- """Creates a new ``GlyphCoordinates`` object with all coordinates set to (0,0)"""
- g = GlyphCoordinates()
- g._a.frombytes(bytes(count * 2 * g._a.itemsize))
- return g
- def copy(self):
- """Creates a new ``GlyphCoordinates`` object which is a copy of the current one."""
- c = GlyphCoordinates()
- c._a.extend(self._a)
- return c
- def __len__(self):
- """Returns the number of coordinates in the array."""
- return len(self._a) // 2
- def __getitem__(self, k):
- """Returns a two element tuple (x,y)"""
- a = self._a
- if isinstance(k, slice):
- indices = range(*k.indices(len(self)))
- # Instead of calling ourselves recursively, duplicate code; faster
- ret = []
- for k in indices:
- x = a[2 * k]
- y = a[2 * k + 1]
- ret.append(
- (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
- )
- return ret
- x = a[2 * k]
- y = a[2 * k + 1]
- return (int(x) if x.is_integer() else x, int(y) if y.is_integer() else y)
- def __setitem__(self, k, v):
- """Sets a point's coordinates to a two element tuple (x,y)"""
- if isinstance(k, slice):
- indices = range(*k.indices(len(self)))
- # XXX This only works if len(v) == len(indices)
- for j, i in enumerate(indices):
- self[i] = v[j]
- return
- self._a[2 * k], self._a[2 * k + 1] = v
- def __delitem__(self, i):
- """Removes a point from the list"""
- i = (2 * i) % len(self._a)
- del self._a[i]
- del self._a[i]
- def __repr__(self):
- return "GlyphCoordinates([" + ",".join(str(c) for c in self) + "])"
- def append(self, p):
- self._a.extend(tuple(p))
- def extend(self, iterable):
- for p in iterable:
- self._a.extend(p)
- def toInt(self, *, round=otRound):
- if round is noRound:
- return
- a = self._a
- for i in range(len(a)):
- a[i] = round(a[i])
- def calcBounds(self):
- a = self._a
- if not a:
- return 0, 0, 0, 0
- xs = a[0::2]
- ys = a[1::2]
- return min(xs), min(ys), max(xs), max(ys)
- def calcIntBounds(self, round=otRound):
- return tuple(round(v) for v in self.calcBounds())
- def relativeToAbsolute(self):
- a = self._a
- x, y = 0, 0
- for i in range(0, len(a), 2):
- a[i] = x = a[i] + x
- a[i + 1] = y = a[i + 1] + y
- def absoluteToRelative(self):
- a = self._a
- x, y = 0, 0
- for i in range(0, len(a), 2):
- nx = a[i]
- ny = a[i + 1]
- a[i] = nx - x
- a[i + 1] = ny - y
- x = nx
- y = ny
- def translate(self, p):
- """
- >>> GlyphCoordinates([(1,2)]).translate((.5,0))
- """
- x, y = p
- if x == 0 and y == 0:
- return
- a = self._a
- for i in range(0, len(a), 2):
- a[i] += x
- a[i + 1] += y
- def scale(self, p):
- """
- >>> GlyphCoordinates([(1,2)]).scale((.5,0))
- """
- x, y = p
- if x == 1 and y == 1:
- return
- a = self._a
- for i in range(0, len(a), 2):
- a[i] *= x
- a[i + 1] *= y
- def transform(self, t):
- """
- >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5)))
- """
- a = self._a
- for i in range(0, len(a), 2):
- x = a[i]
- y = a[i + 1]
- px = x * t[0][0] + y * t[1][0]
- py = x * t[0][1] + y * t[1][1]
- a[i] = px
- a[i + 1] = py
- def __eq__(self, other):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g2 = GlyphCoordinates([(1.0,2)])
- >>> g3 = GlyphCoordinates([(1.5,2)])
- >>> g == g2
- True
- >>> g == g3
- False
- >>> g2 == g3
- False
- """
- if type(self) != type(other):
- return NotImplemented
- return self._a == other._a
- def __ne__(self, other):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g2 = GlyphCoordinates([(1.0,2)])
- >>> g3 = GlyphCoordinates([(1.5,2)])
- >>> g != g2
- False
- >>> g != g3
- True
- >>> g2 != g3
- True
- """
- result = self.__eq__(other)
- return result if result is NotImplemented else not result
- # Math operations
- def __pos__(self):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g
- GlyphCoordinates([(1, 2)])
- >>> g2 = +g
- >>> g2
- GlyphCoordinates([(1, 2)])
- >>> g2.translate((1,0))
- >>> g2
- GlyphCoordinates([(2, 2)])
- >>> g
- GlyphCoordinates([(1, 2)])
- """
- return self.copy()
- def __neg__(self):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g
- GlyphCoordinates([(1, 2)])
- >>> g2 = -g
- >>> g2
- GlyphCoordinates([(-1, -2)])
- >>> g
- GlyphCoordinates([(1, 2)])
- """
- r = self.copy()
- a = r._a
- for i in range(len(a)):
- a[i] = -a[i]
- return r
- def __round__(self, *, round=otRound):
- r = self.copy()
- r.toInt(round=round)
- return r
- def __add__(self, other):
- return self.copy().__iadd__(other)
- def __sub__(self, other):
- return self.copy().__isub__(other)
- def __mul__(self, other):
- return self.copy().__imul__(other)
- def __truediv__(self, other):
- return self.copy().__itruediv__(other)
- __radd__ = __add__
- __rmul__ = __mul__
- def __rsub__(self, other):
- return other + (-self)
- def __iadd__(self, other):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g += (.5,0)
- >>> g
- GlyphCoordinates([(1.5, 2)])
- >>> g2 = GlyphCoordinates([(3,4)])
- >>> g += g2
- >>> g
- GlyphCoordinates([(4.5, 6)])
- """
- if isinstance(other, tuple):
- assert len(other) == 2
- self.translate(other)
- return self
- if isinstance(other, GlyphCoordinates):
- other = other._a
- a = self._a
- assert len(a) == len(other)
- for i in range(len(a)):
- a[i] += other[i]
- return self
- return NotImplemented
- def __isub__(self, other):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g -= (.5,0)
- >>> g
- GlyphCoordinates([(0.5, 2)])
- >>> g2 = GlyphCoordinates([(3,4)])
- >>> g -= g2
- >>> g
- GlyphCoordinates([(-2.5, -2)])
- """
- if isinstance(other, tuple):
- assert len(other) == 2
- self.translate((-other[0], -other[1]))
- return self
- if isinstance(other, GlyphCoordinates):
- other = other._a
- a = self._a
- assert len(a) == len(other)
- for i in range(len(a)):
- a[i] -= other[i]
- return self
- return NotImplemented
- def __imul__(self, other):
- """
- >>> g = GlyphCoordinates([(1,2)])
- >>> g *= (2,.5)
- >>> g *= 2
- >>> g
- GlyphCoordinates([(4, 2)])
- >>> g = GlyphCoordinates([(1,2)])
- >>> g *= 2
- >>> g
- GlyphCoordinates([(2, 4)])
- """
- if isinstance(other, tuple):
- assert len(other) == 2
- self.scale(other)
- return self
- if isinstance(other, Number):
- if other == 1:
- return self
- a = self._a
- for i in range(len(a)):
- a[i] *= other
- return self
- return NotImplemented
- def __itruediv__(self, other):
- """
- >>> g = GlyphCoordinates([(1,3)])
- >>> g /= (.5,1.5)
- >>> g /= 2
- >>> g
- GlyphCoordinates([(1, 1)])
- """
- if isinstance(other, Number):
- other = (other, other)
- if isinstance(other, tuple):
- if other == (1, 1):
- return self
- assert len(other) == 2
- self.scale((1.0 / other[0], 1.0 / other[1]))
- return self
- return NotImplemented
- def __bool__(self):
- """
- >>> g = GlyphCoordinates([])
- >>> bool(g)
- False
- >>> g = GlyphCoordinates([(0,0), (0.,0)])
- >>> bool(g)
- True
- >>> g = GlyphCoordinates([(0,0), (1,0)])
- >>> bool(g)
- True
- >>> g = GlyphCoordinates([(0,.5), (0,0)])
- >>> bool(g)
- True
- """
- return bool(self._a)
- __nonzero__ = __bool__
- if __name__ == "__main__":
- import doctest, sys
- sys.exit(doctest.testmod().failed)
|