UIImage and CGImage have different aspect ratio for SFSymbols

Dear Experts,

I create a UIImage for an SFSymbol using [UIImage systemImageNamed], get its CGImage, and look at the sizes of each:

UIImageConfiguration* config =
  [UIImageSymbolConfiguration configurationWithPointSize: 64
                              weight: UIImageSymbolWeightLight
                              scale: UIImageScaleMedium];

UIImage* img = [UIImage systemImageNamed: @"chevron.compact.down"
                        withConfiguration: config];

CGImageRef c = [img CGImage];

printf("UIImage size %f x %f, CGImage size %f x %f\n",
       img.size.width, img.size.height,
       CGImageGetWidth(c), CGImageGetHeight(c));

(Consider that pseudo-code, it's not an exact copy-paste.)

Results:

UIImage is 70.3333 x 25.6667 and CGImage is 163 x 43.

So the aspect ratios (W/H) are 2.74 and 3.79 respectively. That can't be right!

I don't expect the UIImage and the CGImage dimensions to be the same, because of the UIImage's scale (which is 3 in this case). But that should be the same for both dimensions.

The effect is most pronounced with symbols that have an aspect ratio far from 1, e.g. recordingtape, ellipsis, and this chevron.compact.down.

I believe that the CGImage aspect ratios are the correct ones.

What is going on here?

SFSymbol images have additional metadata that informs of the CGImageRef should be positioned for drawing, allowing us to not have to store as many empty pixels as we would if the sizes matched. If you query a number of different SFSymbols (with the same configuration) you are likely to find many of them have a similar size as queried by UIImage, but different sizes as queried via CGImageRef, reflecting their typographic natures.

This is expected especially for SF Symbols. UIImage has various insets and so on encoded into the image so that it can be optically aligned in various contexts, especially for unbalanced symbols like the chevron symbols. This affects the overall size of the image. However when converted to a CGImage, there is no inset information that can be encoded, so the resulting size is different.

Thanks both. I am aware of UIImage's baseline and alignmentRectInsets; is there anything else?

Looking at the example of chevron.compact.down, on an @3X device I see:

  • UIImage.size = 70.33 x 25.67
  • CGImage.size = 163 x 43
  • UIImage.alignmentRectInsets = left 0 right 0 top -25 bottom -25.67
  • UIImage.baselineOffsetFromBottom = -10.33

I don't immediately see how I can apply the insets and/or baseline to one size to get the other!

I am attempting to draw the symbols into what will eventually be a texture atlas for GPU use. I am not particularly concerned about most of the typographic properties but I do need the symbols to have the correct aspect ratio and the correct relative sizes. So after the code above I did something like

CGRect R = CGRectMake(0,0, img.size.width,img.size.height);
CGContextDrawImage(context, R, cgimg);

That results in wrong aspect ratios. So now I do:

CGRect R = CGRectMake(0,0, CGImageGetWidth(c), CGImageGetHeight(c));
CGContextDrawImage(context, R, cgimg);

Now the aspect ratio is correct, but the image is much larger - and not by a factor of img.scale (== mainScreen.nativeScale), it varies near 2.5.

When I run on an @2X device, the UIImage sizes are almost the same (e.g. 70.5 instead of 70.33) but the CGImage sizes are smaller - they are, I think, exactly 2/3 of the size of the @3X image sizes.

I think I trust that the size of the UIImage will be related to the font size that I ask for in the UIImageSymbolConfiguration in a stable way, but I don't trust that the CGImage size is consistent with the font size in the same way. I almost trust that it will be consistent if I divide the CGImage size by img.scale.

What should I do?

Why not just use UIImage to draw the symbols instead of dropping to CoreGraphics?

Why not just use UIImage to draw the symbols instead of dropping to CoreGraphics?

I have done that in the (distant) past, but it's a few more lines-of-code to set up the thread-local current context, just to draw one image. (Unless there is something that I've missed.)

But also, as I've now learned that the UIImage just pads the CGImage with empty pixels, I'd like to make use of that in my texture atlas. I only need to draw the non-empty area into the texture, and I can add info about the empty padding to my metadata.

UIImage and CGImage have different aspect ratio for SFSymbols
 
 
Q