Objective-C:如何在CALayer中为PDF图标设置自定义绘制颜色?
问题描述
已将黑底透明的PDF图标加载到CGPDFPageRef中,需要在CALayer上以指定颜色绘制。目前能正确缩放和旋转图标,但无法修改其渲染颜色。这是macOS桌面航班地图应用的功能,此前使用PNG图标缩放到40x40时会模糊,因此改用矢量PDF文件。现有代码可正确缩放定位图标,但改色代码块无效,图标仍为黑色透明背景。之前处理PNG时可通过NSImage修改颜色,但不清楚如何对PDF执行类似操作。
现有渲染PDF代码
- (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)); // 此代码块无效,图标仍为黑色透明背景 CGContextSetAlpha (context, _radarView.militaryAlpha); CGContextSetFillColorWithColor (context, _militaryColour); CGContextSetStrokeColorWithColor (context, _militaryColour); CGContextSetBlendMode (context, kCGBlendModeMultiply); // 如何修改图像颜色? CGContextDrawPDFPage (context, acft.pdf); CGRect rect = CGRectMake (0.0 - (_iconSize / 2.0), 0.0 - (_iconSize / 2.0), _iconSize, _iconSize); // 绘制高亮效果 if (acft == _radarView.selected && !_radarView.showOnlySelectedAircraft) [self drawAircraftHighlightInContext:context rect:rect acft:acft]; CGContextRestoreGState (context); }
PDF加载代码
// 测试是否可将PDF视为矢量而非图像 if (acft.pdfIcon) { NSURL *nsurl = [NSURL fileURLWithPath:filename isDirectory:NO]; CFURLRef url = (CFURLRef)CFBridgingRetain(nsurl); CGPDFDocumentRef pdf = CGPDFDocumentCreateWithURL (url); CGPDFPageRef page = CGPDFDocumentGetPage (pdf, 1); /* 可能需要基于原PDF创建新CGPDFPageRef并绘制为指定颜色 */ acft.pdf = page; }
此前PNG改色代码
image = [[NSImage alloc] initWithContentsOfFile:filename]; /* * 我们的PNG图标为黑色,需用指定颜色区分军用、民用或模拟机 */ [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:@{}]); }
解决方案
直接通过CGContext设置颜色混合模式对PDF无效,因为PDF自身的绘制指令会覆盖上下文的颜色设置。可通过以下两种方式实现PDF图标改色:
方法一:将PDF转为NSImage后再改色(复用PNG改色逻辑)
利用NSImage加载PDF,沿用之前PNG的改色流程,既能保留矢量清晰度,又能实现颜色修改:
// 替换原PDF加载逻辑,改为生成带指定颜色的NSImage if (acft.pdfIcon) { NSURL *pdfURL = [NSURL fileURLWithPath:filename isDirectory:NO]; NSImage *pdfImage = [[NSImage alloc] initWithContentsOfURL:pdfURL]; // 设置PDF图像尺寸,确保矢量缩放 [pdfImage setSize:NSMakeSize(_iconSize, _iconSize)]; // 复用PNG的改色逻辑 [pdfImage lockFocus]; [[[NSColor colorWithCGColor:_militaryColour] colorWithAlphaComponent:_radarView.militaryAlpha] set]; NSRect imageRect = {NSZeroPoint, [pdfImage size]}; NSRectFillUsingOperation(imageRect, NSCompositeSourceIn); [pdfImage unlockFocus]; // 将改色后的NSImage转为CGImage存储,后续绘制用 CGRect rect = CGRectMake(0.0, 0.0, _iconSize, _iconSize); CGImageRef coloredCGImage = [pdfImage CGImageForProposedRect:&rect context:nil hints:@{}]; acft.coloredCGImage = coloredCGImage; // 需在Aircraft类中新增CGImageRef属性 } // 修改渲染方法,直接绘制改色后的CGImage - (void)drawAircraftInContext:(CGContextRef)context acft:(Aircraft *)acft { CGContextSaveGState(context); CGContextTranslateCTM(context, acft.x, acft.y); CGContextRotateCTM(context, degreesToRadians(acft.heading)); // 调整绘制位置,对应原PDF的缩放逻辑 CGRect drawRect = CGRectMake(-_iconSize/2, -_iconSize/2, _iconSize, _iconSize); CGContextDrawImage(context, drawRect, acft.coloredCGImage); // 绘制高亮效果(保留原有逻辑) if (acft == _radarView.selected && !_radarView.showOnlySelectedAircraft) { [self drawAircraftHighlightInContext:context rect:drawRect acft:acft]; } CGContextRestoreGState(context); }
方法二:使用CGPDFOperatorOverride拦截PDF绘制指令
通过重写PDF的填充/描边指令,强制使用指定颜色,适合需要直接操作PDF的场景:
// 定义PDF操作拦截回调 static void overrideFillOperator(CGPDFScannerRef scanner, void *info) { CGContextRef context = (CGContextRef)info; // 跳过原PDF的颜色设置,强制使用指定颜色 CGContextSetFillColorWithColor(context, _militaryColour); // 可通过info传递颜色,避免全局变量 CGContextFillPath(context); } static void overrideStrokeOperator(CGPDFScannerRef scanner, void *info) { CGContextRef context = (CGContextRef)info; CGContextSetStrokeColorWithColor(context, _militaryColour); CGContextStrokePath(context); } // 修改渲染方法,添加操作拦截 - (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, -(_iconSize / pdfRect.size.width), -(_iconSize / pdfRect.size.height)); CGContextSetAlpha(context, _radarView.militaryAlpha); // 创建PDF扫描器,拦截填充和描边操作 CGPDFContentStreamRef contentStream = CGPDFContentStreamCreateWithPage(acft.pdf); CGPDFScannerRef scanner = CGPDFScannerCreate(contentStream, context, (__bridge void *)context); // 替换默认的填充和描边操作(根据PDF实际使用的操作符添加更多回调) CGPDFScannerSetOperatorCallback(scanner, "f", overrideFillOperator); CGPDFScannerSetOperatorCallback(scanner, "S", overrideStrokeOperator); CGPDFScannerSetOperatorCallback(scanner, "F", overrideFillOperator); CGPDFScannerSetOperatorCallback(scanner, "s", overrideStrokeOperator); // 扫描并执行修改后的PDF指令 if (!CGPDFScannerScan(scanner)) { // 扫描失败时 fallback 到原绘制逻辑 CGContextDrawPDFPage(context, acft.pdf); } // 释放资源 CGPDFScannerRelease(scanner); CGPDFContentStreamRelease(contentStream); // 绘制高亮效果 CGRect rect = CGRectMake(-_iconSize/2, -_iconSize/2, _iconSize, _iconSize); if (acft == _radarView.selected && !_radarView.showOnlySelectedAircraft) [self drawAircraftHighlightInContext:context rect:rect acft:acft]; CGContextRestoreGState(context); }
注意事项
- 方法一更简单,复用原有PNG改色逻辑,适合快速实现;
- 方法二更灵活,但需要处理PDF中可能用到的各种绘制操作符;
- 确保PDF图标是纯黑色+透明背景,改色才能得到正确效果;
- 记得在Aircraft类中新增对应属性存储改色后的图像,并在合适时机释放
CGImageRef避免内存泄漏。
内容的提问来源于stack exchange,提问作者user9398585
相关产品推荐
相关产品推荐

