【问题标题】:How can I fake superscript and subscript with Core Text and an Attributed String?如何使用核心文本和属性字符串伪造上标和下标?
【发布时间】:2011-04-21 17:47:33
【问题描述】:

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

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

谢谢!

编辑:考虑到我有多少时间可以解决这个问题,我正在考虑编辑一个字体,以便给它一个下标“2”......要么是那个,要么是找到一个内置的 iPad 字体做。有谁知道我可以使用任何带有下标“2”的衬线字体吗?

【问题讨论】:

  • 现在非常绝望,所以提供了赏金!

标签: objective-c ipad nsattributedstring core-text subscript


【解决方案1】:

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

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

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

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

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

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

【讨论】:

  • 很好的答案!不幸的是,这听起来工作量很大,而且我的时间很少,但至少我可以向我的 PM 展示这个,让她看到它不会发生 ;-) 你认为可以创建自定义字体吗有多余的字符?我想我需要的只是一个下标“2”...?
  • 哦,如果你能做到,那肯定会节省时间!烦人的是,CTRunDelegates 似乎不太适用于 iOS - 你得到了你要求的水平间隙,但没有得到垂直间隙。虽然如果这适合你的间距,那么我想你可以叠加一个下标 2 的静态图形?
  • 这也不是一个坏主意。我想我会先尝试字体编辑,如果这不起作用,那么选择静态图形 (niiice ;)) 选项...
  • 最后我意识到我们可以只使用 UTF-8 字符并将其放入文本文件中......我试图在 Core Text 中使用 Subscript 特征,但它不起作用,但是将下标“2”作为 UTF-8 字符放在字符串中效果很好。奇怪!
  • 哦,是的,这是一件愚蠢的事情,不要早点想到——数字在上标和下标中具有唯一的 unicode 条目作为数字符号。很高兴您找到了解决方案!
【解决方案2】:

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

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

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

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

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

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

【讨论】:

    【解决方案3】:

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

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

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

    效果很好!

    【讨论】:

      【解决方案4】:

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

      希望这会有所帮助:)

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

      `

      【讨论】:

      • 对于文本位置的计算,我认为你可以先使用CGContextGetTextPosition,然后进行调整。
      • 是的,它的作用与原始变量相同。
      【解决方案5】:

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

      【讨论】:

      • 如果这个功能在 3½ 年前可用(寻找那个字形很有趣!) - 我将不得不试一试,看看它是如何工作的。如果是这样,我想这个问题应该重新打勾!
      【解决方案6】:

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

      【讨论】:

      • 你能解释一下-25 的神奇作用吗?我删除了它,因为它切断了我的最后一行文本,现在它可以正常工作了。
      • 我现在不记得了。可能只是我使用的特定字体所需的偏移量,以符合眼球的口味。
      【解决方案7】:

      斯威夫特 4

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

      我会解释我在 cmets 中所做的一切。这是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://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 像素估计框架。如果我们做出更好的估计,性能会更好。我已经工作了大约三个小时,所以我称之为退出并将其作为练习留给读者。

      【讨论】:

        猜你喜欢
        • 2011-10-22
        • 2011-04-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-12-23
        • 2011-10-15
        相关资源
        最近更新 更多