1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017 |
- """
- glifLib.py -- Generic module for reading and writing the .glif format.
- More info about the .glif format (GLyphInterchangeFormat) can be found here:
- http://unifiedfontobject.org
- The main class in this module is GlyphSet. It manages a set of .glif files
- in a folder. It offers two ways to read glyph data, and one way to write
- glyph data. See the class doc string for details.
- """
- from __future__ import annotations
- import logging
- import enum
- from warnings import warn
- from collections import OrderedDict
- import fs
- import fs.base
- import fs.errors
- import fs.osfs
- import fs.path
- from fontTools.misc.textTools import tobytes
- from fontTools.misc import plistlib
- from fontTools.pens.pointPen import AbstractPointPen, PointToSegmentPen
- from fontTools.ufoLib.errors import GlifLibError
- from fontTools.ufoLib.filenames import userNameToFileName
- from fontTools.ufoLib.validators import (
- genericTypeValidator,
- colorValidator,
- guidelinesValidator,
- anchorsValidator,
- identifierValidator,
- imageValidator,
- glyphLibValidator,
- )
- from fontTools.misc import etree
- from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion
- from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
- __all__ = [
- "GlyphSet",
- "GlifLibError",
- "readGlyphFromString",
- "writeGlyphToString",
- "glyphNameToFileName",
- ]
- logger = logging.getLogger(__name__)
- # ---------
- # Constants
- # ---------
- CONTENTS_FILENAME = "contents.plist"
- LAYERINFO_FILENAME = "layerinfo.plist"
- class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
- FORMAT_1_0 = (1, 0)
- FORMAT_2_0 = (2, 0)
- @classmethod
- def default(cls, ufoFormatVersion=None):
- if ufoFormatVersion is not None:
- return max(cls.supported_versions(ufoFormatVersion))
- return super().default()
- @classmethod
- def supported_versions(cls, ufoFormatVersion=None):
- if ufoFormatVersion is None:
- # if ufo format unspecified, return all the supported GLIF formats
- return super().supported_versions()
- # else only return the GLIF formats supported by the given UFO format
- versions = {cls.FORMAT_1_0}
- if ufoFormatVersion >= UFOFormatVersion.FORMAT_3_0:
- versions.add(cls.FORMAT_2_0)
- return frozenset(versions)
- # workaround for py3.11, see https://github.com/fonttools/fonttools/pull/2655
- GLIFFormatVersion.__str__ = _VersionTupleEnumMixin.__str__
- # ------------
- # Simple Glyph
- # ------------
- class Glyph:
- """
- Minimal glyph object. It has no glyph attributes until either
- the draw() or the drawPoints() method has been called.
- """
- def __init__(self, glyphName, glyphSet):
- self.glyphName = glyphName
- self.glyphSet = glyphSet
- def draw(self, pen, outputImpliedClosingLine=False):
- """
- Draw this glyph onto a *FontTools* Pen.
- """
- pointPen = PointToSegmentPen(
- pen, outputImpliedClosingLine=outputImpliedClosingLine
- )
- self.drawPoints(pointPen)
- def drawPoints(self, pointPen):
- """
- Draw this glyph onto a PointPen.
- """
- self.glyphSet.readGlyph(self.glyphName, self, pointPen)
- # ---------
- # Glyph Set
- # ---------
- class GlyphSet(_UFOBaseIO):
- """
- GlyphSet manages a set of .glif files inside one directory.
- GlyphSet's constructor takes a path to an existing directory as it's
- first argument. Reading glyph data can either be done through the
- readGlyph() method, or by using GlyphSet's dictionary interface, where
- the keys are glyph names and the values are (very) simple glyph objects.
- To write a glyph to the glyph set, you use the writeGlyph() method.
- The simple glyph objects returned through the dict interface do not
- support writing, they are just a convenient way to get at the glyph data.
- """
- glyphClass = Glyph
- def __init__(
- self,
- path,
- glyphNameToFileNameFunc=None,
- ufoFormatVersion=None,
- validateRead=True,
- validateWrite=True,
- expectContentsFile=False,
- ):
- """
- 'path' should be a path (string) to an existing local directory, or
- an instance of fs.base.FS class.
- The optional 'glyphNameToFileNameFunc' argument must be a callback
- function that takes two arguments: a glyph name and a list of all
- existing filenames (if any exist). It should return a file name
- (including the .glif extension). The glyphNameToFileName function
- is called whenever a file name is created for a given glyph name.
- ``validateRead`` will validate read operations. Its default is ``True``.
- ``validateWrite`` will validate write operations. Its default is ``True``.
- ``expectContentsFile`` will raise a GlifLibError if a contents.plist file is
- not found on the glyph set file system. This should be set to ``True`` if you
- are reading an existing UFO and ``False`` if you create a fresh glyph set.
- """
- try:
- ufoFormatVersion = UFOFormatVersion(ufoFormatVersion)
- except ValueError as e:
- from fontTools.ufoLib.errors import UnsupportedUFOFormat
- raise UnsupportedUFOFormat(
- f"Unsupported UFO format: {ufoFormatVersion!r}"
- ) from e
- if hasattr(path, "__fspath__"): # support os.PathLike objects
- path = path.__fspath__()
- if isinstance(path, str):
- try:
- filesystem = fs.osfs.OSFS(path)
- except fs.errors.CreateFailed:
- raise GlifLibError("No glyphs directory '%s'" % path)
- self._shouldClose = True
- elif isinstance(path, fs.base.FS):
- filesystem = path
- try:
- filesystem.check()
- except fs.errors.FilesystemClosed:
- raise GlifLibError("the filesystem '%s' is closed" % filesystem)
- self._shouldClose = False
- else:
- raise TypeError(
- "Expected a path string or fs object, found %s" % type(path).__name__
- )
- try:
- path = filesystem.getsyspath("/")
- except fs.errors.NoSysPath:
- # network or in-memory FS may not map to the local one
- path = str(filesystem)
- # 'dirName' is kept for backward compatibility only, but it's DEPRECATED
- # as it's not guaranteed that it maps to an existing OSFS directory.
- # Client could use the FS api via the `self.fs` attribute instead.
- self.dirName = fs.path.parts(path)[-1]
- self.fs = filesystem
- # if glyphSet contains no 'contents.plist', we consider it empty
- self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME)
- if expectContentsFile and not self._havePreviousFile:
- raise GlifLibError(f"{CONTENTS_FILENAME} is missing.")
- # attribute kept for backward compatibility
- self.ufoFormatVersion = ufoFormatVersion.major
- self.ufoFormatVersionTuple = ufoFormatVersion
- if glyphNameToFileNameFunc is None:
- glyphNameToFileNameFunc = glyphNameToFileName
- self.glyphNameToFileName = glyphNameToFileNameFunc
- self._validateRead = validateRead
- self._validateWrite = validateWrite
- self._existingFileNames: set[str] | None = None
- self._reverseContents = None
- self.rebuildContents()
- def rebuildContents(self, validateRead=None):
- """
- Rebuild the contents dict by loading contents.plist.
- ``validateRead`` will validate the data, by default it is set to the
- class's ``validateRead`` value, can be overridden.
- """
- if validateRead is None:
- validateRead = self._validateRead
- contents = self._getPlist(CONTENTS_FILENAME, {})
- # validate the contents
- if validateRead:
- invalidFormat = False
- if not isinstance(contents, dict):
- invalidFormat = True
- else:
- for name, fileName in contents.items():
- if not isinstance(name, str):
- invalidFormat = True
- if not isinstance(fileName, str):
- invalidFormat = True
- elif not self.fs.exists(fileName):
- raise GlifLibError(
- "%s references a file that does not exist: %s"
- % (CONTENTS_FILENAME, fileName)
- )
- if invalidFormat:
- raise GlifLibError("%s is not properly formatted" % CONTENTS_FILENAME)
- self.contents = contents
- self._existingFileNames = None
- self._reverseContents = None
- def getReverseContents(self):
- """
- Return a reversed dict of self.contents, mapping file names to
- glyph names. This is primarily an aid for custom glyph name to file
- name schemes that want to make sure they don't generate duplicate
- file names. The file names are converted to lowercase so we can
- reliably check for duplicates that only differ in case, which is
- important for case-insensitive file systems.
- """
- if self._reverseContents is None:
- d = {}
- for k, v in self.contents.items():
- d[v.lower()] = k
- self._reverseContents = d
- return self._reverseContents
- def writeContents(self):
- """
- Write the contents.plist file out to disk. Call this method when
- you're done writing glyphs.
- """
- self._writePlist(CONTENTS_FILENAME, self.contents)
- # layer info
- def readLayerInfo(self, info, validateRead=None):
- """
- ``validateRead`` will validate the data, by default it is set to the
- class's ``validateRead`` value, can be overridden.
- """
- if validateRead is None:
- validateRead = self._validateRead
- infoDict = self._getPlist(LAYERINFO_FILENAME, {})
- if validateRead:
- if not isinstance(infoDict, dict):
- raise GlifLibError("layerinfo.plist is not properly formatted.")
- infoDict = validateLayerInfoVersion3Data(infoDict)
- # populate the object
- for attr, value in infoDict.items():
- try:
- setattr(info, attr, value)
- except AttributeError:
- raise GlifLibError(
- "The supplied layer info object does not support setting a necessary attribute (%s)."
- % attr
- )
- def writeLayerInfo(self, info, validateWrite=None):
- """
- ``validateWrite`` will validate the data, by default it is set to the
- class's ``validateWrite`` value, can be overridden.
- """
- if validateWrite is None:
- validateWrite = self._validateWrite
- if self.ufoFormatVersionTuple.major < 3:
- raise GlifLibError(
- "layerinfo.plist is not allowed in UFO %d."
- % self.ufoFormatVersionTuple.major
- )
- # gather data
- infoData = {}
- for attr in layerInfoVersion3ValueData.keys():
- if hasattr(info, attr):
- try:
- value = getattr(info, attr)
- except AttributeError:
- raise GlifLibError(
- "The supplied info object does not support getting a necessary attribute (%s)."
- % attr
- )
- if value is None or (attr == "lib" and not value):
- continue
- infoData[attr] = value
- if infoData:
- # validate
- if validateWrite:
- infoData = validateLayerInfoVersion3Data(infoData)
- # write file
- self._writePlist(LAYERINFO_FILENAME, infoData)
- elif self._havePreviousFile and self.fs.exists(LAYERINFO_FILENAME):
- # data empty, remove existing file
- self.fs.remove(LAYERINFO_FILENAME)
- def getGLIF(self, glyphName):
- """
- Get the raw GLIF text for a given glyph name. This only works
- for GLIF files that are already on disk.
- This method is useful in situations when the raw XML needs to be
- read from a glyph set for a particular glyph before fully parsing
- it into an object structure via the readGlyph method.
- Raises KeyError if 'glyphName' is not in contents.plist, or
- GlifLibError if the file associated with can't be found.
- """
- fileName = self.contents[glyphName]
- try:
- return self.fs.readbytes(fileName)
- except fs.errors.ResourceNotFound:
- raise GlifLibError(
- "The file '%s' associated with glyph '%s' in contents.plist "
- "does not exist on %s" % (fileName, glyphName, self.fs)
- )
- def getGLIFModificationTime(self, glyphName):
- """
- Returns the modification time for the GLIF file with 'glyphName', as
- a floating point number giving the number of seconds since the epoch.
- Return None if the associated file does not exist or the underlying
- filesystem does not support getting modified times.
- Raises KeyError if the glyphName is not in contents.plist.
- """
- fileName = self.contents[glyphName]
- return self.getFileModificationTime(fileName)
- # reading/writing API
- def readGlyph(self, glyphName, glyphObject=None, pointPen=None, validate=None):
- """
- Read a .glif file for 'glyphName' from the glyph set. The
- 'glyphObject' argument can be any kind of object (even None);
- the readGlyph() method will attempt to set the following
- attributes on it:
- width
- the advance width of the glyph
- height
- the advance height of the glyph
- unicodes
- a list of unicode values for this glyph
- note
- a string
- lib
- a dictionary containing custom data
- image
- a dictionary containing image data
- guidelines
- a list of guideline data dictionaries
- anchors
- a list of anchor data dictionaries
- All attributes are optional, in two ways:
- 1) An attribute *won't* be set if the .glif file doesn't
- contain data for it. 'glyphObject' will have to deal
- with default values itself.
- 2) If setting the attribute fails with an AttributeError
- (for example if the 'glyphObject' attribute is read-
- only), readGlyph() will not propagate that exception,
- but ignore that attribute.
- To retrieve outline information, you need to pass an object
- conforming to the PointPen protocol as the 'pointPen' argument.
- This argument may be None if you don't need the outline data.
- readGlyph() will raise KeyError if the glyph is not present in
- the glyph set.
- ``validate`` will validate the data, by default it is set to the
- class's ``validateRead`` value, can be overridden.
- """
- if validate is None:
- validate = self._validateRead
- text = self.getGLIF(glyphName)
- try:
- tree = _glifTreeFromString(text)
- formatVersions = GLIFFormatVersion.supported_versions(
- self.ufoFormatVersionTuple
- )
- _readGlyphFromTree(
- tree,
- glyphObject,
- pointPen,
- formatVersions=formatVersions,
- validate=validate,
- )
- except GlifLibError as glifLibError:
- # Re-raise with a note that gives extra context, describing where
- # the error occurred.
- fileName = self.contents[glyphName]
- try:
- glifLocation = f"'{self.fs.getsyspath(fileName)}'"
- except fs.errors.NoSysPath:
- # Network or in-memory FS may not map to a local path, so use
- # the best string representation we have.
- glifLocation = f"'{fileName}' from '{str(self.fs)}'"
- glifLibError._add_note(
- f"The issue is in glyph '{glyphName}', located in {glifLocation}."
- )
- raise
- def writeGlyph(
- self,
- glyphName,
- glyphObject=None,
- drawPointsFunc=None,
- formatVersion=None,
- validate=None,
- ):
- """
- Write a .glif file for 'glyphName' to the glyph set. The
- 'glyphObject' argument can be any kind of object (even None);
- the writeGlyph() method will attempt to get the following
- attributes from it:
- width
- the advance width of the glyph
- height
- the advance height of the glyph
- unicodes
- a list of unicode values for this glyph
- note
- a string
- lib
- a dictionary containing custom data
- image
- a dictionary containing image data
- guidelines
- a list of guideline data dictionaries
- anchors
- a list of anchor data dictionaries
- All attributes are optional: if 'glyphObject' doesn't
- have the attribute, it will simply be skipped.
- To write outline data to the .glif file, writeGlyph() needs
- a function (any callable object actually) that will take one
- argument: an object that conforms to the PointPen protocol.
- The function will be called by writeGlyph(); it has to call the
- proper PointPen methods to transfer the outline to the .glif file.
- The GLIF format version will be chosen based on the ufoFormatVersion
- passed during the creation of this object. If a particular format
- version is desired, it can be passed with the formatVersion argument.
- The formatVersion argument accepts either a tuple of integers for
- (major, minor), or a single integer for the major digit only (with
- minor digit implied as 0).
- An UnsupportedGLIFFormat exception is raised if the requested GLIF
- formatVersion is not supported.
- ``validate`` will validate the data, by default it is set to the
- class's ``validateWrite`` value, can be overridden.
- """
- if formatVersion is None:
- formatVersion = GLIFFormatVersion.default(self.ufoFormatVersionTuple)
- else:
- try:
- formatVersion = GLIFFormatVersion(formatVersion)
- except ValueError as e:
- from fontTools.ufoLib.errors import UnsupportedGLIFFormat
- raise UnsupportedGLIFFormat(
- f"Unsupported GLIF format version: {formatVersion!r}"
- ) from e
- if formatVersion not in GLIFFormatVersion.supported_versions(
- self.ufoFormatVersionTuple
- ):
- from fontTools.ufoLib.errors import UnsupportedGLIFFormat
- raise UnsupportedGLIFFormat(
- f"Unsupported GLIF format version ({formatVersion!s}) "
- f"for UFO format version {self.ufoFormatVersionTuple!s}."
- )
- if validate is None:
- validate = self._validateWrite
- fileName = self.contents.get(glyphName)
- if fileName is None:
- if self._existingFileNames is None:
- self._existingFileNames = {
- fileName.lower() for fileName in self.contents.values()
- }
- fileName = self.glyphNameToFileName(glyphName, self._existingFileNames)
- self.contents[glyphName] = fileName
- self._existingFileNames.add(fileName.lower())
- if self._reverseContents is not None:
- self._reverseContents[fileName.lower()] = glyphName
- data = _writeGlyphToBytes(
- glyphName,
- glyphObject,
- drawPointsFunc,
- formatVersion=formatVersion,
- validate=validate,
- )
- if (
- self._havePreviousFile
- and self.fs.exists(fileName)
- and data == self.fs.readbytes(fileName)
- ):
- return
- self.fs.writebytes(fileName, data)
- def deleteGlyph(self, glyphName):
- """Permanently delete the glyph from the glyph set on disk. Will
- raise KeyError if the glyph is not present in the glyph set.
- """
- fileName = self.contents[glyphName]
- self.fs.remove(fileName)
- if self._existingFileNames is not None:
- self._existingFileNames.remove(fileName.lower())
- if self._reverseContents is not None:
- del self._reverseContents[fileName.lower()]
- del self.contents[glyphName]
- # dict-like support
- def keys(self):
- return list(self.contents.keys())
- def has_key(self, glyphName):
- return glyphName in self.contents
- __contains__ = has_key
- def __len__(self):
- return len(self.contents)
- def __getitem__(self, glyphName):
- if glyphName not in self.contents:
- raise KeyError(glyphName)
- return self.glyphClass(glyphName, self)
- # quickly fetch unicode values
- def getUnicodes(self, glyphNames=None):
- """
- Return a dictionary that maps glyph names to lists containing
- the unicode value[s] for that glyph, if any. This parses the .glif
- files partially, so it is a lot faster than parsing all files completely.
- By default this checks all glyphs, but a subset can be passed with glyphNames.
- """
- unicodes = {}
- if glyphNames is None:
- glyphNames = self.contents.keys()
- for glyphName in glyphNames:
- text = self.getGLIF(glyphName)
- unicodes[glyphName] = _fetchUnicodes(text)
- return unicodes
- def getComponentReferences(self, glyphNames=None):
- """
- Return a dictionary that maps glyph names to lists containing the
- base glyph name of components in the glyph. This parses the .glif
- files partially, so it is a lot faster than parsing all files completely.
- By default this checks all glyphs, but a subset can be passed with glyphNames.
- """
- components = {}
- if glyphNames is None:
- glyphNames = self.contents.keys()
- for glyphName in glyphNames:
- text = self.getGLIF(glyphName)
- components[glyphName] = _fetchComponentBases(text)
- return components
- def getImageReferences(self, glyphNames=None):
- """
- Return a dictionary that maps glyph names to the file name of the image
- referenced by the glyph. This parses the .glif files partially, so it is a
- lot faster than parsing all files completely.
- By default this checks all glyphs, but a subset can be passed with glyphNames.
- """
- images = {}
- if glyphNames is None:
- glyphNames = self.contents.keys()
- for glyphName in glyphNames:
- text = self.getGLIF(glyphName)
- images[glyphName] = _fetchImageFileName(text)
- return images
- def close(self):
- if self._shouldClose:
- self.fs.close()
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, exc_tb):
- self.close()
- # -----------------------
- # Glyph Name to File Name
- # -----------------------
- def glyphNameToFileName(glyphName, existingFileNames):
- """
- Wrapper around the userNameToFileName function in filenames.py
- Note that existingFileNames should be a set for large glyphsets
- or performance will suffer.
- """
- if existingFileNames is None:
- existingFileNames = set()
- return userNameToFileName(glyphName, existing=existingFileNames, suffix=".glif")
- # -----------------------
- # GLIF To and From String
- # -----------------------
- def readGlyphFromString(
- aString,
- glyphObject=None,
- pointPen=None,
- formatVersions=None,
- validate=True,
- ):
- """
- Read .glif data from a string into a glyph object.
- The 'glyphObject' argument can be any kind of object (even None);
- the readGlyphFromString() method will attempt to set the following
- attributes on it:
- width
- the advance width of the glyph
- height
- the advance height of the glyph
- unicodes
- a list of unicode values for this glyph
- note
- a string
- lib
- a dictionary containing custom data
- image
- a dictionary containing image data
- guidelines
- a list of guideline data dictionaries
- anchors
- a list of anchor data dictionaries
- All attributes are optional, in two ways:
- 1) An attribute *won't* be set if the .glif file doesn't
- contain data for it. 'glyphObject' will have to deal
- with default values itself.
- 2) If setting the attribute fails with an AttributeError
- (for example if the 'glyphObject' attribute is read-
- only), readGlyphFromString() will not propagate that
- exception, but ignore that attribute.
- To retrieve outline information, you need to pass an object
- conforming to the PointPen protocol as the 'pointPen' argument.
- This argument may be None if you don't need the outline data.
- The formatVersions optional argument define the GLIF format versions
- that are allowed to be read.
- The type is Optional[Iterable[Tuple[int, int], int]]. It can contain
- either integers (for the major versions to be allowed, with minor
- digits defaulting to 0), or tuples of integers to specify both
- (major, minor) versions.
- By default when formatVersions is None all the GLIF format versions
- currently defined are allowed to be read.
- ``validate`` will validate the read data. It is set to ``True`` by default.
- """
- tree = _glifTreeFromString(aString)
- if formatVersions is None:
- validFormatVersions = GLIFFormatVersion.supported_versions()
- else:
- validFormatVersions, invalidFormatVersions = set(), set()
- for v in formatVersions:
- try:
- formatVersion = GLIFFormatVersion(v)
- except ValueError:
- invalidFormatVersions.add(v)
- else:
- validFormatVersions.add(formatVersion)
- if not validFormatVersions:
- raise ValueError(
- "None of the requested GLIF formatVersions are supported: "
- f"{formatVersions!r}"
- )
- _readGlyphFromTree(
- tree,
- glyphObject,
- pointPen,
- formatVersions=validFormatVersions,
- validate=validate,
- )
- def _writeGlyphToBytes(
- glyphName,
- glyphObject=None,
- drawPointsFunc=None,
- writer=None,
- formatVersion=None,
- validate=True,
- ):
- """Return .glif data for a glyph as a UTF-8 encoded bytes string."""
- try:
- formatVersion = GLIFFormatVersion(formatVersion)
- except ValueError:
- from fontTools.ufoLib.errors import UnsupportedGLIFFormat
- raise UnsupportedGLIFFormat(
- "Unsupported GLIF format version: {formatVersion!r}"
- )
- # start
- if validate and not isinstance(glyphName, str):
- raise GlifLibError("The glyph name is not properly formatted.")
- if validate and len(glyphName) == 0:
- raise GlifLibError("The glyph name is empty.")
- glyphAttrs = OrderedDict(
- [("name", glyphName), ("format", repr(formatVersion.major))]
- )
- if formatVersion.minor != 0:
- glyphAttrs["formatMinor"] = repr(formatVersion.minor)
- root = etree.Element("glyph", glyphAttrs)
- identifiers = set()
- # advance
- _writeAdvance(glyphObject, root, validate)
- # unicodes
- if getattr(glyphObject, "unicodes", None):
- _writeUnicodes(glyphObject, root, validate)
- # note
- if getattr(glyphObject, "note", None):
- _writeNote(glyphObject, root, validate)
- # image
- if formatVersion.major >= 2 and getattr(glyphObject, "image", None):
- _writeImage(glyphObject, root, validate)
- # guidelines
- if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None):
- _writeGuidelines(glyphObject, root, identifiers, validate)
- # anchors
- anchors = getattr(glyphObject, "anchors", None)
- if formatVersion.major >= 2 and anchors:
- _writeAnchors(glyphObject, root, identifiers, validate)
- # outline
- if drawPointsFunc is not None:
- outline = etree.SubElement(root, "outline")
- pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
- drawPointsFunc(pen)
- if formatVersion.major == 1 and anchors:
- _writeAnchorsFormat1(pen, anchors, validate)
- # prevent lxml from writing self-closing tags
- if not len(outline):
- outline.text = "\n "
- # lib
- if getattr(glyphObject, "lib", None):
- _writeLib(glyphObject, root, validate)
- # return the text
- data = etree.tostring(
- root, encoding="UTF-8", xml_declaration=True, pretty_print=True
- )
- return data
- def writeGlyphToString(
- glyphName,
- glyphObject=None,
- drawPointsFunc=None,
- formatVersion=None,
- validate=True,
- ):
- """
- Return .glif data for a glyph as a string. The XML declaration's
- encoding is always set to "UTF-8".
- The 'glyphObject' argument can be any kind of object (even None);
- the writeGlyphToString() method will attempt to get the following
- attributes from it:
- width
- the advance width of the glyph
- height
- the advance height of the glyph
- unicodes
- a list of unicode values for this glyph
- note
- a string
- lib
- a dictionary containing custom data
- image
- a dictionary containing image data
- guidelines
- a list of guideline data dictionaries
- anchors
- a list of anchor data dictionaries
- All attributes are optional: if 'glyphObject' doesn't
- have the attribute, it will simply be skipped.
- To write outline data to the .glif file, writeGlyphToString() needs
- a function (any callable object actually) that will take one
- argument: an object that conforms to the PointPen protocol.
- The function will be called by writeGlyphToString(); it has to call the
- proper PointPen methods to transfer the outline to the .glif file.
- The GLIF format version can be specified with the formatVersion argument.
- This accepts either a tuple of integers for (major, minor), or a single
- integer for the major digit only (with minor digit implied as 0).
- By default when formatVesion is None the latest GLIF format version will
- be used; currently it's 2.0, which is equivalent to formatVersion=(2, 0).
- An UnsupportedGLIFFormat exception is raised if the requested UFO
- formatVersion is not supported.
- ``validate`` will validate the written data. It is set to ``True`` by default.
- """
- data = _writeGlyphToBytes(
- glyphName,
- glyphObject=glyphObject,
- drawPointsFunc=drawPointsFunc,
- formatVersion=formatVersion,
- validate=validate,
- )
- return data.decode("utf-8")
- def _writeAdvance(glyphObject, element, validate):
- width = getattr(glyphObject, "width", None)
- if width is not None:
- if validate and not isinstance(width, numberTypes):
- raise GlifLibError("width attribute must be int or float")
- if width == 0:
- width = None
- height = getattr(glyphObject, "height", None)
- if height is not None:
- if validate and not isinstance(height, numberTypes):
- raise GlifLibError("height attribute must be int or float")
- if height == 0:
- height = None
- if width is not None and height is not None:
- etree.SubElement(
- element,
- "advance",
- OrderedDict([("height", repr(height)), ("width", repr(width))]),
- )
- elif width is not None:
- etree.SubElement(element, "advance", dict(width=repr(width)))
- elif height is not None:
- etree.SubElement(element, "advance", dict(height=repr(height)))
- def _writeUnicodes(glyphObject, element, validate):
- unicodes = getattr(glyphObject, "unicodes", None)
- if validate and isinstance(unicodes, int):
- unicodes = [unicodes]
- seen = set()
- for code in unicodes:
- if validate and not isinstance(code, int):
- raise GlifLibError("unicode values must be int")
- if code in seen:
- continue
- seen.add(code)
- hexCode = "%04X" % code
- etree.SubElement(element, "unicode", dict(hex=hexCode))
- def _writeNote(glyphObject, element, validate):
- note = getattr(glyphObject, "note", None)
- if validate and not isinstance(note, str):
- raise GlifLibError("note attribute must be str")
- note = note.strip()
- note = "\n" + note + "\n"
- etree.SubElement(element, "note").text = note
- def _writeImage(glyphObject, element, validate):
- image = getattr(glyphObject, "image", None)
- if validate and not imageValidator(image):
- raise GlifLibError(
- "image attribute must be a dict or dict-like object with the proper structure."
- )
- attrs = OrderedDict([("fileName", image["fileName"])])
- for attr, default in _transformationInfo:
- value = image.get(attr, default)
- if value != default:
- attrs[attr] = repr(value)
- color = image.get("color")
- if color is not None:
- attrs["color"] = color
- etree.SubElement(element, "image", attrs)
- def _writeGuidelines(glyphObject, element, identifiers, validate):
- guidelines = getattr(glyphObject, "guidelines", [])
- if validate and not guidelinesValidator(guidelines):
- raise GlifLibError("guidelines attribute does not have the proper structure.")
- for guideline in guidelines:
- attrs = OrderedDict()
- x = guideline.get("x")
- if x is not None:
- attrs["x"] = repr(x)
- y = guideline.get("y")
- if y is not None:
- attrs["y"] = repr(y)
- angle = guideline.get("angle")
- if angle is not None:
- attrs["angle"] = repr(angle)
- name = guideline.get("name")
- if name is not None:
- attrs["name"] = name
- color = guideline.get("color")
- if color is not None:
- attrs["color"] = color
- identifier = guideline.get("identifier")
- if identifier is not None:
- if validate and identifier in identifiers:
- raise GlifLibError("identifier used more than once: %s" % identifier)
- attrs["identifier"] = identifier
- identifiers.add(identifier)
- etree.SubElement(element, "guideline", attrs)
- def _writeAnchorsFormat1(pen, anchors, validate):
- if validate and not anchorsValidator(anchors):
- raise GlifLibError("anchors attribute does not have the proper structure.")
- for anchor in anchors:
- attrs = {}
- x = anchor["x"]
- attrs["x"] = repr(x)
- y = anchor["y"]
- attrs["y"] = repr(y)
- name = anchor.get("name")
- if name is not None:
- attrs["name"] = name
- pen.beginPath()
- pen.addPoint((x, y), segmentType="move", name=name)
- pen.endPath()
- def _writeAnchors(glyphObject, element, identifiers, validate):
- anchors = getattr(glyphObject, "anchors", [])
- if validate and not anchorsValidator(anchors):
- raise GlifLibError("anchors attribute does not have the proper structure.")
- for anchor in anchors:
- attrs = OrderedDict()
- x = anchor["x"]
- attrs["x"] = repr(x)
- y = anchor["y"]
- attrs["y"] = repr(y)
- name = anchor.get("name")
- if name is not None:
- attrs["name"] = name
- color = anchor.get("color")
- if color is not None:
- attrs["color"] = color
- identifier = anchor.get("identifier")
- if identifier is not None:
- if validate and identifier in identifiers:
- raise GlifLibError("identifier used more than once: %s" % identifier)
- attrs["identifier"] = identifier
- identifiers.add(identifier)
- etree.SubElement(element, "anchor", attrs)
- def _writeLib(glyphObject, element, validate):
- lib = getattr(glyphObject, "lib", None)
- if not lib:
- # don't write empty lib
- return
- if validate:
- valid, message = glyphLibValidator(lib)
- if not valid:
- raise GlifLibError(message)
- if not isinstance(lib, dict):
- lib = dict(lib)
- # plist inside GLIF begins with 2 levels of indentation
- e = plistlib.totree(lib, indent_level=2)
- etree.SubElement(element, "lib").append(e)
- # -----------------------
- # layerinfo.plist Support
- # -----------------------
- layerInfoVersion3ValueData = {
- "color": dict(type=str, valueValidator=colorValidator),
- "lib": dict(type=dict, valueValidator=genericTypeValidator),
- }
- def validateLayerInfoVersion3ValueForAttribute(attr, value):
- """
- This performs very basic validation of the value for attribute
- following the UFO 3 fontinfo.plist specification. The results
- of this should not be interpretted as *correct* for the font
- that they are part of. This merely indicates that the value
- is of the proper type and, where the specification defines
- a set range of possible values for an attribute, that the
- value is in the accepted range.
- """
- if attr not in layerInfoVersion3ValueData:
- return False
- dataValidationDict = layerInfoVersion3ValueData[attr]
- valueType = dataValidationDict.get("type")
- validator = dataValidationDict.get("valueValidator")
- valueOptions = dataValidationDict.get("valueOptions")
- # have specific options for the validator
- if valueOptions is not None:
- isValidValue = validator(value, valueOptions)
- # no specific options
- else:
- if validator == genericTypeValidator:
- isValidValue = validator(value, valueType)
- else:
- isValidValue = validator(value)
- return isValidValue
- def validateLayerInfoVersion3Data(infoData):
- """
- This performs very basic validation of the value for infoData
- following the UFO 3 layerinfo.plist specification. The results
- of this should not be interpretted as *correct* for the font
- that they are part of. This merely indicates that the values
- are of the proper type and, where the specification defines
- a set range of possible values for an attribute, that the
- value is in the accepted range.
- """
- for attr, value in infoData.items():
- if attr not in layerInfoVersion3ValueData:
- raise GlifLibError("Unknown attribute %s." % attr)
- isValidValue = validateLayerInfoVersion3ValueForAttribute(attr, value)
- if not isValidValue:
- raise GlifLibError(f"Invalid value for attribute {attr} ({value!r}).")
- return infoData
- # -----------------
- # GLIF Tree Support
- # -----------------
- def _glifTreeFromFile(aFile):
- if etree._have_lxml:
- tree = etree.parse(aFile, parser=etree.XMLParser(remove_comments=True))
- else:
- tree = etree.parse(aFile)
- root = tree.getroot()
- if root.tag != "glyph":
- raise GlifLibError("The GLIF is not properly formatted.")
- if root.text and root.text.strip() != "":
- raise GlifLibError("Invalid GLIF structure.")
- return root
- def _glifTreeFromString(aString):
- data = tobytes(aString, encoding="utf-8")
- try:
- if etree._have_lxml:
- root = etree.fromstring(data, parser=etree.XMLParser(remove_comments=True))
- else:
- root = etree.fromstring(data)
- except Exception as etree_exception:
- raise GlifLibError("GLIF contains invalid XML.") from etree_exception
- if root.tag != "glyph":
- raise GlifLibError("The GLIF is not properly formatted.")
- if root.text and root.text.strip() != "":
- raise GlifLibError("Invalid GLIF structure.")
- return root
- def _readGlyphFromTree(
- tree,
- glyphObject=None,
- pointPen=None,
- formatVersions=GLIFFormatVersion.supported_versions(),
- validate=True,
- ):
- # check the format version
- formatVersionMajor = tree.get("format")
- if validate and formatVersionMajor is None:
- raise GlifLibError("Unspecified format version in GLIF.")
- formatVersionMinor = tree.get("formatMinor", 0)
- try:
- formatVersion = GLIFFormatVersion(
- (int(formatVersionMajor), int(formatVersionMinor))
- )
- except ValueError as e:
- msg = "Unsupported GLIF format: %s.%s" % (
- formatVersionMajor,
- formatVersionMinor,
- )
- if validate:
- from fontTools.ufoLib.errors import UnsupportedGLIFFormat
- raise UnsupportedGLIFFormat(msg) from e
- # warn but continue using the latest supported format
- formatVersion = GLIFFormatVersion.default()
- logger.warning(
- "%s. Assuming the latest supported version (%s). "
- "Some data may be skipped or parsed incorrectly.",
- msg,
- formatVersion,
- )
- if validate and formatVersion not in formatVersions:
- raise GlifLibError(f"Forbidden GLIF format version: {formatVersion!s}")
- try:
- readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[formatVersion]
- except KeyError:
- raise NotImplementedError(formatVersion)
- readGlyphFromTree(
- tree=tree,
- glyphObject=glyphObject,
- pointPen=pointPen,
- validate=validate,
- formatMinor=formatVersion.minor,
- )
- def _readGlyphFromTreeFormat1(
- tree, glyphObject=None, pointPen=None, validate=None, **kwargs
- ):
- # get the name
- _readName(glyphObject, tree, validate)
- # populate the sub elements
- unicodes = []
- haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False
- for element in tree:
- if element.tag == "outline":
- if validate:
- if haveSeenOutline:
- raise GlifLibError("The outline element occurs more than once.")
- if element.attrib:
- raise GlifLibError(
- "The outline element contains unknown attributes."
- )
- if element.text and element.text.strip() != "":
- raise GlifLibError("Invalid outline structure.")
- haveSeenOutline = True
- buildOutlineFormat1(glyphObject, pointPen, element, validate)
- elif glyphObject is None:
- continue
- elif element.tag == "advance":
- if validate and haveSeenAdvance:
- raise GlifLibError("The advance element occurs more than once.")
- haveSeenAdvance = True
- _readAdvance(glyphObject, element)
- elif element.tag == "unicode":
- try:
- v = element.get("hex")
- v = int(v, 16)
- if v not in unicodes:
- unicodes.append(v)
- except ValueError:
- raise GlifLibError(
- "Illegal value for hex attribute of unicode element."
- )
- elif element.tag == "note":
- if validate and haveSeenNote:
- raise GlifLibError("The note element occurs more than once.")
- haveSeenNote = True
- _readNote(glyphObject, element)
- elif element.tag == "lib":
- if validate and haveSeenLib:
- raise GlifLibError("The lib element occurs more than once.")
- haveSeenLib = True
- _readLib(glyphObject, element, validate)
- else:
- raise GlifLibError("Unknown element in GLIF: %s" % element)
- # set the collected unicodes
- if unicodes:
- _relaxedSetattr(glyphObject, "unicodes", unicodes)
- def _readGlyphFromTreeFormat2(
- tree, glyphObject=None, pointPen=None, validate=None, formatMinor=0
- ):
- # get the name
- _readName(glyphObject, tree, validate)
- # populate the sub elements
- unicodes = []
- guidelines = []
- anchors = []
- haveSeenAdvance = (
- haveSeenImage
- ) = haveSeenOutline = haveSeenLib = haveSeenNote = False
- identifiers = set()
- for element in tree:
- if element.tag == "outline":
- if validate:
- if haveSeenOutline:
- raise GlifLibError("The outline element occurs more than once.")
- if element.attrib:
- raise GlifLibError(
- "The outline element contains unknown attributes."
- )
- if element.text and element.text.strip() != "":
- raise GlifLibError("Invalid outline structure.")
- haveSeenOutline = True
- if pointPen is not None:
- buildOutlineFormat2(
- glyphObject, pointPen, element, identifiers, validate
- )
- elif glyphObject is None:
- continue
- elif element.tag == "advance":
- if validate and haveSeenAdvance:
- raise GlifLibError("The advance element occurs more than once.")
- haveSeenAdvance = True
- _readAdvance(glyphObject, element)
- elif element.tag == "unicode":
- try:
- v = element.get("hex")
- v = int(v, 16)
- if v not in unicodes:
- unicodes.append(v)
- except ValueError:
- raise GlifLibError(
- "Illegal value for hex attribute of unicode element."
- )
- elif element.tag == "guideline":
- if validate and len(element):
- raise GlifLibError("Unknown children in guideline element.")
- attrib = dict(element.attrib)
- for attr in ("x", "y", "angle"):
- if attr in attrib:
- attrib[attr] = _number(attrib[attr])
- guidelines.append(attrib)
- elif element.tag == "anchor":
- if validate and len(element):
- raise GlifLibError("Unknown children in anchor element.")
- attrib = dict(element.attrib)
- for attr in ("x", "y"):
- if attr in element.attrib:
- attrib[attr] = _number(attrib[attr])
- anchors.append(attrib)
- elif element.tag == "image":
- if validate:
- if haveSeenImage:
- raise GlifLibError("The image element occurs more than once.")
- if len(element):
- raise GlifLibError("Unknown children in image element.")
- haveSeenImage = True
- _readImage(glyphObject, element, validate)
- elif element.tag == "note":
- if validate and haveSeenNote:
- raise GlifLibError("The note element occurs more than once.")
- haveSeenNote = True
- _readNote(glyphObject, element)
- elif element.tag == "lib":
- if validate and haveSeenLib:
- raise GlifLibError("The lib element occurs more than once.")
- haveSeenLib = True
- _readLib(glyphObject, element, validate)
- else:
- raise GlifLibError("Unknown element in GLIF: %s" % element)
- # set the collected unicodes
- if unicodes:
- _relaxedSetattr(glyphObject, "unicodes", unicodes)
- # set the collected guidelines
- if guidelines:
- if validate and not guidelinesValidator(guidelines, identifiers):
- raise GlifLibError("The guidelines are improperly formatted.")
- _relaxedSetattr(glyphObject, "guidelines", guidelines)
- # set the collected anchors
- if anchors:
- if validate and not anchorsValidator(anchors, identifiers):
- raise GlifLibError("The anchors are improperly formatted.")
- _relaxedSetattr(glyphObject, "anchors", anchors)
- _READ_GLYPH_FROM_TREE_FUNCS = {
- GLIFFormatVersion.FORMAT_1_0: _readGlyphFromTreeFormat1,
- GLIFFormatVersion.FORMAT_2_0: _readGlyphFromTreeFormat2,
- }
- def _readName(glyphObject, root, validate):
- glyphName = root.get("name")
- if validate and not glyphName:
- raise GlifLibError("Empty glyph name in GLIF.")
- if glyphName and glyphObject is not None:
- _relaxedSetattr(glyphObject, "name", glyphName)
- def _readAdvance(glyphObject, advance):
- width = _number(advance.get("width", 0))
- _relaxedSetattr(glyphObject, "width", width)
- height = _number(advance.get("height", 0))
- _relaxedSetattr(glyphObject, "height", height)
- def _readNote(glyphObject, note):
- lines = note.text.split("\n")
- note = "\n".join(line.strip() for line in lines if line.strip())
- _relaxedSetattr(glyphObject, "note", note)
- def _readLib(glyphObject, lib, validate):
- assert len(lib) == 1
- child = lib[0]
- plist = plistlib.fromtree(child)
- if validate:
- valid, message = glyphLibValidator(plist)
- if not valid:
- raise GlifLibError(message)
- _relaxedSetattr(glyphObject, "lib", plist)
- def _readImage(glyphObject, image, validate):
- imageData = dict(image.attrib)
- for attr, default in _transformationInfo:
- value = imageData.get(attr, default)
- imageData[attr] = _number(value)
- if validate and not imageValidator(imageData):
- raise GlifLibError("The image element is not properly formatted.")
- _relaxedSetattr(glyphObject, "image", imageData)
- # ----------------
- # GLIF to PointPen
- # ----------------
- contourAttributesFormat2 = {"identifier"}
- componentAttributesFormat1 = {
- "base",
- "xScale",
- "xyScale",
- "yxScale",
- "yScale",
- "xOffset",
- "yOffset",
- }
- componentAttributesFormat2 = componentAttributesFormat1 | {"identifier"}
- pointAttributesFormat1 = {"x", "y", "type", "smooth", "name"}
- pointAttributesFormat2 = pointAttributesFormat1 | {"identifier"}
- pointSmoothOptions = {"no", "yes"}
- pointTypeOptions = {"move", "line", "offcurve", "curve", "qcurve"}
- # format 1
- def buildOutlineFormat1(glyphObject, pen, outline, validate):
- anchors = []
- for element in outline:
- if element.tag == "contour":
- if len(element) == 1:
- point = element[0]
- if point.tag == "point":
- anchor = _buildAnchorFormat1(point, validate)
- if anchor is not None:
- anchors.append(anchor)
- continue
- if pen is not None:
- _buildOutlineContourFormat1(pen, element, validate)
- elif element.tag == "component":
- if pen is not None:
- _buildOutlineComponentFormat1(pen, element, validate)
- else:
- raise GlifLibError("Unknown element in outline element: %s" % element)
- if glyphObject is not None and anchors:
- if validate and not anchorsValidator(anchors):
- raise GlifLibError("GLIF 1 anchors are not properly formatted.")
- _relaxedSetattr(glyphObject, "anchors", anchors)
- def _buildAnchorFormat1(point, validate):
- if point.get("type") != "move":
- return None
- name = point.get("name")
- if name is None:
- return None
- x = point.get("x")
- y = point.get("y")
- if validate and x is None:
- raise GlifLibError("Required x attribute is missing in point element.")
- if validate and y is None:
- raise GlifLibError("Required y attribute is missing in point element.")
- x = _number(x)
- y = _number(y)
- anchor = dict(x=x, y=y, name=name)
- return anchor
- def _buildOutlineContourFormat1(pen, contour, validate):
- if validate and contour.attrib:
- raise GlifLibError("Unknown attributes in contour element.")
- pen.beginPath()
- if len(contour):
- massaged = _validateAndMassagePointStructures(
- contour,
- pointAttributesFormat1,
- openContourOffCurveLeniency=True,
- validate=validate,
- )
- _buildOutlinePointsFormat1(pen, massaged)
- pen.endPath()
- def _buildOutlinePointsFormat1(pen, contour):
- for point in contour:
- x = point["x"]
- y = point["y"]
- segmentType = point["segmentType"]
- smooth = point["smooth"]
- name = point["name"]
- pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
- def _buildOutlineComponentFormat1(pen, component, validate):
- if validate:
- if len(component):
- raise GlifLibError("Unknown child elements of component element.")
- for attr in component.attrib.keys():
- if attr not in componentAttributesFormat1:
- raise GlifLibError("Unknown attribute in component element: %s" % attr)
- baseGlyphName = component.get("base")
- if validate and baseGlyphName is None:
- raise GlifLibError("The base attribute is not defined in the component.")
- transformation = []
- for attr, default in _transformationInfo:
- value = component.get(attr)
- if value is None:
- value = default
- else:
- value = _number(value)
- transformation.append(value)
- pen.addComponent(baseGlyphName, tuple(transformation))
- # format 2
- def buildOutlineFormat2(glyphObject, pen, outline, identifiers, validate):
- for element in outline:
- if element.tag == "contour":
- _buildOutlineContourFormat2(pen, element, identifiers, validate)
- elif element.tag == "component":
- _buildOutlineComponentFormat2(pen, element, identifiers, validate)
- else:
- raise GlifLibError("Unknown element in outline element: %s" % element.tag)
- def _buildOutlineContourFormat2(pen, contour, identifiers, validate):
- if validate:
- for attr in contour.attrib.keys():
- if attr not in contourAttributesFormat2:
- raise GlifLibError("Unknown attribute in contour element: %s" % attr)
- identifier = contour.get("identifier")
- if identifier is not None:
- if validate:
- if identifier in identifiers:
- raise GlifLibError(
- "The identifier %s is used more than once." % identifier
- )
- if not identifierValidator(identifier):
- raise GlifLibError(
- "The contour identifier %s is not valid." % identifier
- )
- identifiers.add(identifier)
- try:
- pen.beginPath(identifier=identifier)
- except TypeError:
- pen.beginPath()
- warn(
- "The beginPath method needs an identifier kwarg. The contour's identifier value has been discarded.",
- DeprecationWarning,
- )
- if len(contour):
- massaged = _validateAndMassagePointStructures(
- contour, pointAttributesFormat2, validate=validate
- )
- _buildOutlinePointsFormat2(pen, massaged, identifiers, validate)
- pen.endPath()
- def _buildOutlinePointsFormat2(pen, contour, identifiers, validate):
- for point in contour:
- x = point["x"]
- y = point["y"]
- segmentType = point["segmentType"]
- smooth = point["smooth"]
- name = point["name"]
- identifier = point.get("identifier")
- if identifier is not None:
- if validate:
- if identifier in identifiers:
- raise GlifLibError(
- "The identifier %s is used more than once." % identifier
- )
- if not identifierValidator(identifier):
- raise GlifLibError("The identifier %s is not valid." % identifier)
- identifiers.add(identifier)
- try:
- pen.addPoint(
- (x, y),
- segmentType=segmentType,
- smooth=smooth,
- name=name,
- identifier=identifier,
- )
- except TypeError:
- pen.addPoint((x, y), segmentType=segmentType, smooth=smooth, name=name)
- warn(
- "The addPoint method needs an identifier kwarg. The point's identifier value has been discarded.",
- DeprecationWarning,
- )
- def _buildOutlineComponentFormat2(pen, component, identifiers, validate):
- if validate:
- if len(component):
- raise GlifLibError("Unknown child elements of component element.")
- for attr in component.attrib.keys():
- if attr not in componentAttributesFormat2:
- raise GlifLibError("Unknown attribute in component element: %s" % attr)
- baseGlyphName = component.get("base")
- if validate and baseGlyphName is None:
- raise GlifLibError("The base attribute is not defined in the component.")
- transformation = []
- for attr, default in _transformationInfo:
- value = component.get(attr)
- if value is None:
- value = default
- else:
- value = _number(value)
- transformation.append(value)
- identifier = component.get("identifier")
- if identifier is not None:
- if validate:
- if identifier in identifiers:
- raise GlifLibError(
- "The identifier %s is used more than once." % identifier
- )
- if validate and not identifierValidator(identifier):
- raise GlifLibError("The identifier %s is not valid." % identifier)
- identifiers.add(identifier)
- try:
- pen.addComponent(baseGlyphName, tuple(transformation), identifier=identifier)
- except TypeError:
- pen.addComponent(baseGlyphName, tuple(transformation))
- warn(
- "The addComponent method needs an identifier kwarg. The component's identifier value has been discarded.",
- DeprecationWarning,
- )
- # all formats
- def _validateAndMassagePointStructures(
- contour, pointAttributes, openContourOffCurveLeniency=False, validate=True
- ):
- if not len(contour):
- return
- # store some data for later validation
- lastOnCurvePoint = None
- haveOffCurvePoint = False
- # validate and massage the individual point elements
- massaged = []
- for index, element in enumerate(contour):
- # not <point>
- if element.tag != "point":
- raise GlifLibError(
- "Unknown child element (%s) of contour element." % element.tag
- )
- point = dict(element.attrib)
- massaged.append(point)
- if validate:
- # unknown attributes
- for attr in point.keys():
- if attr not in pointAttributes:
- raise GlifLibError("Unknown attribute in point element: %s" % attr)
- # search for unknown children
- if len(element):
- raise GlifLibError("Unknown child elements in point element.")
- # x and y are required
- for attr in ("x", "y"):
- try:
- point[attr] = _number(point[attr])
- except KeyError as e:
- raise GlifLibError(
- f"Required {attr} attribute is missing in point element."
- ) from e
- # segment type
- pointType = point.pop("type", "offcurve")
- if validate and pointType not in pointTypeOptions:
- raise GlifLibError("Unknown point type: %s" % pointType)
- if pointType == "offcurve":
- pointType = None
- point["segmentType"] = pointType
- if pointType is None:
- haveOffCurvePoint = True
- else:
- lastOnCurvePoint = index
- # move can only occur as the first point
- if validate and pointType == "move" and index != 0:
- raise GlifLibError(
- "A move point occurs after the first point in the contour."
- )
- # smooth is optional
- smooth = point.get("smooth", "no")
- if validate and smooth is not None:
- if smooth not in pointSmoothOptions:
- raise GlifLibError("Unknown point smooth value: %s" % smooth)
- smooth = smooth == "yes"
- point["smooth"] = smooth
- # smooth can only be applied to curve and qcurve
- if validate and smooth and pointType is None:
- raise GlifLibError("smooth attribute set in an offcurve point.")
- # name is optional
- if "name" not in element.attrib:
- point["name"] = None
- if openContourOffCurveLeniency:
- # remove offcurves that precede a move. this is technically illegal,
- # but we let it slide because there are fonts out there in the wild like this.
- if massaged[0]["segmentType"] == "move":
- count = 0
- for point in reversed(massaged):
- if point["segmentType"] is None:
- count += 1
- else:
- break
- if count:
- massaged = massaged[:-count]
- # validate the off-curves in the segments
- if validate and haveOffCurvePoint and lastOnCurvePoint is not None:
- # we only care about how many offCurves there are before an onCurve
- # filter out the trailing offCurves
- offCurvesCount = len(massaged) - 1 - lastOnCurvePoint
- for point in massaged:
- segmentType = point["segmentType"]
- if segmentType is None:
- offCurvesCount += 1
- else:
- if offCurvesCount:
- # move and line can't be preceded by off-curves
- if segmentType == "move":
- # this will have been filtered out already
- raise GlifLibError("move can not have an offcurve.")
- elif segmentType == "line":
- raise GlifLibError("line can not have an offcurve.")
- elif segmentType == "curve":
- if offCurvesCount > 2:
- raise GlifLibError("Too many offcurves defined for curve.")
- elif segmentType == "qcurve":
- pass
- else:
- # unknown segment type. it'll be caught later.
- pass
- offCurvesCount = 0
- return massaged
- # ---------------------
- # Misc Helper Functions
- # ---------------------
- def _relaxedSetattr(object, attr, value):
- try:
- setattr(object, attr, value)
- except AttributeError:
- pass
- def _number(s):
- """
- Given a numeric string, return an integer or a float, whichever
- the string indicates. _number("1") will return the integer 1,
- _number("1.0") will return the float 1.0.
- >>> _number("1")
- 1
- >>> _number("1.0")
- 1.0
- >>> _number("a") # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- GlifLibError: Could not convert a to an int or float.
- """
- try:
- n = int(s)
- return n
- except ValueError:
- pass
- try:
- n = float(s)
- return n
- except ValueError:
- raise GlifLibError("Could not convert %s to an int or float." % s)
- # --------------------
- # Rapid Value Fetching
- # --------------------
- # base
- class _DoneParsing(Exception):
- pass
- class _BaseParser:
- def __init__(self):
- self._elementStack = []
- def parse(self, text):
- from xml.parsers.expat import ParserCreate
- parser = ParserCreate()
- parser.StartElementHandler = self.startElementHandler
- parser.EndElementHandler = self.endElementHandler
- parser.Parse(text)
- def startElementHandler(self, name, attrs):
- self._elementStack.append(name)
- def endElementHandler(self, name):
- other = self._elementStack.pop(-1)
- assert other == name
- # unicodes
- def _fetchUnicodes(glif):
- """
- Get a list of unicodes listed in glif.
- """
- parser = _FetchUnicodesParser()
- parser.parse(glif)
- return parser.unicodes
- class _FetchUnicodesParser(_BaseParser):
- def __init__(self):
- self.unicodes = []
- super().__init__()
- def startElementHandler(self, name, attrs):
- if (
- name == "unicode"
- and self._elementStack
- and self._elementStack[-1] == "glyph"
- ):
- value = attrs.get("hex")
- if value is not None:
- try:
- value = int(value, 16)
- if value not in self.unicodes:
- self.unicodes.append(value)
- except ValueError:
- pass
- super().startElementHandler(name, attrs)
- # image
- def _fetchImageFileName(glif):
- """
- The image file name (if any) from glif.
- """
- parser = _FetchImageFileNameParser()
- try:
- parser.parse(glif)
- except _DoneParsing:
- pass
- return parser.fileName
- class _FetchImageFileNameParser(_BaseParser):
- def __init__(self):
- self.fileName = None
- super().__init__()
- def startElementHandler(self, name, attrs):
- if name == "image" and self._elementStack and self._elementStack[-1] == "glyph":
- self.fileName = attrs.get("fileName")
- raise _DoneParsing
- super().startElementHandler(name, attrs)
- # component references
- def _fetchComponentBases(glif):
- """
- Get a list of component base glyphs listed in glif.
- """
- parser = _FetchComponentBasesParser()
- try:
- parser.parse(glif)
- except _DoneParsing:
- pass
- return list(parser.bases)
- class _FetchComponentBasesParser(_BaseParser):
- def __init__(self):
- self.bases = []
- super().__init__()
- def startElementHandler(self, name, attrs):
- if (
- name == "component"
- and self._elementStack
- and self._elementStack[-1] == "outline"
- ):
- base = attrs.get("base")
- if base is not None:
- self.bases.append(base)
- super().startElementHandler(name, attrs)
- def endElementHandler(self, name):
- if name == "outline":
- raise _DoneParsing
- super().endElementHandler(name)
- # --------------
- # GLIF Point Pen
- # --------------
- _transformationInfo = [
- # field name, default value
- ("xScale", 1),
- ("xyScale", 0),
- ("yxScale", 0),
- ("yScale", 1),
- ("xOffset", 0),
- ("yOffset", 0),
- ]
- class GLIFPointPen(AbstractPointPen):
- """
- Helper class using the PointPen protocol to write the <outline>
- part of .glif files.
- """
- def __init__(self, element, formatVersion=None, identifiers=None, validate=True):
- if identifiers is None:
- identifiers = set()
- self.formatVersion = GLIFFormatVersion(formatVersion)
- self.identifiers = identifiers
- self.outline = element
- self.contour = None
- self.prevOffCurveCount = 0
- self.prevPointTypes = []
- self.validate = validate
- def beginPath(self, identifier=None, **kwargs):
- attrs = OrderedDict()
- if identifier is not None and self.formatVersion.major >= 2:
- if self.validate:
- if identifier in self.identifiers:
- raise GlifLibError(
- "identifier used more than once: %s" % identifier
- )
- if not identifierValidator(identifier):
- raise GlifLibError(
- "identifier not formatted properly: %s" % identifier
- )
- attrs["identifier"] = identifier
- self.identifiers.add(identifier)
- self.contour = etree.SubElement(self.outline, "contour", attrs)
- self.prevOffCurveCount = 0
- def endPath(self):
- if self.prevPointTypes and self.prevPointTypes[0] == "move":
- if self.validate and self.prevPointTypes[-1] == "offcurve":
- raise GlifLibError("open contour has loose offcurve point")
- # prevent lxml from writing self-closing tags
- if not len(self.contour):
- self.contour.text = "\n "
- self.contour = None
- self.prevPointType = None
- self.prevOffCurveCount = 0
- self.prevPointTypes = []
- def addPoint(
- self, pt, segmentType=None, smooth=None, name=None, identifier=None, **kwargs
- ):
- attrs = OrderedDict()
- # coordinates
- if pt is not None:
- if self.validate:
- for coord in pt:
- if not isinstance(coord, numberTypes):
- raise GlifLibError("coordinates must be int or float")
- attrs["x"] = repr(pt[0])
- attrs["y"] = repr(pt[1])
- # segment type
- if segmentType == "offcurve":
- segmentType = None
- if self.validate:
- if segmentType == "move" and self.prevPointTypes:
- raise GlifLibError(
- "move occurs after a point has already been added to the contour."
- )
- if (
- segmentType in ("move", "line")
- and self.prevPointTypes
- and self.prevPointTypes[-1] == "offcurve"
- ):
- raise GlifLibError("offcurve occurs before %s point." % segmentType)
- if segmentType == "curve" and self.prevOffCurveCount > 2:
- raise GlifLibError("too many offcurve points before curve point.")
- if segmentType is not None:
- attrs["type"] = segmentType
- else:
- segmentType = "offcurve"
- if segmentType == "offcurve":
- self.prevOffCurveCount += 1
- else:
- self.prevOffCurveCount = 0
- self.prevPointTypes.append(segmentType)
- # smooth
- if smooth:
- if self.validate and segmentType == "offcurve":
- raise GlifLibError("can't set smooth in an offcurve point.")
- attrs["smooth"] = "yes"
- # name
- if name is not None:
- attrs["name"] = name
- # identifier
- if identifier is not None and self.formatVersion.major >= 2:
- if self.validate:
- if identifier in self.identifiers:
- raise GlifLibError(
- "identifier used more than once: %s" % identifier
- )
- if not identifierValidator(identifier):
- raise GlifLibError(
- "identifier not formatted properly: %s" % identifier
- )
- attrs["identifier"] = identifier
- self.identifiers.add(identifier)
- etree.SubElement(self.contour, "point", attrs)
- def addComponent(self, glyphName, transformation, identifier=None, **kwargs):
- attrs = OrderedDict([("base", glyphName)])
- for (attr, default), value in zip(_transformationInfo, transformation):
- if self.validate and not isinstance(value, numberTypes):
- raise GlifLibError("transformation values must be int or float")
- if value != default:
- attrs[attr] = repr(value)
- if identifier is not None and self.formatVersion.major >= 2:
- if self.validate:
- if identifier in self.identifiers:
- raise GlifLibError(
- "identifier used more than once: %s" % identifier
- )
- if self.validate and not identifierValidator(identifier):
- raise GlifLibError(
- "identifier not formatted properly: %s" % identifier
- )
- attrs["identifier"] = identifier
- self.identifiers.add(identifier)
- etree.SubElement(self.outline, "component", attrs)
- if __name__ == "__main__":
- import doctest
- doctest.testmod()
|