CTFontGetGlyphsForCharacters Returns false when passing emoji - swift

In a class conforming to NSLayoutManagerDelegate I implement this method:
func layoutManager(_ layoutManager: NSLayoutManager,
shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>,
properties props: UnsafePointer<NSLayoutManager.GlyphProperty>,
characterIndexes charIndexes: UnsafePointer<Int>,
font aFont: UIFont,
forGlyphRange glyphRange: NSRange) -> Int {
// First, make sure we'll be able to access the NSTextStorage.
guard let textStorage = layoutManager.textStorage
else { return 0 }
// Get the first and last characters indexes for this glyph range,
// and from that create the characters indexes range.
let firstCharIndex = charIndexes[0]
let lastCharIndex = charIndexes[glyphRange.length - 1]
let charactersRange = NSRange(location: firstCharIndex, length: lastCharIndex - firstCharIndex + 1)
var bulletPointRanges = [NSRange]()
var hiddenRanges = [NSRange]()
let finalGlyphs = UnsafeMutablePointer<CGGlyph>(mutating: glyphs)
// Generate the Middle Dot glyph using aFont.
let middleDot: [UniChar] = [0x00B7] // Middle Dot: U+0x00B7
var myGlyphs: [CGGlyph] = [0]
// Get glyphs for `middleDot` character
guard CTFontGetGlyphsForCharacters(aFont, middleDot, &myGlyphs, middleDot.count) == true
else { fatalError("Failed to get the glyphs for characters \(middleDot).") }
}
The problem is that CTFontGetGlyphsForCharacters returns false when I type an emoji into the textview. I think it might have something to do with UTF-8 vs. UTF-16 but I'm kind of out of my depth a little here. Little help?

The font you are using does not have a glyph for that particular character.
The system maintains a list of "font fallbacks" for times when the specific font you are trying to look at does not have a glyph but another font might.
The list of fallbacks is given by CTFontCopyDefaultCascadeListForLanguages, but since you're at the point where you are being asked for the glyph from a particular font, it seems that fallback generation should be handled higher up in the chain.
You should probably return 0 to indicate that the layout manager should use it's default behavior.

Related

Force page breaks and text containers using NSLayoutManager

I'm trying to implement pagination using NSLayoutManager and multiple text containers. Creating NSTextView/UITextView instances works as expected, and the text flows from text view to another.
However, I'd like to force a page break, ie. determine myself where to break onto the next container. I'm working with parsed content, so I can't insert any additional ASCII control characters into the text.
I tried subclassing NSLayoutManager and overriding textContainer(glyphIndex:, effectiveRange:), and here's a very brute-force example:
override func textContainer(forGlyphAt glyphIndex: Int, effectiveRange effectiveGlyphRange: NSRangePointer?) -> NSTextContainer? {
if glyphIndex > 100 {
return self.textContainers[1]
} else {
return super.textContainer(forGlyphAt: glyphIndex, effectiveRange: effectiveGlyphRange)
}
}
I'd expect this to move any glyphs after 100th index onto the second container, but the results are weird:
I suppose I'd have to subclass NSTextContainer and tell the layout manager that it's already full of text. It has a method called lineFragmentRect(forProposedRect:at:writingDirection:remaining:) but the documentation is very sparse, and I can't find any working examples.
Existing documentation around displaying text is very outdated, so any ideas or hints are welcome. I'm very confused about this, but still hopeful there is a simple way of telling the layout manager where to cut off content in each container.
One possible solution
NSTextContainer.exclusionPaths could be used to rule out the rest of the possible space in containers.
let glyphIndex = layoutManager.glyphIndexForCharacter(at: 100)
var rect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
// Get the line range and used rect
var lineRange = NSMakeRange(NSNotFound, 0)
var usedRect = layoutManager.lineFragmentUsedRect(forGlyphAt: gi, effectiveRange: NSRangePointer(&lineRange))
// Calculate the remainder of the line
let remainder = NSRange(location: glyphIndex, length: NSMaxRange(lineRange) - glyphIndex)
var rectCount:Int = 0
var breakRects = layoutManager.rectArray(forGlyphRange: remainder, withinSelectedGlyphRange: remainder, in: textContainer1, rectCount: UnsafeMutablePointer(&rectCount))
// Create the rect for the remainder of the line
var lineRect = breakRect!.pointee
lineRect.size.width = textContainer1.size.width - lineRect.origin.x
// Then create a rect to cover up the rest
var coverRest = NSMakeRect(0, lineRect.origin.y + lineRect.height, textContainer1.size.width, CGFloat.greatestFiniteMagnitude)
// Add exclusion paths
textContainer1.exclusionPaths = [NSBezierPath(rect: lineRect), NSBezierPath(rect: coverRest)]
This results in expected behavior:
This requires a ton of calculations, and text containers with exclusion paths are noticeably slower. The app could potentially have hundreds of text views, which makes this quite inefficient.

How to get all characters of the font with CTFontCopyCharacterSet() in Swift?

How does one get all characters of the font with CTFontCopyCharacterSet() in Swift? ... for macOS?
The issue occured when implementing the approach from an OSX: CGGlyph to UniChar answer in Swift.
func createUnicodeFontMap() {
// Get all characters of the font with CTFontCopyCharacterSet().
let cfCharacterSet: CFCharacterSet = CTFontCopyCharacterSet(ctFont)
//
let cfCharacterSetStr = "\(cfCharacterSet)"
print("CFCharacterSet: \(cfCharacterSet)")
// Map all Unicode characters to corresponding glyphs
var unichars = [UniChar](…NYI…) // NYI: lacking unichars for CFCharacterSet
var glyphs = [CGGlyph](repeating: 0, count: unichars.count)
guard CTFontGetGlyphsForCharacters(
ctFont, // font: CTFont
&unichars, // characters: UnsafePointer<UniChar>
&glyphs, // UnsafeMutablePointer<CGGlyph>
unichars.count // count: CFIndex
)
else {
return
}
// For each Unicode character and its glyph,
// store the mapping glyph -> Unicode in a dictionary.
// ... NYI
}
What to do with CFCharacterSet to retrieve the actual characters has been elusive. Autocompletion of the cfCharacterSet instance offers show no relavant methods.
And the Core Foundation > CFCharacterSet appears have methods for creating another CFCharacterSet, but not something the provides an array|list|string of unichars to be able to create a mapped dictionary.
Note: I'm looking for a solution which is not specific to iOS as in Get all available characters from a font which uses UIFont.
CFCharacterSet is toll-free bridged with the Cocoa Foundation counterpart NSCharacterSet, and can be bridged to the corresponding Swift value type CharacterSet:
let charset = CTFontCopyCharacterSet(ctFont) as CharacterSet
Then the approach from NSArray from NSCharacterSet can be used to enumerate all Unicode scalar values of that character set (including non-BMP points, i.e. Unicode scalar values greater than U+FFFF).
The CTFontGetGlyphsForCharacters() expects non-BMP characters as surrogate pair, i.e. as an array of UTF-16 code units.
Putting it together, the function would look like this:
func createUnicodeFontMap(ctFont: CTFont) -> [CGGlyph : UnicodeScalar] {
let charset = CTFontCopyCharacterSet(ctFont) as CharacterSet
var glyphToUnicode = [CGGlyph : UnicodeScalar]() // Start with empty map.
// Enumerate all Unicode scalar values from the character set:
for plane: UInt8 in 0...16 where charset.hasMember(inPlane: plane) {
for unicode in UTF32Char(plane) << 16 ..< UTF32Char(plane + 1) << 16 {
if let uniChar = UnicodeScalar(unicode), charset.contains(uniChar) {
// Get glyph for this `uniChar` ...
let utf16 = Array(uniChar.utf16)
var glyphs = [CGGlyph](repeating: 0, count: utf16.count)
if CTFontGetGlyphsForCharacters(ctFont, utf16, &glyphs, utf16.count) {
// ... and add it to the map.
glyphToUnicode[glyphs[0]] = uniChar
}
}
}
}
return glyphToUnicode
}
You can do something like this.
let cs = CTFontCopyCharacterSet(font) as NSCharacterSet
let bitmapRepresentation = cs.bitmapRepresentation
The format of the bitmap is defined in the reference page for CFCharacterSetCreateWithBitmapRepresentation

How to get the range of the first line in a string?

I would like to change the formatting of the first line of text in an NSTextView (give it a different font size and weight to make it look like a headline). Therefore, I need the range of the first line. One way to go is this:
guard let firstLineString = textView.string.components(separatedBy: .newlines).first else {
return
}
let range = NSRange(location: 0, length: firstLineString.count)
However, I might be working with quite long texts so it appears to be inefficient to first split the entire string into line components when all I need is the first line component. Thus, it seems to make sense to use the firstIndex(where:) method:
let firstNewLineIndex = textView.string.firstIndex { character -> Bool in
return CharacterSet.newlines.contains(character)
}
// Then: Create an NSRange from 0 up to firstNewLineIndex.
This doesn't work and I get an error:
Cannot convert value of type '(Unicode.Scalar) -> Bool' to expected argument type 'Character'
because the contains method accepts not a Character but a Unicode.Scalar as a parameter (which doesn't really make sense to me because then it should be called a UnicodeScalarSet and not a CharacterSet, but nevermind...).
My question is:
How can I implement this in an efficient way, without first slicing the whole string?
(It doesn't necessarily have to use the firstIndex(where:) method, but appears to be the way to go.)
A String.Index range for the first line in string can be obtained with
let range = string.lineRange(for: ..<string.startIndex)
If you need that as an NSRange then
let nsRange = NSRange(range, in: string)
does the trick.
You can use rangeOfCharacter, which returns the Range<String.Index> of the first character from a set in your string:
extension StringProtocol where Index == String.Index {
var partialRangeOfFirstLine: PartialRangeUpTo<String.Index> {
return ..<(rangeOfCharacter(from: .newlines)?.lowerBound ?? endIndex)
}
var rangeOfFirstLine: Range<Index> {
return startIndex..<partialRangeOfFirstLine.upperBound
}
var firstLine: SubSequence {
return self[partialRangeOfFirstLine]
}
}
You can use it like so:
var str = """
some string
with new lines
"""
var attributedString = NSMutableAttributedString(string: str)
let firstLine = NSAttributedString(string: String(str.firstLine))
// change firstLine as you wish
let range = NSRange(str.rangeOfFirstLine, in: str)
attributedString.replaceCharacters(in: range, with: firstLine)

How to stop NSTextView from displaying characters that are not supported in a specified font

I am looking for a way to stop an NSTextView from falling back on a cascading font when certain characters in the display string are unavailable in the specified font.
I am writing an app in which it is important that the user know if certain characters (those in East Asian character sets, for example) are available in a given font. If these characters are not available, I want the field to display these as boxes, blank spaces, or something similar. However, the default is for the textview to fall back on a cascading font in which the characters are supported. Is there any way to change this behavior?
If this is not possible, might there be some way to detect which languages are supported by a given font?
Any help would be greatly appreciated.
I was able to make it work by creating extensions partially based on this answer.
The solution I ended up using was to iterate through each character in the string in question and check to see if it is contained in the font's character set. If it is not, the appearance is changed (in this case, changing the color and adding strikethrough) before it is added to an attributed string containing all of characters of the original string.
This is what it ended up looking like:
extension UnicodeScalar {
func isIn(font:NSFont)-> Bool {
let coreFont:CTFont = font
let charSet:CharacterSet = CTFontCopyCharacterSet(coreFont) as CharacterSet
if charSet.contains(self) {
return true
}
return false
}
}
extension NSFont {
func supportString(_ str:String)-> NSAttributedString {
let attString = NSMutableAttributedString(string: "")
for scalar in str.unicodeScalars {
if !scalar.isIn(font: self) {
let r:CGFloat = 200
let g:CGFloat = 0
let b:CGFloat = 0
let a:CGFloat = 0.2
let color = NSColor(red: r, green: g, blue: b, alpha: a)
let attcha = NSMutableAttributedString(string: String(scalar), attributes: [NSAttributedStringKey.foregroundColor:color, NSAttributedStringKey.strikethroughStyle:NSUnderlineStyle.styleSingle.rawValue])
attString.append(attcha)
} else {
let attcha = NSAttributedString(string: String(scalar), attributes: [NSAttributedStringKey.foregroundColor:NSColor.black])
attString.append(attcha)
}
}
return attString
}
}

Get all available characters from a font

I am developing an iOS app in Swift 3.
In this app I am listing all available fonts (system provided) but I would like to list all available characters for them too.
For example I am using Font Awesome to and I want the user to be able to select any of the characters/symbols from a list. How can I do this?
This is how I get an array of the fonts. How can I get an array of all characters for a selected font?
UIFont.familyNames.map({ UIFont.fontNames(forFamilyName: $0)}).reduce([]) { $0 + $1 }
For each UIFont, you have to get characterSet of that font. For example, I take first UIFont.
let firsttFont = UIFont.familyNames.first
let first = UIFont(name: firsttFont!, size: 14)
let fontDescriptor = first!.fontDescriptor
let characterSet : NSCharacterSet = fontDescriptor.object(forKey: UIFontDescriptorCharacterSetAttribute) as! NSCharacterSet
Then, use this extension to get all characters of that NSCharacterSet:
extension NSCharacterSet {
var characters:[String] {
var chars = [String]()
for plane:UInt8 in 0...16 {
if self.hasMemberInPlane(plane) {
let p0 = UInt32(plane) << 16
let p1 = (UInt32(plane) + 1) << 16
for c:UTF32Char in p0..<p1 {
if self.longCharacterIsMember(c) {
var c1 = c.littleEndian
let s = NSString(bytes: &c1, length: 4, encoding: String.Encoding.utf32LittleEndian.rawValue)!
chars.append(String(s))
}
}
}
}
return chars
}
}
(Ref: NSArray from NSCharacterset)
So, at last, just call characterSet.characters to get all characters (in String)
I don't think you'll be able to without a lot of coding. Here's a few links to Apple documentation:
In their main font page you'll have to scroll down a bit to get to a list of documentation, but in that list is their TrueType reference manual. The characters are stored as glyphs, meaning they are vector-based to allow for clean font sizes. (I believe the simple drop-down of font sizes in IB are merely "suggestions", and you can type in any size you care to.)
In that second link, scroll down to the lengthy list of font tables. One looks promising - the cmap table. But reading through this, it's possible to (a) have foreign characters like "umlaut A" or Chinese and (b) omit characters in each font. Also, this is just a lookup table - you'll then maybe need to use the mapping table to get the location of the glyph.
If you are targeting English only, you might be better off finding a way to check if the letters "Aa" exist for the font and display them.