From fc1ac6b4e28260be8ee3e6f8b3bec37678cca594 Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Wed, 5 Sep 2018 18:45:23 -0700 Subject: housekeeping of misc folder --- misc/fontsample/.gitignore | 2 - misc/fontsample/Makefile | 44 ---- misc/fontsample/README.md | 21 -- misc/fontsample/fontsample | Bin 24500 -> 0 bytes misc/fontsample/fontsample.mm | 270 --------------------- misc/kerndiff/README.md | 28 --- misc/kerndiff/getKerningPairsFromOTF.py | 323 -------------------------- misc/kerndiff/getKerningPairsFromUFO.py | 144 ------------ misc/kerndiff/kerndiff.sh | 96 -------- misc/tools/fontsample/.gitignore | 2 + misc/tools/fontsample/Makefile | 44 ++++ misc/tools/fontsample/README.md | 21 ++ misc/tools/fontsample/fontsample | Bin 0 -> 24500 bytes misc/tools/fontsample/fontsample.mm | 270 +++++++++++++++++++++ misc/tools/kerndiff/README.md | 28 +++ misc/tools/kerndiff/getKerningPairsFromOTF.py | 323 ++++++++++++++++++++++++++ misc/tools/kerndiff/getKerningPairsFromUFO.py | 144 ++++++++++++ misc/tools/kerndiff/kerndiff.sh | 96 ++++++++ 18 files changed, 928 insertions(+), 928 deletions(-) delete mode 100644 misc/fontsample/.gitignore delete mode 100644 misc/fontsample/Makefile delete mode 100644 misc/fontsample/README.md delete mode 100755 misc/fontsample/fontsample delete mode 100644 misc/fontsample/fontsample.mm delete mode 100644 misc/kerndiff/README.md delete mode 100644 misc/kerndiff/getKerningPairsFromOTF.py delete mode 100644 misc/kerndiff/getKerningPairsFromUFO.py delete mode 100755 misc/kerndiff/kerndiff.sh create mode 100644 misc/tools/fontsample/.gitignore create mode 100644 misc/tools/fontsample/Makefile create mode 100644 misc/tools/fontsample/README.md create mode 100755 misc/tools/fontsample/fontsample create mode 100644 misc/tools/fontsample/fontsample.mm create mode 100644 misc/tools/kerndiff/README.md create mode 100644 misc/tools/kerndiff/getKerningPairsFromOTF.py create mode 100644 misc/tools/kerndiff/getKerningPairsFromUFO.py create mode 100755 misc/tools/kerndiff/kerndiff.sh (limited to 'misc') diff --git a/misc/fontsample/.gitignore b/misc/fontsample/.gitignore deleted file mode 100644 index 6142305dc..000000000 --- a/misc/fontsample/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.o -*.d diff --git a/misc/fontsample/Makefile b/misc/fontsample/Makefile deleted file mode 100644 index 063d7262d..000000000 --- a/misc/fontsample/Makefile +++ /dev/null @@ -1,44 +0,0 @@ -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/fontsample/README.md b/misc/fontsample/README.md deleted file mode 100644 index 357b93e6f..000000000 --- a/misc/fontsample/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# fontsample - -A macOS-specific program for generating a PDF with text sample of a -specific font file. - -``` -$ make -$ ./fontsample -h -usage: ./fontsample [options] - -options: - -h, -help Show usage and exit. - -z, -size Font size to render. Defaults to 96. - -t, -text Text line to render. Defaults to "Rags78 **A**". - -o Write output to instead of default filename. - Defaults to .pdf. If the provided filename - ends with ".png" a PNG is written instead of a PDF. - - - Any font file that macOS can read. -``` diff --git a/misc/fontsample/fontsample b/misc/fontsample/fontsample deleted file mode 100755 index d405bf511..000000000 Binary files a/misc/fontsample/fontsample and /dev/null differ diff --git a/misc/fontsample/fontsample.mm b/misc/fontsample/fontsample.mm deleted file mode 100644 index 2534603cd..000000000 --- a/misc/fontsample/fontsample.mm +++ /dev/null @@ -1,270 +0,0 @@ -#import -#import -#import -#import - - -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] \n" -"\n" -"options:\n" -" -h, -help Show usage and exit.\n" -" -z, -size Font size to render. Defaults to %g.\n" -" -t, -text Text line to render. Defaults to \"%s\".\n" -" -o Write output to instead of default filename.\n" -" Defaults to .pdf. If the provided filename\n" -" ends with \".png\" a PNG is written instead of a PDF.\n" -"\n" -"\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* parseargs(int argc, const char * argv[]) { - auto args = [NSMutableArray 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 \n", prog); - usage(); - return 1; - } else if (fontfiles.count > 1) { - fprintf(stderr, "%s: extraneous argument after \n", prog); - usage(); - return 1; - } - - pdfmake(fontfiles[0]); - } - return 0; -} diff --git a/misc/kerndiff/README.md b/misc/kerndiff/README.md deleted file mode 100644 index 61a98ad4c..000000000 --- a/misc/kerndiff/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# kerndiff - -Shows unified diff for kerning pairs in two font files. - -Accepts OTF, TTF and UFO fonts. - -Synopsis: - -``` -kerndiff.sh -``` - -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/kerndiff/getKerningPairsFromOTF.py b/misc/kerndiff/getKerningPairsFromOTF.py deleted file mode 100644 index 889a36813..000000000 --- a/misc/kerndiff/getKerningPairsFromOTF.py +++ /dev/null @@ -1,323 +0,0 @@ -#!/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 - -''' - -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/kerndiff/getKerningPairsFromUFO.py b/misc/kerndiff/getKerningPairsFromUFO.py deleted file mode 100644 index 47e986006..000000000 --- a/misc/kerndiff/getKerningPairsFromUFO.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/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/kerndiff/kerndiff.sh b/misc/kerndiff/kerndiff.sh deleted file mode 100755 index 64ce8c2f1..000000000 --- a/misc/kerndiff/kerndiff.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash -e - -function usage() { - cat 1>&2 <<__END -usage: $0 [options] -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" 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] + +options: + -h, -help Show usage and exit. + -z, -size Font size to render. Defaults to 96. + -t, -text Text line to render. Defaults to "Rags78 **A**". + -o Write output to instead of default filename. + Defaults to .pdf. If the provided filename + ends with ".png" a PNG is written instead of a PDF. + + + 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 Binary files /dev/null and b/misc/tools/fontsample/fontsample 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 +#import +#import +#import + + +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] \n" +"\n" +"options:\n" +" -h, -help Show usage and exit.\n" +" -z, -size Font size to render. Defaults to %g.\n" +" -t, -text Text line to render. Defaults to \"%s\".\n" +" -o Write output to instead of default filename.\n" +" Defaults to .pdf. If the provided filename\n" +" ends with \".png\" a PNG is written instead of a PDF.\n" +"\n" +"\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* parseargs(int argc, const char * argv[]) { + auto args = [NSMutableArray 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 \n", prog); + usage(); + return 1; + } else if (fontfiles.count > 1) { + fprintf(stderr, "%s: extraneous argument after \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 +``` + +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 + +''' + +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] +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" -- cgit v1.2.3