I'm using an NSMutableAttribtuedString
in order to build a string with formatting, which I then pass to Core Text to render into a frame. The problem is, that I need to use superscript and subscript. Unless these characters are available in the font (most fonts don't support it), then setting the property kCTSuperscriptAttributeName
does nothing at all.
我正在使用NSMutableAttribtuedString来构建带有格式的字符串,然后我将其传递给Core Text以渲染到框架中。问题是,我需要使用上标和下标。除非这些字符在字体中可用(大多数字体不支持它),否则设置属性kCTSuperscriptAttributeName什么都不做。
So I guess I'm left with the only option, which is to fake it by changing the font size and moving the base line. I can do the font size bit, but don't know the code for altering the base line. Can anyone help please?
所以我想我只留下了唯一的选择,即通过改变字体大小和移动基线来伪造它。我可以做字体大小位,但不知道改变基线的代码。有人可以帮忙吗?
Thanks!
EDIT: I'm thinking, considering the amount of time I have available to sort this problem, of editing a font so that it's given a subscript "2"... Either that or finding a built-in iPad font which does. Does anyone know of any serif font with a subscript "2" I can use?
编辑:我正在考虑,考虑到我可以用来解决这个问题的时间,编辑一个字体,以便给它一个下标“2”......或者找到一个内置的iPad字体。有没有人知道任何带有下标“2”的衬线字体我可以使用?
7 个解决方案
#1
12
There is no baseline setting amongst the CTParagraphStyleSpecifiers or the defined string attribute name constants. I think it's therefore safe to conclude that CoreText does not itself support a baseline adjust property on text. There's a reference made to baseline placement in CTTypesetter, but I can't tie that to any ability to vary the baseline over the course of a line in the iPad's CoreText.
CTParagraphStyleSpecifiers或定义的字符串属性名称常量中没有基线设置。因此,我认为可以安全地断定CoreText本身不支持文本的基线调整属性。在CTTypesetter中有一个对基线位置的引用,但我无法将其与任何能够在iPad的CoreText中的一行中改变基线的能力联系起来。
Hence, you probably need to interfere in the rendering process yourself. For example:
因此,您可能需要自己干预渲染过程。例如:
- create a CTFramesetter, e.g. via
CTFramesetterCreateWithAttributedString
- get a CTFrame from that via
CTFramesetterCreateFrame
- use
CTFrameGetLineOrigins
andCTFrameGetLines
to get an array of CTLines and where they should be drawn (ie, the text with suitable paragraph/line breaks and all your other kerning/leading/other positioning text attributes applied) - from those, for lines with no superscript or subscript, just use
CTLineDraw
and forget about it - for those with superscript or subscript, use
CTLineGetGlyphRuns
to get an array of CTRun objects describing the various glyphs on the line - on each run, use
CTRunGetStringIndices
to determine which source characters are in the run; if none that you want to superscript or subscript are included, just useCTRunDraw
to draw the thing - otherwise, use
CTRunGetGlyphs
to break the run into individual glyphs andCTRunGetPositions
to figure out where they would be drawn in the normal run of things - use
CGContextShowGlyphsAtPoint
as appropriate, having tweaked the text matrix for those you want in superscript or subscript
创建一个CTFramesetter,例如通过CTFramesetterCreateWithAttributedString
通过CTFramesetterCreateFrame获取CTFrame
使用CTFrameGetLineOrigins和CTFrameGetLines来获取CTLines数组以及它们应该被绘制的位置(即,应用了合适的段落/换行符以及所有其他字距调整/前导/其他定位文本属性的文本)
从那些,对于没有上标或下标的行,只需使用CTLineDraw并忘记它
对于那些带有上标或下标的人,使用CTLineGetGlyphRuns获取描述线上各种字形的CTRun对象数组
在每次运行中,使用CTRunGetStringIndices来确定运行中的源字符;如果没有你想要的上标或下标,只需使用CTRunDraw绘制东西
否则,使用CTRunGetGlyphs打破单个字形和CTRunGetPositions的运行,找出它们在正常运行中的绘制位置
适当地使用CGContextShowGlyphsAtPoint,为上标或下标中的那些调整文本矩阵
I haven't yet found a way to query whether a font has the relevant hints for automatic superscript/subscript generation, which makes things a bit tricky. If you're desperate and don't have a solution to that, it's probably easier just not to use CoreText's stuff at all — in which case you should probably define your own attribute (that's why [NS/CF]AttributedString allow arbitrary attributes to be applied, identified by string name) and use the normal NSString searching methods to identify regions that need to be printed in superscript or subscript from blind.
我还没有找到一种方法来查询字体是否具有自动上标/下标生成的相关提示,这使得事情有点棘手。如果你是绝望的并且没有解决方案,那么根本不使用CoreText的东西可能更容易 - 在这种情况下你应该定义你自己的属性(这就是为什么[NS / CF] AttributedString允许任意属性应用,由字符串名称标识)并使用正常的NSString搜索方法来识别需要在上标或下标中打印的区域。
For performance reasons, binary search is probably the way to go on searching all lines, the runs within a line and the glyphs within a run for those you're interested in. Assuming you have a custom UIView subclass to draw CoreText content, it's probably smarter to do it ahead of time rather than upon every drawRect: (or the equivalent methods, if e.g. you're using a CATiledLayer).
出于性能原因,二进制搜索可能是继续搜索所有行,行内运行以及运行中的字形的方式。假设您有一个自定义UIView子类来绘制CoreText内容,它可能是更聪明地提前做,而不是每个drawRect :(或等效的方法,如果你使用CATiledLayer)。
Also, the CTRun methods have variants that request a pointer to a C array containing the things you're asking for copies of, possibly saving you a copy operation but not necessarily succeeding. Check the documentation. I've just made sure that I'm sketching a workable solution rather than necessarily plotting the absolutely optimal route through the CoreText API.
此外,CTRun方法的变体请求指向C数组的指针,该数组包含您要求复制的内容,可能会保存复制操作但不一定成功。查看文档。我刚刚确定我正在草拟一个可行的解决方案,而不是通过CoreText API绘制绝对最佳的路线。
#2
4
Here is some code based on Tommy's outline that does the job quite well (tested on only single lines though). Set the baseline on your attributed string with @"MDBaselineAdjust"
, and this code draws the line to offset
, a CGPoint
. To get superscript, also lower the font size a notch. Preview of what's possible: http://cloud.mochidev.com/IfPF (the line that reads "[Xe] 4f14...")
下面是一些基于Tommy大纲的代码,它可以很好地完成工作(尽管只在单行测试)。使用@“MDBaselineAdjust”在属性字符串上设置基线,此代码将线条绘制为偏移量CGPoint。要获得上标,还要将字体大小降低一个档次。预览可能的内容:http://cloud.mochidev.com/IfPF(读取“[Xe] 4f14 ...”的行)
Hope this helps :)
希望这可以帮助 :)
NSAttributedString *string = ...;
CGPoint origin = ...;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, string.length), NULL, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), NULL);
CGPathRef path = CGPathCreateWithRect(CGRectMake(origin.x, origin.y, suggestedSize.width, suggestedSize.height), NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, string.length), path, NULL);
NSArray *lines = (NSArray *)CTFrameGetLines(frame);
if (lines.count) {
CGPoint *lineOrigins = malloc(lines.count * sizeof(CGPoint));
CTFrameGetLineOrigins(frame, CFRangeMake(0, lines.count), lineOrigins);
int i = 0;
for (id aLine in lines) {
NSArray *glyphRuns = (NSArray *)CTLineGetGlyphRuns((CTLineRef)aLine);
CGFloat width = origin.x+lineOrigins[i].x-lineOrigins[0].x;
for (id run in glyphRuns) {
CFRange range = CTRunGetStringRange((CTRunRef)run);
NSDictionary *dict = [string attributesAtIndex:range.location effectiveRange:NULL];
CGFloat baselineAdjust = [[dict objectForKey:@"MDBaselineAdjust"] doubleValue];
CGContextSetTextPosition(context, width, origin.y+baselineAdjust);
CTRunDraw((CTRunRef)run, context, CFRangeMake(0, 0));
}
i++;
}
free(lineOrigins);
}
CFRelease(frame);
CGPathRelease(path);
CFRelease(framesetter);
`
#3
3
You can mimic subscripts now using TextKit in iOS7. Example:
您现在可以使用iOS7中的TextKit模仿下标。例:
NSMutableAttributedString *carbonDioxide = [[NSMutableAttributedString alloc] initWithString:@"CO2"];
[carbonDioxide addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:8] range:NSMakeRange(2, 1)];
[carbonDioxide addAttribute:NSBaselineOffsetAttributeName value:@(-2) range:NSMakeRange(2, 1)];
#4
2
I've been having trouble with this myself. Apple's Core Text documentation claims that there has been support in iOS since version 3.2, but for some reason it still just doesn't work. Even in iOS 5... how very frustrating >.<
我自己一直遇到麻烦。 Apple的Core Text文档声称自3.2版以来iOS一直受到支持,但由于某种原因它仍然无效。即使在iOS 5中......也非常令人沮丧>
I managed to find a workaround if you only really care about superscript or subscript numbers. Say you have a block of text can might contain a "sub2" tag where you want a subscript number 2. Use NSRegularExpression to find the tags, and then use replacementStringForResult method on your regex object to replace each tag with unicode characters:
如果您只关心上标或下标数字,我设法找到了解决方法。假设您有一个文本块可能包含“sub2”标记,您需要下标编号2.使用NSRegularExpression查找标记,然后在regex对象上使用replacementStringForResult方法将每个标记替换为unicode字符:
if ([match isEqualToString:@"<sub2/>"])
{
replacement = @"₂";
}
If you use the OSX character viewer, you can drop unicode characters right into your code. There's a set of characters in there called "Digits" which has all the superscript and subscript number characters. Just leave your cursor at the appropriate spot in your code window and double-click in the character viewer to insert the character you want.
如果使用OSX字符查看器,则可以将unicode字符直接删除到代码中。那里有一组叫做“数字”的字符,里面有所有的上标和下标数字。只需将光标留在代码窗口中的适当位置,然后双击字符查看器即可插入所需的字符。
With the right font, you could probably do this with any letter as well, but the character map only has a handful of non-numbers available for this that I've seen.
使用正确的字体,您也可以使用任何字母执行此操作,但字符映射只有少数可用于此的非数字,我已经看到了。
Alternatively you can just put the unicode characters in your source content, but in a lot of cases (like mine), that isn't possible.
或者,您可以将unicode字符放在源内容中,但在很多情况下(如我的),这是不可能的。
#5
1
I struggled with this problem as well. It turns out, as some of the posters above suggested, that none of the fonts that come with IOS support superscripting or subscripting. My solution was to purchase and install two custom superscript and subscript fonts (They were $9.99 each and here's a link to the site http://superscriptfont.com/).
我也在努力解决这个问题。事实证明,正如上面提到的一些海报所示,IOS附带的任何字体都不支持上标或下标。我的解决方案是购买和安装两个自定义上标和下标字体(每个9.99美元,这里是链接到网站http://superscriptfont.com/)。
Not really that hard to do. Just add the font files as resources and add info.plist entries for "Font provided by application".
真的很难做到。只需将字体文件添加为资源,并为“应用程序提供的字体”添加info.plist条目。
The next step was to search for the appropriate tags in my NSAttributedString, remove the tags and apply the font to the text.
下一步是在我的NSAttributedString中搜索相应的标签,删除标签并将字体应用于文本。
Works great!
#6
0
A Swift 2 twist on Dimitri's answer; effectively implements NSBaselineOffsetAttributeName.
Dimitri答案的Swift 2扭曲;有效地实现了NSBaselineOffsetAttributeName。
When coding I was in a UIView so had a reasonable bounds rect to use. His answer calculated its own rect.
编码时,我在UIView中,因此有一个合理的界限使用。他的答案计算了自己的矩形。
func drawText(context context:CGContextRef, attributedText: NSAttributedString) {
// All this CoreText iteration just to add support for superscripting.
// NSBaselineOffsetAttributeName isn't supported by CoreText. So we manully iterate through
// all the text ranges, rendering each, and offsetting the baseline where needed.
let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
let textRect = CGRectOffset(bounds, 0, 0)
let path = CGPathCreateWithRect(textRect, nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
// All the lines of text we'll render...
let lines = CTFrameGetLines(frame) as [AnyObject]
let lineCount = lines.count
// And their origin coordinates...
var lineOrigins = [CGPoint](count: lineCount, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);
for lineIndex in 0..<lineCount {
let lineObject = lines[lineIndex]
// Each run of glyphs we'll render...
let glyphRuns = CTLineGetGlyphRuns(lineObject as! CTLine) as [AnyObject]
for r in glyphRuns {
let run = r as! CTRun
let runRange = CTRunGetStringRange(run)
// What attributes are in the NSAttributedString here? If we find NSBaselineOffsetAttributeName,
// adjust the baseline.
let attrs = attributedText.attributesAtIndex(runRange.location, effectiveRange: nil)
var baselineAdjustment: CGFloat = 0.0
if let adjust = attrs[NSBaselineOffsetAttributeName as String] as? NSNumber {
baselineAdjustment = CGFloat(adjust.floatValue)
}
CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y - 25 + baselineAdjustment)
CTRunDraw(run, context, CFRangeMake(0, 0))
}
}
}
#7
0
Swift 4
Very loosely based off of Graham Perks' answer. I could not make his code work as is but after three hours of work I've created something that works great!
基于Graham Perks的回答非常宽松。我不能让他的代码按原样工作,但经过三个小时的工作后,我创造了一些非常好的东西!
I explain everything I'm doing in the comments. This is the draw method, to be called from drawRect:
我在评论中解释我正在做的一切。这是draw方法,可以从drawRect调用:
/// Draw text on a given context. Supports superscript using NSBaselineOffsetAttributeName
///
/// This method works by drawing the text backwards (i.e. last line first). This is very very important because it's how we ensure superscripts don't overlap the text above it. In other words, we need to start from the bottom, get the height of the text we just drew, and then draw the next text above it. This could be done in a forward direction but you'd have to use lookahead which IMO is more work.
///
/// If you have to modify on this, remember that CT uses a mathmatical origin (i.e. 0,0 is bottom left like a cartisian plane)
/// - Parameters:
/// - context: A core graphics draw context
/// - attributedText: An attributed string
func drawText(context:CGContext, attributedText: NSAttributedString) {
//Create our CT boiler plate
let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
let textRect = bounds
let path = CGPath(rect: textRect, transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
//Fetch our lines, bridging to swift from CFArray
let lines = CTFrameGetLines(frame) as [AnyObject]
let lineCount = lines.count
//Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications)
var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);
//Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off)
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
if lineCount > 0 {
CTLineGetTypographicBounds(lines.last as! CTLine, &ascent, &descent, &leading)
}
//This variable holds the current draw position, relative to CT origin of the bottom left
//https://*.com/a/27631737/1166266
var drawYPositionFromOrigin:CGFloat = descent
//Again, draw the lines in reverse so we don't need look ahead
for lineIndex in (0..<lineCount).reversed() {
//Calculate the current line height so we can accurately move the position up later
let lastLinePosition = lineIndex > 0 ? lineOrigins[lineIndex - 1].y: textRect.height
let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y
//Throughout the loop below this variable will be updated to the tallest value for the current line
var maxLineHeight:CGFloat = currentLineHeight
//Grab the current run glyph. This is used for attributed string interop
let glyphRuns = CTLineGetGlyphRuns(lines[lineIndex] as! CTLine) as [AnyObject]
for run in glyphRuns {
let run = run as! CTRun
//Convert the format range to something we can match to our string
let runRange = CTRunGetStringRange(run)
let attribuetsAtPosition = attributedText.attributes(at: runRange.location, effectiveRange: nil)
var baselineAdjustment: CGFloat = 0.0
if let adjust = attribuetsAtPosition[NSAttributedStringKey.baselineOffset] as? NSNumber {
//We have a baseline offset!
baselineAdjustment = CGFloat(adjust.floatValue)
}
//Check if this glyph run is tallest, and move it if it is
maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight)
//Move the draw head. Note that we're drawing from the unupdated drawYPositionFromOrigin. This is again thanks to CT cartisian plane where we draw from the bottom left of text too.
context.textPosition = CGPoint.init(x: lineOrigins[lineIndex].x, y: drawYPositionFromOrigin)
//Draw!
CTRunDraw(run, context, CFRangeMake(0, 0))
}
//Move our position because we've completed the drawing of the line which is at most `maxLineHeight`
drawYPositionFromOrigin += maxLineHeight
}
}
I also made a method which calculates the required height of the text given a width. It's exactly the same code except it doesn't draw anything.
我还制作了一个方法来计算给定宽度的文本所需的高度。这是完全相同的代码,除了它没有绘制任何东西。
/// Calculate the height if it were drawn using `drawText`
/// Uses the same code as drawText except it doesn't draw.
///
/// - Parameters:
/// - attributedText: The text to calculate the height of
/// - width: The constraining width
/// - estimationHeight: Optional paramater, default 30,000px. This is the container height used to layout the text. DO NOT USE CGFLOATMAX AS IT CORE TEXT CANNOT CREATE A FRAME OF THAT SIZE.
/// - Returns: The size required to fit the text
static func size(of attributedText:NSAttributedString,width:CGFloat, estimationHeight:CGFloat?=30000) -> CGSize {
let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
let textRect = CGRect.init(x: 0, y: 0, width: width, height: estimationHeight!)
let path = CGPath(rect: textRect, transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
//Fetch our lines, bridging to swift from CFArray
let lines = CTFrameGetLines(frame) as [AnyObject]
let lineCount = lines.count
//Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications)
var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);
//Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off)
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
if lineCount > 0 {
CTLineGetTypographicBounds(lines.last as! CTLine, &ascent, &descent, &leading)
}
//This variable holds the current draw position, relative to CT origin of the bottom left
var drawYPositionFromOrigin:CGFloat = descent
//Again, draw the lines in reverse so we don't need look ahead
for lineIndex in (0..<lineCount).reversed() {
//Calculate the current line height so we can accurately move the position up later
let lastLinePosition = lineIndex > 0 ? lineOrigins[lineIndex - 1].y: textRect.height
let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y
//Throughout the loop below this variable will be updated to the tallest value for the current line
var maxLineHeight:CGFloat = currentLineHeight
//Grab the current run glyph. This is used for attributed string interop
let glyphRuns = CTLineGetGlyphRuns(lines[lineIndex] as! CTLine) as [AnyObject]
for run in glyphRuns {
let run = run as! CTRun
//Convert the format range to something we can match to our string
let runRange = CTRunGetStringRange(run)
let attribuetsAtPosition = attributedText.attributes(at: runRange.location, effectiveRange: nil)
var baselineAdjustment: CGFloat = 0.0
if let adjust = attribuetsAtPosition[NSAttributedStringKey.baselineOffset] as? NSNumber {
//We have a baseline offset!
baselineAdjustment = CGFloat(adjust.floatValue)
}
//Check if this glyph run is tallest, and move it if it is
maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight)
//Skip drawing since this is a height calculation
}
//Move our position because we've completed the drawing of the line which is at most `maxLineHeight`
drawYPositionFromOrigin += maxLineHeight
}
return CGSize.init(width: width, height: drawYPositionFromOrigin)
}
Like everything I write, I also did some benchmarks against some public libraries and system functions (even though they won't work here). I used a huge, complex string here to keep anyone from taking unfair shortcuts.
就像我写的一切一样,我也对一些公共图书馆和系统功能做了一些基准测试(尽管它们在这里不起作用)。我在这里使用了一个巨大而复杂的字符串,以防止任何人采取不公平的捷径。
---HEIGHT CALCULATION---
Runtime for 1000 iterations (ms) BoundsForRect: 5415.030002593994
Runtime for 1000 iterations (ms) layoutManager: 5370.990991592407
Runtime for 1000 iterations (ms) CTFramesetterSuggestFrameSizeWithConstraints: 2372.151017189026
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame ObjC: 2300.302028656006
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame-Swift: 2313.6669397354126
Runtime for 1000 iterations (ms) THIS ANSWER size(of:): 2566.351056098938
---RENDER---
Runtime for 1000 iterations (ms) AttributedLabel: 35.032033920288086
Runtime for 1000 iterations (ms) UILabel: 45.948028564453125
Runtime for 1000 iterations (ms) TTTAttributedLabel: 301.1329174041748
Runtime for 1000 iterations (ms) THIS ANSWER: 20.398974418640137
So summary time: we did very well! size(of...)
is nearly equal to stock CT layout which means that our addon for superscript is fairly cheap despite using a hash table lookup. We do, however, flat out win on draw calls. I suspect that this is due to the very expensive 30k pixel estimation frame we have to create. If we make a better estimate performance will be better. I've already been working for about three hours so I'm calling it quits and leaving that as an exercise to the reader.
总结时间:我们做得很好! size(of ...)几乎等于库存CT布局,这意味着尽管使用哈希表查找,我们的上标也相当便宜。但是,我们确实在平局电话中获胜。我怀疑这是由于我们必须创建的非常昂贵的30k像素估计帧。如果我们做出更好的估计,性能会更好。我已经工作了大约三个小时,所以我称之为退出并将其作为练习留给读者。
#1
12
There is no baseline setting amongst the CTParagraphStyleSpecifiers or the defined string attribute name constants. I think it's therefore safe to conclude that CoreText does not itself support a baseline adjust property on text. There's a reference made to baseline placement in CTTypesetter, but I can't tie that to any ability to vary the baseline over the course of a line in the iPad's CoreText.
CTParagraphStyleSpecifiers或定义的字符串属性名称常量中没有基线设置。因此,我认为可以安全地断定CoreText本身不支持文本的基线调整属性。在CTTypesetter中有一个对基线位置的引用,但我无法将其与任何能够在iPad的CoreText中的一行中改变基线的能力联系起来。
Hence, you probably need to interfere in the rendering process yourself. For example:
因此,您可能需要自己干预渲染过程。例如:
- create a CTFramesetter, e.g. via
CTFramesetterCreateWithAttributedString
- get a CTFrame from that via
CTFramesetterCreateFrame
- use
CTFrameGetLineOrigins
andCTFrameGetLines
to get an array of CTLines and where they should be drawn (ie, the text with suitable paragraph/line breaks and all your other kerning/leading/other positioning text attributes applied) - from those, for lines with no superscript or subscript, just use
CTLineDraw
and forget about it - for those with superscript or subscript, use
CTLineGetGlyphRuns
to get an array of CTRun objects describing the various glyphs on the line - on each run, use
CTRunGetStringIndices
to determine which source characters are in the run; if none that you want to superscript or subscript are included, just useCTRunDraw
to draw the thing - otherwise, use
CTRunGetGlyphs
to break the run into individual glyphs andCTRunGetPositions
to figure out where they would be drawn in the normal run of things - use
CGContextShowGlyphsAtPoint
as appropriate, having tweaked the text matrix for those you want in superscript or subscript
创建一个CTFramesetter,例如通过CTFramesetterCreateWithAttributedString
通过CTFramesetterCreateFrame获取CTFrame
使用CTFrameGetLineOrigins和CTFrameGetLines来获取CTLines数组以及它们应该被绘制的位置(即,应用了合适的段落/换行符以及所有其他字距调整/前导/其他定位文本属性的文本)
从那些,对于没有上标或下标的行,只需使用CTLineDraw并忘记它
对于那些带有上标或下标的人,使用CTLineGetGlyphRuns获取描述线上各种字形的CTRun对象数组
在每次运行中,使用CTRunGetStringIndices来确定运行中的源字符;如果没有你想要的上标或下标,只需使用CTRunDraw绘制东西
否则,使用CTRunGetGlyphs打破单个字形和CTRunGetPositions的运行,找出它们在正常运行中的绘制位置
适当地使用CGContextShowGlyphsAtPoint,为上标或下标中的那些调整文本矩阵
I haven't yet found a way to query whether a font has the relevant hints for automatic superscript/subscript generation, which makes things a bit tricky. If you're desperate and don't have a solution to that, it's probably easier just not to use CoreText's stuff at all — in which case you should probably define your own attribute (that's why [NS/CF]AttributedString allow arbitrary attributes to be applied, identified by string name) and use the normal NSString searching methods to identify regions that need to be printed in superscript or subscript from blind.
我还没有找到一种方法来查询字体是否具有自动上标/下标生成的相关提示,这使得事情有点棘手。如果你是绝望的并且没有解决方案,那么根本不使用CoreText的东西可能更容易 - 在这种情况下你应该定义你自己的属性(这就是为什么[NS / CF] AttributedString允许任意属性应用,由字符串名称标识)并使用正常的NSString搜索方法来识别需要在上标或下标中打印的区域。
For performance reasons, binary search is probably the way to go on searching all lines, the runs within a line and the glyphs within a run for those you're interested in. Assuming you have a custom UIView subclass to draw CoreText content, it's probably smarter to do it ahead of time rather than upon every drawRect: (or the equivalent methods, if e.g. you're using a CATiledLayer).
出于性能原因,二进制搜索可能是继续搜索所有行,行内运行以及运行中的字形的方式。假设您有一个自定义UIView子类来绘制CoreText内容,它可能是更聪明地提前做,而不是每个drawRect :(或等效的方法,如果你使用CATiledLayer)。
Also, the CTRun methods have variants that request a pointer to a C array containing the things you're asking for copies of, possibly saving you a copy operation but not necessarily succeeding. Check the documentation. I've just made sure that I'm sketching a workable solution rather than necessarily plotting the absolutely optimal route through the CoreText API.
此外,CTRun方法的变体请求指向C数组的指针,该数组包含您要求复制的内容,可能会保存复制操作但不一定成功。查看文档。我刚刚确定我正在草拟一个可行的解决方案,而不是通过CoreText API绘制绝对最佳的路线。
#2
4
Here is some code based on Tommy's outline that does the job quite well (tested on only single lines though). Set the baseline on your attributed string with @"MDBaselineAdjust"
, and this code draws the line to offset
, a CGPoint
. To get superscript, also lower the font size a notch. Preview of what's possible: http://cloud.mochidev.com/IfPF (the line that reads "[Xe] 4f14...")
下面是一些基于Tommy大纲的代码,它可以很好地完成工作(尽管只在单行测试)。使用@“MDBaselineAdjust”在属性字符串上设置基线,此代码将线条绘制为偏移量CGPoint。要获得上标,还要将字体大小降低一个档次。预览可能的内容:http://cloud.mochidev.com/IfPF(读取“[Xe] 4f14 ...”的行)
Hope this helps :)
希望这可以帮助 :)
NSAttributedString *string = ...;
CGPoint origin = ...;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)string);
CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, string.length), NULL, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), NULL);
CGPathRef path = CGPathCreateWithRect(CGRectMake(origin.x, origin.y, suggestedSize.width, suggestedSize.height), NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, string.length), path, NULL);
NSArray *lines = (NSArray *)CTFrameGetLines(frame);
if (lines.count) {
CGPoint *lineOrigins = malloc(lines.count * sizeof(CGPoint));
CTFrameGetLineOrigins(frame, CFRangeMake(0, lines.count), lineOrigins);
int i = 0;
for (id aLine in lines) {
NSArray *glyphRuns = (NSArray *)CTLineGetGlyphRuns((CTLineRef)aLine);
CGFloat width = origin.x+lineOrigins[i].x-lineOrigins[0].x;
for (id run in glyphRuns) {
CFRange range = CTRunGetStringRange((CTRunRef)run);
NSDictionary *dict = [string attributesAtIndex:range.location effectiveRange:NULL];
CGFloat baselineAdjust = [[dict objectForKey:@"MDBaselineAdjust"] doubleValue];
CGContextSetTextPosition(context, width, origin.y+baselineAdjust);
CTRunDraw((CTRunRef)run, context, CFRangeMake(0, 0));
}
i++;
}
free(lineOrigins);
}
CFRelease(frame);
CGPathRelease(path);
CFRelease(framesetter);
`
#3
3
You can mimic subscripts now using TextKit in iOS7. Example:
您现在可以使用iOS7中的TextKit模仿下标。例:
NSMutableAttributedString *carbonDioxide = [[NSMutableAttributedString alloc] initWithString:@"CO2"];
[carbonDioxide addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:8] range:NSMakeRange(2, 1)];
[carbonDioxide addAttribute:NSBaselineOffsetAttributeName value:@(-2) range:NSMakeRange(2, 1)];
#4
2
I've been having trouble with this myself. Apple's Core Text documentation claims that there has been support in iOS since version 3.2, but for some reason it still just doesn't work. Even in iOS 5... how very frustrating >.<
我自己一直遇到麻烦。 Apple的Core Text文档声称自3.2版以来iOS一直受到支持,但由于某种原因它仍然无效。即使在iOS 5中......也非常令人沮丧>
I managed to find a workaround if you only really care about superscript or subscript numbers. Say you have a block of text can might contain a "sub2" tag where you want a subscript number 2. Use NSRegularExpression to find the tags, and then use replacementStringForResult method on your regex object to replace each tag with unicode characters:
如果您只关心上标或下标数字,我设法找到了解决方法。假设您有一个文本块可能包含“sub2”标记,您需要下标编号2.使用NSRegularExpression查找标记,然后在regex对象上使用replacementStringForResult方法将每个标记替换为unicode字符:
if ([match isEqualToString:@"<sub2/>"])
{
replacement = @"₂";
}
If you use the OSX character viewer, you can drop unicode characters right into your code. There's a set of characters in there called "Digits" which has all the superscript and subscript number characters. Just leave your cursor at the appropriate spot in your code window and double-click in the character viewer to insert the character you want.
如果使用OSX字符查看器,则可以将unicode字符直接删除到代码中。那里有一组叫做“数字”的字符,里面有所有的上标和下标数字。只需将光标留在代码窗口中的适当位置,然后双击字符查看器即可插入所需的字符。
With the right font, you could probably do this with any letter as well, but the character map only has a handful of non-numbers available for this that I've seen.
使用正确的字体,您也可以使用任何字母执行此操作,但字符映射只有少数可用于此的非数字,我已经看到了。
Alternatively you can just put the unicode characters in your source content, but in a lot of cases (like mine), that isn't possible.
或者,您可以将unicode字符放在源内容中,但在很多情况下(如我的),这是不可能的。
#5
1
I struggled with this problem as well. It turns out, as some of the posters above suggested, that none of the fonts that come with IOS support superscripting or subscripting. My solution was to purchase and install two custom superscript and subscript fonts (They were $9.99 each and here's a link to the site http://superscriptfont.com/).
我也在努力解决这个问题。事实证明,正如上面提到的一些海报所示,IOS附带的任何字体都不支持上标或下标。我的解决方案是购买和安装两个自定义上标和下标字体(每个9.99美元,这里是链接到网站http://superscriptfont.com/)。
Not really that hard to do. Just add the font files as resources and add info.plist entries for "Font provided by application".
真的很难做到。只需将字体文件添加为资源,并为“应用程序提供的字体”添加info.plist条目。
The next step was to search for the appropriate tags in my NSAttributedString, remove the tags and apply the font to the text.
下一步是在我的NSAttributedString中搜索相应的标签,删除标签并将字体应用于文本。
Works great!
#6
0
A Swift 2 twist on Dimitri's answer; effectively implements NSBaselineOffsetAttributeName.
Dimitri答案的Swift 2扭曲;有效地实现了NSBaselineOffsetAttributeName。
When coding I was in a UIView so had a reasonable bounds rect to use. His answer calculated its own rect.
编码时,我在UIView中,因此有一个合理的界限使用。他的答案计算了自己的矩形。
func drawText(context context:CGContextRef, attributedText: NSAttributedString) {
// All this CoreText iteration just to add support for superscripting.
// NSBaselineOffsetAttributeName isn't supported by CoreText. So we manully iterate through
// all the text ranges, rendering each, and offsetting the baseline where needed.
let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
let textRect = CGRectOffset(bounds, 0, 0)
let path = CGPathCreateWithRect(textRect, nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
// All the lines of text we'll render...
let lines = CTFrameGetLines(frame) as [AnyObject]
let lineCount = lines.count
// And their origin coordinates...
var lineOrigins = [CGPoint](count: lineCount, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);
for lineIndex in 0..<lineCount {
let lineObject = lines[lineIndex]
// Each run of glyphs we'll render...
let glyphRuns = CTLineGetGlyphRuns(lineObject as! CTLine) as [AnyObject]
for r in glyphRuns {
let run = r as! CTRun
let runRange = CTRunGetStringRange(run)
// What attributes are in the NSAttributedString here? If we find NSBaselineOffsetAttributeName,
// adjust the baseline.
let attrs = attributedText.attributesAtIndex(runRange.location, effectiveRange: nil)
var baselineAdjustment: CGFloat = 0.0
if let adjust = attrs[NSBaselineOffsetAttributeName as String] as? NSNumber {
baselineAdjustment = CGFloat(adjust.floatValue)
}
CGContextSetTextPosition(context, lineOrigins[lineIndex].x, lineOrigins[lineIndex].y - 25 + baselineAdjustment)
CTRunDraw(run, context, CFRangeMake(0, 0))
}
}
}
#7
0
Swift 4
Very loosely based off of Graham Perks' answer. I could not make his code work as is but after three hours of work I've created something that works great!
基于Graham Perks的回答非常宽松。我不能让他的代码按原样工作,但经过三个小时的工作后,我创造了一些非常好的东西!
I explain everything I'm doing in the comments. This is the draw method, to be called from drawRect:
我在评论中解释我正在做的一切。这是draw方法,可以从drawRect调用:
/// Draw text on a given context. Supports superscript using NSBaselineOffsetAttributeName
///
/// This method works by drawing the text backwards (i.e. last line first). This is very very important because it's how we ensure superscripts don't overlap the text above it. In other words, we need to start from the bottom, get the height of the text we just drew, and then draw the next text above it. This could be done in a forward direction but you'd have to use lookahead which IMO is more work.
///
/// If you have to modify on this, remember that CT uses a mathmatical origin (i.e. 0,0 is bottom left like a cartisian plane)
/// - Parameters:
/// - context: A core graphics draw context
/// - attributedText: An attributed string
func drawText(context:CGContext, attributedText: NSAttributedString) {
//Create our CT boiler plate
let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
let textRect = bounds
let path = CGPath(rect: textRect, transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
//Fetch our lines, bridging to swift from CFArray
let lines = CTFrameGetLines(frame) as [AnyObject]
let lineCount = lines.count
//Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications)
var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);
//Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off)
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
if lineCount > 0 {
CTLineGetTypographicBounds(lines.last as! CTLine, &ascent, &descent, &leading)
}
//This variable holds the current draw position, relative to CT origin of the bottom left
//https://*.com/a/27631737/1166266
var drawYPositionFromOrigin:CGFloat = descent
//Again, draw the lines in reverse so we don't need look ahead
for lineIndex in (0..<lineCount).reversed() {
//Calculate the current line height so we can accurately move the position up later
let lastLinePosition = lineIndex > 0 ? lineOrigins[lineIndex - 1].y: textRect.height
let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y
//Throughout the loop below this variable will be updated to the tallest value for the current line
var maxLineHeight:CGFloat = currentLineHeight
//Grab the current run glyph. This is used for attributed string interop
let glyphRuns = CTLineGetGlyphRuns(lines[lineIndex] as! CTLine) as [AnyObject]
for run in glyphRuns {
let run = run as! CTRun
//Convert the format range to something we can match to our string
let runRange = CTRunGetStringRange(run)
let attribuetsAtPosition = attributedText.attributes(at: runRange.location, effectiveRange: nil)
var baselineAdjustment: CGFloat = 0.0
if let adjust = attribuetsAtPosition[NSAttributedStringKey.baselineOffset] as? NSNumber {
//We have a baseline offset!
baselineAdjustment = CGFloat(adjust.floatValue)
}
//Check if this glyph run is tallest, and move it if it is
maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight)
//Move the draw head. Note that we're drawing from the unupdated drawYPositionFromOrigin. This is again thanks to CT cartisian plane where we draw from the bottom left of text too.
context.textPosition = CGPoint.init(x: lineOrigins[lineIndex].x, y: drawYPositionFromOrigin)
//Draw!
CTRunDraw(run, context, CFRangeMake(0, 0))
}
//Move our position because we've completed the drawing of the line which is at most `maxLineHeight`
drawYPositionFromOrigin += maxLineHeight
}
}
I also made a method which calculates the required height of the text given a width. It's exactly the same code except it doesn't draw anything.
我还制作了一个方法来计算给定宽度的文本所需的高度。这是完全相同的代码,除了它没有绘制任何东西。
/// Calculate the height if it were drawn using `drawText`
/// Uses the same code as drawText except it doesn't draw.
///
/// - Parameters:
/// - attributedText: The text to calculate the height of
/// - width: The constraining width
/// - estimationHeight: Optional paramater, default 30,000px. This is the container height used to layout the text. DO NOT USE CGFLOATMAX AS IT CORE TEXT CANNOT CREATE A FRAME OF THAT SIZE.
/// - Returns: The size required to fit the text
static func size(of attributedText:NSAttributedString,width:CGFloat, estimationHeight:CGFloat?=30000) -> CGSize {
let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
let textRect = CGRect.init(x: 0, y: 0, width: width, height: estimationHeight!)
let path = CGPath(rect: textRect, transform: nil)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
//Fetch our lines, bridging to swift from CFArray
let lines = CTFrameGetLines(frame) as [AnyObject]
let lineCount = lines.count
//Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications)
var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins);
//Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off)
var ascent:CGFloat = 0
var descent:CGFloat = 0
var leading:CGFloat = 0
if lineCount > 0 {
CTLineGetTypographicBounds(lines.last as! CTLine, &ascent, &descent, &leading)
}
//This variable holds the current draw position, relative to CT origin of the bottom left
var drawYPositionFromOrigin:CGFloat = descent
//Again, draw the lines in reverse so we don't need look ahead
for lineIndex in (0..<lineCount).reversed() {
//Calculate the current line height so we can accurately move the position up later
let lastLinePosition = lineIndex > 0 ? lineOrigins[lineIndex - 1].y: textRect.height
let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y
//Throughout the loop below this variable will be updated to the tallest value for the current line
var maxLineHeight:CGFloat = currentLineHeight
//Grab the current run glyph. This is used for attributed string interop
let glyphRuns = CTLineGetGlyphRuns(lines[lineIndex] as! CTLine) as [AnyObject]
for run in glyphRuns {
let run = run as! CTRun
//Convert the format range to something we can match to our string
let runRange = CTRunGetStringRange(run)
let attribuetsAtPosition = attributedText.attributes(at: runRange.location, effectiveRange: nil)
var baselineAdjustment: CGFloat = 0.0
if let adjust = attribuetsAtPosition[NSAttributedStringKey.baselineOffset] as? NSNumber {
//We have a baseline offset!
baselineAdjustment = CGFloat(adjust.floatValue)
}
//Check if this glyph run is tallest, and move it if it is
maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight)
//Skip drawing since this is a height calculation
}
//Move our position because we've completed the drawing of the line which is at most `maxLineHeight`
drawYPositionFromOrigin += maxLineHeight
}
return CGSize.init(width: width, height: drawYPositionFromOrigin)
}
Like everything I write, I also did some benchmarks against some public libraries and system functions (even though they won't work here). I used a huge, complex string here to keep anyone from taking unfair shortcuts.
就像我写的一切一样,我也对一些公共图书馆和系统功能做了一些基准测试(尽管它们在这里不起作用)。我在这里使用了一个巨大而复杂的字符串,以防止任何人采取不公平的捷径。
---HEIGHT CALCULATION---
Runtime for 1000 iterations (ms) BoundsForRect: 5415.030002593994
Runtime for 1000 iterations (ms) layoutManager: 5370.990991592407
Runtime for 1000 iterations (ms) CTFramesetterSuggestFrameSizeWithConstraints: 2372.151017189026
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame ObjC: 2300.302028656006
Runtime for 1000 iterations (ms) CTFramesetterCreateFrame-Swift: 2313.6669397354126
Runtime for 1000 iterations (ms) THIS ANSWER size(of:): 2566.351056098938
---RENDER---
Runtime for 1000 iterations (ms) AttributedLabel: 35.032033920288086
Runtime for 1000 iterations (ms) UILabel: 45.948028564453125
Runtime for 1000 iterations (ms) TTTAttributedLabel: 301.1329174041748
Runtime for 1000 iterations (ms) THIS ANSWER: 20.398974418640137
So summary time: we did very well! size(of...)
is nearly equal to stock CT layout which means that our addon for superscript is fairly cheap despite using a hash table lookup. We do, however, flat out win on draw calls. I suspect that this is due to the very expensive 30k pixel estimation frame we have to create. If we make a better estimate performance will be better. I've already been working for about three hours so I'm calling it quits and leaving that as an exercise to the reader.
总结时间:我们做得很好! size(of ...)几乎等于库存CT布局,这意味着尽管使用哈希表查找,我们的上标也相当便宜。但是,我们确实在平局电话中获胜。我怀疑这是由于我们必须创建的非常昂贵的30k像素估计帧。如果我们做出更好的估计,性能会更好。我已经工作了大约三个小时,所以我称之为退出并将其作为练习留给读者。