Adds fontsample tool for rendering PDF and PNG font samples on macOS
This commit is contained in:
parent
4e6c626ab3
commit
50ca3807f7
5 changed files with 337 additions and 0 deletions
2
misc/fontsample/.gitignore
vendored
Normal file
2
misc/fontsample/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.o
|
||||||
|
*.d
|
||||||
44
misc/fontsample/Makefile
Normal file
44
misc/fontsample/Makefile
Normal file
|
|
@ -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
|
||||||
21
misc/fontsample/README.md
Normal file
21
misc/fontsample/README.md
Normal file
|
|
@ -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.
|
||||||
|
```
|
||||||
BIN
misc/fontsample/fontsample
Executable file
BIN
misc/fontsample/fontsample
Executable file
Binary file not shown.
270
misc/fontsample/fontsample.mm
Normal file
270
misc/fontsample/fontsample.mm
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
Reference in a new issue