How to convert a Swift String to an array of CGGlyph

2.9k Views Asked by At

This snippet can be used for drawing CGGlyphs with a CGContext:

//drawing
let coreGraphicsFont = CTFontCopyGraphicsFont(coreTextFont, nil)
CGContextSetFont(context, coreGraphicsFont);
CGContextSetFontSize(context, CTFontGetSize(coreTextFont))
CGContextSetFillColorWithColor(context, Color.blueColor().CGColor)          
CGContextShowGlyphsAtPositions(context, glyphs, positions, length)

But how do I obtain the CGGlyphs from a Swift string which contains emoji symbols like flags or accented characters?

let string = "swift: \u{1F496} \u{65}\u{301} \u{E9}\u{20DD} \u{1F1FA}\u{1F1F8}"

Neither of the below approaches shows the special characters, even though they are correctly printed to the console. Note that this first approach returns NSGlyph but CGGlyphs are required for drawing:

var progress = CGPointZero
for character in string.characters
{
    let glyph = font.glyphWithName(String(character))
    glyphs.append(CGGlyph(glyph))
    let advancement = font.advancementForGlyph(glyph)
    positions.append(progress)
    progress.x += advancement.width
}

or this second approach which requires casting to NSString:

var buffer = Array<unichar>(count: length, repeatedValue: 0)
let range = NSRange(location: 0, length: length)
(string as NSString).getCharacters(&buffer, range: range)

glyphs = Array<CGGlyph>(count: length, repeatedValue: 0)
CTFontGetGlyphsForCharacters(coreTextFont, &buffer, &glyphs, length)

//glyph positions
advances = Array<CGSize>(count: length, repeatedValue: CGSize.zero)
CTFontGetAdvancesForGlyphs(ctFont, CTFontOrientation.Default, glyphs, &advances, length)
positions = []
var progress = CGPointZero
for advance in advances
{
    positions.append(progress)
    progress.x += advance.width
}

Some of the characters are drawn as empty boxes with either approach. Kinda stuck here, hoping you can help.

Edit:

Using CTFontDrawGlyphs renders the glyphs correctly, but setting the font, size and text matrix directly before calling CGContextShowGlyphsAtPositions draws nothing. I find that rather odd.

1

There are 1 best solutions below

2
On

If you generate glyphs yourself, you also need to perform font substitution yourself. When you use Core Text or TextKit to lay out and draw the text, they perform font substitution for you. For example:

let richText = NSAttributedString(string: "Hello→")
let line = CTLineCreateWithAttributedString(richText)
print(line)

Output:

<CTLine: 0x7fa349505f00>{run count = 3, string range = (0, 8), width = 55.3457, A/D/L = 15/4.6875/0, glyph count = 7, runs = (

<CTRun: 0x7fa34969f600>{string range = (0, 5), string = "Hello", attributes = <CFBasicHash 0x7fa3496902d0 [0x10e85a7b0]>{type = mutable dict, count = 1,
entries =>
    2 : <CFString 0x1153bb720 [0x10e85a7b0]>{contents = "NSFont"} = <CTFont: 0x7fa3496182f0>{name = Helvetica, size = 12.000000, matrix = 0x0, descriptor = <CTFontDescriptor: 0x7fa34968f860>{attributes = <CFBasicHash 0x7fa34968f8b0 [0x10e85a7b0]>{type = mutable dict, count = 1,
entries =>
    2 : <CFString 0x1153c16c0 [0x10e85a7b0]>{contents = "NSFontNameAttribute"} = <CFString 0x1153b4700 [0x10e85a7b0]>{contents = "Helvetica"}
}
>}}
}
}


<CTRun: 0x7fa3496cde40>{string range = (5, 2), string = "\U0001F600", attributes = <CFBasicHash 0x7fa34b11a150 [0x10e85a7b0]>{type = mutable dict, count = 1,
entries =>
    2 : <CFString 0x1153bb720 [0x10e85a7b0]>{contents = "NSFont"} = <CTFont: 0x7fa3496c3eb0>{name = AppleColorEmoji, size = 12.000000, matrix = 0x0, descriptor = <CTFontDescriptor: 0x7fa3496a3c30>{attributes = <CFBasicHash 0x7fa3496a3420 [0x10e85a7b0]>{type = mutable dict, count = 1,
entries =>
    2 : <CFString 0x1153c16c0 [0x10e85a7b0]>{contents = "NSFontNameAttribute"} = <CFString 0x11cf63bb0 [0x10e85a7b0]>{contents = "AppleColorEmoji"}
}
>}}
}
}


<CTRun: 0x7fa3496cf3e0>{string range = (7, 1), string = "\u2192", attributes = <CFBasicHash 0x7fa34b10ed00 [0x10e85a7b0]>{type = mutable dict, count = 1,
entries =>
    2 : <CFString 0x1153bb720 [0x10e85a7b0]>{contents = "NSFont"} = <CTFont: 0x7fa3496cf2c0>{name = PingFangSC-Regular, size = 12.000000, matrix = 0x0, descriptor = <CTFontDescriptor: 0x7fa3496a45a0>{attributes = <CFBasicHash 0x7fa3496a5660 [0x10e85a7b0]>{type = mutable dict, count = 1,
entries =>
    2 : <CFString 0x1153c16c0 [0x10e85a7b0]>{contents = "NSFontNameAttribute"} = <CFString 0x11cf63230 [0x10e85a7b0]>{contents = "PingFangSC-Regular"}
}
>}}
}
}

)
}

We can see here that Core Text recognized that the default font (Helvetica) doesn't have glyphs for the emoji or the arrow, so it split the line into three runs, each with the needed font.

The Core Text Programming Guide says this:

Most of the time you should just use a CTLine object to get this information because one font may not encode the entire string. In addition, simple character-to-glyph mapping will not get the correct appearance for complex scripts. This simple glyph mapping may be appropriate if you are trying to display specific Unicode characters for a font.

Your best bet is to use CTLineCreateWithAttributedString to generate glyphs and choose fonts. Then, if you want to adjust the position of the glyphs, use CTLineGetGlyphRuns to get the runs out of the line, and then ask the run for the glyphs, the font, and whatever else you need.

If you want to handle font substitution yourself, I think you're going to want to look into “font cascading”.