1# Copyright 2014 Adobe. All rights reserved.
2
3"""
4This module supports using the Adobe FDK tools which operate on 'bez'
5files with UFO fonts. It provides low level utilities to manipulate UFO
6data without fully parsing and instantiating UFO objects, and without
7requiring that the AFDKO contain the robofab libraries.
8
9Modified in Nov 2014, when AFDKO added the robofab libraries. It can now
10be used with UFO fonts only to support the hash mechanism.
11
12Developed in order to support checkOutlines and autohint, the code
13supports two main functions:
14- convert between UFO GLIF and bez formats
15- keep a history of processing in a hash map, so that the (lengthy)
16processing by autohint and checkOutlines can be avoided if the glyph has
17already been processed, and the source data has not changed.
18
19The basic model is:
20 - read GLIF file
21 - transform GLIF XML element to bez file
22 - call FDK tool on bez file
23 - transform new bez file to GLIF XML element with new data, and save in list
24
25After all glyphs are done save all the new GLIF XML elements to GLIF
26files, and update the hash map.
27
28A complication in the Adobe UFO workflow comes from the fact we want to
29make sure that checkOutlines and autohint have been run on each glyph in
30a UFO font, when building an OTF font from the UFO font. We need to run
31checkOutlines, because we no longer remove overlaps from source UFO font
32data, because this can make revising a glyph much easier. We need to run
33autohint, because the glyphs must be hinted after checkOutlines is run,
34and in any case we want all glyphs to have been autohinted. The problem
35with this is that it can take a minute or two to run autohint or
36checkOutlines on a 2K-glyph font. The way we avoid this is to make and
37keep a hash of the source glyph drawing operators for each tool. When
38the tool is run, it calculates a hash of the source glyph, and compares
39it to the saved hash. If these are the same, the tool can skip the
40glyph. This saves a lot of time: if checkOutlines and autohint are run
41on all glyphs in a font, then a second pass is under 2 seconds.
42
43Another issue is that since we no longer remove overlaps from the source
44glyph files, checkOutlines must write any edited glyph data to a
45different layer in order to not destroy the source data. The ufoFont
46defines an Adobe-specific glyph layer for processed glyphs, named
47"glyphs.com.adobe.type.processedGlyphs".
48checkOutlines writes new glyph files to the processed glyphs layer only
49when it makes a change to the glyph data.
50
51When the autohint program is run, the ufoFont must be able to tell
52whether checkOutlines has been run and has altered a glyph: if so, the
53input file needs to be from the processed glyph layer, else it needs to
54be from the default glyph layer.
55
56The way the hashmap works is that we keep an entry for every glyph that
57has been processed, identified by a hash of its marking path data. Each
58entry contains:
59- a hash of the glyph point coordinates, from the default layer.
60This is set after a program has been run.
61- a history list: a list of the names of each program that has been run,
62  in order.
63- an editStatus flag.
64Altered GLIF data is always written to the Adobe processed glyph layer. The
65program may or may not have altered the outline data. For example, autohint
66adds private hint data, and adds names to points, but does not change any
67paths.
68
69If the stored hash for the glyph does not exist, the ufoFont lib will save the
70new hash in the hash map entry and will set the history list to contain just
71the current program. The program will read the glyph from the default layer.
72
73If the stored hash matches the hash for the current glyph file in the default
74layer, and the current program name is in the history list,then ufoFont
75will return "skip=1", and the calling program may skip the glyph.
76
77If the stored hash matches the hash for the current glyph file in the default
78layer, and the current program name is not in the history list, then the
79ufoFont will return "skip=0". If the font object field 'usedProcessedLayer' is
80set True, the program will read the glyph from the from the Adobe processed
81layer, if it exists, else it will always read from the default layer.
82
83If the hash differs between the hash map entry and the current glyph in the
84default layer, and usedProcessedLayer is False, then ufoFont will return
85"skip=0". If usedProcessedLayer is True, then the program will consult the
86list of required programs. If any of these are in the history list, then the
87program will report an error and return skip =1, else it will return skip=1.
88The program will then save the new hash in the hash map entry and reset the
89history list to contain just the current program. If the old and new hash
90match, but the program name is not in the history list, then the ufoFont will
91not skip the glyph, and will add the program name to the history list.
92
93
94The only tools using this are, at the moment, checkOutlines, checkOutlinesUFO
95and autohint. checkOutlines and checkOutlinesUFO use the hash map to skip
96processing only when being used to edit glyphs, not when reporting them.
97checkOutlines necessarily flattens any components in the source glyph file to
98actual outlines. autohint adds point names, and saves the hint data as a
99private data in the new GLIF file.
100
101autohint saves the hint data in the GLIF private data area, /lib/dict,
102as a key and data pair. See below for the format.
103
104autohint started with _hintFormat1_, a reasonably compact XML representation of
105the data. In Sep 2105, autohhint switched to _hintFormat2_ in order to be plist
106compatible. This was necessary in order to be compatible with the UFO spec, by
107was driven more immediately by the fact the the UFO font file normalization
108tools stripped out the _hintFormat1_ hint data as invalid elements.
109
110
111"""
112
113import ast
114import hashlib
115import logging
116import os
117import re
118import shutil
119
120from collections import OrderedDict
121from types import SimpleNamespace
122
123from fontTools.pens.basePen import BasePen
124from fontTools.pens.pointPen import AbstractPointPen
125from fontTools.ufoLib import UFOReader, UFOWriter
126from fontTools.ufoLib.errors import UFOLibError
127
128from . import fdTools, FontParseError
129
130
131log = logging.getLogger(__name__)
132
133_hintFormat1_ = """
134
135Deprecated. See _hintFormat2_ below.
136
137A <hintset> element identifies a specific point by its name, and
138describes a new set of stem hints which should be applied before the
139specific point.
140
141A <flex> element identifies a specific point by its name. The point is
142the first point of a curve. The presence of the <flex> element is a
143processing suggestion, that the curve and its successor curve should
144be converted to a flex operator.
145
146One challenge in applying the hintset and flex elements is that in the
147GLIF format, there is no explicit start and end operator: the first path
148operator is both the end and the start of the path. I have chosen to
149convert this to T1 by taking the first path operator, and making it a
150move-to. I then also use it as the last path operator. An exception is a
151line-to; in T1, this is omitted, as it is implied by the need to close
152the path. Hence, if a hintset references the first operator, there is a
153potential ambiguity: should it be applied before the T1 move-to, or
154before the final T1 path operator? The logic here applies it before the
155move-to only.
156
157 <glyph>
158...
159    <lib>
160        <dict>
161            <key><com.adobe.type.autohint><key>
162            <data>
163                <hintSetList>
164                    <hintset pointTag="point name">
165                      (<hstem pos="<decimal value>" width="decimal value" />)*
166                      (<vstem pos="<decimal value>" width="decimal value" />)*
167                      <!-- where n1-5 are decimal values -->
168                      <hstem3 stem3List="n0,n1,n2,n3,n4,n5" />*
169                      <!-- where n1-5 are decimal values -->
170                      <vstem3 stem3List="n0,n1,n2,n3,n4,n5" />*
171                    </hintset>*
172                    (<hintSetList>*
173                        (<hintset pointIndex="positive integer">
174                            (<stemindex>positive integer</stemindex>)+
175                        </hintset>)+
176                    </hintSetList>)*
177                    <flexList>
178                        <flex pointTag="point Name" />
179                    </flexList>*
180                </hintSetList>
181            </data>
182        </dict>
183    </lib>
184</glyph>
185
186Example from "B" in SourceCodePro-Regular
187<key><com.adobe.type.autohint><key>
188<data>
189<hintSetList id="64bf4987f05ced2a50195f971cd924984047eb1d79c8c43e6a0054f59cc85
190dea23a49deb20946a4ea84840534363f7a13cca31a81b1e7e33c832185173369086">
191    <hintset pointTag="hintSet0000">
192        <hstem pos="0" width="28" />
193        <hstem pos="338" width="28" />
194        <hstem pos="632" width="28" />
195        <vstem pos="100" width="32" />
196        <vstem pos="496" width="32" />
197    </hintset>
198    <hintset pointTag="hintSet0005">
199        <hstem pos="0" width="28" />
200        <hstem pos="338" width="28" />
201        <hstem pos="632" width="28" />
202        <vstem pos="100" width="32" />
203        <vstem pos="454" width="32" />
204        <vstem pos="496" width="32" />
205    </hintset>
206    <hintset pointTag="hintSet0016">
207        <hstem pos="0" width="28" />
208        <hstem pos="338" width="28" />
209        <hstem pos="632" width="28" />
210        <vstem pos="100" width="32" />
211        <vstem pos="496" width="32" />
212    </hintset>
213</hintSetList>
214</data>
215
216"""
217
218_hintFormat2_ = """
219
220A <dict> element in the hintSetList array identifies a specific point by its
221name, and describes a new set of stem hints which should be applied before the
222specific point.
223
224A <string> element in the flexList identifies a specific point by its name.
225The point is the first point of a curve. The presence of the element is a
226processing suggestion, that the curve and its successor curve should be
227converted to a flex operator.
228
229One challenge in applying the hintSetList and flexList elements is that in
230the GLIF format, there is no explicit start and end operator: the first path
231operator is both the end and the start of the path. I have chosen to convert
232this to T1 by taking the first path operator, and making it a move-to. I then
233also use it as the last path operator. An exception is a line-to; in T1, this
234is omitted, as it is implied by the need to close the path. Hence, if a hintset
235references the first operator, there is a potential ambiguity: should it be
236applied before the T1 move-to, or before the final T1 path operator? The logic
237here applies it before the move-to only.
238 <glyph>
239...
240    <lib>
241        <dict>
242            <key><com.adobe.type.autohint></key>
243            <dict>
244                <key>id</key>
245                <string> <fingerprint for glyph> </string>
246                <key>hintSetList</key>
247                <array>
248                    <dict>
249                      <key>pointTag</key>
250                      <string> <point name> </string>
251                      <key>stems</key>
252                      <array>
253                        <string>hstem <position value> <width value></string>*
254                        <string>vstem <position value> <width value></string>*
255                        <string>hstem3 <position value 0>...<position value 5>
256                        </string>*
257                        <string>vstem3 <position value 0>...<position value 5>
258                        </string>*
259                      </array>
260                    </dict>*
261                </array>
262
263                <key>flexList</key>*
264                <array>
265                    <string><point name></string>+
266                </array>
267            </dict>
268        </dict>
269    </lib>
270</glyph>
271
272Example from "B" in SourceCodePro-Regular
273<key><com.adobe.type.autohint><key>
274<dict>
275    <key>id</key>
276    <string>64bf4987f05ced2a50195f971cd924984047eb1d79c8c43e6a0054f59cc85dea23
277    a49deb20946a4ea84840534363f7a13cca31a81b1e7e33c832185173369086</string>
278    <key>hintSetList</key>
279    <array>
280        <dict>
281            <key>pointTag</key>
282            <string>hintSet0000</string>
283            <key>stems</key>
284            <array>
285                <string>hstem 338 28</string>
286                <string>hstem 632 28</string>
287                <string>hstem 100 32</string>
288                <string>hstem 496 32</string>
289            </array>
290        </dict>
291        <dict>
292            <key>pointTag</key>
293            <string>hintSet0005</string>
294            <key>stems</key>
295            <array>
296                <string>hstem 0 28</string>
297                <string>hstem 338 28</string>
298                <string>hstem 632 28</string>
299                <string>hstem 100 32</string>
300                <string>hstem 454 32</string>
301                <string>hstem 496 32</string>
302            </array>
303        </dict>
304        <dict>
305            <key>pointTag</key>
306            <string>hintSet0016</string>
307            <key>stems</key>
308            <array>
309                <string>hstem 0 28</string>
310                <string>hstem 338 28</string>
311                <string>hstem 632 28</string>
312                <string>hstem 100 32</string>
313                <string>hstem 496 32</string>
314            </array>
315        </dict>
316    </array>
317<dict>
318
319"""
320
321# UFO names
322PUBLIC_GLYPH_ORDER = "public.glyphOrder"
323
324ADOBE_DOMAIN_PREFIX = "com.adobe.type"
325
326PROCESSED_LAYER_NAME = "%s.processedglyphs" % ADOBE_DOMAIN_PREFIX
327PROCESSED_GLYPHS_DIRNAME = "glyphs.%s" % PROCESSED_LAYER_NAME
328
329HASHMAP_NAME = "%s.processedHashMap" % ADOBE_DOMAIN_PREFIX
330HASHMAP_VERSION_NAME = "hashMapVersion"
331HASHMAP_VERSION = (1, 0)  # If major version differs, do not use.
332AUTOHINT_NAME = "autohint"
333CHECKOUTLINE_NAME = "checkOutlines"
334
335BASE_FLEX_NAME = "flexCurve"
336FLEX_INDEX_LIST_NAME = "flexList"
337HINT_DOMAIN_NAME1 = "com.adobe.type.autohint"
338HINT_DOMAIN_NAME2 = "com.adobe.type.autohint.v2"
339HINT_SET_LIST_NAME = "hintSetList"
340HSTEM3_NAME = "hstem3"
341HSTEM_NAME = "hstem"
342POINT_NAME = "name"
343POINT_TAG = "pointTag"
344STEMS_NAME = "stems"
345VSTEM3_NAME = "vstem3"
346VSTEM_NAME = "vstem"
347STACK_LIMIT = 46
348
349
350class BezParseError(ValueError):
351    pass
352
353
354class UFOFontData:
355    def __init__(self, path, log_only, write_to_default_layer):
356        self._reader = UFOReader(path, validate=False)
357
358        self.path = path
359        self._glyphmap = None
360        self._processed_layer_glyphmap = None
361        self.newGlyphMap = {}
362        self._fontInfo = None
363        self._glyphsets = {}
364        # If True, we are running in report mode and not doing any changes, so
365        # we skip the hash map and process all glyphs.
366        self.log_only = log_only
367        # Used to store the hash of glyph data of already processed glyphs. If
368        # the stored hash matches the calculated one, we skip the glyph.
369        self._hashmap = None
370        self.fontDict = None
371        self.hashMapChanged = False
372        # If True, then write data to the default layer
373        self.writeToDefaultLayer = write_to_default_layer
374
375    def getUnitsPerEm(self):
376        return self.fontInfo.get("unitsPerEm", 1000)
377
378    def getPSName(self):
379        return self.fontInfo.get("postscriptFontName", "PSName-Undefined")
380
381    @staticmethod
382    def isCID():
383        return False
384
385    @staticmethod
386    def hasFDArray():
387        return False
388
389    def convertToBez(self, name, read_hints, round_coords, doAll=False):
390        # We do not yet support reading hints, so read_hints is ignored.
391        width, bez, skip = self._get_or_skip_glyph(name, round_coords, doAll)
392        if skip:
393            return None
394
395        bezString = "\n".join(bez)
396        bezString = "\n".join(["% " + name, "sc", bezString, "ed", ""])
397        return bezString
398
399    def updateFromBez(self, bezData, name, mm_hint_info=None):
400        layer = None
401        if name in self.processedLayerGlyphMap:
402            layer = PROCESSED_LAYER_NAME
403        glyphset = self._get_glyphset(layer)
404
405        glyph = BezGlyph(bezData)
406        glyphset.readGlyph(name, glyph)
407        if hasattr(glyph, 'width'):
408            glyph.width = norm_float(glyph.width)
409        self.newGlyphMap[name] = glyph
410
411        # updateFromBez is called only if the glyph has been autohinted which
412        # might also change its outline data. We need to update the edit status
413        # in the hash map entry. I assume that convertToBez has been run
414        # before, which will add an entry for this glyph.
415        self.updateHashEntry(name)
416
417    def save(self, path):
418        if path is None:
419            path = self.path
420
421        if os.path.abspath(self.path) != os.path.abspath(path):
422            # If user has specified a path other than the source font path,
423            # then copy the entire UFO font, and operate on the copy.
424            log.info("Copying from source UFO font to output UFO font before "
425                     "processing...")
426            if os.path.exists(path):
427                shutil.rmtree(path)
428            shutil.copytree(self.path, path)
429
430        writer = UFOWriter(path, self._reader.formatVersion, validate=False)
431
432        layer = PROCESSED_LAYER_NAME
433        if self.writeToDefaultLayer:
434            layer = None
435
436        # Write layer contents.
437        layers = writer.layerContents.copy()
438        if self.writeToDefaultLayer and PROCESSED_LAYER_NAME in layers:
439            # Delete processed glyphs directory
440            writer.deleteGlyphSet(PROCESSED_LAYER_NAME)
441            # Remove entry from 'layercontents.plist' file
442            del layers[PROCESSED_LAYER_NAME]
443        elif self.processedLayerGlyphMap or not self.writeToDefaultLayer:
444            layers[PROCESSED_LAYER_NAME] = PROCESSED_GLYPHS_DIRNAME
445        writer.layerContents.update(layers)
446        writer.writeLayerContents()
447
448        # Write glyphs.
449        glyphset = writer.getGlyphSet(layer, defaultLayer=layer is None)
450        for name, glyph in self.newGlyphMap.items():
451            filename = self.glyphMap[name]
452            if not self.writeToDefaultLayer and \
453                    name in self.processedLayerGlyphMap:
454                filename = self.processedLayerGlyphMap[name]
455            # Recalculate glyph hashes
456            if self.writeToDefaultLayer:
457                self.recalcHashEntry(name, glyph)
458            glyphset.contents[name] = filename
459            glyphset.writeGlyph(name, glyph, glyph.drawPoints)
460        glyphset.writeContents()
461
462        # Write hashmap
463        if self.hashMapChanged:
464            self.writeHashMap(writer)
465
466    @property
467    def hashMap(self):
468        if self._hashmap is None:
469            try:
470                data = self._reader.readData(HASHMAP_NAME)
471            except UFOLibError:
472                data = None
473            if data:
474                hashmap = ast.literal_eval(data.decode("utf-8"))
475            else:
476                hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION}
477
478            version = (0, 0)
479            if HASHMAP_VERSION_NAME in hashmap:
480                version = hashmap[HASHMAP_VERSION_NAME]
481
482            if version[0] > HASHMAP_VERSION[0]:
483                raise FontParseError("Hash map version is newer than "
484                                     "psautohint. Please update.")
485            elif version[0] < HASHMAP_VERSION[0]:
486                log.info("Updating hash map: was older version")
487                hashmap = {HASHMAP_VERSION_NAME: HASHMAP_VERSION}
488
489            self._hashmap = hashmap
490        return self._hashmap
491
492    def writeHashMap(self, writer):
493        hashMap = self.hashMap
494        if not hashMap:
495            return  # no glyphs were processed.
496
497        data = ["{"]
498        for gName in sorted(hashMap.keys()):
499            data.append("'%s': %s," % (gName, hashMap[gName]))
500        data.append("}")
501        data.append("")
502        data = "\n".join(data)
503
504        writer.writeData(HASHMAP_NAME, data.encode("utf-8"))
505
506    def updateHashEntry(self, glyphName):
507        # srcHash has already been set: we are fixing the history list.
508
509        # Get hash entry for glyph
510        srcHash, historyList = self.hashMap[glyphName]
511
512        self.hashMapChanged = True
513        # If the program is not in the history list, add it.
514        if AUTOHINT_NAME not in historyList:
515            historyList.append(AUTOHINT_NAME)
516
517    def recalcHashEntry(self, glyphName, glyph):
518        hashBefore, historyList = self.hashMap[glyphName]
519
520        hash_pen = HashPointPen(glyph)
521        glyph.drawPoints(hash_pen)
522        hashAfter = hash_pen.getHash()
523
524        if hashAfter != hashBefore:
525            self.hashMap[glyphName] = [hashAfter, historyList]
526            self.hashMapChanged = True
527
528    def checkSkipGlyph(self, glyphName, newSrcHash, doAll):
529        skip = False
530        if self.log_only:
531            return skip
532
533        srcHash = None
534        historyList = []
535
536        # Get hash entry for glyph
537        if glyphName in self.hashMap:
538            srcHash, historyList = self.hashMap[glyphName]
539
540        if srcHash == newSrcHash:
541            if AUTOHINT_NAME in historyList:
542                # The glyph has already been autohinted, and there have been no
543                # changes since.
544                skip = not doAll
545            if not skip and AUTOHINT_NAME not in historyList:
546                historyList.append(AUTOHINT_NAME)
547        else:
548            if CHECKOUTLINE_NAME in historyList:
549                log.error("Glyph '%s' has been edited. You must first "
550                          "run '%s' before running '%s'. Skipping.",
551                          glyphName, CHECKOUTLINE_NAME, AUTOHINT_NAME)
552                skip = True
553
554            # If the source hash has changed, we need to delete the processed
555            # layer glyph.
556            self.hashMapChanged = True
557            self.hashMap[glyphName] = [newSrcHash, [AUTOHINT_NAME]]
558            if glyphName in self.processedLayerGlyphMap:
559                del self.processedLayerGlyphMap[glyphName]
560
561        return skip
562
563    def _get_glyphset(self, layer_name=None):
564        if layer_name not in self._glyphsets:
565            glyphset = None
566            try:
567                glyphset = self._reader.getGlyphSet(layer_name)
568            except UFOLibError:
569                pass
570            self._glyphsets[layer_name] = glyphset
571        return self._glyphsets[layer_name]
572
573    @staticmethod
574    def get_glyph_bez(glyph, round_coords):
575        pen = BezPen(glyph.glyphSet, round_coords)
576        glyph.draw(pen)
577        if not hasattr(glyph, "width"):
578            glyph.width = 0
579        return pen.bez
580
581    def _get_or_skip_glyph(self, name, round_coords, doAll):
582        # Get default glyph layer data, so we can check if the glyph
583        # has been edited since this program was last run.
584        # If the program name is in the history list, and the srcHash
585        # matches the default glyph layer data, we can skip.
586        glyphset = self._get_glyphset()
587        glyph = glyphset[name]
588        bez = self.get_glyph_bez(glyph, round_coords)
589
590        # Hash is always from the default glyph layer.
591        hash_pen = HashPointPen(glyph)
592        glyph.drawPoints(hash_pen)
593        skip = self.checkSkipGlyph(name, hash_pen.getHash(), doAll)
594
595        # If there is a glyph in the processed layer, get the outline from it.
596        if name in self.processedLayerGlyphMap:
597            glyphset = self._get_glyphset(PROCESSED_LAYER_NAME)
598            glyph = glyphset[name]
599            bez = self.get_glyph_bez(glyph, round_coords)
600
601        return glyph.width, bez, skip
602
603    def getGlyphList(self):
604        glyphOrder = self._reader.readLib().get(PUBLIC_GLYPH_ORDER, [])
605        glyphList = list(self._get_glyphset().keys())
606
607        # Sort the returned glyph list by the glyph order as we depend in the
608        # order for expanding glyph ranges.
609        def key_fn(v):
610            if v in glyphOrder:
611                return glyphOrder.index(v)
612            return len(glyphOrder)
613        return sorted(glyphList, key=key_fn)
614
615    @property
616    def glyphMap(self):
617        if self._glyphmap is None:
618            glyphset = self._get_glyphset()
619            self._glyphmap = glyphset.contents
620        return self._glyphmap
621
622    @property
623    def processedLayerGlyphMap(self):
624        if self._processed_layer_glyphmap is None:
625            self._processed_layer_glyphmap = {}
626            glyphset = self._get_glyphset(PROCESSED_LAYER_NAME)
627            if glyphset is not None:
628                self._processed_layer_glyphmap = glyphset.contents
629        return self._processed_layer_glyphmap
630
631    @property
632    def fontInfo(self):
633        if self._fontInfo is None:
634            info = SimpleNamespace()
635            self._reader.readInfo(info)
636            self._fontInfo = vars(info)
637        return self._fontInfo
638
639    def getFontInfo(self, allow_no_blues, noFlex,
640                    vCounterGlyphs, hCounterGlyphs, fdIndex=0):
641        if self.fontDict is not None:
642            return self.fontDict
643
644        fdDict = fdTools.FDDict()
645        # should be 1 if the glyphs are ideographic, else 0.
646        fdDict.LanguageGroup = self.fontInfo.get("languagegroup", "0")
647        fdDict.OrigEmSqUnits = self.getUnitsPerEm()
648        fdDict.FontName = self.getPSName()
649        upm = self.getUnitsPerEm()
650        low = min(-upm * 0.25,
651                  self.fontInfo.get("openTypeOS2WinDescent", 0) - 200)
652        high = max(upm * 1.25,
653                   self.fontInfo.get("openTypeOS2WinAscent", 0) + 200)
654        # Make a set of inactive alignment zones: zones outside of the font
655        # bbox so as not to affect hinting. Used when src font has no
656        # BlueValues or has invalid BlueValues. Some fonts have bad BBox
657        # values, so I don't let this be smaller than -upm*0.25, upm*1.25.
658        inactiveAlignmentValues = [low, low, high, high]
659        blueValues = self.fontInfo.get("postscriptBlueValues", [])
660        numBlueValues = len(blueValues)
661        if numBlueValues < 4:
662            if allow_no_blues:
663                blueValues = inactiveAlignmentValues
664                numBlueValues = len(blueValues)
665            else:
666                raise FontParseError(
667                    "Font must have at least four values in its "
668                    "BlueValues array for PSAutoHint to work!")
669        blueValues.sort()
670        # The first pair only is a bottom zone, where the first value is the
671        # overshoot position; the rest are top zones, and second value of the
672        # pair is the overshoot position.
673        blueValues[0] = blueValues[0] - blueValues[1]
674        for i in range(3, numBlueValues, 2):
675            blueValues[i] = blueValues[i] - blueValues[i - 1]
676
677        blueValues = [str(v) for v in blueValues]
678        numBlueValues = min(numBlueValues, len(fdTools.kBlueValueKeys))
679        for i in range(numBlueValues):
680            key = fdTools.kBlueValueKeys[i]
681            value = blueValues[i]
682            setattr(fdDict, key, value)
683
684        otherBlues = self.fontInfo.get("postscriptOtherBlues", [])
685
686        if len(otherBlues) > 0:
687            i = 0
688            numBlueValues = len(otherBlues)
689            otherBlues.sort()
690            for i in range(0, numBlueValues, 2):
691                otherBlues[i] = otherBlues[i] - otherBlues[i + 1]
692            otherBlues = [str(v) for v in otherBlues]
693            numBlueValues = min(numBlueValues,
694                                len(fdTools.kOtherBlueValueKeys))
695            for i in range(numBlueValues):
696                key = fdTools.kOtherBlueValueKeys[i]
697                value = otherBlues[i]
698                setattr(fdDict, key, value)
699
700        vstems = self.fontInfo.get("postscriptStemSnapV", [])
701        if not vstems:
702            if allow_no_blues:
703                # dummy value. Needs to be larger than any hint will likely be,
704                # as the autohint program strips out any hint wider than twice
705                # the largest global stem width.
706                vstems = [fdDict.OrigEmSqUnits]
707            else:
708                raise FontParseError("Font does not have postscriptStemSnapV!")
709
710        vstems.sort()
711        if not vstems or (len(vstems) == 1 and vstems[0] < 1):
712            # dummy value that will allow PyAC to run
713            vstems = [fdDict.OrigEmSqUnits]
714            log.warning("There is no value or 0 value for DominantV.")
715        fdDict.DominantV = "[" + " ".join([str(v) for v in vstems]) + "]"
716
717        hstems = self.fontInfo.get("postscriptStemSnapH", [])
718        if not hstems:
719            if allow_no_blues:
720                # dummy value. Needs to be larger than any hint will likely be,
721                # as the autohint program strips out any hint wider than twice
722                # the largest global stem width.
723                hstems = [fdDict.OrigEmSqUnits]
724            else:
725                raise FontParseError("Font does not have postscriptStemSnapH!")
726
727        hstems.sort()
728        if not hstems or (len(hstems) == 1 and hstems[0] < 1):
729            # dummy value that will allow PyAC to run
730            hstems = [fdDict.OrigEmSqUnits]
731            log.warning("There is no value or 0 value for DominantH.")
732        fdDict.DominantH = "[" + " ".join([str(v) for v in hstems]) + "]"
733
734        if noFlex:
735            fdDict.FlexOK = "false"
736        else:
737            fdDict.FlexOK = "true"
738
739        # Add candidate lists for counter hints, if any.
740        if vCounterGlyphs:
741            temp = " ".join(vCounterGlyphs)
742            fdDict.VCounterChars = "( %s )" % (temp)
743        if hCounterGlyphs:
744            temp = " ".join(hCounterGlyphs)
745            fdDict.HCounterChars = "( %s )" % (temp)
746
747        fdDict.BlueFuzz = self.fontInfo.get("postscriptBlueFuzz", 1)
748        # postscriptBlueShift
749        # postscriptBlueScale
750        self.fontDict = fdDict
751        return fdDict
752
753    def getfdInfo(self, allow_no_blues, noFlex, vCounterGlyphs, hCounterGlyphs,
754                  glyphList, fdIndex=0):
755        fdGlyphDict = None
756        fdDict = self.getFontInfo(allow_no_blues, noFlex,
757                                  vCounterGlyphs, hCounterGlyphs, fdIndex)
758        fontDictList = [fdDict]
759
760        # Check the fontinfo file, and add any other font dicts
761        srcFontInfo = os.path.dirname(self.path)
762        srcFontInfo = os.path.join(srcFontInfo, "fontinfo")
763        maxX = self.getUnitsPerEm() * 2
764        maxY = maxX
765        minY = -self.getUnitsPerEm()
766        if os.path.exists(srcFontInfo):
767            with open(srcFontInfo, "r", encoding="utf-8") as fi:
768                fontInfoData = fi.read()
769            fontInfoData = re.sub(r"#[^\r\n]+", "", fontInfoData)
770
771            if "FDDict" in fontInfoData:
772                fdGlyphDict, fontDictList, finalFDict = \
773                    fdTools.parseFontInfoFile(
774                        fontDictList, fontInfoData, glyphList, maxY, minY,
775                        self.getPSName())
776                if finalFDict is None:
777                    # If a font dict was not explicitly specified for the
778                    # output font, use the first user-specified font dict.
779                    fdTools.mergeFDDicts(fontDictList[1:], self.fontDict)
780                else:
781                    fdTools.mergeFDDicts([finalFDict], self.fontDict)
782
783        return fdGlyphDict, fontDictList
784
785    @staticmethod
786    def close():
787        return
788
789
790class BezPen(BasePen):
791    def __init__(self, glyph_set, round_coords):
792        super(BezPen, self).__init__(glyph_set)
793        self.round_coords = round_coords
794        self.bez = []
795
796    def _point(self, point):
797        if self.round_coords:
798            return " ".join("%d" % round(pt) for pt in point)
799        return " ".join("%3f" % pt for pt in point)
800
801    def _moveTo(self, pt):
802        self.bez.append("%s mt" % self._point(pt))
803
804    def _lineTo(self, pt):
805        self.bez.append("%s dt" % self._point(pt))
806
807    def _curveToOne(self, pt1, pt2, pt3):
808        self.bez.append("%s ct" % self._point(pt1 + pt2 + pt3))
809
810    @staticmethod
811    def _qCurveToOne(pt1, pt2):
812        raise FontParseError("Quadratic curves are not supported")
813
814    def _closePath(self):
815        self.bez.append("cp")
816
817
818class HashPointPen(AbstractPointPen):
819
820    def __init__(self, glyph):
821        self.glyphset = getattr(glyph, "glyphSet", None)
822        self.width = norm_float(round(getattr(glyph, "width", 0), 9))
823        self.data = ["w%s" % self.width]
824
825    def getHash(self):
826        data = "".join(self.data)
827        if len(data) >= 128:
828            data = hashlib.sha512(data.encode("ascii")).hexdigest()
829        return data
830
831    def beginPath(self, identifier=None, **kwargs):
832        pass
833
834    def endPath(self):
835        pass
836
837    def addPoint(self, pt, segmentType=None, smooth=False, name=None,
838                 identifier=None, **kwargs):
839        if segmentType is None:
840            pt_type = ""
841        else:
842            pt_type = segmentType[0]
843        self.data.append("%s%s%s" % (pt_type,
844                                     repr(norm_float(round(pt[0], 9))),
845                                     repr(norm_float(round(pt[1], 9)))))
846
847    def addComponent(self, baseGlyphName, transformation, identifier=None,
848                     **kwargs):
849        self.data.append("base:%s" % baseGlyphName)
850
851        for v in transformation:
852            self.data.append(str(norm_float(round(v, 9))))
853
854        self.data.append("w%s" % self.width)
855        glyph = self.glyphset[baseGlyphName]
856        glyph.drawPoints(self)
857
858
859class BezGlyph(object):
860    def __init__(self, bez):
861        self._bez = bez
862        self.lib = {}
863
864    @staticmethod
865    def _draw(contours, pen):
866        for contour in contours:
867            pen.beginPath()
868            for point in contour:
869                x = point.get("x")
870                y = point.get("y")
871                segmentType = point.get("type", None)
872                name = point.get("name", None)
873                pen.addPoint((x, y), segmentType=segmentType, name=name)
874            pen.endPath()
875
876    def drawPoints(self, pen):
877        contours, hints = convertBezToOutline(self._bez)
878        self._draw(contours, pen)
879
880        # Add the stem hints.
881        if hints is not None:
882            # Add this hash to the glyph data, as it is the hash which matches
883            # the output outline data. This is not necessarily the same as the
884            # hash of the source data; autohint can be used to change outlines.
885            hash_pen = HashPointPen(self)
886            self._draw(contours, hash_pen)
887            hints["id"] = hash_pen.getHash()
888
889            # Remove any existing hint data.
890            for key in (HINT_DOMAIN_NAME1, HINT_DOMAIN_NAME2):
891                if key in self.lib:
892                    del self.lib[key]
893
894            self.lib[HINT_DOMAIN_NAME2] = hints
895
896
897class HintMask:
898    # class used to collect hints for the current
899    # hint mask when converting bez to T2.
900    def __init__(self, listPos):
901        # The index into the pointList is kept
902        # so we can quickly find them later.
903        self.listPos = listPos
904        self.hList = []  # These contain the actual hint values.
905        self.vList = []
906        self.hstem3List = []
907        self.vstem3List = []
908        # The name attribute of the point which follows the new hint set.
909        self.pointName = "hintSet" + str(listPos).zfill(4)
910
911    def getHintSet(self):
912        hintset = OrderedDict()
913        hintset[POINT_TAG] = self.pointName
914        hintset[STEMS_NAME] = []
915
916        if len(self.hList) > 0 or len(self.hstem3List):
917            hintset[STEMS_NAME].extend(
918                makeHintSet(self.hList, self.hstem3List, isH=True))
919
920        if len(self.vList) > 0 or len(self.vstem3List):
921            hintset[STEMS_NAME].extend(
922                makeHintSet(self.vList, self.vstem3List, isH=False))
923
924        return hintset
925
926
927def norm_float(value):
928    """Converts a float (whose decimal part is zero) to integer"""
929    if isinstance(value, float) and value.is_integer():
930        return int(value)
931    return value
932
933
934def makeStemHintList(hintsStem3, isH):
935    # In bez terms, the first coordinate in each pair is
936    # absolute, second is relative, and hence is the width.
937    if isH:
938        op = HSTEM3_NAME
939    else:
940        op = VSTEM3_NAME
941    posList = [op]
942    for stem3 in hintsStem3:
943        for pos, width in stem3:
944            posList.append("%s %s" % (norm_float(pos), norm_float(width)))
945    return " ".join(posList)
946
947
948def makeHintList(hints, isH):
949    # Add the list of hint operators
950    # In bez terms, the first coordinate in each pair is
951    # absolute, second is relative, and hence is the width.
952    hintset = []
953    for hint in hints:
954        if not hint:
955            continue
956        pos = hint[0]
957        width = hint[1]
958        if isH:
959            op = HSTEM_NAME
960        else:
961            op = VSTEM_NAME
962        hintset.append("%s %s %s" % (op, norm_float(pos), norm_float(width)))
963    return hintset
964
965
966def fixStartPoint(contour, operators):
967    # For the GLIF format, the idea of first/last point is funky, because
968    # the format avoids identifying a start point. This means there is no
969    # implied close-path line-to. If the last implied or explicit path-close
970    # operator is a line-to, then replace the "mt" with linto, and remove
971    # the last explicit path-closing line-to, if any. If the last op is a
972    # curve, then leave the first two point args on the stack at the end of
973    # the point list, and move the last curveto to the first op, replacing
974    # the move-to.
975
976    _, firstX, firstY = operators[0]
977    lastOp, lastX, lastY = operators[-1]
978    point = contour[0]
979    if (firstX == lastX) and (firstY == lastY):
980        del contour[-1]
981        point["type"] = lastOp
982    else:
983        # we have an implied final line to. All we need to do
984        # is convert the inital moveto to a lineto.
985        point["type"] = "line"
986
987
988bezToUFOPoint = {
989    "mt": 'move',
990    "rmt": 'move',
991    "dt": 'line',
992    "ct": 'curve',
993}
994
995
996def convertCoords(current_x, current_y):
997    return norm_float(current_x), norm_float(current_y)
998
999
1000def convertBezToOutline(bezString):
1001    """
1002    Since the UFO outline element as no attributes to preserve,
1003    I can just make a new one.
1004    """
1005    # convert bez data to a UFO glif XML representation
1006    #
1007    # Convert all bez ops to simplest UFO equivalent
1008    # Add all hints to vertical and horizontal hint lists as encountered;
1009    # insert a HintMask class whenever a new set of hints is encountered
1010    # after all operators have been processed, convert HintMask items into
1011    # hintmask ops and hintmask bytes add all hints as prefix review operator
1012    # list to optimize T2 operators.
1013    # if useStem3 == 1, then any counter hints must be processed as stem3
1014    # hints, else the opposite.
1015    # Counter hints are used only in LanguageGroup 1 glyphs, aka ideographs
1016
1017    bezString = re.sub(r"%.+?\n", "", bezString)  # supress comments
1018    bez = re.findall(r"(\S+)", bezString)
1019    flexes = []
1020    # Create an initial hint mask. We use this if
1021    # there is no explicit initial hint sub.
1022    hintmask = HintMask(0)
1023    hintmasks = [hintmask]
1024    vstem3_args = []
1025    hstem3_args = []
1026    args = []
1027    operators = []
1028    hintmask_name = None
1029    in_preflex = False
1030    hints = None
1031    op_index = 0
1032    current_x = 0
1033    current_y = 0
1034    contours = []
1035    contour = None
1036    has_hints = False
1037
1038    for token in bez:
1039        try:
1040            val = float(token)
1041            args.append(val)
1042            continue
1043        except ValueError:
1044            pass
1045        if token == "newcolors":
1046            pass
1047        elif token in ["beginsubr", "endsubr"]:
1048            pass
1049        elif token == "snc":
1050            hintmask = HintMask(op_index)
1051            # If the new hints precedes any marking operator,
1052            # then we want throw away the initial hint mask we
1053            # made, and use the new one as the first hint mask.
1054            if op_index == 0:
1055                hintmasks = [hintmask]
1056            else:
1057                hintmasks.append(hintmask)
1058            hintmask_name = hintmask.pointName
1059        elif token == "enc":
1060            pass
1061        elif token == "rb":
1062            if hintmask_name is None:
1063                hintmask_name = hintmask.pointName
1064            hintmask.hList.append(args)
1065            args = []
1066            has_hints = True
1067        elif token == "ry":
1068            if hintmask_name is None:
1069                hintmask_name = hintmask.pointName
1070            hintmask.vList.append(args)
1071            args = []
1072            has_hints = True
1073        elif token == "rm":  # vstem3's are vhints
1074            if hintmask_name is None:
1075                hintmask_name = hintmask.pointName
1076            has_hints = True
1077            vstem3_args.append(args)
1078            args = []
1079            if len(vstem3_args) == 3:
1080                hintmask.vstem3List.append(vstem3_args)
1081                vstem3_args = []
1082
1083        elif token == "rv":  # hstem3's are hhints
1084            has_hints = True
1085            hstem3_args.append(args)
1086            args = []
1087            if len(hstem3_args) == 3:
1088                hintmask.hstem3List.append(hstem3_args)
1089                hstem3_args = []
1090
1091        elif token == "preflx1":
1092            # the preflx1/preflx2a sequence provides the same i as the flex
1093            # sequence; the difference is that the preflx1/preflx2a sequence
1094            # provides the argument values needed for building a Type1 string
1095            # while the flex sequence is simply the 6 rcurveto points. Both
1096            # sequences are always provided.
1097            args = []
1098            # need to skip all move-tos until we see the "flex" operator.
1099            in_preflex = True
1100        elif token == "preflx2a":
1101            args = []
1102        elif token == "flxa":  # flex with absolute coords.
1103            in_preflex = False
1104            flex_point_name = BASE_FLEX_NAME + str(op_index).zfill(4)
1105            flexes.append(flex_point_name)
1106            # The first 12 args are the 6 args for each of
1107            # the two curves that make up the flex feature.
1108            i = 0
1109            while i < 2:
1110                current_x = args[0]
1111                current_y = args[1]
1112                x, y = convertCoords(current_x, current_y)
1113                point = {"x": x, "y": y}
1114                contour.append(point)
1115                current_x = args[2]
1116                current_y = args[3]
1117                x, y = convertCoords(current_x, current_y)
1118                point = {"x": x, "y": y}
1119                contour.append(point)
1120                current_x = args[4]
1121                current_y = args[5]
1122                x, y = convertCoords(current_x, current_y)
1123                point_type = 'curve'
1124                point = {"x": x, "y": y, "type": point_type}
1125                contour.append(point)
1126                operators.append([point_type, current_x, current_y])
1127                op_index += 1
1128                if i == 0:
1129                    args = args[6:12]
1130                i += 1
1131            # attach the point name to the first point of the first curve.
1132            contour[-6][POINT_NAME] = flex_point_name
1133            if hintmask_name is not None:
1134                # We have a hint mask that we want to attach to the first
1135                # point of the flex op. However, there is already a flex
1136                # name in that attribute. What we do is set the flex point
1137                # name into the hint mask.
1138                hintmask.pointName = flex_point_name
1139                hintmask_name = None
1140            args = []
1141        elif token == "sc":
1142            pass
1143        elif token == "cp":
1144            pass
1145        elif token == "ed":
1146            pass
1147        else:
1148            if in_preflex and token in ["rmt", "mt"]:
1149                continue
1150
1151            if token in ["rmt", "mt", "dt", "ct"]:
1152                op_index += 1
1153            else:
1154                raise BezParseError(
1155                    "Unhandled operation: '%s' '%s'." % (args, token))
1156            point_type = bezToUFOPoint[token]
1157            if token in ["rmt", "mt", "dt"]:
1158                if token in ["mt", "dt"]:
1159                    current_x = args[0]
1160                    current_y = args[1]
1161                else:
1162                    current_x += args[0]
1163                    current_y += args[1]
1164                x, y = convertCoords(current_x, current_y)
1165                point = {"x": x, "y": y, "type": point_type}
1166
1167                if point_type == "move":
1168                    if contour is not None:
1169                        if len(contour) == 1:
1170                            # Just in case we see two moves in a row,
1171                            # delete the previous contour if it has
1172                            # only the move-to
1173                            log.info("Deleting moveto: %s adding %s",
1174                                     contours[-1], contour)
1175                            del contours[-1]
1176                        else:
1177                            # Fix the start/implied end path
1178                            # of the previous path.
1179                            fixStartPoint(contour, operators)
1180                    operators = []
1181                    contour = []
1182                    contours.append(contour)
1183
1184                if hintmask_name is not None:
1185                    point[POINT_NAME] = hintmask_name
1186                    hintmask_name = None
1187                contour.append(point)
1188                operators.append([point_type, current_x, current_y])
1189            else:  # "ct"
1190                current_x = args[0]
1191                current_y = args[1]
1192                x, y = convertCoords(current_x, current_y)
1193                point = {"x": x, "y": y}
1194                contour.append(point)
1195                current_x = args[2]
1196                current_y = args[3]
1197                x, y = convertCoords(current_x, current_y)
1198                point = {"x": x, "y": y}
1199                contour.append(point)
1200                current_x = args[4]
1201                current_y = args[5]
1202                x, y = convertCoords(current_x, current_y)
1203                point = {"x": x, "y": y, "type": point_type}
1204                contour.append(point)
1205                if hintmask_name is not None:
1206                    # attach the pointName to the first point of the curve.
1207                    contour[-3][POINT_NAME] = hintmask_name
1208                    hintmask_name = None
1209                operators.append([point_type, current_x, current_y])
1210            args = []
1211
1212    if contour is not None:
1213        if len(contour) == 1:
1214            # Just in case we see two moves in a row, delete
1215            # the previous contour if it has zero length.
1216            del contours[-1]
1217        else:
1218            fixStartPoint(contour, operators)
1219
1220    # Add hints, if any.
1221    # Must be done at the end of op processing to make sure we have seen all
1222    # the hints in the bez string.
1223    # Note that the hintmasks are identified in the operators by the point
1224    # name. We will follow the T1 spec: a glyph may have stem3 counter hints
1225    # or regular hints, but not both.
1226
1227    if has_hints or len(flexes) > 0:
1228        hints = OrderedDict()
1229        hints["id"] = ""
1230
1231        # Convert the rest of the hint masks to a hintmask op and hintmask
1232        # bytes.
1233        hints[HINT_SET_LIST_NAME] = []
1234        for hintmask in hintmasks:
1235            hints[HINT_SET_LIST_NAME].append(hintmask.getHintSet())
1236
1237        if len(flexes) > 0:
1238            hints[FLEX_INDEX_LIST_NAME] = []
1239            for pointTag in flexes:
1240                hints[FLEX_INDEX_LIST_NAME].append(pointTag)
1241
1242    return contours, hints
1243
1244
1245def makeHintSet(hints, hintsStem3, isH):
1246    # A charstring may have regular v stem hints or vstem3 hints, but not both.
1247    # Same for h stem hints and hstem3 hints.
1248    hintset = []
1249    if len(hintsStem3) > 0:
1250        hintsStem3.sort()
1251        numHints = len(hintsStem3)
1252        hintLimit = int((STACK_LIMIT - 2) / 2)
1253        if numHints >= hintLimit:
1254            hintsStem3 = hintsStem3[:hintLimit]
1255        hintset.append(makeStemHintList(hintsStem3, isH))
1256    else:
1257        hints.sort()
1258        numHints = len(hints)
1259        hintLimit = int((STACK_LIMIT - 2) / 2)
1260        if numHints >= hintLimit:
1261            hints = hints[:hintLimit]
1262        hintset.extend(makeHintList(hints, isH))
1263
1264    return hintset
1265