Search code examples
htmlobjective-cfontsnsattributedstringmacos-sonoma

Why am I seeing different behavior in macOS 14 when creating NSAttributedString from HTML vs doing the same thing with initWithString:attributes:?


I let my user choose a font from their installed fonts, then use that font in three different contexts in my macOS app.

  1. The list of fonts itself uses NSFontDescriptor to create an attributed string, then displays that in the list of choices:
    NSFontDescriptor *fontDescriptor = [NSFontDescriptor fontDescriptorWithName:@"Candara" size:14];
    NSDictionary *attributes = @{NSFontAttributeName: [NSFont fontWithDescriptor:fontDescriptor size:14]};
    attributedString = [[NSAttributedString alloc] initWithString:@"New Attributed String" attributes:attributes];
  1. In some places I have a very small HTML document where I set the body font to the selected font, convert to an attributed string, and display in an NSTextField.
    NSString *htmlString = @"<!DOCTYPE html>"
                             "<html>"
                             "<head>"
                             "<style>"
                             "body { font-family: 'Candara', serif; font-size: 14px; }"
                             "</style>"
                             "</head>"
                             "<body>"
                             "<p>Text with <b>bold</b> or <i>italics</i> maybe.</p>"
                             "</body>"
                             "</html>";
    NSData *htmlData = [htmlString dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *options = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                              NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)};
    NSError *error;
    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:htmlData
                                                                            options:options
                                                                 documentAttributes:nil
                                                                              error:&error];
  1. Finally, I have places where I have longer HTML documents with the same font used as the body font and displayed in a WKWebView.

This was all working well for years. When macOS 14 came along, what I find is that option 1 (initializing an NSAttributedString with some text using the Candara font) works fine, as does option 3 (displaying a lot of HTML that uses the Candara font in a WKWebView). But option 2 doesn't work at all. The output is all the "undefined character" glyph — a box containing a question mark, one for each character in the string.

I'm seeing this behavior when the user selects Calibri or Candara, which are both Microsoft fonts. I'm also seeing it with SF Pro, which is an Apple font. Any other font is fine. (At least from what I've seen so far.)

Providing a fallback font (as shown above with "serif") doesn't change anything. The rendering does not fall back to the fallback font; it seems to really think it can use Candara when obviously it can't.

This behavior very definitely appeared with the introduction of macOS 14, and mostly affects those who have chosen one of the Microsoft Office fonts (Calibri and Candara for sure). But as I said, other fonts can produce these results.


Solution

  • Definitively this looks like a bug, some fonts exhibit the buggy behavior some don't. For example non-existing fonts, or maybe fonts with typos in names, work just file, the system falls back to the Times New Roman font.

    Printing a problematic attributed string outputs this:

    > po attributedString
    
    Text with bold or italics maybe.
    {
        NSFont = "\"LastResort 14.00 pt. P [] (0x7ff575f14980) fobj=0x7ff575f14980, spc=16.06\"";
        NSKern = 0;
        NSParagraphStyle = "Alignment Natural, LineSpacing 0, ParagraphSpacing 14, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode WordWrapping, Tabs (\n), DefaultTabInterval 36, Blocks (\n), Lists (\n), BaseWritingDirection LeftToRight, HyphenationFactor 0, TighteningForTruncation YES, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint ''";
        NSStrokeColor = "sRGB IEC61966-2.1 colorspace 0 0 0 1";
        NSStrokeWidth = 0;
    }
    

    For some fonts (or perhaps some string values), macOS falls back to Apple's Last Resort font font. This is clearly the incorrect behaviour for some of the existing fonts, and this is why you see those odd glyphs, as macOS incorrectly maps the font to the LastResort one, instead to the specified font, or at least to the second font specified in the CSS rule.