I've loaded a black and transparent .PDF icon to a CGPDFPageRef and need to draw it out to a CALayer with a specified colour. I can scale and rotate it correctly but don't know how to change the colour it's rendered in.
This is part of a macOS desktop application that maps aircraft in flight. I have been using an NSImage to draw .PNG aircraft icons but as my source icons are 128x128 they often appear slightly blurry when an NSImage is scaled down to 40x40. I have been converting the icons to 128x128 Vector PDF files and the code below draws them correctly scaled and orientated on a CALayer but I don’t seem to be able to change the colour they are drawn in.
When I used NSImages to load .png’s I have some code to read the .png to an NSImage then create a new NSImage with the changed colour and I suspect I’ll need to do the same with a PDF but have no idea how to do that.
My code to render the .PDF icon to the map is:
// Render a PDF vector icon
- (void)drawAircraftInContext:(CGContextRef)context acft:(Aircraft *)acft
{
CGContextSaveGState (context);
CGRect pdfRect = CGPDFPageGetBoxRect (acft.pdf, kCGPDFCropBox);
CGContextTranslateCTM (context, acft.x, acft.y);
CGContextRotateCTM (context, degreesToRadians (acft.heading));
CGContextTranslateCTM (context, (_iconSize / 2), (_iconSize / 2));
CGContextScaleCTM (context, .0 - (_iconSize / pdfRect.size.width), .0 - (_iconSize / pdfRect.size.height));
// None of this block actually works - the colour remains black with a clear background.
CGContextSetAlpha (context, _radarView.militaryAlpha);
CGContextSetFillColorWithColor (context, _militaryColour);
CGContextSetStrokeColorWithColor (context, _militaryColour);
CGContextSetBlendMode (context, kCGBlendModeMultiply);
// How do we change the colour of the image?
CGContextDrawPDFPage (context, acft.pdf);
CGRect rect = CGRectMake (0.0 - (_iconSize / 2.0), 0.0 - (_iconSize / 2.0), _iconSize, _iconSize);
// Draw a highlight
if (acft == _radarView.selected && !_radarView.showOnlySelectedAircraft)
[self drawAircraftHighlightInContext:context rect:rect acft:acft];
CGContextRestoreGState (context);
}
The code to load the PDF is:
//Experimental to see if PDF images can be treated as vectors rather than images.
if (acft.pdfIcon)
{
NSURL *nsurl = [NSURL fileURLWithPath:filename isDirectory:NO];
CFURLRef url = (CFURLRef)CFBridgingRetain(nsurl);
CGPDFDocumentRef pdf = CGPDFDocumentCreateWithURL (url);
CGPDFPageRef page = CGPDFDocumentGetPage (pdf, 1);
/* Might need to create another CGPDFPageRef from the original and draw
into it with the correct colour. */
acft.pdf = page;
}
Which then falls through to create an NSImage which I don’t need to do if I can change the colour:
image = [[NSImage alloc] initWithContentsOfFile:filename];
/*
* Our .png images are just black but we'll need to use the specified colour
* to indicate military, civilian or simulator.
*/
[image lockFocus];
[[[NSColor colorWithCGColor:colour] colorWithAlphaComponent:alpha] set];
NSRect imageRect = { NSZeroPoint, [image size] };
NSRectFillUsingOperation (imageRect, NSCompositeSourceIn);
[image unlockFocus];
}
if (image)
{
CGRect rect = CGRectMake (0.0, 0.0, _iconSize, _iconSize);
return ([image CGImageForProposedRect:&rect
context:nil
hints:@{}]);
}
return ([image CGImageForProposedRect:&rect
context:nil
hints:@{}]);
}
A few notes...
First, as I mentioned in my comments, shapes / paths in PDF files can be finicky.
Using the original PDF you linked to - http://www.iridiumblue.com/pages/AW109.pdf :
And, using a PDF of an AC130 (can't remember where I got it):
You can edit the paths in the PDF file to fix problems... but, as you can see, fixing that AC130 might be a fair amount of work.
If it were me, I would generate my own CGMutablePath
-- most PDF / vector editors can export the paths in various formats - such as SVG which can be converted fairly easily to CGPathMoveToPoint(...)
/ CGPathAddLineToPoint(...)
/ CGPathAddCurveToPoint(...)
/ etc.
If you're set on using PDF files and extracting the paths, here is some Example code...
Extractor.h
#import <Foundation/Foundation.h>
#import <PDFKit/PDFKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface Extractor : NSObject
+ (NSArray<id> *)extractVectorPathsFromPDF:(NSURL *)pdfURL;
@end
NS_ASSUME_NONNULL_END
Extractor.m
#import "Extractor.h"
@implementation Extractor
void ExtractPathsFromContent(CGPDFScannerRef scanner, void *info);
void moveToCallback(CGPDFScannerRef scanner, void *info);
void lineToCallback(CGPDFScannerRef scanner, void *info);
void curveToCallback(CGPDFScannerRef scanner, void *info);
void closePathCallback(CGPDFScannerRef scanner, void *info);
CGMutablePathRef currentPath = NULL;
+ (NSArray<id> *)extractVectorPathsFromPDF:(NSURL *)pdfURL {
PDFDocument *pdfDocument = [[PDFDocument alloc] initWithURL:pdfURL];
if (!pdfDocument) {
NSLog(@"Unable to load PDF document.");
return nil;
}
NSMutableArray<id> *vectorPaths = [NSMutableArray array];
// Iterate through all pages in the PDF document
for (NSInteger pageIndex = 0; pageIndex < pdfDocument.pageCount; pageIndex++) {
PDFPage *pdfPage = [pdfDocument pageAtIndex:pageIndex];
if (!pdfPage) continue;
CGPDFPageRef cgPage = pdfPage.pageRef;
if (!cgPage) continue;
// Set up the scanner to process PDF content streams
CGPDFContentStreamRef contentStream = CGPDFContentStreamCreateWithPage(cgPage);
CGPDFOperatorTableRef operatorTable = CGPDFOperatorTableCreate();
// Register custom callbacks for common operators for path extraction
CGPDFOperatorTableSetCallback(operatorTable, "m", &moveToCallback); // move to
CGPDFOperatorTableSetCallback(operatorTable, "l", &lineToCallback); // line to
CGPDFOperatorTableSetCallback(operatorTable, "c", &curveToCallback); // curve to
CGPDFOperatorTableSetCallback(operatorTable, "h", &closePathCallback); // close path
CGPDFScannerRef scanner = CGPDFScannerCreate(contentStream, operatorTable, (__bridge void *)(vectorPaths));
// Initialize a new path for the page
currentPath = CGPathCreateMutable();
// Scan the PDF content
CGPDFScannerScan(scanner);
// After scanning, add the current path to the paths array
if (currentPath) {
[vectorPaths addObject:(__bridge_transfer id)currentPath];
currentPath = NULL;
}
// Clean up
CGPDFScannerRelease(scanner);
CGPDFContentStreamRelease(contentStream);
CGPDFOperatorTableRelease(operatorTable);
}
return [vectorPaths copy];
}
// Callback for "move to" operator
void moveToCallback(CGPDFScannerRef scanner, void *info) {
CGPDFReal x, y;
if (CGPDFScannerPopNumber(scanner, &y) && CGPDFScannerPopNumber(scanner, &x)) {
CGPathMoveToPoint(currentPath, NULL, x, y);
}
}
// Callback for "line to" operator
void lineToCallback(CGPDFScannerRef scanner, void *info) {
CGPDFReal x, y;
if (CGPDFScannerPopNumber(scanner, &y) && CGPDFScannerPopNumber(scanner, &x)) {
CGPathAddLineToPoint(currentPath, NULL, x, y);
}
}
// Callback for "curve to" operator
void curveToCallback(CGPDFScannerRef scanner, void *info) {
CGPDFReal x1, y1, x2, y2, x3, y3;
if (CGPDFScannerPopNumber(scanner, &y3) && CGPDFScannerPopNumber(scanner, &x3) &&
CGPDFScannerPopNumber(scanner, &y2) && CGPDFScannerPopNumber(scanner, &x2) &&
CGPDFScannerPopNumber(scanner, &y1) && CGPDFScannerPopNumber(scanner, &x1)) {
CGPathAddCurveToPoint(currentPath, NULL, x1, y1, x2, y2, x3, y3);
}
}
// Callback for "close path" operator
void closePathCallback(CGPDFScannerRef scanner, void *info) {
CGPathCloseSubpath(currentPath);
}
@end
which can be used like this:
NSString *pdfName = @"AW109";
NSString *pdfPath = [[NSBundle mainBundle] pathForResource:pdfName ofType:@"pdf"];
NSURL *pdfUrl = [NSURL fileURLWithPath:pdfPath isDirectory:NO];
// get the paths from the PDF
// the PDF has only one page, so we're expecting only one path
NSArray<id> *vectorPaths = [Extractor extractVectorPathsFromPDF:pdfUrl];
CGPathRef pth = (CGPathRef)CFBridgingRetain([vectorPaths firstObject]);
// we now have a CGPathRef which can be drawn using CGContextDrawPath
// or set as the .path of a CAShapeLayer
Another approach, which might be a better option -- use an NSImageView
with the PDF file as an NSImage
, and then scale / rotate / position that image view as a subview:
NSImageView *imgView = [NSImageView new];
imgView = [NSImageView new];
imgView.wantsLayer = YES;
imgView.imageScaling = NSImageScaleProportionallyUpOrDown;
// desired color
imgView.contentTintColor = NSColor.redColor;
NSString *pdfName = @"AW109";
NSString *pdfPath = [[NSBundle mainBundle] pathForResource:pdfName ofType:@"pdf"];
NSURL *pdfUrl = [NSURL fileURLWithPath:pdfPath isDirectory:NO];
NSImage *img = [[NSImage alloc] initWithContentsOfURL:pdfUrl];
img.template = YES;
imgView.image = img;
After some quick testing, Cocoa appears to do a nice job of processing the PDF file, and scales its paths when it is rendered.
For example - the original AW109.pdf, scaled to 3 sizes (image views are also each rotated 45º):
All the above images are placed on top of a "checker board pattern" so we can see the transparency.