forked from adobe-type-tools/python-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmakeinstances.py
More file actions
1679 lines (1471 loc) · 52.9 KB
/
makeinstances.py
File metadata and controls
1679 lines (1471 loc) · 52.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/env python
__copyright__ = """Copyright 2017 Adobe Systems Incorporated (http://www.adobe.com/). All Rights Reserved.
"""
__doc__ = """
"""
__usage__ = """
makeinstances program v1.34
makeinstances -h
makeinstances -u
makeinstances [-a] [-f <instance file path>] [-m <MM font path>]
[-o <instance parent directory>] [-d <instance file name>]
[-uf] [-ui] [-a]
-f <instance file path> .... Specifies alternate path to instance specification
file. Default: instances
-m <MM font path> .......... Specified alternate path to source MM Type1 font
file. Default: mmfont.pfa
-o <instance parent dir> ... Specifies alternate parent directory for instance
directories. Default: parent dir of instances file
-d <instance font file name> .... Specifies alternate name for instance font file.
Default is font.pfa.
-uf ... Use pre-existing font.pfa file's FontDict values for new instance font.
-ui ... As '-uf', but also updates the instance file with the original font.pfa
values.
-i <list of instance indices> Build only the listed instances, as 0-based instance index in the isntance file.
-a ... do NOT autohint the instance font files. Default is to do so, as IS strips out all hints.
-c ... do NOT run checkoutlines to remove overlaps from the instance font files. Default is to do so.
"""
__help__ = __usage__ + """
makeinstances builds font.pfa instances from a multiple master font file.
The tool is needed because FontLab does not do intelligent scaling when
making MM instances. When run, it will create all the instances named in the
instance file.
makeinstances requires two input files: the MM font file, and the instances
file. The instances file provides the information needed for each instance.
By default, it is assumed to be in the current directory, and to be named
'instances'. Another path may be specified with the option '-f'.
The MM font file is assumed to be in the same directory as the instances
file, and to be named "mmfont.pfa". Another path may be specified with the
option '-m'.
By default, the instances are created in sub-directories of the directory
that contains the instances file. A different parent directory may be
specified with the option '-o'.
By default, the name of the instance font files is "font.pfa". This can be
overridden with the '-d' option.
The sub-directory names are derived by taking the portion of the font's
PostScript name after the hyphen. If there is no hyphen, the "Regular" is
used.
The global values that are taken from the original font.pfa file with the
'-ui' and '-uf' options are applied are:
- FontBBox
- BlueValues
- OtherBlues
- FamilyBlues
- FamilyOtherBlues
- StdHW
- StdVW
- StemSnapH
- StemSnapV
The '-ui' and -uf' options are useful when the previous instance fonts have
been edited to have individual global alignment zones and stems. When this
has been done, the results are much better than what you get by using the
interpolated values from the MM font. In order to avoid losing the
individuak instance values whenever the instances are regenerated from the
parent MM font, it is useful to use the -ui and -uf options to get these
values from the previous instance font, and optionally to store them in the
instances file.
The instance file is a flat text file. Each line represents a record
describing a single instance. Each record consists of a series of
tab-delimited fields. The first 6 fields are always, order:
FamilyName : This is the Preferred Family name.
FontName : This is the PostScript name.
FullName : This is the Preferred Full name.
Weight : This is the Weight name.
Coords : This is a single value, or a list of comma-separated integer values.
Each integer value corresponds to an axis.
IsBold : This must be either 1 (True) or 0 (False). This will be translated into
Postscript FontDict ForceBold field with a value of
'true' for instances where then font is bold, and will be
copied to the fontinfo file
If only these six fields are used, then there is no need for a header line.
However, if any additional fields are used, then the file must contain a
line starting with "#KEYS:", and continuing with tab-delimited field names.
ExceptionSuffixes : A list of suffixes, used to identify MM exception
glyphs. An MM exception glyph is one which is designed for use in only one
instance, and is used by replacing every occurence of the glyphs that match
the MM exception glyph's base name. The MM exception glyph is written to no
other instance. This allows the developer to fix problems with just a few
glyphs in each instance. For example, if the record for HypatiaSansPro-Black
in the 'instances' file specifies an ExceptionSuffix suffix list which
contains the suffix ".black", and there is an MM exception glyph named
"a.black", then the glyph "a" will be replaced by the glyph "a.black", and
all composite glyphs that use "a" will be updated to use the contours from
"a.black" instead.
NOTE! In order for the MM Exception glyphs to be applied to what where
originally composite glyphs in the MM FontLab font file, makeinstances
requires a composite glyph data file. You must generate this before running
makeinstances. It is generated by the Adobe script for FontLab, "Instance
Generator".
ExtraGlyphs : A list of working glyph names, to be omitted from the
instances. The may be a complete glyph name, or a wild-card pattern. A
pattern may take two forms: "*<suffix>", which will match any glyph ending
with that suffix, or a regular expression which must match entire glyph
names. The pattern must begin with "^" and ends with "$". You do not need to
include glyph names which match an MM Exception glyph suffix: such glyphs
will not be written to any instance.
Any additional field names are assumed to be the names for Postscript
FontDict keys. The script will modify the values in the instance's
PostScript FontDict for the keys that have these names. The additional
Postscript keys and values must be formatted exactly as they are in the
detype1 output.
Example 1:
#KEYS:<tab>FamilyName<tab>FontName<tab>FullName<tab>Weight<tab>Coords<tab>IsBold<tab>FontBBox<tab>BlueValues
ArnoPro<tab><ArnoPro-Regular<tab>Arno Pro Regular<tab>Regular<tab>160,451<tab>0<tab>{-147 -376 1511 871}<tab>{[-18 0 395 410 439 453 596 608 615 633 672 678]
ArnoPro<tab><ArnoPro-Bold<tab>Arno Pro Bold<tab>Bold<tab>1000,451<tab>1<tab>{-148 -380 1515 880} <tab> [-18 0 400 414 439 453 584 596 603 621 653 664]
Example 2:
#KEYS:<tab>FamilyName<tab>FontName(PSName)<tab>FullName<tab>Weight<tab>AxisValues<tab>isBold<tab>ExceptionSuffixes<tab>ExtraGlyphs
HypatiaSansPro<tab>HypatiaSansPro-ExtraLight<tab>Hypatia Sans Pro ExtraLight<tab>ExtraLight<tab>0<tab>0<tab><tab>
HypatiaSansPro<tab>HypatiaSansPro-Black<tab>Hypatia Sans Pro Black<tab>Black<tab>1000<tab>0<tab>[".black", "black"]<tab>['^serif.+$','^[Dd]escender.*$']
"""
import sys
import os
import re
import time
import copy
import copy
import traceback
import fdkutils
from subprocess import PIPE, Popen
kInstanceName = "font.pfa"
kCompositeDataName = "temp.composite.dat"
kFieldsKey = "#KEYS:"
kFamilyName = "FamilyName"
kFontName = "FontName"
kFullName = "FullName"
kWeight = "Weight"
kCoordsKey = "Coords"
kIsBoldKey = "IsBold"
kForceBold = "ForceBold"
kIsItalicKey = "IsItalic"
kDefaultValue = "Default"
kExceptionSuffixes = "ExceptionSuffixes"
kExtraGlyphs = "ExtraGlyphs"
kFixedFieldKeys = {
# field index: key name
0:kFamilyName,
1:kFontName,
2:kFullName,
3:kWeight,
4:kCoordsKey,
5:kIsBoldKey,
}
kNumFixedFields = len(kFixedFieldKeys)
kDefaultInstancePath = "instances"
kDefaultMMFontPath = "mmfont.pfa"
kNonPostScriptKeys = [kCoordsKey, kIsBoldKey, kIsItalicKey, kExtraGlyphs, kExceptionSuffixes]
kPSNameKeys = [kFullName, kFamilyName, kForceBold]
kPSFontDictKeys = {
"FontBBox": None,
"BlueValues": None,
"OtherBlues": None,
"FamilyBlues": None,
"FamilyOtherBlues": None,
kForceBold: None,
"StdHW": None,
"StdVW": None,
"StemSnapH": None,
"StemSnapV": None,
"BlueScale": None,
"BlueShift": None,
"BlueFuzz": None,
}
class OptError(ValueError):
pass
class ParseError(ValueError):
pass
class SnapShotError(ValueError):
pass
class Options:
def __init__(self, args):
self.instancePath = kDefaultInstancePath
self.mmFontPath = kDefaultMMFontPath
self.instancePath = None
self.mmFontPath = None
self.parentDir = None
self.InstanceFontName = None
self.useExisting = 0
self.updateInstances = 0
self.doAutoHint = 1
self.doOverlapRemoval = 1
self.logFile = None
self.indexList = []
lenArgs = len(args)
i = 0
hadError = 0
while i < lenArgs:
arg = args[i]
i += 1
if arg == "-h":
logMsg.log(__help__)
sys.exit(0)
elif arg == "-u":
logMsg.log(__usage__)
sys.exit(0)
elif arg == "-f":
self.instancePath = args[i]
i +=1
elif arg == "-m":
self.mmFontPath = args[i]
i +=1
elif arg == "-o":
self.parentDir = args[i]
i +=1
elif arg == "-log":
logMsg.sendTo( args[i])
i +=1
elif arg == "-d":
self.InstanceFontName = args[i]
i +=1
elif arg == "-uf":
self.useExisting = 1
elif arg == "-a":
self.doAutoHint = 0
elif arg == "-c":
self.doOverlapRemoval = 0
elif arg == "-ui":
self.useExisting = 1
self.updateInstances = 1
elif arg == "-i":
ilist = args[i]
i +=1
self.indexList =eval(ilist)
if type(self.indexList) == type(0):
self.indexList = (self.indexList,)
else:
logMsg.log("Error: unrcognized argument:", arg)
hadError = 1
if hadError:
raise(OptError)
if self.instancePath == None:
self.instancePath = kDefaultInstancePath
if self.InstanceFontName == None:
self.InstanceFontName = kInstanceName
if self.mmFontPath == None:
self.mmFontPath = os.path.dirname(self.instancePath)
self.mmFontPath = os.path.join(self.mmFontPath, kDefaultMMFontPath)
if self.parentDir == None:
self.parentDir = os.path.dirname(self.instancePath)
if not os.path.exists(self.instancePath):
logMsg.log("Error: could not find instance file path:", self.instancePath)
hadError = 1
if not os.path.exists(self.mmFontPath):
logMsg.log("Error: could not find MM font file path:", self.mmFontPath)
hadError = 1
if self.parentDir and not os.path.exists(self.parentDir):
logMsg.log("Error: could not find parent directory for instance fonts.", self.parentDir)
hadError = 1
if os.path.exists(self.mmFontPath):
self.emSquare = getEmSquare(self.mmFontPath)
#print self.instancePath
#print self.mmFontPath
#print repr(self.parentDir)
if hadError:
raise(OptError)
class LogMsg:
def __init__(self, logFilePath = None):
self.logFilePath = logFilePath
self.logFile = None
if self.logFilePath:
self.logFile = open(logFilePath, "wt")
def log(self, *args):
txt = " ".join(args)
print txt
if self.logFilePath:
self.logFile.write(txt + os.linesep)
self.logFile.flush()
def sendTo(self, logFilePath):
self.logFilePath = logFilePath
self.logFile = open(logFilePath, "wt")
logMsg = LogMsg()
def getEmSquare(mmFontPath):
emSquare = "1000"
command = "tx -dump -0 \"%s\" 2>&1" % (mmFontPath)
report = fdkutils.runShellCmd(command)
m = re.search(r"sup.UnitsPerEm[ \t]+(\d+)", report)
if m: # no match is OK, means the em aquare is the default 1000.
emSquare = m.group(1)
return emSquare
def readInstanceFile(options):
f = open(options.instancePath, "rt")
data = f.read()
f.close()
lines = data.splitlines()
i = 0
parseError = 0
keyDict = copy.copy(kFixedFieldKeys)
numKeys = kNumFixedFields
numLines = len(lines)
instancesList = []
for i in range(numLines):
line = lines[i]
# Get rid of all comments. If we find a key definition comment line, parse it.
commentIndex = line.find('#')
if commentIndex >= 0:
if line.startswith(kFieldsKey):
if instancesList:
logMsg.log("Error. Field header line must preceed an data line")
raise ParseError
# parse the line with the field names.
line = line[len(kFieldsKey):]
line = line.strip()
keys = line.split('\t')
keys = map(lambda name: name.strip(), keys)
numKeys = len(keys)
k = kNumFixedFields
while k < numKeys:
keyDict[k] = keys[k]
k +=1
continue
else:
line = line[:commentIndex]
# get rid of blank lines
line2 = line.strip()
if not line2:
continue # skip blank lines
# Must be a data line.
fields = line.split('\t')
fields = map(lambda datum: datum.strip(), fields)
numFields = len(fields)
if (numFields != numKeys):
logMsg.log("Error: at line %s, the number of fields %s do not match the number of key names %s." % (i, numFields, numKeys))
parseError = 1
continue
instanceDict= {}
#Build a dict from key to value. Some kinds of values needs special processing.
for k in range(numFields):
key = keyDict[k]
field = fields[k]
if not field:
continue
if field in ["Default", "None", "FontBBox"]:
# FontBBox is no longer supported - I calculate the real
# instance fontBBox from the glyph metrics instead,
continue
if key == kFontName:
value = "/%s" % field
elif key in [kExtraGlyphs, kExceptionSuffixes]:
value = eval(field)
elif key in [kIsBoldKey, kIsItalicKey, kCoordsKey]:
value = eval(field) # this works for all three fields.
if key == kIsBoldKey: # need to convert to Type 1 field key.
if value == 1:
value = "true"
else:
value = "false"
elif key == kIsItalicKey:
if value == 1:
value = "true"
else:
value = "false"
elif key == kCoordsKey:
if type(value) == type(0):
value = (value,)
elif field[0] in ["[","{"]: # it is a Type 1 array value. Pass as is, as a string.
value = field
else:
# either a single number or a string.
if re.match(r"^[-.\d]+$", field):
value = field #it is a Type 1 number. Pass as is, as a string.
else:
# Wasn't a number. Format as Type 1 string, i.e add parens around it.
value = "(%s)" % (field)
instanceDict[key] = value
instancesList.append(instanceDict)
if parseError:
raise(ParseError)
return instancesList
class InstanceFontPaths:
def __init__(self, options, instanceDict):
""" Set the font directories and temp and final font paths for the current instance.
"""
psName = instanceDict[kFontName]
# Figure out style name, for use in setting directory paths.
parts = psName.split("-")
if len(parts) > 1:
style = parts[1]
else:
style = "Regular"
# Set output directory path, and temp file names.
dirPath = style
if options.parentDir:
dirPath = os.path.join(options.parentDir, style)
if not os.path.exists(dirPath):
logMsg.log("Warning: making new font instance path", dirPath)
os.makedirs(dirPath)
self.fontInstancePath = os.path.join(dirPath, options.InstanceFontName)
self.tempInstance = self.fontInstancePath + ".tmp"
self.textInstance = self.fontInstancePath + ".txt"
def getExistingValues(instanceDict, instanceFontPaths):
""" Get the PostScript FontDict key/value pairs of interest
from a pre-existing font instance.
"""
fontInstancePath = instanceFontPaths.fontInstancePath
if not os.path.exists(fontInstancePath):
logMsg.log("Error. Cannot open pre-existing instance file %s" % (fontInstancePath))
raise(SnapShotError)
#Decompile
command = "detype1 \"%s\"" % (fontInstancePath)
log = fdkutils.runShellCmd(command)
if not "cleartomark" in log[-200:]:
logMsg.log("Error. Cannot decompile pre-existing instance file %s" % (fontInstancePath))
logMsg.log(log)
raise(SnapShotError)
# Get key values
updateDict = copy.copy(kPSFontDictKeys)
missingList = []
for key in updateDict:
m1 = re.search(key + r"\s*", log)
if not m1:
logMsg.log("Warning: Failed to match key %s in original font" % (key))
missingList.append(key)
continue
start = m1.end()
limit = re.search(r"[\r\n]", log[start:]).start()
m2 = re.search(r"(readonly)*(\s+def|\s*\|-)", log[start:start+limit])
end = start + m2.start()
value = log[start:end]
updateDict[key] = value
if missingList:
# Remove from updateDict all the keys that
# I did n't find in the pre-existing font.
for key in missingList:
del updateDict[key]
return updateDict
def getFontBBox(fontPath):
"""Get the real FontBox from the font instance. This often differs a lot
from the interpolation of FontBBox in the parent MM font.
"""
command = "tx -mtx \"%s\" 2>&1" % (fontPath)
log = fdkutils.runShellCmd(command)
mtxList = re.findall(r",\{([^,]+),([^,]+),([^,]+),([^}]+)", log)
mtxList = mtxList[1:] # get rid of header line
llxList = map(lambda entry: eval(entry[0]), mtxList)
llyList = map(lambda entry: eval(entry[1]), mtxList)
urxList = map(lambda entry: eval(entry[2]), mtxList)
uryList = map(lambda entry: eval(entry[3]), mtxList)
updateValue = "{ %s %s %s %s }" % ( min(llxList), min(llyList), max(urxList), max(uryList) )
return updateValue
def doSnapshot(coords, emSquare, mmFontPath, tempInstance):
coords = repr(coords)[1:-1] # get rid of parens
if coords[-1] == ",": # If it is a one-item list
coords = coords[:-1]
coords = re.sub(r"\s+", "", coords) # get rid of spaces after commas
command = "IS -t1 -Z -U %s -z %s \"%s\" \"%s\" 2>&1" % (coords, emSquare, mmFontPath, tempInstance)
log = fdkutils.runShellCmd(command)
if ("error" in log) or not os.path.exists(tempInstance):
# try again with 'tx'.
command = "tx -t1 -Z -U %s \"%s\" \"%s\" 2>&1" % (coords, mmFontPath, tempInstance)
log = fdkutils.runShellCmd(command)
if ("error" in log) or not os.path.exists(tempInstance):
logMsg.log("Error in IS snapshotting to %s" % (tempInstance))
logMsg.log("Command:", command)
logMsg.log("log:", log)
raise(SnapShotError)
def applyUpdateValues(log, instanceDict, updateDict, diffValueDict):
""" For each {key,value} pairs in updateDict, check that it exists in
the new instance. If so, then override the instanceDict value from the
updateDict value.
"""
for key,updateValue in updateDict.items():
m1 = re.search(key + r"\s*", log)
if not m1:
value = None
else:
start = m1.end()
limit = re.search(r"[\r\n]", log[start:]).start()
m2 = re.search(r"(readonly)*(\s+def|\s*\|-)", log[start:start+limit])
end = start + m2.start()
value = log[start+1:end-1]
try:
if instanceDict[key] != updateValue:
instanceDict[key] = updateValue
diffValueDict[key] = updateValue
# else instanceDict has key and the update value is the same as the value from the instances file
except KeyError:
if value != updateValue:
instanceDict[key] = updateValue
diffValueDict[key] = updateValue
# else don't need to add diff entry; since entry is not
# in the instance receord, it is coming out right as a result
class FontParts:
def __init__(self, log):
# Parse text into prefix, suffix, and charstring dict.
m1 = re.search(r"(/CharStrings\s+)\d+([^/]+)", log, re.DOTALL)
if not m1:
logMsg.log("Error. Was unable to parse decompiled font to extract glyphs.")
raise ParseError
charStart = m1.end(2)
self.prefix = log[:m1.end(1)] # up to the end of /CharStrings\s+
self.prefix2 = log[m1.start(2) : charStart]
m2 = re.search(r"\send\s", log[charStart:], re.DOTALL)
if not m2:
logMsg.log("Error. Was unable to parse decompiled font to extract glyphs.")
raise ParseError
charEnd = m1.end(2) + m2.start()
self.suffix = log[charEnd:]
charBlock = log[charStart:charEnd]
charList = re.findall(r"(/\S+)([^/]+)", charBlock)
n = len(charList)
self.charDict = {}
charDict = self.charDict
for i in range(n):
entry = charList[i]
try:
charDict[entry[0][1:]] = Charstring(i, entry[0], entry[1])
except CharParseError:
print "Error: could not parse a component glyph in %s. Will skip merging exception glyph %s" % (glyphName, exceptionName )
return
class CharParseError:
pass
def rm(coords, x, y):
x += coords[0]
y += coords[1]
return x,y, None
def hm(coords, x, y):
x += coords[0]
return x,y, [ [coords[0], 0], "rmoveto"]
def vm(coords, x, y):
y += coords[0]
return x,y, [ [0, coords[0] ], "rmoveto"]
def rt(coords, x, y):
x += coords[0]
y += coords[1]
return x,y, None
def ht(coords, x, y):
x += coords[0]
return x,y, [ [ coords[0], 0], "rlineto"]
def vt(coords, x, y):
y += coords[0]
return x,y, [ [ 0, coords[0] ], "rlineto"]
def rc(coords, x, y):
x += coords[0] + coords[2] + coords[4]
y += coords[1] + coords[3] + coords[5]
return x,y, None
def vhc(coords, x, y):
x += coords[1] + coords[3]
y += coords[0] + coords[2]
return x,y, [ [0] + coords + [0], "rrcurveto"]
def hvc(coords, x, y):
x += coords[0] + coords[1]
y += coords[2] + coords[3]
return x,y, [ [coords[0]] + [0] + coords[1:3] + [0] + [coords[3]], "rrcurveto"]
class Path:
updateDict = {
'rmoveto': rm,
'hmoveto': hm,
'vmoveto': vm,
'rlineto': rt,
'hlineto': ht,
'vlineto': vt,
'rrcurveto': rc,
'vhcurveto': vhc,
'hvcurveto': hvc,
}
def __init__(self):
self.opList = []
self.end = (0,0)
self.start = (0,0)
self.startIndex = 0
def updateCurPoint(self, coords, op, x, y):
return self.updateDict[op](coords, x, y)
def __cmp__(self, otherPath):
return cmp(self.getString(1), otherPath.getString(1))
def __str__(self):
return "Path start: %s. %s" % (self.start, self.getString())
def __repr__(self):
return "Path() # start: %s. %s" % (self.start, self.getString())
def fuzzyMatch(self, otherPath, tolerance = 1):
opList1 = self.opList[1:]
opList2 = otherPath.opList[1:]
numOps = len(opList1)
match = cmp(numOps, len(opList2))
if match != 0:
return 0
for i in range(numOps):
entry1 = opList1[i]
entry2 = opList2[i]
if entry1[1] != entry2[1]:
return 0
coords1 = entry1[0]
coords2 = entry2[0]
numCoords = len(coords1)
for j in range(numCoords):
if abs(coords1[j] - coords2[j]) > tolerance:
return 0
return 1
def getString(self, forCompare = 0):
list = []
if forCompare:
opList = self.opList[1:] # ignore initial moveto
else:
opList = self.opList # ignore initial moveto
for opEntry in opList:
coords = map(str, opEntry[0])
list.extend(coords)
list.extend(opEntry[1])
pathString = " ".join(list)
return pathString
def replacePath(self, otherPath):
self.opList = otherPath.opList
def adjustMoveTo(self):
dx = self.start[0] - self.origin[0]
dy = self.start[1] - self.origin[1]
opEntry = self.opList[0]
if dx == 0:
opEntry[0] = [dy]
opEntry[1][0] = 'vmoveto'
elif dy == 0:
opEntry[0] = [dx]
opEntry[1][0] = 'hmoveto'
else:
opEntry[0] = [dx, dy]
opEntry[1][0] = 'rmoveto'
def pointDiff(first, second):
diff = [ first[0] - second[0], first[1] - second[1] ]
return diff
def pointAdd(first, second):
sum = [ first[0] + second[0], first[1] + second[1] ]
return sum
class Charstring:
kMovetoPat = re.compile(r"(?:[-\d]+\s+)+[hvr]moveto")
kOpPat = re.compile(r"((?:[-\d]+\s+)+)([^0-9\s]+)")
def __init__(self, gid, glyphName, charstring):
self.gid = gid
self.glyphName = glyphName # this is the Type1 name, with a "/" prefix.
self.origCharstring = charstring # Charstring after the glyph name.
self.charstring = self.origCharstring
self.parsed = 0
self.beforeMove = ""
self.paths = []
def __cmp__(self, otherChar):
return cmp(self.gid, otherChar.gid)
def __str__(self):
return "Char %s %s: %s" % (self.gid, self.glyphName, self.charstring)
def __repr__(self):
return "Charstring( %s,%s,%s)" % (self.gid, self.glyphName, self.charstring)
def parsePaths(self, opList):
paths = []
numOps = len(opList)
x = y = 0
path = None
for i in range(numOps):
opEntry = opList[i] # looks like ('coord 1-coordn' , 'op1 op2')
coords = opEntry[0]
ops = opEntry[1]
if ops[0].endswith('moveto'):
if path:
oldPath = path
oldPath.end = (x, y)
oldPath.opList = opList[startIndex:i]
path = Path()
paths.append(path)
path.origin = (x,y)
x, y, newEntry = path.updateCurPoint(coords, ops[0], x, y)
startIndex = i
path.start = (x, y)
else:
x, y, newEntry = path.updateCurPoint(coords, ops[0], x, y)
# reduce optimization, e.g "dy vlineto" -> "dx dy rlineto".
# This so that rounding differences won't produce different path operators.
if newEntry:
opEntry[0] = newEntry[0] # replace expanded coords
opEntry[1][0] = newEntry[1] # replace path op: e.g vlineto->rlineto.
path.end = (x, y)
path.opList = opList[startIndex:]
return paths
def parse(self):
if self.parsed:
return
self.parsed = 1
self.paths = []
self.movetos = []
charstring = self.charstring
self.seac = "seac" in charstring
if self.seac:
return
m = self.kMovetoPat.search(charstring)
if not m:
print "Failed to find moveto in %s." % (glyphName)
#import pdb
#pdb.set_trace()
raise CharParseError
start = m.start()
self.prefix = charstring[:start]
endCharIndex = charstring.find("endchar")
self.suffix = charstring[endCharIndex:]
charstring = charstring[start:endCharIndex]
opList = []
startPos = 0
m = self.kOpPat.search(charstring[startPos:])
while m:
if m:
coords = m.group(1).strip()
op = m.group(2)
startPos2 = startPos + m.end()
m2 = self.kOpPat.search(charstring[startPos2:])
if m2:
ops = charstring[startPos + m.start(2): startPos2 + m2.start()]
startPos = startPos2
else:
ops = charstring[startPos + m.start(2):]
coords = coords.split()
coords = map(int, coords)
ops = ops.split()
opList.append( [coords, ops] )
m = m2
self.paths = self.parsePaths(opList)
def getCharString(self):
return self.charstring
def checkMatch(self, otherChar, pathIndex, compMetrics):
if not self.parsed:
self.parse()
if not otherChar.parsed:
otherChar.parse()
if self.seac:
return 0, 0 # SHoudln't ever see a seac here. The IS command in teh snapshort function should remove them.
# compMetrics is [shift, scale]. Each of these contain [x, y].
numPaths = len(otherChar.paths)
# First see if we match at pathIndex. This fails if the 'self' was originally a seac glyph,
# as IS does not decompose the seac in the same order as the FontLabcomposite glyph. IS always writes
# the accent glyph first, base character second.
ok = 1
for i in range(numPaths):
try:
path1 = self.paths[pathIndex + i]
path2 = otherChar.paths[i]
except IndexError:
#import pdb
#pdb.set_trace()
#print self
raise CharParseError
if not path1.fuzzyMatch(path2):
ok = 0
break
if not ok:
# See if we can match at any other positions.
numCompositePaths = len(self.paths)
for pi in range(numCompositePaths):
if pi == pathIndex:
continue
ok = 1
for i in range(numPaths):
pn = pi + i
if pn >= numCompositePaths:
break
try:
path1 = self.paths[pi + i]
path2 = otherChar.paths[i]
except IndexError:
#import pdb
#pdb.set_trace()
#print self
raise CharParseError
if not path1.fuzzyMatch(path2):
ok = 0
break
if ok: # We matched all the base glyphs paths in the exception glyph!
pathIndex = pi
break
if not ok:
print "\tError: Can't find base glyph in composite glyph. Composite glyph %s, base glyph %s. " % (self.glyphName, otherChar.glyphName)
return ok, pathIndex
def replace(self, otherChar, pathIndex, stdChar):
if not self.parsed:
self.parse()
if not otherChar.parsed:
otherChar.parse()
if not stdChar.parsed:
stdChar.parse()
if self.seac:
return
newChar = copy.deepcopy(otherChar)
# Note that the exception Glyph and can have a different number of paths than the stdChar.
numPaths = len(stdChar.paths) # target paths in the curent glyph
numOtherPaths = len(newChar.paths) # paths which will replace the target paths.
if numPaths == numOtherPaths:
for i in range(numPaths):
self.paths[pathIndex + i].replacePath(newChar.paths[i])
elif numPaths > numOtherPaths:
for i in range(numOtherPaths):
self.paths[pathIndex + i].replacePath(newChar.paths[i])
i += 1
del_i = pathIndex + i
while i < numPaths:
del self.paths[del_i]
i += 1
else: # numPaths < numOtherPaths
for i in range(numPaths):
self.paths[pathIndex + i].replacePath(newChar.paths[i])
i += 1
while i < numOtherPaths:
self.paths.insert(pathIndex + i, newChar.paths[i])
i += 1
# we want to line up not the start points of the std and other chars,
# rather the origins. See if there is a delta that needs to be applied between the start point
# of the stdChar and the otherChar.
diffStart = pointDiff(newChar.paths[0].start, stdChar.paths[0].start)
#if diffStart != [0,0]:
# print "Nonzero diffStart", diffStart, self.glyphName
firstPath = self.paths[pathIndex]
firstPath.start = pointAdd(firstPath.start, diffStart)
self.paths[pathIndex].adjustMoveTo() # fixes moveto coords to go from origin to start.
componentShift = pointDiff(firstPath.start, newChar.paths[0].start)
lastComponentPath = self.paths[pathIndex + numOtherPaths-1]
lastOtherPath = newChar.paths[numOtherPaths-1]
if numOtherPaths > 1: # don't need to fix the start of the first == last component again.
lastComponentPath.start = pointAdd(lastOtherPath.start, componentShift)
lastComponentPath.end = pointAdd(lastOtherPath.end, componentShift)
if len(self.paths) > pathIndex + numOtherPaths:
nextPath = self.paths[pathIndex + numOtherPaths]
nextPath.origin = lastComponentPath.end
nextPath.adjustMoveTo()
list = [self.prefix]
for i in range(len(self.paths)):
list.append(self.paths[i].getString())
list.append(self.suffix)
self.charstring = " ".join(list)
return
def replaceAllPaths(self, otherChar):
self.paths = copy.deepcopy(otherChar.paths)
self.charstring = copy.copy(otherChar.charstring)
def scale(self, scale):
if not self.parsed:
self.parse()
scaledChar = copy.deepcopy(self)
if (scale[0] != 1.0) or (scale[1] != 1.0):
print "Help! I need to scale exception %s." % (self.glyphName)
#import pdb
#pdb.set_trace()
raise CharParseError
return scaledChar
def getExceptionEntries(charDict, exceptionSuffixList):
glyphList = charDict.keys()
exceptionList = []
seenExceptionDict = {}
for name in glyphList:
for suffix in exceptionSuffixList:
if name.endswith(suffix):
stdName = name[:-len(suffix)]
if charDict.has_key(stdName):
exceptionList.append( [stdName, name])
else:
print"Error: standard glyph name %s is not in font (derived from exception glyph name %s.)" % (stdName, name)
seenExceptionDict[suffix] = 1
break
# Check that all suffixes matched something.
for suffix in exceptionSuffixList:
try:
seen = seenExceptionDict[suffix]
except KeyError:
logMsg.log("Error. There are no glyphs with the exception glyph suffx", suffix)