Как мне подделать верхний и нижний индексы с помощью Core Text и Attributed String? - PullRequest
11 голосов
/ 06 октября 2010

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

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

Спасибо!

РЕДАКТИРОВАТЬ: я думаю, учитывая количество времени, которое у меня есть, чтобы решить эту проблему, отредактировать шрифт так, чтобы ему дали индекс «2» ... Либо это, либо поиск встроенного шрифта iPad, делает. Кто-нибудь знает какой-нибудь шрифт с засечками с индексом «2», который я могу использовать?

Ответы [ 7 ]

14 голосов
/ 22 ноября 2010

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

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

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

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

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

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

5 голосов
/ 11 августа 2012

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

Теперь вы можете имитировать подписки, используя 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

2 голосов
/ 26 августа 2018

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

2 голосов
/ 19 апреля 2012

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

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

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

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

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

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

1 голос
/ 08 мая 2012

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

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

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

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

0 голосов
/ 07 марта 2016

поворот 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))
        }
    }
}
...