Change SF Symbol representation size

We are developing a game on UnrealEngine4 and we need the PNG representation of a SF Symbol to display it on our UI.

After watching WWDC sessions and reading the documentation, we achieved it by calling:

Code Block
NSImage* nsImage = [NSImage imageWithSystemSymbolName:symbolNameString accessibilityDescription:nil];
NSImageSymbolConfiguration* config = [NSImageSymbolConfiguration configurationWithPointSize:100 weight:NSFontWeightBlack scale:NSImageSymbolScaleLarge];
NSImage* nsImageWithConfig = [nsImage imageWithSymbolConfiguration:config];
NSData* tiff = [nsImageWithConfig TIFFRepresentation];
NSBitmapImageRep* raw_img = [NSBitmapImageRep imageRepWithData:tiff];
NSDictionary* dict = [NSDictionary dictionary];
NSData* nsData = [raw_img representationUsingType:NSBitmapImageFileTypePNG properties:dict];


However, it outputs an image of 18x14 pixels. We need an image of size 64x64 pixels.

How can we resize it before getting the PNG representation so it can be displayed larger on the screen?

Replies

Don't use TIFFRepresentation. Once you have your symbol configured, call CGImageForProposedRect:. You can pass nil for the rect and it will use the natural size of the symbol at the given scale, pointSize, weight, etc.

Code Block Obj-C
NSImage* nsImage = [NSImage imageWithSystemSymbolName:symbolNameString accessibilityDescription:nil];
NSImageSymbolConfiguration* config = [NSImageSymbolConfiguration configurationWithPointSize:100 weight:NSFontWeightBlack scale:NSImageSymbolScaleLarge];
NSImage* nsImageWithConfig = [nsImage imageWithSymbolConfiguration:config];
CGImageRef cgImage = [nsImageWithConfig CGImageForProposedRect:nil context:nil hints:nil];

The CGImage that's returned will be sized for the default display here, because I didn't specify a context.

You can put this CGImage back into an NSImage with imageWithCGImage if you want, or do whatever else you need to do with it.

To position this symbol your code needs to pay attention to the alignmentRect property of the symbol image. The alignmentRect.origin.y is the baseline, i.e. the offset into the image where the baseline should intersect the image. The cap line is the top, and the left and right side bearings (spacing between this symbol and any content to either side of it) is left and right.
Thanks for the fast reply!

How could we get a 64x64 PNG image from a symbol then, independent from the display?

Also, is there a simple way of tinting it? We'd like a white symbol image to be manipulated inside UE4's UI system. We couldn't find an equivalent for UIImage's imageWithTintColor method on AppKit.


If you pass a rect with the desired size to CGImageForProposedRect it will scale the symbol to fit in that rect:

Code Block
    NSRect symbolRect = NSMakeRect(0, 0, 64, 64);
    CGImageRef cgImage = [configuredSymbol CGImageForProposedRect:&symbolRect context:nil hints:nil];


However this will still scale to the default display's scaleFactor, so on a 2x display you will get a 128x128 CGImage. If you must have it at 1x display scaleFactor no matter what, then you will need to supply your own CTM in the hints dictionary. You can just pass the identity transform:

Code Block
    NSRect symbolRect = NSMakeRect(0, 0, 64, 64);
    CGImageRef cgImage = [configuredSymbol CGImageForProposedRect:&symbolRect context:nil hints:@{NSImageHintCTM: [NSAffineTransform transform]}];


This isn't something I would normally recommend for user interface code, but I suppose for a game engine you have certain constraints.

As for tinting the image, unfortunately we don't have API for that in NSImage yet. It can be done with CG drawing using a transparency layer and sourceIn blending:

Code Block
            CGContextBeginTransparencyLayerWithRect(context, dstRect, nil);
            CGContextSetBlendMode(context, kCGBlendModeNormal);
            CGContextDrawImage(context, glyphAlignmentRect, glyphImage);
            CGContextSetBlendMode(context, kCGBlendModeSourceIn);
            CGContextSetFillColorWithColor(context, CGColorGetConstantColor(kCGColorWhite));
            CGContextFillRect(context, dstRect);            
            CGContextEndTransparencyLayer(context);


(assuming you have a CGContext to draw into, even if only an offscreen bitmap).