summaryrefslogtreecommitdiff
path: root/misc/tools
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2018-09-06 04:45:23 +0300
committerRasmus Andersson <rasmus@notion.se>2018-09-06 04:45:23 +0300
commitfc1ac6b4e28260be8ee3e6f8b3bec37678cca594 (patch)
treeb9c0bcfb741b5149bd7ccb1a59499a247aa95fa8 /misc/tools
parent3a38dd4737471501b136b76255ef13854c4ba5d0 (diff)
downloadinter-fc1ac6b4e28260be8ee3e6f8b3bec37678cca594.tar.xz
housekeeping of misc folder
Diffstat (limited to 'misc/tools')
-rw-r--r--misc/tools/fontsample/.gitignore2
-rw-r--r--misc/tools/fontsample/Makefile44
-rw-r--r--misc/tools/fontsample/README.md21
-rwxr-xr-xmisc/tools/fontsample/fontsamplebin0 -> 24500 bytes
-rw-r--r--misc/tools/fontsample/fontsample.mm270
-rw-r--r--misc/tools/kerndiff/README.md28
-rw-r--r--misc/tools/kerndiff/getKerningPairsFromOTF.py323
-rw-r--r--misc/tools/kerndiff/getKerningPairsFromUFO.py144
-rwxr-xr-xmisc/tools/kerndiff/kerndiff.sh96
9 files changed, 928 insertions, 0 deletions
diff --git a/misc/tools/fontsample/.gitignore b/misc/tools/fontsample/.gitignore
new file mode 100644
index 000000000..6142305dc
--- /dev/null
+++ b/misc/tools/fontsample/.gitignore
@@ -0,0 +1,2 @@
+*.o
+*.d
diff --git a/misc/tools/fontsample/Makefile b/misc/tools/fontsample/Makefile
new file mode 100644
index 000000000..063d7262d
--- /dev/null
+++ b/misc/tools/fontsample/Makefile
@@ -0,0 +1,44 @@
+sources = fontsample.mm
+
+CC = clang
+CXX = clang
+LD = clang
+
+XFLAGS := -Wall -g -fno-common
+CFLAGS += -std=c11
+CXXFLAGS += -std=c++11 -stdlib=libc++ -fno-rtti -fno-exceptions
+LDFLAGS += -lc++
+
+#flags release and debug targets (e.g. make DEBUG=1)
+ifeq ($(strip $(DEBUG)),1)
+ XFLAGS += -O0
+# else
+# XFLAGS += -Os -DNDEBUG
+endif
+
+libs := -lobjc
+frameworks := -framework Foundation \
+ -framework CoreText \
+ -framework CoreServices \
+ -framework CoreGraphics \
+ -framework ImageIO
+
+c_flags = $(CFLAGS) $(XFLAGS) -MMD -fobjc-arc
+cxx_flags = $(CXXFLAGS) $(XFLAGS) -MMD -fobjc-arc
+ld_flags = $(LDFLAGS) $(libs) $(frameworks)
+
+objects := $(sources:%.c=%.o)
+objects := $(objects:%.cc=%.o)
+objects := $(objects:%.mm=%.o)
+
+fontsample: $(objects)
+ $(LD) $(ld_flags) -o $@ $^
+
+%.o: %.mm
+ $(CXX) $(cxx_flags) -c $<
+
+clean:
+ rm -f *.o
+
+all: fontsample
+.PHONY: all clean
diff --git a/misc/tools/fontsample/README.md b/misc/tools/fontsample/README.md
new file mode 100644
index 000000000..357b93e6f
--- /dev/null
+++ b/misc/tools/fontsample/README.md
@@ -0,0 +1,21 @@
+# fontsample
+
+A macOS-specific program for generating a PDF with text sample of a
+specific font file.
+
+```
+$ make
+$ ./fontsample -h
+usage: ./fontsample [options] <fontfile>
+
+options:
+ -h, -help Show usage and exit.
+ -z, -size <size> Font size to render. Defaults to 96.
+ -t, -text <text> Text line to render. Defaults to "Rags78 **A**".
+ -o <file> Write output to <file> instead of default filename.
+ Defaults to <fontfile>.pdf. If the provided filename
+ ends with ".png" a PNG is written instead of a PDF.
+
+<fontfile>
+ Any font file that macOS can read.
+```
diff --git a/misc/tools/fontsample/fontsample b/misc/tools/fontsample/fontsample
new file mode 100755
index 000000000..d405bf511
--- /dev/null
+++ b/misc/tools/fontsample/fontsample
Binary files differ
diff --git a/misc/tools/fontsample/fontsample.mm b/misc/tools/fontsample/fontsample.mm
new file mode 100644
index 000000000..2534603cd
--- /dev/null
+++ b/misc/tools/fontsample/fontsample.mm
@@ -0,0 +1,270 @@
+#import <Foundation/Foundation.h>
+#import <CoreGraphics/CoreGraphics.h>
+#import <CoreText/CoreText.h>
+#import <ImageIO/ImageIO.h>
+
+
+static const char* prog = "?";
+
+static struct Options {
+ NSString* output = nil;
+ CGFloat size = 96;
+ NSString* text = @"Rags78 **A**";
+}options{};
+
+static const char usagetemplate[] = ""
+"usage: %s [options] <fontfile>\n"
+"\n"
+"options:\n"
+" -h, -help Show usage and exit.\n"
+" -z, -size <size> Font size to render. Defaults to %g.\n"
+" -t, -text <text> Text line to render. Defaults to \"%s\".\n"
+" -o <file> Write output to <file> instead of default filename.\n"
+" Defaults to <fontfile>.pdf. If the provided filename\n"
+" ends with \".png\" a PNG is written instead of a PDF.\n"
+"\n"
+"<fontfile>\n"
+" Any font file that macOS can read.\n"
+;
+
+void usage() {
+ printf(usagetemplate, prog, options.size, options.text.UTF8String);
+}
+
+
+// must call CFRelease on the returned pointer
+CTFontRef loadFont(NSString* filename, CGFloat size) {
+ CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (CFStringRef)filename, kCFURLPOSIXPathStyle, false);
+
+ CGDataProviderRef dataProvider = CGDataProviderCreateWithURL(url);
+ if (!dataProvider) {
+ fprintf(stderr, "%s: failed to read file %s\n", prog, filename.UTF8String);
+ exit(1);
+ }
+
+ CGFontRef cgf = CGFontCreateWithDataProvider(dataProvider);
+ if (!cgf) {
+ fprintf(stderr, "%s: failed to parse font %s\n", prog, filename.UTF8String);
+ exit(1);
+ }
+
+ CTFontRef ctf = CTFontCreateWithGraphicsFont(cgf, size, nil, nil);
+ if (!ctf) {
+ fprintf(stderr, "%s: CTFontCreateWithGraphicsFont failed\n", prog);
+ exit(1);
+ }
+
+ CFRelease(cgf);
+ CFRelease(dataProvider);
+ CFRelease(url);
+ return ctf;
+}
+
+
+CTLineRef createTextLine(CTFontRef font, NSString* text) {
+ NSDictionary* attr = @{ (NSString*)kCTFontAttributeName: (__bridge id)font };
+ return CTLineCreateWithAttributedString((CFAttributedStringRef)[[NSAttributedString alloc] initWithString:text attributes:attr]);
+}
+
+
+void draw(CGContextRef ctx,
+ CTLineRef textLine,
+ CGFloat width,
+ CGFloat height,
+ CGFloat descent)
+{
+ // white background
+ CGContextSetRGBFillColor(ctx, 1.0, 1.0, 1.0, 1.0);
+ CGContextFillRect(ctx, {{0,0},{width,height}});
+
+ // draw text
+ CGContextSetTextPosition(ctx, 0, descent);
+ CTLineDraw(textLine, ctx);
+}
+
+
+void makePDF(CTLineRef textLine,
+ CGFloat width,
+ CGFloat height,
+ CGFloat descent,
+ NSString* filename)
+{
+ CFMutableDataRef consumerData = CFDataCreateMutable(kCFAllocatorDefault, 0);
+ CGDataConsumerRef contextConsumer = CGDataConsumerCreateWithCFData(consumerData);
+ assert(contextConsumer);
+ const CGRect mediaBox{{0,0},{width,height}};
+ auto ctx = CGPDFContextCreate(contextConsumer, &mediaBox, nil);
+ assert(ctx);
+ CGPDFContextBeginPage(ctx, nil);
+
+ draw(ctx, textLine, width, height, descent);
+
+ // CGContextDrawPDFPage(ctx, page);
+ CGPDFContextEndPage(ctx);
+ CGPDFContextClose(ctx);
+ CGContextRelease(ctx);
+ [(__bridge NSData*)consumerData writeToFile:filename atomically:NO];
+}
+
+
+BOOL writePNG(CGImageRef image, NSString *filename) {
+ CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:filename];
+ CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL);
+ if (!destination) {
+ NSLog(@"Failed to create CGImageDestination for %@", filename);
+ return NO;
+ }
+
+ CGImageDestinationAddImage(destination, image, nil);
+
+ if (!CGImageDestinationFinalize(destination)) {
+ NSLog(@"Failed to write image to %@", filename);
+ CFRelease(destination);
+ return NO;
+ }
+
+ CFRelease(destination);
+ return YES;
+}
+
+
+void makePNG(CTLineRef textLine,
+ CGFloat width,
+ CGFloat height,
+ CGFloat descent,
+ NSString* filename)
+{
+ size_t widthz = (size_t)ceilf(width);
+ size_t heightz = (size_t)ceilf(height);
+
+ void* data = malloc(widthz * heightz * 4);
+
+ // Create the context and fill it with white background
+ CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
+ CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast;
+ CGContextRef ctx = CGBitmapContextCreate(data, widthz, heightz, 8, widthz*4, space, bitmapInfo);
+ CGColorSpaceRelease(space);
+
+ draw(ctx, textLine, (CGFloat)widthz, (CGFloat)heightz, descent);
+
+ CGImageRef imageRef = CGBitmapContextCreateImage(ctx);
+ writePNG(imageRef, filename);
+
+ free(data);
+ CGImageRelease(imageRef);
+ CGContextRelease(ctx);
+}
+
+
+void pdfmake(NSString* fontfile) {
+ NSString* text = @"Rags78 **A**";
+
+ NSString* outfile = options.output;
+ if (outfile == nil) {
+ // default to fontfile.pdf
+ outfile = [fontfile.stringByDeletingPathExtension stringByAppendingPathExtension:@"pdf"];
+ }
+
+ // Create an attributed string with string and font information
+ CTFontRef font = loadFont(fontfile, options.size);
+
+ CTLineRef textLine = createTextLine(font, text);
+ if (!textLine) {
+ fprintf(stderr, "%s: invalid sample text\n", prog);
+ exit(1);
+ }
+
+ // get font metrics
+ CGFloat ascent, descent, leading;
+ CGFloat width = CTLineGetTypographicBounds(textLine, &ascent, &descent, &leading);
+ CGFloat height = ascent + descent;
+
+ printf("write %s\n", outfile.UTF8String);
+ if ([outfile.pathExtension.lowercaseString isEqualToString:@"png"]) {
+ makePNG(textLine, width, height, descent, outfile);
+ } else {
+ makePDF(textLine, width, height, descent, outfile);
+ }
+
+ CFRelease(textLine);
+ CFRelease(font);
+}
+
+
+void badarg(const char* msg, const char* arg) {
+ fprintf(stderr, "%s: %s %s\n", prog, msg, arg);
+ usage();
+ exit(1);
+}
+
+
+const char* getargval(const char* arg, int argi, int argc, const char * argv[]) {
+ int i = argi + 1;
+ if (i == argc || strlen(argv[i]) == 0 || argv[i][0] == '-') {
+ fprintf(stderr, "%s: missing value for argument %s\n", prog, arg);
+ usage();
+ exit(1);
+ }
+ return argv[i];
+}
+
+
+NSMutableArray<NSString*>* parseargs(int argc, const char * argv[]) {
+ auto args = [NSMutableArray<NSString*> new];
+ if (argc == 0) {
+ fprintf(stderr, "invalid arguments\n");
+ exit(0);
+ }
+ prog = argv[0];
+ for (int i = 1; i < argc; i++) {
+ auto arg = argv[i];
+ if (strlen(arg) > 1 && arg[0] == '-') {
+ if (strcmp(arg, "-h") == 0 || strcmp(arg, "-help") == 0) {
+ usage();
+ exit(0);
+ }
+ if (strcmp(arg, "-o") == 0) {
+ auto val = getargval(arg, i++, argc, argv);
+ options.output = [NSString stringWithUTF8String:val];
+
+ } else if (strcmp(arg, "-z") == 0 || strcmp(arg, "-size") == 0) {
+ auto val = getargval(arg, i++, argc, argv);
+ auto f = atof(val);
+ if (isnan(f) || f < 1) {
+ badarg("invalid number", val);
+ }
+ options.size = (CGFloat)f;
+
+ } else if (strcmp(arg, "-t") == 0 || strcmp(arg, "-text") == 0) {
+ auto val = getargval(arg, i++, argc, argv);
+ options.text = [NSString stringWithUTF8String:val];
+
+ } else {
+ badarg("unknown flag", arg);
+ }
+ continue;
+ }
+ [args addObject:[NSString stringWithUTF8String:arg]];
+ }
+ return args;
+}
+
+
+int main(int argc, const char * argv[]) {
+ @autoreleasepool {
+ auto fontfiles = parseargs(argc, argv);
+
+ if (fontfiles.count < 1) {
+ fprintf(stderr, "%s: missing <fontfile>\n", prog);
+ usage();
+ return 1;
+ } else if (fontfiles.count > 1) {
+ fprintf(stderr, "%s: extraneous argument after <fontfile>\n", prog);
+ usage();
+ return 1;
+ }
+
+ pdfmake(fontfiles[0]);
+ }
+ return 0;
+}
diff --git a/misc/tools/kerndiff/README.md b/misc/tools/kerndiff/README.md
new file mode 100644
index 000000000..61a98ad4c
--- /dev/null
+++ b/misc/tools/kerndiff/README.md
@@ -0,0 +1,28 @@
+# kerndiff
+
+Shows unified diff for kerning pairs in two font files.
+
+Accepts OTF, TTF and UFO fonts.
+
+Synopsis:
+
+```
+kerndiff.sh <font1> <font2>
+```
+
+Example:
+
+```
+kerndiff.sh Inter-UI-Regular-v2.4.otf src/Inter-UI-Regular.ufo
+--- Inter-UI-Regular-v2.4.otf 2018-08-30 19:16:47.000000000 -0700
++++ Inter-UI-Regular.ufo 2018-08-30 19:16:47.000000000 -0700
+@@ -35126,7 +35081,6 @@
+ /s /Ydieresis -149
+ /s /Ygrave -149
+ /s /Yhook -149
+-/s /a 0
++/s /a 8
+ /s /afii10026 -47
+ /s /b -47
+ /s /dagger -29
+```
diff --git a/misc/tools/kerndiff/getKerningPairsFromOTF.py b/misc/tools/kerndiff/getKerningPairsFromOTF.py
new file mode 100644
index 000000000..889a36813
--- /dev/null
+++ b/misc/tools/kerndiff/getKerningPairsFromOTF.py
@@ -0,0 +1,323 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2013-2016 Adobe Systems Incorporated. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE. import sys import os import itertools
+
+from __future__ import print_function
+import os
+import sys
+from fontTools import ttLib
+
+__doc__ = '''\
+
+Prints all possible kerning pairs within font.
+Supports RTL kerning.
+
+Usage:
+------
+python getKerningPairsFromOTF.py <path to font file>
+
+'''
+
+kKernFeatureTag = 'kern'
+kGPOStableName = 'GPOS'
+finalList = []
+
+
+class myLeftClass:
+ def __init__(self):
+ self.glyphs = []
+ self.class1Record = 0
+
+
+class myRightClass:
+ def __init__(self):
+ self.glyphs = []
+ self.class2Record = 0
+
+
+def collectUniqueKernLookupListIndexes(featureRecord):
+ uniqueKernLookupIndexList = []
+ for featRecItem in featureRecord:
+ # print(featRecItem.FeatureTag)
+ # GPOS feature tags (e.g. kern, mark, mkmk, size) of each ScriptRecord
+ if featRecItem.FeatureTag == kKernFeatureTag:
+ feature = featRecItem.Feature
+
+ for featLookupItem in feature.LookupListIndex:
+ if featLookupItem not in uniqueKernLookupIndexList:
+ uniqueKernLookupIndexList.append(featLookupItem)
+
+ return uniqueKernLookupIndexList
+
+
+class OTFKernReader(object):
+
+ def __init__(self, fontPath):
+ self.font = ttLib.TTFont(fontPath)
+ self.kerningPairs = {}
+ self.singlePairs = {}
+ self.classPairs = {}
+ self.pairPosList = []
+ self.allLeftClasses = {}
+ self.allRightClasses = {}
+
+ if kGPOStableName not in self.font:
+ print("The font has no %s table" % kGPOStableName, file=sys.stderr)
+ self.goodbye()
+
+ else:
+ self.analyzeFont()
+ self.findKerningLookups()
+ self.getPairPos()
+ self.getSinglePairs()
+ self.getClassPairs()
+
+ def goodbye(self):
+ print('The fun ends here.', file=sys.stderr)
+ return
+
+ def analyzeFont(self):
+ self.gposTable = self.font[kGPOStableName].table
+
+ 'ScriptList:'
+ self.scriptList = self.gposTable.ScriptList
+ 'FeatureList:'
+ self.featureList = self.gposTable.FeatureList
+
+ self.featureCount = self.featureList.FeatureCount
+ self.featureRecord = self.featureList.FeatureRecord
+
+ self.uniqueKernLookupIndexList = collectUniqueKernLookupListIndexes(self.featureRecord)
+
+ def findKerningLookups(self):
+ if not len(self.uniqueKernLookupIndexList):
+ print("The font has no %s feature." % kKernFeatureTag, file=sys.stderr)
+ self.goodbye()
+
+ 'LookupList:'
+ self.lookupList = self.gposTable.LookupList
+ self.lookups = []
+ for kernLookupIndex in sorted(self.uniqueKernLookupIndexList):
+ lookup = self.lookupList.Lookup[kernLookupIndex]
+
+ # Confirm this is a GPOS LookupType 2; or
+ # using an extension table (GPOS LookupType 9):
+
+ '''
+ Lookup types:
+ 1 Single adjustment Adjust position of a single glyph
+ 2 Pair adjustment Adjust position of a pair of glyphs
+ 3 Cursive attachment Attach cursive glyphs
+ 4 MarkToBase attachment Attach a combining mark to a base glyph
+ 5 MarkToLigature attachment Attach a combining mark to a ligature
+ 6 MarkToMark attachment Attach a combining mark to another mark
+ 7 Context positioning Position one or more glyphs in context
+ 8 Chained Context positioning Position one or more glyphs in chained context
+ 9 Extension positioning Extension mechanism for other positionings
+ 10+ Reserved for future use
+ '''
+
+ if lookup.LookupType not in [2, 9]:
+ print('''
+ Info: GPOS LookupType %s found.
+ This type is neither a pair adjustment positioning lookup (GPOS LookupType 2),
+ nor using an extension table (GPOS LookupType 9), which are the only ones supported.
+ ''' % lookup.LookupType, file=sys.stderr)
+ continue
+ self.lookups.append(lookup)
+
+
+ def getPairPos(self):
+ for lookup in self.lookups:
+ for subtableItem in lookup.SubTable:
+
+ if subtableItem.LookupType == 9: # extension table
+ if subtableItem.ExtensionLookupType == 8: # contextual
+ print('Contextual Kerning not (yet?) supported.', file=sys.stderr)
+ continue
+ elif subtableItem.ExtensionLookupType == 2:
+ subtableItem = subtableItem.ExtSubTable
+
+
+ # if subtableItem.Coverage.Format not in [1, 2]: # previous fontTools
+ if subtableItem.Format not in [1, 2]:
+ print("WARNING: Coverage format %d is not yet supported." % subtableItem.Coverage.Format, file=sys.stderr)
+
+ if subtableItem.ValueFormat1 not in [0, 4, 5]:
+ print("WARNING: ValueFormat1 format %d is not yet supported." % subtableItem.ValueFormat1, file=sys.stderr)
+
+ if subtableItem.ValueFormat2 not in [0]:
+ print("WARNING: ValueFormat2 format %d is not yet supported." % subtableItem.ValueFormat2, file=sys.stderr)
+
+
+ self.pairPosList.append(subtableItem)
+
+ # Each glyph in this list will have a corresponding PairSet
+ # which will contain all the second glyphs and the kerning
+ # value in the form of PairValueRecord(s)
+ # self.firstGlyphsList.extend(subtableItem.Coverage.glyphs)
+
+
+ def getSinglePairs(self):
+ for pairPos in self.pairPosList:
+ if pairPos.Format == 1:
+ # single pair adjustment
+
+ firstGlyphsList = pairPos.Coverage.glyphs
+
+ # This iteration is done by index so we have a way
+ # to reference the firstGlyphsList:
+ for pairSetIndex, pairSetInstance in enumerate(pairPos.PairSet):
+ for pairValueRecordItem in pairPos.PairSet[pairSetIndex].PairValueRecord:
+ secondGlyph = pairValueRecordItem.SecondGlyph
+ valueFormat = pairPos.ValueFormat1
+
+ if valueFormat == 5: # RTL kerning
+ kernValue = "<%d 0 %d 0>" % (
+ pairValueRecordItem.Value1.XPlacement,
+ pairValueRecordItem.Value1.XAdvance)
+ elif valueFormat == 0: # RTL pair with value <0 0 0 0>
+ kernValue = "<0 0 0 0>"
+ elif valueFormat == 4: # LTR kerning
+ kernValue = pairValueRecordItem.Value1.XAdvance
+ else:
+ print("\tValueFormat1 = %d" % valueFormat, file=sys.stdout)
+ continue # skip the rest
+
+ self.kerningPairs[(firstGlyphsList[pairSetIndex], secondGlyph)] = kernValue
+ self.singlePairs[(firstGlyphsList[pairSetIndex], secondGlyph)] = kernValue
+
+ def getClassPairs(self):
+ for loop, pairPos in enumerate(self.pairPosList):
+ if pairPos.Format == 2:
+
+ leftClasses = {}
+ rightClasses = {}
+
+ # Find left class with the Class1Record index="0".
+ # This first class is mixed into the "Coverage" table
+ # (e.g. all left glyphs) and has no class="X" property
+ # that is why we have to find the glyphs in that way.
+
+ lg0 = myLeftClass()
+
+ # list of all glyphs kerned to the left of a pair:
+ allLeftGlyphs = pairPos.Coverage.glyphs
+ # list of all glyphs contained within left-sided kerning classes:
+ # allLeftClassGlyphs = pairPos.ClassDef1.classDefs.keys()
+
+ singleGlyphs = []
+ classGlyphs = []
+
+ for gName, classID in pairPos.ClassDef1.classDefs.items():
+ if classID == 0:
+ singleGlyphs.append(gName)
+ else:
+ classGlyphs.append(gName)
+
+ # lg0.glyphs = list(set(allLeftGlyphs) - set(allLeftClassGlyphs)) # coverage glyphs minus glyphs in a class (including class 0)
+ lg0.glyphs = list(set(allLeftGlyphs) - set(classGlyphs)) # coverage glyphs minus glyphs in real class (without class 0)
+
+ lg0.glyphs.sort()
+ leftClasses[lg0.class1Record] = lg0
+ className = "class_%s_%s" % (loop, lg0.class1Record)
+ self.allLeftClasses[className] = lg0.glyphs
+
+ # Find all the remaining left classes:
+ for leftGlyph in pairPos.ClassDef1.classDefs:
+ class1Record = pairPos.ClassDef1.classDefs[leftGlyph]
+
+ if class1Record != 0: # this was the crucial line.
+ lg = myLeftClass()
+ lg.class1Record = class1Record
+ leftClasses.setdefault(class1Record, lg).glyphs.append(leftGlyph)
+ self.allLeftClasses.setdefault("class_%s_%s" % (loop, lg.class1Record), lg.glyphs)
+
+ # Same for the right classes:
+ for rightGlyph in pairPos.ClassDef2.classDefs:
+ class2Record = pairPos.ClassDef2.classDefs[rightGlyph]
+ rg = myRightClass()
+ rg.class2Record = class2Record
+ rightClasses.setdefault(class2Record, rg).glyphs.append(rightGlyph)
+ self.allRightClasses.setdefault("class_%s_%s" % (loop, rg.class2Record), rg.glyphs)
+
+ for record_l in leftClasses:
+ for record_r in rightClasses:
+ if pairPos.Class1Record[record_l].Class2Record[record_r]:
+ valueFormat = pairPos.ValueFormat1
+
+ if valueFormat in [4, 5]:
+ kernValue = pairPos.Class1Record[record_l].Class2Record[record_r].Value1.XAdvance
+ elif valueFormat == 0:
+ # valueFormat zero is caused by a value of <0 0 0 0> on a class-class pair; skip these
+ continue
+ else:
+ print("\tValueFormat1 = %d" % valueFormat, file=sys.stdout)
+ continue # skip the rest
+
+ if kernValue != 0:
+ leftClassName = 'class_%s_%s' % (loop, leftClasses[record_l].class1Record)
+ rightClassName = 'class_%s_%s' % (loop, rightClasses[record_r].class2Record)
+
+ self.classPairs[(leftClassName, rightClassName)] = kernValue
+
+ for l in leftClasses[record_l].glyphs:
+ for r in rightClasses[record_r].glyphs:
+ if (l, r) in self.kerningPairs:
+ # if the kerning pair has already been assigned in pair-to-pair kerning
+ continue
+ else:
+ if valueFormat == 5: # RTL kerning
+ kernValue = "<%d 0 %d 0>" % (pairPos.Class1Record[record_l].Class2Record[record_r].Value1.XPlacement, pairPos.Class1Record[record_l].Class2Record[record_r].Value1.XAdvance)
+
+
+ self.kerningPairs[(l, r)] = kernValue
+
+ else:
+ print('ERROR', file=sys.stderr)
+
+
+if __name__ == "__main__":
+
+ if len(sys.argv) == 2:
+ assumedFontPath = sys.argv[1]
+ if os.path.exists(assumedFontPath) and os.path.splitext(assumedFontPath)[1].lower() in ['.otf', '.ttf']:
+ fontPath = sys.argv[1]
+ f = OTFKernReader(fontPath)
+
+ finalList = []
+ for pair, value in f.kerningPairs.items():
+ finalList.append('/%s /%s %s' % ( pair[0], pair[1], value ))
+
+ finalList.sort()
+
+ output = '\n'.join(sorted(finalList))
+ print(output, file=sys.stdout)
+
+ # print('\nTotal number of kerning pairs:', file=sys.stdout)
+ # print(len(f.kerningPairs), file=sys.stdout)
+ # for i in sorted(f.allLeftClasses):
+ # print(i, f.allLeftClasses[i], file=sys.stdout)
+
+ else:
+ print('That is not a valid font.', file=sys.stderr)
+ else:
+ print('Please provide a font.', file=sys.stderr)
diff --git a/misc/tools/kerndiff/getKerningPairsFromUFO.py b/misc/tools/kerndiff/getKerningPairsFromUFO.py
new file mode 100644
index 000000000..47e986006
--- /dev/null
+++ b/misc/tools/kerndiff/getKerningPairsFromUFO.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+# coding: utf-8
+#
+# Copyright (c) 2013-2016 Adobe Systems Incorporated. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import sys, os, itertools
+
+
+class UFOkernReader(object):
+
+ def __init__(self, font, includeZero=False):
+ self.f = font
+ self.group_group_pairs = {}
+ self.group_glyph_pairs = {}
+ self.glyph_group_pairs = {}
+ self.glyph_glyph_pairs = {}
+
+ self.allKerningPairs = self.makePairDicts(includeZero)
+ self.output = self.makeOutput(self.allKerningPairs)
+
+ self.totalKerning = sum(self.allKerningPairs.values())
+ self.absoluteKerning = sum([abs(value) for value in self.allKerningPairs.values()])
+
+ def makeOutput(self, kerningDict):
+ output = []
+ for (left, right), value in kerningDict.items():
+ output.append('/%s /%s %s' % (left, right, value))
+ output.sort()
+ return output
+
+ def allCombinations(self, left, right):
+ leftGlyphs = self.f.groups.get(left, [left])
+ rightGlyphs = self.f.groups.get(right, [right])
+ combinations = list(itertools.product(leftGlyphs, rightGlyphs))
+ return combinations
+
+ def makePairDicts(self, includeZero):
+ kerningPairs = {}
+
+ for (left, right), value in self.f.kerning.items():
+
+ if '@' in left and '@' in right:
+ # group-to-group-pair
+ for combo in self.allCombinations(left, right):
+ self.group_group_pairs[combo] = value
+
+ elif '@' in left and '@' not in right:
+ # group-to-glyph-pair
+ for combo in self.allCombinations(left, right):
+ self.group_glyph_pairs[combo] = value
+
+ elif '@' not in left and '@' in right:
+ # glyph-to-group-pair
+ for combo in self.allCombinations(left, right):
+ self.glyph_group_pairs[combo] = value
+
+ else:
+ # glyph-to-glyph-pair a.k.a. single pair
+ self.glyph_glyph_pairs[(left, right)] = value
+
+ # The updates occur from the most general pairs to the most specific.
+ # This means that any given class kerning values are overwritten with
+ # the intended exceptions.
+ kerningPairs.update(self.group_group_pairs)
+ kerningPairs.update(self.group_glyph_pairs)
+ kerningPairs.update(self.glyph_group_pairs)
+ kerningPairs.update(self.glyph_glyph_pairs)
+
+ if includeZero is False:
+ # delete any kerning values == 0.
+ # This cannot be done in the previous loop, since exceptions
+ # might set a previously established kerning pair to be 0.
+ cleanKerningPairs = dict(kerningPairs)
+ for pair in kerningPairs:
+ if kerningPairs[pair] == 0:
+ del cleanKerningPairs[pair]
+ return cleanKerningPairs
+
+ else:
+ return kerningPairs
+
+
+def run(font):
+ ukr = UFOkernReader(font, includeZero=True)
+ print('\n'.join(sorted(ukr.output)))
+ # scrap = os.popen('pbcopy', 'w')
+ # output = '\n'.join(ukr.output)
+ # scrap.write(output)
+ # scrap.close()
+
+ if inRF:
+ pass
+ # print('Total length of kerning:', ukr.totalKerning)
+
+ # if inCL:
+ # print('\n'.join(ukr.output), '\n')
+
+ # print('Total amount of kerning pairs:', len(ukr.output))
+ # print('List of kerning pairs copied to clipboard.')
+
+
+if __name__ == '__main__':
+ inRF = False
+ inCL = False
+
+ try:
+ import mojo
+ inRF = True
+ f = CurrentFont()
+ if f:
+ run(f)
+ else:
+ print(u'You need to open a font first. \U0001F625')
+
+ except ImportError:
+ try:
+ import defcon
+ inCL = True
+ path = os.path.normpath(sys.argv[-1])
+ if os.path.splitext(path)[-1] in ['.ufo', '.UFO']:
+ f = defcon.Font(path)
+ run(f)
+ else:
+ print('No UFO file given.')
+ except ImportError:
+ print(u'You don’t have Defcon installed. \U0001F625')
diff --git a/misc/tools/kerndiff/kerndiff.sh b/misc/tools/kerndiff/kerndiff.sh
new file mode 100755
index 000000000..64ce8c2f1
--- /dev/null
+++ b/misc/tools/kerndiff/kerndiff.sh
@@ -0,0 +1,96 @@
+#!/bin/bash -e
+
+function usage() {
+ cat 1>&2 <<__END
+usage: $0 [options] <font1> <font2>
+options:
+ -h, --help Show usage and exit
+ Rest of options are forwarded to the "diff" program
+__END
+}
+
+diffargs=()
+file1=
+file2=
+
+while [ "$1" != "" ]; do
+ PARAM=`echo $1 | awk -F= '{print $1}'`
+ VALUE=`echo $1 | awk -F= '{print $2}'`
+ case $PARAM in
+ -h | -help | --help)
+ usage
+ exit
+ ;;
+ -*)
+ diffargs[${#diffargs[*]}]=$1
+ ;;
+ *)
+ if [[ "$file1" == "" ]]; then
+ file1=$PARAM
+ elif [[ "$file2" == "" ]]; then
+ file2=$PARAM
+ else
+ echo "Too many files" 1>&2
+ exit 1
+ fi
+ ;;
+ esac
+ shift
+done
+
+if [[ "$file1" == "" ]] && [[ "$file2" == "" ]]; then
+ usage
+ exit 1
+elif [[ "$file1" == "" ]] || [[ "$file2" == "" ]]; then
+ echo "Not enough files" 1>&2
+ exit 1
+fi
+
+tmpdir=$TMPDIR
+if [[ "$tmpdir" == "" ]]; then
+ tmpdir=/tmp
+fi
+tmpdir=$tmpdir/kerndiff-tmp
+mkdir -p "$tmpdir"
+
+file1x="$(basename "$file1")"
+file2x="$(basename "$file2")"
+
+pushd "$(dirname "$0")" >/dev/null
+KERNDIFF_DIR=$PWD
+popd >/dev/null
+
+case $file1 in
+ *.otf)
+ python "$KERNDIFF_DIR/getKerningPairsFromOTF.py" "$file1" \
+ > "$tmpdir/$file1x"
+ ;;
+ *.ufo)
+ python "$KERNDIFF_DIR/getKerningPairsFromUFO.py" "$file1" \
+ > "$tmpdir/$file1x"
+ ;;
+ *)
+ echo "unsupported file format: $file1"
+ exit 1
+ ;;
+esac
+
+case $file2 in
+ *.otf)
+ python "$KERNDIFF_DIR/getKerningPairsFromOTF.py" "$file2" \
+ > "$tmpdir/$file2x"
+ ;;
+ *.ufo)
+ python "$KERNDIFF_DIR/getKerningPairsFromUFO.py" "$file2" \
+ > "$tmpdir/$file2x"
+ ;;
+ *)
+ echo "unsupported file format: $file2"
+ exit 1
+ ;;
+esac
+
+pushd $tmpdir >/dev/null
+diff -u "${diffargs[@]}" "$file1x" "$file2x"
+popd >/dev/null
+rm -rf "$tmpdir"