objective-ccalayercgcontextdrawpdfpage

Draw PDF with a different colour in a CALayer. (Objective-C)


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:@{}]);
  }

Solution

  • 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 :

    AW109

    And, using a PDF of an AC130 (can't remember where I got it):

    AC130

    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º):

    Image Views


    All the above images are placed on top of a "checker board pattern" so we can see the transparency.