11

我正在使用一个NSMutableAttribtuedString来构建一个带格式的字符串,然后我将它传递给 Core Text 以呈现到一个框架中。问题是,我需要使用上标和下标。除非这些字符在字体中可用(大多数字体不支持它),否则设置属性kCTSuperscriptAttributeName根本不会做任何事情。

所以我想我只有唯一的选择,那就是通过改变字体大小和移动基线来伪造它。我可以做字体大小位,但不知道更改基线的代码。有人可以帮忙吗?

谢谢!

编辑:我正在考虑,考虑到我有多少时间可以对这个问题进行排序,编辑字体以便给它一个下标“2”......或者找到一个内置的 iPad 字体。有谁知道我可以使用任何带有下标“2”的衬线字体?

4

8 回答 8

14

CTParagraphStyleSpecifiers 或定义的字符串属性名称常量之间没有基线设置。因此,我认为可以安全地得出结论,CoreText 本身不支持文本的基线调整属性。CTTypesetter 中提到了基线放置,但我无法将其与在 iPad 的 CoreText 中改变基线的任何能力联系起来。

因此,您可能需要自己干预渲染过程。例如:

  • 创建一个 CTFramesetter,例如通过CTFramesetterCreateWithAttributedString
  • 从中得到一个 CTFrame 通过CTFramesetterCreateFrame
  • 使用CTFrameGetLineOriginsandCTFrameGetLines获取 CTLines 数组以及它们应该绘制的位置(即,具有合适段落/换行符的文本以及应用的所有其他字距调整/前导/其他定位文本属性)
  • 从那些中,对于没有上标或下标的行,只需使用CTLineDraw并忘记它
  • 对于那些带有上标或下标的,用于CTLineGetGlyphRuns获取描述行上各种字形的 CTRun 对象数组
  • 在每次运行时,用于CTRunGetStringIndices确定运行中的源字符;如果没有包含您想要上标或下标的内容,则只需用于CTRunDraw绘制事物
  • 否则,用于CTRunGetGlyphs将运行分解为单独的字形,并CTRunGetPositions确定它们在正常运行中的绘制位置
  • 酌情使用CGContextShowGlyphsAtPoint,在上标或下标中调整了您想要的文本矩阵

我还没有找到一种方法来查询字体是否具有自动上标/下标生成的相关提示,这让事情变得有点棘手。如果您绝望并且没有解决方案,那么根本不使用 CoreText 的东西可能更容易——在这种情况下,您可能应该定义自己的属性(这就是为什么 [NS/CF]AttributedString 允许任意属性被应用,由字符串名称标识)并使用正常的 NSString 搜索方法从盲区中识别需要以上标或下标打印的区域。

出于性能原因,二分搜索可能是继续搜索所有行、一行中的运行以及运行中的字形对于您感兴趣的人的方式。假设您有一个自定义 UIView 子类来绘制 CoreText 内容,它可能是提前而不是每次drawRect:(或等效方法,例如,如果您使用的是CATiledLayer)更聪明。

此外,CTRun 方法具有请求指向 C 数组的指针的变体,其中包含您要求复制的内容,可能会为您节省复制操作,但不一定会成功。检查文档。我刚刚确定我正在草拟一个可行的解决方案,而不是必须通过 CoreText API 绘制绝对最佳的路线。

于 2010-11-22T01:26:58.053 回答
5

下面是一些基于 Tommy 的大纲的代码,它可以很好地完成这项工作(尽管仅在单行上进行了测试)。使用 设置属性字符串的基线@"MDBaselineAdjust",此代码将线绘制到offset, a CGPoint。要获得上标,还要将字体大小降低一个档次。预览什么是可能的:http ://cloud.mochidev.com/IfPF (读取“[Xe] 4f 14 ...”的行)

希望这可以帮助 :)

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);

`

于 2012-08-11T19:49:31.257 回答
3

您现在可以在 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)];

属性字符串输出的图像

于 2014-02-19T23:43:56.693 回答
2

我自己也遇到了麻烦。Apple 的 Core Text 文档声称自 3.2 版以来已支持 iOS,但由于某种原因,它仍然无法正常工作。即使在 iOS 5 中...多么令人沮丧>.<

如果您只关心上标或下标数字,我设法找到了解决方法。假设您有一个文本块,可能包含一个“sub2”标签,您需要一个下标数字 2。使用 NSRegularExpression 查找标签,然后在您的正则表达式对象上使用 replacementStringForResult 方法将每个标签替换为 unicode 字符:

if ([match isEqualToString:@"<sub2/>"])
{
   replacement = @"₂";
}

如果您使用 OSX 字符查看器,您可以将 unicode 字符直接放入您的代码中。那里有一组称为“数字”的字符,其中包含所有上标和下标数字字符。只需将光标放在代码窗口中的适当位置,然后在字符查看器中双击以插入所需的字符。

使用正确的字体,您可能也可以对任何字母执行此操作,但字符映射只有少数可用的非数字,我已经看到了。

或者,您可以只将 unicode 字符放在源内容中,但在很多情况下(如我的),这是不可能的。

于 2012-04-18T22:00:24.407 回答
2

斯威夫特 4

非常松散地基于 Graham Perks 的回答。我无法让他的代码按原样工作,但经过三个小时的工作,我创造了一些很棒的东西!如果您更喜欢此功能的完整实现以及一堆漂亮的其他性能和功能附加组件(链接、异步绘图等),请查看我的单个文件库DYLabel。如果没有,请继续阅读。

我在评论中解释了我所做的一切。这是从drawRect调用的draw方法:

/// 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://stackoverflow.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
    }
}

我还做了一个方法,计算给定宽度的文本所需的高度。这是完全相同的代码,只是它不绘制任何东西。

/// 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)
}

就像我写的所有东西一样,我也对一些公共库和系统函数进行了一些基准测试(即使它们在这里不起作用)。我在这里使用了一个巨大而复杂的字符串来防止任何人走不公平的捷径。

---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

所以总结时间:我们做得很好!size(of...)几乎等于股票 CT 布局,这意味着我们的上标插件相当便宜,尽管使用了哈希表查找。然而,我们确实在平局电话中完全获胜。我怀疑这是由于我们必须创建非常昂贵的 30k 像素估计框架。如果我们做出更好的估计,性能会更好。我已经工作了大约三个小时,所以我称之为退出并将其作为练习留给读者。

于 2018-08-25T23:46:24.263 回答
1

我也为这个问题而苦苦挣扎。事实证明,正如上面的一些海报所暗示的那样,IOS 附带的字体都不支持上标或下标。我的解决方案是购买并安装两种自定义的上标和下标字体(它们每个是 9.99 美元,这里是网站http://superscriptfont.com/的链接)。

做起来真的没那么难。只需将字体文件添加为资源并为“应用程序提供的字体”添加 info.plist 条目。

下一步是在我的 NSAttributedString 中搜索适当的标签,删除标签并将字体应用于文本。

效果很好!

于 2012-05-07T21:30:00.843 回答
0

Dimitri 回答的 Swift 2 转折;有效地实现了 NSBaselineOffsetAttributeName。

编码时,我在 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))
        }
    }
}
于 2016-03-07T20:05:38.090 回答
0

在 IOS 11 中,Apple 引入了一个新的字符串属性名称: kCTBaselineOffsetAttributeName它适用于 Core Text。

NSBaselineOffsetAttributeName请注意,偏移方向与在 UILabel 等上与 NSAttributedStrings 一起使用的方向不同(正偏移使基线向下移动)。

于 2022-01-06T10:01:24.140 回答