Is there a way to get all the glyphs of a UIFont that contain a true descender? It seems that using CTLineGetTypographicBounds is not accurate and returns the exact same descent value for every line. I thought it would provide the information that I needed but it did not. So now I am looking to see if I can build a character set from the glyphs that contain true descenders unless there is another way. The ultimate goal would be able to see if a line of text is below the baseline.
let line = CTLineCreateWithAttributedString(NSAttributedString(string: s, attributes: attr))
//let's get the real descent test
var a : CGFloat = 0
var d : CGFloat = 0
var l : CGFloat = 0
let bounds = CTLineGetTypographicBounds(line, &a, &d, &l)
print("the descent is \(d)")
print("the ascent is \(a)")
print("the leading is \(l)")
Since it seems that your actual goal is to determine whether a string contains a character with a descender, you can use Core Text to look at the bounding rect of each glyph. If the bounding rect's origin in negative, this means the glyph starts below the baseline. This will be true for characters such as y but also ,.
func checkDescender(string: String) {
let uiFont = UIFont.systemFont(ofSize: 14) // Pick your font
let font = CTFontCreateWithName(uiFont.fontName as CFString, uiFont.pointSize, nil)
for ch in string.unicodeScalars {
let utf16codepoints = Array(ch.utf16)
var glyphs: [CGGlyph] = [0, 0]
let hasGlyph = CTFontGetGlyphsForCharacters(font, utf16codepoints, &glyphs, utf16codepoints.count)
if hasGlyph {
let rect = CTFontGetBoundingRectsForGlyphs(font, .default, glyphs, nil, 1)
// print("\(ch) has bounding box of \(rect)")
if rect.origin.y < 0 {
print("\(ch) goes below the baseline by \(-rect.origin.y)")
}
}
}
}
checkDescender(string: "Ymy,")
You might want to add additional checks to only look at letters depending on your needs.
Related
The app I'm working on supports hundreds of different fonts. Some of these fonts, particularly the script fonts, have significant ascenders and descenders. When sizeToFit() is called on a UITextView with some of these fonts, I end up with significant top and bottom padding (the image on the left). The goal is to end up with the image on the right, such that the tallest glyph is aligned flush with the top of the text view's bounding box.
Here's the log for the image above:
Point Size: 59.0
Ascender: 70.21
Descender: -33.158
Line Height: 103.368
Leading: 1.416
TextView Height: 105.0
My first thought was to look at the height of each glyph in the first line of text, and then calculate the offset between the top of the container and the top of the tallest glyph. Then I could use textContainerInset to adjust the top margin accordingly.
I tried something like this in my UITextView subclass:
for location in 0 ..< lastGlyphIndexInFirstLine {
let glphyRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: location, length: 1), in: self.textContainer)
print(glphyRect.size.height) // prints 104.78399999999999 for each glyph
}
Unfortunately, this doesn't work because boundRect(forGlyphRange:in:) doesn't appear to return the rect of the glyph itself (I'm guessing this is always the same value because it's returning the height of the line fragment?).
Is this the simplest way to solve this problem? If it is, how can I calculate the distance between the top of the text view and the top of the tallest glyph in the first line of text?
This doesn't appear to be possible using TextKit, but it is possible using CoreText directly. Specifically, CGFont's getGlyphBBoxes returns the correct rect in glyph space units, which can then be converted to points relative to the font size.
Credit goes to this answer for making me aware of getGlyphBBoxes as well as documenting how to convert the resulting rects to points.
Below is the complete solution. This assumes you have a UITextView subclass with the following set beforehand:
self.contentInset = .zero
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0.0
This function will now return the distance from the top of the text view's bounds to the top of the tallest used glyph:
private var distanceToGlyphs: CGFloat {
// sanity
guard
let font = self.font,
let fontRef = CGFont(font.fontName as CFString),
let attributedText = self.attributedText,
let firstLine = attributedText.string.components(separatedBy: .newlines).first
else { return 0.0 }
// obtain the first line of text as an attributed string
let attributedFirstLine = attributedText.attributedSubstring(from: NSRange(location: 0, length: firstLine.count)) as CFAttributedString
// create the line for the first line of attributed text
let line = CTLineCreateWithAttributedString(attributedFirstLine)
// get the runs within this line (there will typically only be one run when using a single font)
let glyphRuns = CTLineGetGlyphRuns(line) as NSArray
guard let runs = glyphRuns as? [CTRun] else { return 0.0 }
// this will store the maximum distance from the baseline
var maxDistanceFromBaseline: CGFloat = 0.0
// iterate each run
for run in runs {
// get the total number of glyphs in this run
let glyphCount = CTRunGetGlyphCount(run)
// initialize empty arrays of rects and glyphs
var rects = Array<CGRect>(repeating: .zero, count: glyphCount)
var glyphs = Array<CGGlyph>(repeating: 0, count: glyphCount)
// obtain the glyphs
self.layoutManager.getGlyphs(in: NSRange(location: 0, length: glyphCount), glyphs: &glyphs, properties: nil, characterIndexes: nil, bidiLevels: nil)
// obtain the rects per-glyph in "glyph space units", each of which needs to be scaled using units per em and the font size
fontRef.getGlyphBBoxes(glyphs: &glyphs, count: glyphCount, bboxes: &rects)
// iterate each glyph rect
for rect in rects {
// obtain the units per em from the font ref so we can convert the rect
let unitsPerEm = CGFloat(fontRef.unitsPerEm)
// sanity to prevent divide by zero
guard unitsPerEm != 0.0 else { continue }
// calculate the actual distance up or down from the glyph's baseline
let glyphY = (rect.origin.y / unitsPerEm) * font.pointSize
// calculate the actual height of the glyph
let glyphHeight = (rect.size.height / unitsPerEm) * font.pointSize
// calculate the distance from the baseline to the top of the glyph
let glyphDistanceFromBaseline = glyphHeight + glyphY
// store the max distance amongst the glyphs
maxDistanceFromBaseline = max(maxDistanceFromBaseline, glyphDistanceFromBaseline)
}
}
// the final top margin, calculated by taking the largest ascender of all the glyphs in the font and subtracting the max calculated distance from the baseline
return font.ascender - maxDistanceFromBaseline
}
You can now set the text view's top contentInset to -distanceToGlyphs to achieve the desired result.
I've been playing around with attributed text in a UITextView (Swift 4.2 and noticed that once I introduced "paragraphSpacingBefore" into my design, the Caret becmae too large on the first line of each new paragraph.
I found this suggested fix on Stackoverflow which seemed to work ok to fix the caret size. The problem I found was the caret itself floats above the target line when that line was the start of a new paragraph.
UITextView lineSpacing make cursor height not same
Caret Floats above the target line
I tried solving it, maintaining the core idea of the original solution and adding some offset logic. During debugging I noticed that the original answer for caret size always adjusts the size even when not required so I added a variance filter (only adjust if variance > 10%). Did this because I think adjusting every time will interfere with my soln. to the floating caret problem.
If someone can take a look at my proposed approach, suggest improvements or a better way etc i'd be grateful:
override func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else {
return superRect
}
let proposedHeight: CGFloat = isFont.pointSize - isFont.descender
var delta: CGFloat = superRect.size.height - proposedHeight
delta = (delta * delta).squareRoot()
//If the delta is < 10% of the original height just return the original rect
if delta / superRect.size.height < 0.1 {
return superRect
}
superRect.size.height = isFont.pointSize - isFont.descender
// "descender" is expressed as a negative value,
// so to add its height you must subtract its value
superRect.origin.y = superRect.origin.y + delta
// delta is used to correct for resized caret floating above the target line
return superRect
}
I got a solution:
// Fix long cursor height when at the end of paragraph with paragraphspacing and wrong cursor position in titles with paragraph spacing before
override public func caretRect(for position: UITextPosition) -> CGRect {
var superRect = super.caretRect(for: position)
guard let isFont = self.font else { return superRect }
let location = self.offset(from: self.beginningOfDocument, to: position)
if let paragrahStyle = self.storage.attribute(.paragraphStyle, at: location, effectiveRange: nil) as? NSParagraphStyle {
superRect.origin.y += paragrahStyle.paragraphSpacingBefore
}
superRect.size.height = isFont.pointSize - isFont.descender
return superRect
}
The real problem paragraphSpacingBefore. So all you have to do is to get the paragraph styling attributes, get the spacing and move the cursor by that spacing. This works well with all the text.
I would like to display math terms inside a text, in particular in an inline mode, i.e. inside a sentence.
Using LaTeX, this would for example look like:
"Given a right triangle having catheti of length \(a\) resp. \(b\) and a hypotenuse of length \(c\), we have
\[a^2 + b^2 = c^2.\]
This fact is known as the Pythagorean theorem."
Does anybody know how this can be achieved in Swift?
(I know that this example may be achieved in Swift without LaTeX-like tools. However, the expressions in my mind are in fact more complex than in this example, I do need the power of LaTeX.)
The optimal way would be a UITextView-like class which recognizes the math delimiters \(,\) resp. \[,\], recognizes LaTeX code inside these delimiters, and formats the text accordingly.
In the Khan Academy app, this problem seems to be solved as the screenshots in the Apple App Store/Google Play Store show inline (LaTeX) math.
I’ve found the package iosMath which provides a UILabel-like class MTMathUILabel. As this class can display solely formulas, this seems to be not good enough for my purpose, except if there was a method which takes a LaTeX source text such as in the example above, formats expressions such as \(a\) into tiny MTMathUILabels and sets these labels between the other text components. As I am new to Swift, I do not know whether and how this can be achieved. Moreover, this seems to be very difficult from a typographical point of view as there will surely occur difficulties with line breaks. And there might occur performance issues if there are a large number of such labels on the screen at the same time?
It is possible to achieve what I want using a WKWebView and MathJax or KaTeX, which is also a hack, of course. This leads to other difficulties, e.g. if one wants to set several of these WKWebViews on a screen, e.g. inside UITableViewCells.
Using iosMath, my solution on how to get a UILabel to have inline LaTeX is to include LATEX and ENDLATEX markers with no space. I replaced all ranges with an image of the MTMathUILabel, going from last range to first range so the positions don't get screwed up (This solution allows for multiple markers). The image returned from my function is flipped so i used .downMirrored orientation, and i sized it to fit my text, so you might need to fix the numbers a little for the flip scale of 2.5 and the y value for the attachment.bounds.
import UIKit
import iosMath
let question = UILabel()
let currentQuestion = "Given a right triangle having catheti of length LATEX(a)ENDLATEX resp. LATEX(b)ENDLATEX and a hypotenuse of length LATEX(c)ENDLATEX, we have LATEX[a^2 + b^2 = c^2]ENDLATEX. This fact is known as the Pythagorean theorem."
question.text = currentQuestion
if (question.text?.contains("LATEX"))! {
let tempString = question.text!
let tempMutableString = NSMutableAttributedString(string: tempString)
let pattern = NSRegularExpression.escapedPattern(for: "LATEX")
let regex = try? NSRegularExpression(pattern: pattern, options: [])
if let matches = regex?.matches(in: tempString, options: [], range: NSRange(location: 0, length: tempString.count)) {
var i = 0
while i < matches.count {
let range1 = matches.reversed()[i+1].range
let range2 = matches.reversed()[i].range
let finalDistance = range2.location - range1.location + 5
let finalRange = NSRange(location: range1.location, length: finalDistance)
let startIndex = String.Index(utf16Offset: range1.location + 5, in: tempString)
let endIndex = String.Index(utf16Offset: range2.location - 3, in: tempString)
let substring = String(tempString[startIndex..<endIndex])
var image = UIImage()
image = imageWithLabel(string: substring)
let flip = UIImage(cgImage: image.cgImage!, scale: 2.5, orientation: .downMirrored)
let attachment = NSTextAttachment()
attachment.image = flip
attachment.bounds = CGRect(x: 0, y: -flip.size.height/2 + 10, width: flip.size.width, height: flip.size.height)
let replacement = NSAttributedString(attachment: attachment)
tempMutableString.replaceCharacters(in: finalRange, with: replacement)
question.attributedText = tempMutableString
i += 2
}
}
}
func imageWithLabel(string: String) -> UIImage {
let label = MTMathUILabel()
label.latex = string
label.sizeToFit()
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
label.layer.render(in: UIGraphicsGetCurrentContext()!)
return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}
Some unicode characters cannot be displayed on iOS but are displayed correctly on macOS. Similarly, some unicode characters that iOS can display cannot be displayed on watchOS. This is due to different built-in fonts installed on these platforms.
When a character cannot be displayed it appears as a ? inside a box, like so:
I've also seen some characters display as an alien instead (not sure why the difference):
Is there a way to know when a specific unicode character will not be displayed properly given a string of the unicode character such as "ᄥ"?
I am in need of a solution that works for both iOS and watchOS.
You can use CTFontGetGlyphsForCharacters() to determine if a font has a glyph for a particular code point (note that supplementary characters need to be checked as surrogate pairs):
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 12, NULL);
const UniChar code_point[] = { 0xD83C, 0xDCA1 }; // U+1F0A1
CGGlyph glyph[] = { 0, 0 };
bool has_glyph = CTFontGetGlyphsForCharacters(font, code_point, glyph, 2);
Or, in Swift:
let font = CTFontCreateWithName("Helvetica", 12, nil)
var code_point: [UniChar] = [0xD83C, 0xDCA1]
var glyphs: [CGGlyph] = [0, 0]
let has_glyph = CTFontGetGlyphsForCharacters(font, &code_point, &glyph, 2)
If you want to check the complete set of fallback fonts that the system will try to load a glyph from, you will need to check all of the fonts returned by CTFontCopyDefaultCascadeListForLanguages(). Check the answer to this question for information on how the fallback font list is created.
Compare against the known, undefined character U+1FFF:
/// - Parameter font: a UIFont
/// - Returns: true if glyph exists
func glyphAvailable(forFont font:UIFont) -> Bool {
if let refUnicodePng = Character("\u{1fff}").png(forFont: font),
let myPng = self.png(forFont: font) {
return refUnicodePng != myPng
}
return false
}
using a png bitmap:
/// - Parameter font: a UIFont
/// - Returns: an optional png representation
func png(forFont font: UIFont) -> Data? {
let attributes = [NSAttributedStringKey.font: font]
let charStr = "\(self)" as NSString
let size = charStr.size(withAttributes: attributes)
UIGraphicsBeginImageContext(size)
charStr.draw(at: CGPoint(x: 0,y :0), withAttributes: attributes)
var png:Data? = nil
if let charImage = UIGraphicsGetImageFromCurrentImageContext() {
png = UIImagePNGRepresentation(charImage)
}
UIGraphicsEndImageContext()
return png
}
Answered here.
Some unicode characters cannot be displayed on iOS but are displayed correctly on macOS. Similarly, some unicode characters that iOS can display cannot be displayed on watchOS. This is due to different built-in fonts installed on these platforms.
When a character cannot be displayed it appears as a ? inside a box, like so:
I've also seen some characters display as an alien instead (not sure why the difference):
Is there a way to know when a specific unicode character will not be displayed properly given a string of the unicode character such as "ᄥ"?
I am in need of a solution that works for both iOS and watchOS.
You can use CTFontGetGlyphsForCharacters() to determine if a font has a glyph for a particular code point (note that supplementary characters need to be checked as surrogate pairs):
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 12, NULL);
const UniChar code_point[] = { 0xD83C, 0xDCA1 }; // U+1F0A1
CGGlyph glyph[] = { 0, 0 };
bool has_glyph = CTFontGetGlyphsForCharacters(font, code_point, glyph, 2);
Or, in Swift:
let font = CTFontCreateWithName("Helvetica", 12, nil)
var code_point: [UniChar] = [0xD83C, 0xDCA1]
var glyphs: [CGGlyph] = [0, 0]
let has_glyph = CTFontGetGlyphsForCharacters(font, &code_point, &glyph, 2)
If you want to check the complete set of fallback fonts that the system will try to load a glyph from, you will need to check all of the fonts returned by CTFontCopyDefaultCascadeListForLanguages(). Check the answer to this question for information on how the fallback font list is created.
Compare against the known, undefined character U+1FFF:
/// - Parameter font: a UIFont
/// - Returns: true if glyph exists
func glyphAvailable(forFont font:UIFont) -> Bool {
if let refUnicodePng = Character("\u{1fff}").png(forFont: font),
let myPng = self.png(forFont: font) {
return refUnicodePng != myPng
}
return false
}
using a png bitmap:
/// - Parameter font: a UIFont
/// - Returns: an optional png representation
func png(forFont font: UIFont) -> Data? {
let attributes = [NSAttributedStringKey.font: font]
let charStr = "\(self)" as NSString
let size = charStr.size(withAttributes: attributes)
UIGraphicsBeginImageContext(size)
charStr.draw(at: CGPoint(x: 0,y :0), withAttributes: attributes)
var png:Data? = nil
if let charImage = UIGraphicsGetImageFromCurrentImageContext() {
png = UIImagePNGRepresentation(charImage)
}
UIGraphicsEndImageContext()
return png
}
Answered here.