Как я могу подделать Надстрочный и подстрочный индекс с основным текстом и приписываемой строкой?

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

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

спасибо!

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

7 ответов


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

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

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

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

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

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


вот некоторый код, основанный на контуре Томми, который делает работу довольно хорошо (проверено только на отдельных строках). Установите базовую линию для приписываемой строки с помощью @"MDBaselineAdjust" и этот код рисует линию к offset, a CGPoint. Чтобы получить superscript, также уменьшите размер шрифта на выемку. Предварительный просмотр того, что возможно: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);

`


теперь вы можете имитировать индексы с помощью 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


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

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

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

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

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

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


Я боролся с этой проблемой. Получается, что на некоторых плакатах выше предложил, что ни один из шрифтов, которые поставляются с iOS поддержка простановка или индексации. Мое решение заключалось в покупке и установке двух пользовательских шрифтов superscript и subscript (они были по $9.99 каждый, и вот ссылка на сайт http://superscriptfont.com/).

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

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

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


быстрый поворот 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))
        }
    }
}

Swift 4

очень слабо основано на ответе Грэма Перкса. Я не мог заставить его код работать как есть, но после трех часов работы я создал что-то, что отлично работает!

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