iOS 17:CTFramesetterSuggestFrameSizeWithConstraints does not provide the correct text size.

In iOS 17, when I try to use CTFramesetterSuggestFrameSizeWithConstraints to obtain the size of a text, if the system-provided bold font is used, the resulting rectangular area may not be sufficient to accommodate the text.

The provided example code is not allowing me to display the complete text within the CTFrame.

code:

     NSString *testString = @"《测试》";
    CFStringRef cfTestString =  CFStringCreateWithCString(kCFAllocatorDefault, [testString cStringUsingEncoding:NSUTF8StringEncoding], kCFStringEncodingUTF8);
    
    UIFont *uiFont = [UIFont boldSystemFontOfSize:50.0];
    UIFontDescriptor *fontDescriptor = uiFont.fontDescriptor;
    CTFontDescriptorRef ctFontDescriptor = (__bridge CTFontDescriptorRef)fontDescriptor;
    CTFontRef ct_font = CTFontCreateWithFontDescriptor(ctFontDescriptor, uiFont.pointSize, NULL);
    
    CGFloat color_components[4] = {1.0, 1.0, 1.0, 1.0};
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGColorRef cgColor = CGColorCreate(colorSpace, color_components);
    
    CTParagraphStyleSetting styleSetting;
    styleSetting.spec = kCTParagraphStyleSpecifierAlignment;
    CTTextAlignment ctAlignment = kCTTextAlignmentCenter;
    styleSetting.value = &ctAlignment;
    styleSetting.valueSize = sizeof(kCTTextAlignmentCenter);
    CTParagraphStyleRef ctParagraphStyle = CTParagraphStyleCreate(&styleSetting, 1);
    
    const void* keys[] = {kCTFontAttributeName, kCTForegroundColorAttributeName,
                          kCTParagraphStyleAttributeName};
    const void* values[] = {ct_font, cgColor, ctParagraphStyle};
    CFDictionaryRef attr = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 3,
                                              &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    
    
    CFAttributedStringRef cfAttributedString =  CFAttributedStringCreate(kCFAllocatorDefault, cfTestString, attr);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(cfAttributedString);
    
    CGSize constraints = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
    CGSize stringSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, constraints, NULL);
    
    float width = ceil(stringSize.width);
    float height = ceil(stringSize.height);
    CGPathRef path = CGPathCreateWithRect(CGRectMake(0.0, 0.0, width, height), NULL);

    CTFrameRef cfFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
    CFRange stringRange =  CTFrameGetStringRange(cfFrame);
    CFRange visibleStringRange =  CTFrameGetVisibleStringRange(cfFrame);
      
    NSLog(@"stringRange.location: %@ stringRange.length: %@", @(stringRange.location), @(stringRange.length));
    NSLog(@"visibleStringRange.location: %@ visibleStringRange.length: %@", @(visibleStringRange.location), @(visibleStringRange.length));

log:

stringRange.location: 0 stringRange.length: 4
visibleStringRange.location: 0 visibleStringRange.length: 2

Same problem here.

It seems that CTFramesetterCreateWithAttributedString is actually getting the correct result. The actual problem happens in CTFramesetterCreateFrame and CTFrameDraw. I made a simple multiplatform SpriteKit demo to reproduce the issue:

  1. I tried to draw a SKLabelNode with AppleSystemBoldFont with font size set to 16 * [UIScreen.mainScreen scale], numberOfLines set to 0, preferredMaxLayoutWidth set to 0.
  2. The text content is set to "《眼中《星》眼中星眼中星眼中星眼中星眼中星》".

From the conditions above, the expected behavior should be: The text is finally drawn in a single line, and its size should be 1006x58. CTFramesetterSuggestFrameSizeWithConstraints and CTFramesetterCreateFrame is returning the correct result, which means the returned size is equal to 1006x58, the CTFrame suggests that there is only one CTLine. But its internal state is actually wrong. In CTFrame, the glyph count should be 22, but the CTFrame take it as 20. This means when finally calling CTFrameDraw, the text "星》" will be dropped.

Here is the memory dump when I dive into CoreText.framework by disassembling the SpriteKit.framework -> SKCLabelNode::rebuildText() and check its CTFrameDraw call:

SpriteKit.framework -> SKCLabelNode::rebuildText() -> CTFrameDraw:

At 0x19ce483b0 <+40>, po $x1 <CGContext 0x281a5e700> (kCGContextTypeBitmap) <<CGColorSpace 0x280b580c0> (kCGColorSpaceDeviceRGB)> width = 1008, height = 60, bpc = 8, bpp = 32, row bytes = 4032 kCGImageAlphaPremultipliedLast | kCGImageByteOrder32Big | kCGImagePixelFormatPacked(default )

At 0x19ce483f4 <+108>, po $x0 <__NSArrayM 0x282139ce0>( <CTLine: 0x28105c2d0>{run count = 1, string range = (0, 20/** THIS SHOULD BE 22!!!!!! /), width = 932.64 /* THIS SHOULD BE 1006!!!!!! */, A/D/L = 45.7031/11.5781/0, glyph count = 20, runs = ( <CTRun: 0x14fd275f0>{string range = (0, 20), string = "\u300A\u773C\u4E2D\u300A\u661F\u300B\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D", attributes = { NSColor = "UIExtendedGrayColorSpace 1 1"; NSFont = "<UICTFont: 0x14fd166b0> font-family: ".PingFangSC-Semibold"; font-weight: bold; font-style: normal; font-size: 48.00pt"; }} ) } )

The CTFrame and drawing result get correct when I use the AppleSystemNormalFont, or simply add the CTFramesetterSuggestFrameSizeWithConstraints returned CGSize dimensions by 1 px before CTFramesetterCreateFrame. Here is the expected result when calling CTFrameDraw with AppleSystemNormalFont:

THE ONLY PROPER DATA SHOULD BE:

<__NSArrayM 0x280c33960>( <CTLine: 0x283d4e1c0>{run count = 1, string range = (0, 22), width = 1001.47, A/D/L = 45.7031/11.5781/0, glyph count = 22, runs = ( <CTRun: 0x11340ebc0>{string range = (0, 22), string = "\u300A\u773C\u4E2D\u300A\u661F\u300B\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u300B", attributes = { CTForegroundColor = "<CGColor 0x282640600> [<CGColorSpace 0x2826540c0> (kCGColorSpaceDeviceRGB)] ( 1 1 1 1 )"; NSFont = "<UICTFont: 0x11340e7e0> font-family: ".PingFangSC-Regular"; font-weight: normal; font-style: normal; font-size: 48.00pt"; }} ) } )

The reproduction code could be found in: https://github.com/Autokaka/ios17_core_text_repro. The Core Text is a really important fundamental library that Apple and its developers used. From iOS 17, the above issue is bringing great trouble to any game engine or any app that use the Core Text library to render rich texts in Chinese. Since the bug reproduction is actually really clear now, I believe Apple should prioritize this issue and do not leave it to developers to get a workaround for this.

Same problem here.

It seems that CTFramesetterCreateWithAttributedString is actually getting the correct result. The actual problem happens in CTFramesetterCreateFrame and CTFrameDraw. I made a simple multiplatform SpriteKit demo to reproduce the issue:

  1. I tried to draw a SKLabelNode with AppleSystemBoldFont with font size set to 16 * [UIScreen.mainScreen scale], numberOfLines set to 0, preferredMaxLayoutWidth set to 0.
  2. The text content is set to "《眼中《星》眼中星眼中星眼中星眼中星眼中星》".

From the conditions above, the expected behavior should be: The text is finally drawn in a single line, and its size should be 1006x58. CTFramesetterSuggestFrameSizeWithConstraints and CTFramesetterCreateFrame is returning the correct result, which means the returned size is equal to 1006x58, the CTFrame suggests that there is only one CTLine. But its internal state is actually wrong. In CTFrame, the glyph count should be 22, but the CTFrame take it as 20. This means when finally calling CTFrameDraw, the text "星》" will be dropped.

Here is the memory dump when I dive into CoreText.framework by disassembling the SpriteKit.framework -> SKCLabelNode::rebuildText() and check its CTFrameDraw call:

// SpriteKit.framework -> SKCLabelNode::rebuildText() -> CTFrameDraw:

At 0x19ce483b0 <+40>, po $x1
<CGContext 0x281a5e700> (kCGContextTypeBitmap)
	<<CGColorSpace 0x280b580c0> (kCGColorSpaceDeviceRGB)>
		width = 1008, height = 60, bpc = 8, bpp = 32, row bytes = 4032 
kCGImageAlphaPremultipliedLast | kCGImageByteOrder32Big | kCGImagePixelFormatPacked(default ) 

At 0x19ce483f4 <+108>, po $x0
<__NSArrayM 0x282139ce0>(
<CTLine: 0x28105c2d0>{run count = 1, string range = (0, 20/** THIS SHOULD BE 22!!!!!! */), width = 932.64 /** THIS SHOULD BE 1005.xxxxx!!!!!! */, A/D/L = 45.7031/11.5781/0, glyph count = 20, runs = (
<CTRun: 0x14fd275f0>{string range = (0, 20), string = "\u300A\u773C\u4E2D\u300A\u661F\u300B\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D", attributes = {
    NSColor = "UIExtendedGrayColorSpace 1 1";
    NSFont = "<UICTFont: 0x14fd166b0> font-family: \".PingFangSC-Semibold\"; font-weight: bold; font-style: normal; font-size: 48.00pt";
}}
)
}
)

The CTFrame and drawing result get correct when I use the AppleSystemNormalFont, or simply add the CTFramesetterSuggestFrameSizeWithConstraints returned CGSize dimensions by 1 px before CTFramesetterCreateFrame. Here is the expected result when calling CTFrameDraw with AppleSystemNormalFont:

// THE PROPER DATA SHOULD BE:

<__NSArrayM 0x280c33960>(
<CTLine: 0x283d4e1c0>{run count = 1, string range = (0, 22), width = 1001.47, A/D/L = 45.7031/11.5781/0, glyph count = 22, runs = (
<CTRun: 0x11340ebc0>{string range = (0, 22), string = "\u300A\u773C\u4E2D\u300A\u661F\u300B\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u773C\u4E2D\u661F\u300B", attributes = {
    CTForegroundColor = "<CGColor 0x282640600> [<CGColorSpace 0x2826540c0> (kCGColorSpaceDeviceRGB)] ( 1 1 1 1 )";
    NSFont = "<UICTFont: 0x11340e7e0> font-family: \".PingFangSC-Regular\"; font-weight: normal; font-style: normal; font-size: 48.00pt";
}}
)
}
)

The reproduction code could be found in: https://github.com/Autokaka/ios17_core_text_repro.

The Core Text is a really important fundamental library that Apple and its developers used. From iOS 17, the above issue is bringing great trouble to any game engine or any app that use the Core Text library to render rich texts in Chinese. Since the bug reproduction is actually really clear now, I believe Apple should prioritize this issue and do not force developers to get a workaround for this.

iOS 17:CTFramesetterSuggestFrameSizeWithConstraints does not provide the correct text size.
 
 
Q