From 4a8808b0fbcdb633c6b9c7063b2992ba57d52fdf Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Tue, 2 Jan 2018 23:17:04 +0200 Subject: [PATCH] Fix handling of combined ligatures and kerning. Remove hasGlyph helper and ligature tracking array. Use single call to: - CFAttributedStringCreate - CTLineCreateWithAttributedString and a call per bi-di/layout run to: - CTRunGetGlyphs for glyphs - CTFontGetAdvancesForGlyphs for advances - CTRunGetStringIndices for indices of ligatures And an efficient loop over the substituted characters of ligatures, to make glyph context calls for incrementing the attribute indices. --- ios/Text/RNSVGTSpan.m | 999 ++++++++++++++++++++---------------------- 1 file changed, 469 insertions(+), 530 deletions(-) diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index c073a0d6..3499c4e0 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -11,6 +11,7 @@ #import "FontData.h" NSCharacterSet *separators = nil; +static double radToDeg = 180 / M_PI; @implementation RNSVGTSpan { @@ -93,31 +94,10 @@ NSCharacterSet *separators = nil; { // Create a dictionary for this font CTFontRef fontRef = [self getFontFromContext]; - CFDictionaryRef attributes; - if (fontRef != nil) { - attributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTFontAttributeName: (__bridge id)fontRef, - (NSString *)kCTForegroundColorFromContextAttributeName: @YES, - (NSString *)NSLigatureAttributeName: @0 }; - } else { - attributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTForegroundColorFromContextAttributeName: @YES, - (NSString *)NSLigatureAttributeName: @0 }; - } - - CFStringRef string = (__bridge CFStringRef)str; - CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); - CTLineRef line = CTLineCreateWithAttributedString(attrString); - CGMutablePathRef path = CGPathCreateMutable(); - CFArrayRef runs = CTLineGetGlyphRuns(line); GlyphContext* gc = [[self getTextRoot] getGlyphContext]; FontData* font = [gc getFont]; NSUInteger n = str.length; - bool ligature[n]; - for (NSUInteger i = 0; i < n; i++){ - ligature[i] = NO; - } /* * * Three properties affect the space between characters and words: @@ -250,513 +230,499 @@ NSCharacterSet *separators = nil; // OpenType.js font data NSDictionary * fontData = font->fontData; + NSNumber *lig = [NSNumber numberWithInt:allowOptionalLigatures ? 2 : 1]; + CFDictionaryRef attributes; + if (fontRef != nil) { + attributes = (__bridge CFDictionaryRef)@{ + (NSString *)kCTFontAttributeName: (__bridge id)fontRef, + (NSString *)NSLigatureAttributeName: lig }; + } else { + attributes = (__bridge CFDictionaryRef)@{ + (NSString *)NSLigatureAttributeName: lig }; + } - double tau = 2 * M_PI; - double radToDeg = 360 / tau; + CFStringRef string = (__bridge CFStringRef)str; + CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); + CTLineRef line = CTLineCreateWithAttributedString(attrString); + + /* + Determine the startpoint-on-the-path for the first glyph using attribute ‘startOffset’ + and property text-anchor. + + For text-anchor:start, startpoint-on-the-path is the point + on the path which represents the point on the path which is ‘startOffset’ distance + along the path from the start of the path, calculated using the user agent's distance + along the path algorithm. + + For text-anchor:middle, startpoint-on-the-path is the point + on the path which represents the point on the path which is [ ‘startOffset’ minus half + of the total advance values for all of the glyphs in the ‘textPath’ element ] distance + along the path from the start of the path, calculated using the user agent's distance + along the path algorithm. + + For text-anchor:end, startpoint-on-the-path is the point on + the path which represents the point on the path which is [ ‘startOffset’ minus the + total advance values for all of the glyphs in the ‘textPath’ element ]. + + Before rendering the first glyph, the horizontal component of the startpoint-on-the-path + is adjusted to take into account various horizontal alignment text properties and + attributes, such as a ‘dx’ attribute value on a ‘tspan’ element. + */ + enum TextAnchor textAnchor = font->textAnchor; + CGRect textBounds = CTLineGetBoundsWithOptions(line, 0); + double textMeasure = CGRectGetWidth(textBounds); + double offset = getTextAnchorOffset(textAnchor, textMeasure); + + bool hasTextPath = textPath != nil; + + int side = 1; + double startOfRendering = 0; + double endOfRendering = _pathLength; + double fontSize = [gc getFontSize]; + bool sharpMidLine = false; + if (hasTextPath) { + sharpMidLine = TextPathMidLineFromString([textPath midLine]) == TextPathMidLineSharp; + /* + Name + side + Value + left | right + initial value + left + Animatable + yes + + Determines the side of the path the text is placed on + (relative to the path direction). + + Specifying a value of right effectively reverses the path. + + Added in SVG 2 to allow text either inside or outside closed subpaths + and basic shapes (e.g. rectangles, circles, and ellipses). + + Adding 'side' was resolved at the Sydney (2015) meeting. + */ + side = TextPathSideFromString([textPath side]) == TextPathSideRight ? -1 : 1; + /* + Name + startOffset + Value + | | + initial value + 0 + Animatable + yes + + An offset from the start of the path for the initial current text position, + calculated using the user agent's distance along the path algorithm, + after converting the path to the ‘textPath’ element's coordinate system. + + If a other than a percentage is given, then the ‘startOffset’ + represents a distance along the path measured in the current user coordinate + system for the ‘textPath’ element. + + If a percentage is given, then the ‘startOffset’ represents a percentage + distance along the entire path. Thus, startOffset="0%" indicates the start + point of the path and startOffset="100%" indicates the end point of the path. + + Negative values and values larger than the path length (e.g. 150%) are allowed. + + Any typographic characters with mid-points that are not on the path are not rendered + + For paths consisting of a single closed subpath (including an equivalent path for a + basic shape), typographic characters are rendered along one complete circuit of the + path. The text is aligned as determined by the text-anchor property to a position + along the path set by the ‘startOffset’ attribute. + + For the start (end) value, the text is rendered from the start (end) of the line + until the initial position along the path is reached again. + + For the middle, the text is rendered from the middle point in both directions until + a point on the path equal distance in both directions from the initial position on + the path is reached. + */ + double absoluteStartOffset = [PropHelper fromRelativeWithNSString:textPath.startOffset + relative:_pathLength + offset:0 + scale:1 + fontSize:fontSize]; + offset += absoluteStartOffset; + if (isClosed) { + double halfPathDistance = _pathLength / 2; + startOfRendering = absoluteStartOffset + (textAnchor == TextAnchorMiddle ? -halfPathDistance : 0); + endOfRendering = startOfRendering + _pathLength; + } + /* + TextPathSpacing spacing = textPath.getSpacing(); + if (spacing == TextPathSpacing.auto) { + // Hmm, what to do here? + // https://svgwg.org/svg2-draft/text.html#TextPathElementSpacingAttribute + } + */ + } + + /* + Name + method + Value + align | stretch + initial value + align + Animatable + yes + Indicates the method by which text should be rendered along the path. + + A value of align indicates that the typographic character should be rendered using + simple 2×3 matrix transformations such that there is no stretching/warping of the + typographic characters. Typically, supplemental rotation, scaling and translation + transformations are done for each typographic characters to be rendered. + + As a result, with align, in fonts where the typographic characters are designed to be + connected (e.g., cursive fonts), the connections may not align properly when text is + rendered along a path. + + A value of stretch indicates that the typographic character outlines will be converted + into paths, and then all end points and control points will be adjusted to be along the + perpendicular vectors from the path, thereby stretching and possibly warping the glyphs. + + With this approach, connected typographic characters, such as in cursive scripts, + will maintain their connections. (Non-vertical straight path segments should be + converted to Bézier curves in such a way that horizontal straight paths have an + (approximately) constant offset from the path along which the typographic characters + are rendered.) + + TODO implement stretch + */ + + /* + Name Value Initial value Animatable + textLength | | See below yes + + The author's computation of the total sum of all of the advance values that correspond + to character data within this element, including the advance value on the glyph + (horizontal or vertical), the effect of properties letter-spacing and word-spacing and + adjustments due to attributes ‘dx’ and ‘dy’ on this ‘text’ or ‘tspan’ element or any + descendants. This value is used to calibrate the user agent's own calculations with + that of the author. + + The purpose of this attribute is to allow the author to achieve exact alignment, + in visual rendering order after any bidirectional reordering, for the first and + last rendered glyphs that correspond to this element; thus, for the last rendered + character (in visual rendering order after any bidirectional reordering), + any supplemental inter-character spacing beyond normal glyph advances are ignored + (in most cases) when the user agent determines the appropriate amount to expand/compress + the text string to fit within a length of ‘textLength’. + + If attribute ‘textLength’ is specified on a given element and also specified on an + ancestor, the adjustments on all character data within this element are controlled by + the value of ‘textLength’ on this element exclusively, with the possible side-effect + that the adjustment ratio for the contents of this element might be different than the + adjustment ratio used for other content that shares the same ancestor. The user agent + must assume that the total advance values for the other content within that ancestor is + the difference between the advance value on that ancestor and the advance value for + this element. + + This attribute is not intended for use to obtain effects such as shrinking or + expanding text. + + A negative value is an error (see Error processing). + + The ‘textLength’ attribute is only applied when the wrapping area is not defined by the + TODO shape-inside or the inline-size properties. It is also not applied for any ‘text’ or + TODO ‘tspan’ element that has forced line breaks (due to a white-space value of pre or + pre-line). + + If the attribute is not specified anywhere within a ‘text’ element, the effect is as if + the author's computation exactly matched the value calculated by the user agent; + thus, no advance adjustments are made. + */ + double scaleSpacingAndGlyphs = 1; + NSString *mTextLength = [self textLength]; + enum TextLengthAdjust mLengthAdjust = TextLengthAdjustFromString([self lengthAdjust]); + if (mTextLength != nil) { + double author = [PropHelper fromRelativeWithNSString:mTextLength + relative:[gc getWidth] + offset:0 + scale:1 + fontSize:fontSize]; + if (author < 0) { + NSException *e = [NSException + exceptionWithName:@"NegativeTextLength" + reason:@"Negative textLength value" + userInfo:nil]; + @throw e; + } + switch (mLengthAdjust) { + default: + case TextLengthAdjustSpacing: + // TODO account for ligatures + letterSpacing += (author - textMeasure) / (n - 1); + break; + case TextLengthAdjustSpacingAndGlyphs: + scaleSpacingAndGlyphs = author / textMeasure; + break; + } + } + double scaledDirection = scaleSpacingAndGlyphs * side; + + /* + https://developer.mozilla.org/en/docs/Web/CSS/vertical-align + https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html + https://www.microsoft.com/typography/otspec/base.htm + http://apike.ca/prog_svg_text_style.html + https://www.w3schools.com/tags/canvas_textbaseline.asp + http://vanseodesign.com/web-design/svg-text-baseline-alignment/ + https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align + https://tympanus.net/codrops/css_reference/vertical-align/ + + https://svgwg.org/svg2-draft/text.html#AlignmentBaselineProperty + 11.10.2.6. The ‘alignment-baseline’ property + + This property is defined in the CSS Line Layout Module 3 specification. See 'alignment-baseline'. [css-inline-3] + https://drafts.csswg.org/css-inline/#propdef-alignment-baseline + + The vertical-align property shorthand should be preferred in new content. + + SVG 2 introduces some changes to the definition of this property. + In particular: the values 'auto', 'before-edge', and 'after-edge' have been removed. + For backwards compatibility, 'text-before-edge' should be mapped to 'text-top' and + 'text-after-edge' should be mapped to 'text-bottom'. + + Neither 'text-before-edge' nor 'text-after-edge' should be used with the vertical-align property. + */ + /* + CGRect fontBounds = CTFontGetBoundingBox(fontRef); + double textHeight = CGRectGetHeight(textBounds); + double fontWidth = CGRectGetWidth(textBounds); + CGPoint fontOrigin = fontBounds.origin; + + CGFloat fontMinX = fontOrigin.x; + CGFloat fontMinY = fontOrigin.y; + CGFloat fontMaxX = fontMinX + fontWidth; + CGFloat fontMaxY = fontMinY + textHeight; + */ + // TODO + double descenderDepth = CTFontGetDescent(fontRef); + double bottom = descenderDepth + CTFontGetLeading(fontRef); + double ascenderHeight = CTFontGetAscent(fontRef); + double top = ascenderHeight; + double totalHeight = top + bottom; + double baselineShift = 0; + NSString *baselineShiftString = [self getBaselineShift]; + enum AlignmentBaseline baseline = AlignmentBaselineFromString([self getAlignmentBaseline]); + if (baseline != AlignmentBaselineBaseline) { + // TODO alignment-baseline, test / verify behavior + // TODO get per glyph baselines from font baseline table, for high-precision alignment + CGFloat xHeight = CTFontGetXHeight(fontRef); + switch (baseline) { + // https://wiki.apache.org/xmlgraphics-fop/LineLayout/AlignmentHandling + default: + case AlignmentBaselineBaseline: + // Use the dominant baseline choice of the parent. + // Match the box’s corresponding baseline to that of its parent. + baselineShift = 0; + break; + + case AlignmentBaselineTextBottom: + case AlignmentBaselineAfterEdge: + case AlignmentBaselineTextAfterEdge: + // Match the bottom of the box to the bottom of the parent’s content area. + // text-after-edge = text-bottom + // text-after-edge = descender depth + baselineShift = -descenderDepth; + break; + + case AlignmentBaselineAlphabetic: + // Match the box’s alphabetic baseline to that of its parent. + // alphabetic = 0 + baselineShift = 0; + break; + + case AlignmentBaselineIdeographic: + // Match the box’s ideographic character face under-side baseline to that of its parent. + // ideographic = descender depth + baselineShift = -descenderDepth; + break; + + case AlignmentBaselineMiddle: + // Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. TODO + // middle = x height / 2 + baselineShift = xHeight / 2; + break; + + case AlignmentBaselineCentral: + // Match the box’s central baseline to the central baseline of its parent. + // central = (ascender height - descender depth) / 2 + baselineShift = (ascenderHeight - descenderDepth) / 2; + break; + + case AlignmentBaselineMathematical: + // Match the box’s mathematical baseline to that of its parent. + // Hanging and mathematical baselines + // There are no obvious formulas to calculate the position of these baselines. + // At the time of writing FOP puts the hanging baseline at 80% of the ascender + // height and the mathematical baseline at 50%. + baselineShift = 0.5 * ascenderHeight; + break; + + case AlignmentBaselineHanging: + baselineShift = 0.8 * ascenderHeight; + break; + + case AlignmentBaselineTextTop: + case AlignmentBaselineBeforeEdge: + case AlignmentBaselineTextBeforeEdge: + // Match the top of the box to the top of the parent’s content area. + // text-before-edge = text-top + // text-before-edge = ascender height + baselineShift = ascenderHeight; + break; + + case AlignmentBaselineBottom: + // Align the top of the aligned subtree with the top of the line box. + baselineShift = bottom; + break; + + case AlignmentBaselineCenter: + // Align the center of the aligned subtree with the center of the line box. + baselineShift = totalHeight / 2; + break; + + case AlignmentBaselineTop: + // Align the bottom of the aligned subtree with the bottom of the line box. + baselineShift = top; + break; + } + } + /* + 2.2.2. Alignment Shift: baseline-shift longhand + + This property specifies by how much the box is shifted up from its alignment point. + It does not apply when alignment-baseline is top or bottom. + + Authors should use the vertical-align shorthand instead of this property. + + Values have the following meanings: + + + Raise (positive value) or lower (negative value) by the specified length. + + Raise (positive value) or lower (negative value) by the specified percentage of the line-height. + TODO sub + Lower by the offset appropriate for subscripts of the parent’s box. + (The UA should use the parent’s font data to find this offset whenever possible.) + TODO super + Raise by the offset appropriate for superscripts of the parent’s box. + (The UA should use the parent’s font data to find this offset whenever possible.) + + User agents may additionally support the keyword baseline as computing to 0 + if is necessary for them to support legacy SVG content. + Issue: We would prefer to remove this, + and are looking for feedback from SVG user agents as to whether it’s necessary. + + https://www.w3.org/TR/css-inline-3/#propdef-baseline-shift + */ + if (baselineShiftString != nil && ![baselineShiftString isEqualToString:@""]) { + switch (baseline) { + case AlignmentBaselineTop: + case AlignmentBaselineBottom: + break; + + default: + if (fontData != nil && [baselineShiftString isEqualToString:@"sub"]) { + // TODO + NSDictionary* tables = [fontData objectForKey:@"tables"]; + NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; + NSDictionary* os2 = [tables objectForKey:@"os2"]; + NSNumber* ySubscriptYOffset = [os2 objectForKey:@"ySubscriptYOffset"]; + if (ySubscriptYOffset) { + double subOffset = [ySubscriptYOffset doubleValue]; + baselineShift += fontSize * subOffset / [unitsPerEm doubleValue]; + } + } else if (fontData != nil && [baselineShiftString isEqualToString:@"super"]) { + // TODO + NSDictionary* tables = [fontData objectForKey:@"tables"]; + NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; + NSDictionary* os2 = [tables objectForKey:@"os2"]; + NSNumber* ySuperscriptYOffset = [os2 objectForKey:@"ySuperscriptYOffset"]; + if (ySuperscriptYOffset) { + double superOffset = [ySuperscriptYOffset doubleValue]; + baselineShift -= fontSize * superOffset / [unitsPerEm doubleValue]; + } + } else if ([baselineShiftString isEqualToString:@"baseline"]) { + } else { + baselineShift -= [PropHelper fromRelativeWithNSString:baselineShiftString + relative:fontSize + offset:0 + scale:1 + fontSize:fontSize]; + + } + break; + } + } + + CFArrayRef runs = CTLineGetGlyphRuns(line); CFIndex runEnd = CFArrayGetCount(runs); for (CFIndex r = 0; r < runEnd; r++) { CTRunRef run = CFArrayGetValueAtIndex(runs, r); - CFIndex runGlyphCount = CTRunGetGlyphCount(run); + CFIndex indices[runGlyphCount]; CGSize advances[runGlyphCount]; CGGlyph glyphs[runGlyphCount]; // Grab the glyphs and font CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); + CTRunGetStringIndices(run, CFRangeMake(0, 0), indices); CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName); CTFontGetAdvancesForGlyphs(runFont, kCTFontOrientationHorizontal, glyphs, advances, runGlyphCount); - /* - Determine the startpoint-on-the-path for the first glyph using attribute ‘startOffset’ - and property text-anchor. - - For text-anchor:start, startpoint-on-the-path is the point - on the path which represents the point on the path which is ‘startOffset’ distance - along the path from the start of the path, calculated using the user agent's distance - along the path algorithm. - - For text-anchor:middle, startpoint-on-the-path is the point - on the path which represents the point on the path which is [ ‘startOffset’ minus half - of the total advance values for all of the glyphs in the ‘textPath’ element ] distance - along the path from the start of the path, calculated using the user agent's distance - along the path algorithm. - - For text-anchor:end, startpoint-on-the-path is the point on - the path which represents the point on the path which is [ ‘startOffset’ minus the - total advance values for all of the glyphs in the ‘textPath’ element ]. - - Before rendering the first glyph, the horizontal component of the startpoint-on-the-path - is adjusted to take into account various horizontal alignment text properties and - attributes, such as a ‘dx’ attribute value on a ‘tspan’ element. - */ - enum TextAnchor textAnchor = font->textAnchor; - CGRect textBounds = CTLineGetBoundsWithOptions(line, 0); - double textMeasure = CGRectGetWidth(textBounds); - double offset = getTextAnchorOffset(textAnchor, textMeasure); - - bool hasTextPath = textPath != nil; - - int side = 1; - double startOfRendering = 0; - double endOfRendering = _pathLength; - double fontSize = [gc getFontSize]; - bool sharpMidLine = false; - if (hasTextPath) { - sharpMidLine = TextPathMidLineFromString([textPath midLine]) == TextPathMidLineSharp; - /* - Name - side - Value - left | right - initial value - left - Animatable - yes - - Determines the side of the path the text is placed on - (relative to the path direction). - - Specifying a value of right effectively reverses the path. - - Added in SVG 2 to allow text either inside or outside closed subpaths - and basic shapes (e.g. rectangles, circles, and ellipses). - - Adding 'side' was resolved at the Sydney (2015) meeting. - */ - side = TextPathSideFromString([textPath side]) == TextPathSideRight ? -1 : 1; - /* - Name - startOffset - Value - | | - initial value - 0 - Animatable - yes - - An offset from the start of the path for the initial current text position, - calculated using the user agent's distance along the path algorithm, - after converting the path to the ‘textPath’ element's coordinate system. - - If a other than a percentage is given, then the ‘startOffset’ - represents a distance along the path measured in the current user coordinate - system for the ‘textPath’ element. - - If a percentage is given, then the ‘startOffset’ represents a percentage - distance along the entire path. Thus, startOffset="0%" indicates the start - point of the path and startOffset="100%" indicates the end point of the path. - - Negative values and values larger than the path length (e.g. 150%) are allowed. - - Any typographic characters with mid-points that are not on the path are not rendered - - For paths consisting of a single closed subpath (including an equivalent path for a - basic shape), typographic characters are rendered along one complete circuit of the - path. The text is aligned as determined by the text-anchor property to a position - along the path set by the ‘startOffset’ attribute. - - For the start (end) value, the text is rendered from the start (end) of the line - until the initial position along the path is reached again. - - For the middle, the text is rendered from the middle point in both directions until - a point on the path equal distance in both directions from the initial position on - the path is reached. - */ - double absoluteStartOffset = [PropHelper fromRelativeWithNSString:textPath.startOffset - relative:_pathLength - offset:0 - scale:1 - fontSize:fontSize]; - offset += absoluteStartOffset; - if (isClosed) { - double halfPathDistance = _pathLength / 2; - startOfRendering = absoluteStartOffset + (textAnchor == TextAnchorMiddle ? -halfPathDistance : 0); - endOfRendering = startOfRendering + _pathLength; - } - /* - TextPathSpacing spacing = textPath.getSpacing(); - if (spacing == TextPathSpacing.auto) { - // Hmm, what to do here? - // https://svgwg.org/svg2-draft/text.html#TextPathElementSpacingAttribute - } - */ - } - - /* - Name - method - Value - align | stretch - initial value - align - Animatable - yes - Indicates the method by which text should be rendered along the path. - - A value of align indicates that the typographic character should be rendered using - simple 2×3 matrix transformations such that there is no stretching/warping of the - typographic characters. Typically, supplemental rotation, scaling and translation - transformations are done for each typographic characters to be rendered. - - As a result, with align, in fonts where the typographic characters are designed to be - connected (e.g., cursive fonts), the connections may not align properly when text is - rendered along a path. - - A value of stretch indicates that the typographic character outlines will be converted - into paths, and then all end points and control points will be adjusted to be along the - perpendicular vectors from the path, thereby stretching and possibly warping the glyphs. - - With this approach, connected typographic characters, such as in cursive scripts, - will maintain their connections. (Non-vertical straight path segments should be - converted to Bézier curves in such a way that horizontal straight paths have an - (approximately) constant offset from the path along which the typographic characters - are rendered.) - - TODO implement stretch - */ - - /* - Name Value Initial value Animatable - textLength | | See below yes - - The author's computation of the total sum of all of the advance values that correspond - to character data within this element, including the advance value on the glyph - (horizontal or vertical), the effect of properties letter-spacing and word-spacing and - adjustments due to attributes ‘dx’ and ‘dy’ on this ‘text’ or ‘tspan’ element or any - descendants. This value is used to calibrate the user agent's own calculations with - that of the author. - - The purpose of this attribute is to allow the author to achieve exact alignment, - in visual rendering order after any bidirectional reordering, for the first and - last rendered glyphs that correspond to this element; thus, for the last rendered - character (in visual rendering order after any bidirectional reordering), - any supplemental inter-character spacing beyond normal glyph advances are ignored - (in most cases) when the user agent determines the appropriate amount to expand/compress - the text string to fit within a length of ‘textLength’. - - If attribute ‘textLength’ is specified on a given element and also specified on an - ancestor, the adjustments on all character data within this element are controlled by - the value of ‘textLength’ on this element exclusively, with the possible side-effect - that the adjustment ratio for the contents of this element might be different than the - adjustment ratio used for other content that shares the same ancestor. The user agent - must assume that the total advance values for the other content within that ancestor is - the difference between the advance value on that ancestor and the advance value for - this element. - - This attribute is not intended for use to obtain effects such as shrinking or - expanding text. - - A negative value is an error (see Error processing). - - The ‘textLength’ attribute is only applied when the wrapping area is not defined by the - TODO shape-inside or the inline-size properties. It is also not applied for any ‘text’ or - TODO ‘tspan’ element that has forced line breaks (due to a white-space value of pre or - pre-line). - - If the attribute is not specified anywhere within a ‘text’ element, the effect is as if - the author's computation exactly matched the value calculated by the user agent; - thus, no advance adjustments are made. - */ - double scaleSpacingAndGlyphs = 1; - NSString *mTextLength = [self textLength]; - enum TextLengthAdjust mLengthAdjust = TextLengthAdjustFromString([self lengthAdjust]); - if (mTextLength != nil) { - double author = [PropHelper fromRelativeWithNSString:mTextLength - relative:[gc getWidth] - offset:0 - scale:1 - fontSize:fontSize]; - if (author < 0) { - NSException *e = [NSException - exceptionWithName:@"NegativeTextLength" - reason:@"Negative textLength value" - userInfo:nil]; - @throw e; - } - switch (mLengthAdjust) { - default: - case TextLengthAdjustSpacing: - letterSpacing += (author - textMeasure) / (n - 1); - break; - case TextLengthAdjustSpacingAndGlyphs: - scaleSpacingAndGlyphs = author / textMeasure; - break; - } - } - double scaledDirection = scaleSpacingAndGlyphs * side; - - /* - https://developer.mozilla.org/en/docs/Web/CSS/vertical-align - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html - https://www.microsoft.com/typography/otspec/base.htm - http://apike.ca/prog_svg_text_style.html - https://www.w3schools.com/tags/canvas_textbaseline.asp - http://vanseodesign.com/web-design/svg-text-baseline-alignment/ - https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align - https://tympanus.net/codrops/css_reference/vertical-align/ - - https://svgwg.org/svg2-draft/text.html#AlignmentBaselineProperty - 11.10.2.6. The ‘alignment-baseline’ property - - This property is defined in the CSS Line Layout Module 3 specification. See 'alignment-baseline'. [css-inline-3] - https://drafts.csswg.org/css-inline/#propdef-alignment-baseline - - The vertical-align property shorthand should be preferred in new content. - - SVG 2 introduces some changes to the definition of this property. - In particular: the values 'auto', 'before-edge', and 'after-edge' have been removed. - For backwards compatibility, 'text-before-edge' should be mapped to 'text-top' and - 'text-after-edge' should be mapped to 'text-bottom'. - - Neither 'text-before-edge' nor 'text-after-edge' should be used with the vertical-align property. - */ - /* - CGRect fontBounds = CTFontGetBoundingBox(fontRef); - double textHeight = CGRectGetHeight(textBounds); - double fontWidth = CGRectGetWidth(textBounds); - CGPoint fontOrigin = fontBounds.origin; - - CGFloat fontMinX = fontOrigin.x; - CGFloat fontMinY = fontOrigin.y; - CGFloat fontMaxX = fontMinX + fontWidth; - CGFloat fontMaxY = fontMinY + textHeight; - */ - // TODO - double descenderDepth = CTFontGetDescent(fontRef); - double bottom = descenderDepth + CTFontGetLeading(fontRef); - double ascenderHeight = CTFontGetAscent(fontRef); - double top = ascenderHeight; - double totalHeight = top + bottom; - double baselineShift = 0; - // TODO, alignmentBaseline and baselineShift are always nil for some reason? - NSString *baselineShiftString = [self getBaselineShift]; - enum AlignmentBaseline baseline = AlignmentBaselineFromString([self getAlignmentBaseline]); - if (baseline != AlignmentBaselineBaseline) { - // TODO alignment-baseline, test / verify behavior - // TODO get per glyph baselines from font baseline table, for high-precision alignment - CGFloat xHeight = CTFontGetXHeight(fontRef); - switch (baseline) { - // https://wiki.apache.org/xmlgraphics-fop/LineLayout/AlignmentHandling - default: - case AlignmentBaselineBaseline: - // Use the dominant baseline choice of the parent. - // Match the box’s corresponding baseline to that of its parent. - baselineShift = 0; - break; - - case AlignmentBaselineTextBottom: - case AlignmentBaselineAfterEdge: - case AlignmentBaselineTextAfterEdge: - // Match the bottom of the box to the bottom of the parent’s content area. - // text-after-edge = text-bottom - // text-after-edge = descender depth - baselineShift = -descenderDepth; - break; - - case AlignmentBaselineAlphabetic: - // Match the box’s alphabetic baseline to that of its parent. - // alphabetic = 0 - baselineShift = 0; - break; - - case AlignmentBaselineIdeographic: - // Match the box’s ideographic character face under-side baseline to that of its parent. - // ideographic = descender depth - baselineShift = -descenderDepth; - break; - - case AlignmentBaselineMiddle: - // Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. TODO - // middle = x height / 2 - baselineShift = xHeight / 2; - break; - - case AlignmentBaselineCentral: - // Match the box’s central baseline to the central baseline of its parent. - // central = (ascender height - descender depth) / 2 - baselineShift = (ascenderHeight - descenderDepth) / 2; - break; - - case AlignmentBaselineMathematical: - // Match the box’s mathematical baseline to that of its parent. - // Hanging and mathematical baselines - // There are no obvious formulas to calculate the position of these baselines. - // At the time of writing FOP puts the hanging baseline at 80% of the ascender - // height and the mathematical baseline at 50%. - baselineShift = 0.5 * ascenderHeight; - break; - - case AlignmentBaselineHanging: - baselineShift = 0.8 * ascenderHeight; - break; - - case AlignmentBaselineTextTop: - case AlignmentBaselineBeforeEdge: - case AlignmentBaselineTextBeforeEdge: - // Match the top of the box to the top of the parent’s content area. - // text-before-edge = text-top - // text-before-edge = ascender height - baselineShift = ascenderHeight; - break; - - case AlignmentBaselineBottom: - // Align the top of the aligned subtree with the top of the line box. - baselineShift = bottom; - break; - - case AlignmentBaselineCenter: - // Align the center of the aligned subtree with the center of the line box. - baselineShift = totalHeight / 2; - break; - - case AlignmentBaselineTop: - // Align the bottom of the aligned subtree with the bottom of the line box. - baselineShift = top; - break; - } - } - /* - 2.2.2. Alignment Shift: baseline-shift longhand - - This property specifies by how much the box is shifted up from its alignment point. - It does not apply when alignment-baseline is top or bottom. - - Authors should use the vertical-align shorthand instead of this property. - - Values have the following meanings: - - - Raise (positive value) or lower (negative value) by the specified length. - - Raise (positive value) or lower (negative value) by the specified percentage of the line-height. - TODO sub - Lower by the offset appropriate for subscripts of the parent’s box. - (The UA should use the parent’s font data to find this offset whenever possible.) - TODO super - Raise by the offset appropriate for superscripts of the parent’s box. - (The UA should use the parent’s font data to find this offset whenever possible.) - - User agents may additionally support the keyword baseline as computing to 0 - if is necessary for them to support legacy SVG content. - Issue: We would prefer to remove this, - and are looking for feedback from SVG user agents as to whether it’s necessary. - - https://www.w3.org/TR/css-inline-3/#propdef-baseline-shift - */ - if (baselineShiftString != nil && ![baselineShiftString isEqualToString:@""]) { - switch (baseline) { - case AlignmentBaselineTop: - case AlignmentBaselineBottom: - break; - - default: - if (fontData != nil && [baselineShiftString isEqualToString:@"sub"]) { - // TODO - NSDictionary* tables = [fontData objectForKey:@"tables"]; - NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; - NSDictionary* os2 = [tables objectForKey:@"os2"]; - NSNumber* ySubscriptYOffset = [os2 objectForKey:@"ySubscriptYOffset"]; - if (ySubscriptYOffset) { - double subOffset = [ySubscriptYOffset doubleValue]; - baselineShift += fontSize * subOffset / [unitsPerEm doubleValue]; - } - } else if (fontData != nil && [baselineShiftString isEqualToString:@"super"]) { - // TODO - NSDictionary* tables = [fontData objectForKey:@"tables"]; - NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; - NSDictionary* os2 = [tables objectForKey:@"os2"]; - NSNumber* ySuperscriptYOffset = [os2 objectForKey:@"ySuperscriptYOffset"]; - if (ySuperscriptYOffset) { - double superOffset = [ySuperscriptYOffset doubleValue]; - baselineShift -= fontSize * superOffset / [unitsPerEm doubleValue]; - } - } else if ([baselineShiftString isEqualToString:@"baseline"]) { - } else { - baselineShift -= [PropHelper fromRelativeWithNSString:baselineShiftString - relative:fontSize - offset:0 - scale:1 - fontSize:fontSize]; - - } - break; - } - } - CFDictionaryRef ligattributes; - NSNumber *lig = [NSNumber numberWithInt:allowOptionalLigatures ? 2 : 1]; - - if (fontRef != nil) { - ligattributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTFontAttributeName: (__bridge id)fontRef, - (NSString *)NSLigatureAttributeName: lig - }; - } else { - ligattributes = (__bridge CFDictionaryRef)@{ - (NSString *)NSLigatureAttributeName: lig - }; - } for(CFIndex g = 0; g < runGlyphCount; g++) { CGGlyph glyph = glyphs[g]; - bool hasLigature = false; - CGFloat charWidth = 0; - double advance = 0; - bool alreadyRenderedGraphemeCluster = ligature[g]; - if (!alreadyRenderedGraphemeCluster) { - /* - Determine the glyph's charwidth (i.e., the amount which the current text position - advances horizontally when the glyph is drawn using horizontal text layout). - */ - double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, glyphs + g, NULL, 1); - charWidth = unkernedAdvance * scaleSpacingAndGlyphs; + /* + Determine the glyph's charwidth (i.e., the amount which the current text position + advances horizontally when the glyph is drawn using horizontal text layout). + */ + double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, &glyph, NULL, 1); + CGFloat charWidth = unkernedAdvance * scaleSpacingAndGlyphs; - /* - For each subsequent glyph, set a new startpoint-on-the-path as the previous - endpoint-on-the-path, but with appropriate adjustments taking into account - horizontal kerning tables in the font and current values of various attributes - and properties, including spacing properties (e.g. letter-spacing and word-spacing) - and ‘tspan’ elements with values provided for attributes ‘dx’ and ‘dy’. All - adjustments are calculated as distance adjustments along the path, calculated - using the user agent's distance along the path algorithm. - */ - if (autoKerning) { - double kerned = advances[g].width * scaleSpacingAndGlyphs; - kerning = kerned - charWidth; - } - - NSUInteger len = 2; - NSUInteger nextIndex = g; - while (++nextIndex < n) { - NSString* nextLigature = [str substringWithRange:NSMakeRange(g, len++)]; - bool hasNextLigature = hasGlyph(fontRef, nextLigature, &glyph, ligattributes); - if (hasNextLigature) { - ligature[nextIndex] = true; - hasLigature = true; - } else { - break; - } - } - if (hasLigature) { - charWidth = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, &glyph, NULL, 1); - } - - char currentChar = [str characterAtIndex:g]; - bool isWordSeparator = [separators characterIsMember:currentChar]; - double wordSpace = isWordSeparator ? wordSpacing : 0; - double spacing = wordSpace + letterSpacing; - advance = charWidth + spacing; + /* + For each subsequent glyph, set a new startpoint-on-the-path as the previous + endpoint-on-the-path, but with appropriate adjustments taking into account + horizontal kerning tables in the font and current values of various attributes + and properties, including spacing properties (e.g. letter-spacing and word-spacing) + and ‘tspan’ elements with values provided for attributes ‘dx’ and ‘dy’. All + adjustments are calculated as distance adjustments along the path, calculated + using the user agent's distance along the path algorithm. + */ + if (autoKerning) { + double kerned = advances[g].width * scaleSpacingAndGlyphs; + kerning = kerned - charWidth; } - double x = [gc nextXWithDouble:(alreadyRenderedGraphemeCluster ? 0 : kerning + advance)]; + CFIndex currIndex = indices[g]; + char currentChar = [str characterAtIndex:currIndex]; + bool isWordSeparator = [separators characterIsMember:currentChar]; + double wordSpace = isWordSeparator ? wordSpacing : 0; + double spacing = wordSpace + letterSpacing; + double advance = charWidth + spacing; + + double x = [gc nextXWithDouble:kerning + advance]; double y = [gc nextY]; double dx = [gc nextDeltaX]; double dy = [gc nextDeltaY]; double r = [[gc nextRotation] doubleValue] / radToDeg; - if (alreadyRenderedGraphemeCluster) { + CFIndex endIndex = g + 1 == runGlyphCount ? currIndex : indices[g + 1]; + while (++currIndex < endIndex) { // Skip rendering other grapheme clusters of ligatures (already rendered), - // But, make sure to increment index positions by making gc.next() calls. - continue; + // And, make sure to increment index positions by making gc.next() calls. + [gc nextXWithDouble:0]; + [gc nextY]; + [gc nextDeltaX]; + [gc nextDeltaY]; + [gc nextRotation]; } CGPathRef glyphPath = CTFontCreatePathForGlyph(runFont, glyph, nil); @@ -890,31 +856,4 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) } } -bool hasGlyph(CTFontRef fontRef, NSString * str, CGGlyph* glyph, CFDictionaryRef attributes) -{ - CFStringRef string = (__bridge CFStringRef)str; - CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); - CTLineRef line = CTLineCreateWithAttributedString(attrString); - CFArrayRef runs = CTLineGetGlyphRuns(line); - - CFIndex runEnd = CFArrayGetCount(runs); - if (runEnd > 1) { - CFRelease(attrString); - CFRelease(line); - return false; - } - CTRunRef run = CFArrayGetValueAtIndex(runs, 0); - CFIndex runGlyphCount = CTRunGetGlyphCount(run); - bool hasGlyph = runGlyphCount == 1; - - if (hasGlyph) { - CTRunGetGlyphs(run, CFRangeMake(0, 1), glyph); - } - - CFRelease(attrString); - CFRelease(line); - - return hasGlyph; -} - @end