Pregunta

Estoy usando un NSMutableAttribtuedString con el fin de construir una cadena con el formato, que luego me pase a Core texto a traducir a un marco. El problema es, que necesito para uso superíndice y subíndice. A menos que estos personajes están disponibles en la fuente (la mayoría de las fuentes no lo soportan), a continuación, establecer la propiedad kCTSuperscriptAttributeName no hace nada en absoluto.

Así que supongo que me quedo con la única opción, que consiste en fingir cambiando el tamaño de la fuente y moviendo la línea de base. Puedo hacer el bit de tamaño de fuente, pero no sé el código para la modificación de la línea de base. ¿Puede alguien por favor ayuda?

Gracias!

EDIT: Estoy pensando, teniendo en cuenta la cantidad de tiempo que tengo disponible para ordenar este problema, la edición de un tipo de letra para que se administra un subíndice "2" ... O eso, o la búsqueda de una fuente integrada iPad cuales hace. ¿Alguien sabe de cualquier fuente serif con un subíndice "2" que pueda usar?

¿Fue útil?

Solución

No hay ninguna opción de línea de base entre los CTParagraphStyleSpecifiers o las constantes de nombre de atributo de cadena definidos. Creo que es por lo tanto, seguro concluir que CoreText sí ??mismo no admite una línea de base ajustar la propiedad de texto. Hay una referencia a la colocación de la línea base en CTTypesetter, pero no puedo atar que a cualquier posibilidad de variar la línea de base a lo largo de una línea en CoreText del iPad.

Por lo tanto, es probable que tenga que intervenir en el proceso de renderizado a sí mismo. Por ejemplo:

  • crear un CTFramesetter, por ejemplo a través de CTFramesetterCreateWithAttributedString
  • conseguir un CTFrame de que a través de CTFramesetterCreateFrame
  • Uso CTFrameGetLineOrigins y CTFrameGetLines para conseguir una serie de CTLines y dónde deben extraerse (es decir, el texto con saltos de párrafo / línea adecuados y todos los demás interletraje / líder / otro texto posicionamiento atributos aplicados)
  • de los que, por líneas sin superíndice o subíndice, el uso justo CTLineDraw y olvidarse de él
  • para aquellos con superíndice o subíndice, el uso CTLineGetGlyphRuns para obtener una matriz de ctrun objetos describir los diversos glifos en la línea
  • en cada CTRunGetStringIndices plazo, el uso para determinar qué caracteres fuente están en la carrera; Si se incluye a ninguno que desea superíndice o subíndice, solo uso CTRunDraw para dibujar la cosa
  • de lo contrario, el uso CTRunGetGlyphs para romper la carrera en glifos individuales y CTRunGetPositions de averiguar donde serían dibujan en el curso normal de las cosas
  • Uso CGContextShowGlyphsAtPoint según el caso, después de haber ajustado la matriz de texto para las que desee en superíndice o subíndice

todavía no he encontrado una forma de consulta si una fuente tiene las sugerencias pertinentes para superíndice automática / subíndice generación, que hace las cosas un poco complicado. Si usted está desesperado y no tienen una solución a esto, es probable que sea más fácil simplemente no usar cosas de CoreText en absoluto - en cuyo caso probablemente debería definir su propio atributo (por eso [NS / CF] AttributedString permiten atributos arbitrarios a aplicarse, identificado por el nombre de cadena) y utilizar los métodos normales de búsqueda NSString para identificar las regiones que necesitan ser impreso en superíndice o subíndice del ciego.

Por motivos de rendimiento, la búsqueda binaria es probablemente el camino a seguir en la búsqueda de todas las líneas, se ejecuta dentro de una línea y los glifos dentro de un plazo para aquellos que le interesa. Asumiendo que tiene una costumbre UIView subclase para extraer el contenido CoreText , es probable que sea más inteligente que hacerlo antes de tiempo, más que en todos los drawRect:. (o los métodos equivalentes, por ejemplo, si está utilizando un CATiledLayer)

Además, los métodos ctrun tienen variantes que solicitan un puntero a una matriz C que contiene las cosas que está pidiendo copias de, posiblemente, le ahorra una operación de copia, pero no necesariamente tener éxito. Compruebe la documentación. Yo sólo he asegurado de que estoy esbozando una solución viable y no necesariamente el trazado de la ruta absolutamente óptima a través de la API CoreText.

Otros consejos

Aquí hay un código basado en el contorno de Tommy que hace el trabajo bastante bien (probado en sólo líneas únicas, aunque). Establecer la línea de base en su cadena con atributos y @"MDBaselineAdjust", y este código traza la línea a offset, un CGPoint. Para obtener superíndice, también reducir el tamaño de fuente de una muesca. Vista previa de lo que es posible: http://cloud.mochidev.com/IfPF (la línea que dice "[ Xe] 4f 14 ... ")

Espero que esto ayude:)

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

`

Puede subíndices imitan ahora usando TextKit en iOS7. Ejemplo:

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

Imagen de la salida de una cadena con atributos

he estado teniendo problemas con esto por mí mismo. reclamaciones de Apple Core documentación texto que ha habido apoyo en IOS desde la versión 3.2, pero por alguna razón que todavía no funciona. Incluso en iOS 5 ... cómo muy frustrante>. <

Me las arreglé para encontrar una solución si en realidad sólo se preocupan por los números en superíndice o subíndice. Digamos que tiene un bloque de texto puede contener una etiqueta podría "sub2" en la que desea un número subíndice 2. Uso NSRegularExpression para encontrar las etiquetas y, a continuación, utilizar el método replacementStringForResult en el objeto de expresiones regulares para reemplazar cada etiqueta con caracteres Unicode:

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

Si utiliza el visor de carácter OSX, se puede eliminar caracteres Unicode a la derecha en su código. Hay una serie de personajes de allí llamado "dígitos", que tiene todo el superíndice y subíndice caracteres numéricos. Acaba de salir el cursor en el lugar adecuado en su ventana de código y haga doble clic en el visor de caracteres para insertar el carácter que desea.

Con la fuente correcta, probablemente podría hacer esto con cualquier letra también, pero el mapa de caracteres sólo tiene un puñado de números que no son disponibles para esta que he visto.

Como alternativa, simplemente puede poner los caracteres Unicode en su contenido de código, pero en muchos casos (como el mío), que no es posible.

Swift 4

muy vagamente basada fuera de respuesta Graham Perks'. Yo no podría hacer su trabajo código como es pero después de tres horas de trabajo que he creado algo que funciona muy bien! Si prefiere una implementación completa de este junto con un montón de otra ingeniosa característica de rendimiento y complementos (enlaces, dibujo asíncrono, etc), echa un vistazo a mi biblioteca en fila india DYLabel . Si no es así, siga leyendo.

explico todo lo que estoy haciendo en los comentarios. Este es el método de sorteo, que se llamará a partir 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
    }
}

I también hizo un método que calcula la altura requerida de la texto dado una anchura. Es exactamente el mismo código, excepto que no se dibuja nada.

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

Al igual que todo lo que escribo, también hizo algunos puntos de referencia contra algunas bibliotecas públicas y las funciones del sistema (a pesar de que no van a trabajar aquí). He utilizado una gran cadena, complejo aquí para evitar que alguien tomar atajos injustos.

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

Así que el tiempo Resumen: lo hicimos muy bien! size(of...) es casi igual al diseño stock CT cual significa que nuestro complemento para superíndice es bastante barato a pesar de utilizar una búsqueda en la tabla hash. Nosotros, sin embargo, la victoria a toda máquina en llamadas de sorteo. Sospecho que esto se debe a la estructura de estimación 30k píxeles muy caro tenemos que crear. Si hacemos un mejor rendimiento estimado será mejor. Ya he estado trabajando durante unas tres horas, así que estoy de poner punto final y dejando que como un ejercicio para el lector.

Luché con este problema también. Resulta que, como algunos de los carteles anteriores sugirieron, que ninguna de las fuentes que vienen con superíndices apoyo IOS o subíndices. Mi solución fue comprar e instalar dos superíndice costumbre y fuentes subíndice (Eran $ 9.99 cada uno y aquí hay un enlace a la página http: // superscriptfont .com / ).

No es tan difícil de hacer. Sólo tiene que añadir los archivos de fuentes como recursos y añadir entradas Info.plist para "Fuente provista por la aplicación".

El siguiente paso fue buscar las etiquetas apropiadas en mi NSAttributedString, eliminar las etiquetas y aplicar la fuente al texto.

Funciona muy bien!

A Swift 2 giro en la respuesta de Dimitri; efectivamente implementa NSBaselineOffsetAttributeName.

Cuando se codifica yo estaba en un UIView así que tenía un rect límites razonables a su uso. Su respuesta calculó su propia rect.

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))
        }
    }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top