From 50ca3807f77eec9c01dcb5107da9348026e83c6c Mon Sep 17 00:00:00 2001 From: Rasmus Andersson Date: Sat, 1 Sep 2018 12:57:55 -0700 Subject: Adds fontsample tool for rendering PDF and PNG font samples on macOS --- misc/fontsample/.gitignore | 2 + misc/fontsample/Makefile | 44 +++++++ misc/fontsample/README.md | 21 ++++ misc/fontsample/fontsample | Bin 0 -> 24500 bytes misc/fontsample/fontsample.mm | 270 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 337 insertions(+) create mode 100644 misc/fontsample/.gitignore create mode 100644 misc/fontsample/Makefile create mode 100644 misc/fontsample/README.md create mode 100755 misc/fontsample/fontsample create mode 100644 misc/fontsample/fontsample.mm diff --git a/misc/fontsample/.gitignore b/misc/fontsample/.gitignore new file mode 100644 index 000000000..6142305dc --- /dev/null +++ b/misc/fontsample/.gitignore @@ -0,0 +1,2 @@ +*.o +*.d diff --git a/misc/fontsample/Makefile b/misc/fontsample/Makefile new file mode 100644 index 000000000..063d7262d --- /dev/null +++ b/misc/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/fontsample/README.md b/misc/fontsample/README.md new file mode 100644 index 000000000..357b93e6f --- /dev/null +++ b/misc/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/fontsample/fontsample b/misc/fontsample/fontsample new file mode 100755 index 000000000..d405bf511 Binary files /dev/null and b/misc/fontsample/fontsample differ diff --git a/misc/fontsample/fontsample.mm b/misc/fontsample/fontsample.mm new file mode 100644 index 000000000..2534603cd --- /dev/null +++ b/misc/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; +} -- cgit v1.2.3