Как я могу подделывать надстрочный индекс и индекс с помощью Core Text и Attributed String?

Я использую NSMutableAttribtuedString для создания строки с форматированием, которую затем передаю в Core Text для рендеринга в фрейм. Проблема в том, что мне нужно использовать верхний индекс и индекс. Если эти символы не доступны в шрифте (большинство шрифтов не поддерживают его), то установка свойства kCTSuperscriptAttributeName ничего не делает.

Итак, я думаю, что у меня остался единственный вариант, который должен подделать его, изменив размер шрифта и перемещая базовую линию. Я могу сделать бит размера шрифта, но не знаю код для изменения базовой линии. Может ли кто-нибудь помочь?

Спасибо!

EDIT: Я думаю, учитывая количество времени, которое у меня есть для сортировки этой проблемы, редактирования шрифта, чтобы он дал индекс "2"... Либо это, либо найти встроенный iPad-шрифт, который делает. Кто-нибудь знает какой-либо шрифт с засечками с индексом "2", который я могу использовать?

Ответ 1

Не существует базовой установки среди CTParagraphStyleSpecifiers или констант имени имени строкового атрибута. Поэтому я считаю, что безопасно заключить, что CoreText сам по себе не поддерживает базовую настройку свойства для текста. Там ссылка на базовое размещение в CTTypesetter, но я не могу связать это с любой способностью изменять базовый уровень в течение строки в iPad CoreText.

Следовательно, вам, вероятно, необходимо вмешаться в процесс рендеринга самостоятельно. Например:

  • создать CTFramesetter, например. через CTFramesetterCreateWithAttributedString
  • получить CTFrame с помощью CTFramesetterCreateFrame
  • используйте CTFrameGetLineOrigins и CTFrameGetLines, чтобы получить массив CTLines и где они должны быть нарисованы (т.е. текст с подходящими разрывами параграфа/строки и всеми вашими другими атрибутами текста кернинга/ведущего/другого позиционирования) li >
  • от тех, для строк без надстрочного индекса или индекса, просто используйте CTLineDraw и забудьте об этом
  • для лиц с индексом или индексом используйте CTLineGetGlyphRuns, чтобы получить массив объектов CTRun, описывающих различные глифы в строке
  • в каждом прогоне используйте CTRunGetStringIndices, чтобы определить, какие исходные символы находятся в запуске; если вы не хотите, чтобы вы указали верхний индекс или индекс, просто используйте CTRunDraw, чтобы нарисовать вещь
  • в противном случае используйте CTRunGetGlyphs, чтобы разбить выполнение на отдельные глифы и CTRunGetPositions, чтобы выяснить, где они будут рисоваться при обычном прогоне вещей.
  • используйте CGContextShowGlyphsAtPoint, если это необходимо, изменив текстовую матрицу для тех, которые вы хотите в верхнем индексе или индексе

Я еще не нашел способ запросить, имеет ли шрифт соответствующие подсказки для автоматического создания надстрочного/индексного кода, что делает вещи немного сложными. Если вы в отчаянии и не имеете решения для этого, возможно, просто не использовать материал CoreText вообще - в этом случае вам, вероятно, следует определить свой собственный атрибут (что почему [NS/CF] AttributedString разрешает произвольные атрибуты применять, идентифицировать по имени строки) и использовать обычные методы поиска NSString для определения областей, которые должны быть напечатаны в верхнем индексе или индексе от слепых.

По соображениям производительности двоичный поиск - это, вероятно, способ поиска всех строк, пробегов в строке и глифов в рамках запуска для интересующих вас. Предполагая, что у вас есть собственный подкласс UIView для рисования содержимого CoreText, вероятно, умнее делать это раньше времени, а не при каждом drawRect: (или эквивалентные методы, если, например, вы используете CATiledLayer).

Кроме того, методы CTRun имеют варианты, которые запрашивают указатель на массив C, содержащий вещи, которые вы запрашиваете для копий, возможно, сохраняете операцию копирования, но не обязательно преуспеваете. Проверьте документацию. Я только что удостоверился, что я нарисовал реальное решение, а не обязательно построил абсолютно оптимальный маршрут через API CoreText.

Ответ 2

Вот некоторый код, основанный на контуре Томми, который хорошо выполняет работу (тестируется только на одной строке). Установите базовую линию в вашей атрибутной строке с помощью @"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);

`

Ответ 3

Теперь вы можете имитировать индексы, используя TextKit в iOS7. Пример:

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

Image of attributed string output

Ответ 4

У меня были проблемы с этим самим. Документация Apple Core Text утверждает, что поддержка поддерживалась в iOS с версии 3.2, но по какой-то причине она по-прежнему просто не работает. Даже в iOS 5... как очень расстраивает > . <

Мне удалось найти обходное решение, если вы действительно заботитесь только о индексах или индексах. Скажем, у вас есть блок текста, который может содержать тег "sub2", где вы хотите номер индекса 2. Используйте NSRegularExpression, чтобы найти теги, а затем используйте метод replaceStringForResult для вашего объекта regex, чтобы заменить каждый тег символами Unicode:

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

Если вы используете просмотрщик символов OSX, вы можете отбросить символы Юникода прямо в свой код. Там набор символов в названии "Цифры", который имеет все символы надстрочного и индексного номеров. Просто оставьте курсор в соответствующем месте в окне кода и дважды щелкните в средстве просмотра символов, чтобы вставить нужный символ.

С правильным шрифтом вы могли бы, вероятно, сделать это и с любой буквой, но карта символов содержит только несколько не-чисел, доступных для этого, что я видел.

В качестве альтернативы вы можете просто поместить символы юникода в исходное содержимое, но во многих случаях (например, у меня) это невозможно.

Ответ 5

Swift 4

Очень слабо основано на ответе Грэма Перка. Я не мог заставить его код работать как есть, но после трех часов работы я создал что-то, что прекрасно работает! Если вы предпочитаете полную реализацию этого, а также кучу других отличных дополнений по производительности и функциям (ссылки, асинхронное рисование и т.д.), Посмотрите мою библиотеку DYLabel с одним файлом. Если нет, то читайте дальше.

Все, что я делаю, я объясняю в комментариях. Это метод 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 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 пикселей, который мы должны создать. Если мы сделаем лучшую оценку, производительность будет лучше. Я уже работаю около трех часов, поэтому я называю это увольнением и оставляю это в качестве упражнения для читателя.

Ответ 6

Я тоже боролся с этой проблемой. Оказывается, поскольку некоторые из предложенных плакатов предположили, что ни один из шрифтов, которые поставляются с IOS, не поддерживает надписью или подписью. Мое решение состояло в том, чтобы купить и установить два пользовательских надстрочных и подстрочных шрифта (они составляли 9,99 долларов США каждый, а здесь ссылка на сайт http://superscriptfont.com/).

Не так уж сложно. Просто добавьте файлы шрифтов в качестве ресурсов и добавьте записи info.plist для "Шрифта, предоставляемого приложением".

Следующим шагом было поиск соответствующих тегов в моей NSAttributedString, удаление тегов и применение шрифта к тексту.

Отлично работает!

Ответ 7

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