From 058183c0de054b7ae14de5da2a1bd59e8803cda5 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Tue, 8 Aug 2017 03:04:46 +0300 Subject: [PATCH] Start https://www.w3.org/TR/SVG2/text.html#TextLayoutAlgorithm To implement support for bidirectional and vertical text etc. --- .../main/java/com/horcrux/svg/Direction.java | 6 + .../java/com/horcrux/svg/TSpanShadowNode.java | 2 +- .../com/horcrux/svg/TextLayoutAlgorithm.java | 2123 ++++++++++------- 3 files changed, 1236 insertions(+), 895 deletions(-) create mode 100644 android/src/main/java/com/horcrux/svg/Direction.java diff --git a/android/src/main/java/com/horcrux/svg/Direction.java b/android/src/main/java/com/horcrux/svg/Direction.java new file mode 100644 index 00000000..3cc0ce72 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/Direction.java @@ -0,0 +1,6 @@ +package com.horcrux.svg; + +enum Direction { + ltr, + rtl +} diff --git a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java index e75060a2..59448cbf 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java @@ -45,7 +45,7 @@ class TSpanShadowNode extends TextShadowNode { private static final String TTF = ".ttf"; private Path mCache; - private @Nullable String mContent; + @Nullable String mContent; private TextPathShadowNode textPath; @ReactProp(name = "content") diff --git a/android/src/main/java/com/horcrux/svg/TextLayoutAlgorithm.java b/android/src/main/java/com/horcrux/svg/TextLayoutAlgorithm.java index b0de6d8b..178df423 100644 --- a/android/src/main/java/com/horcrux/svg/TextLayoutAlgorithm.java +++ b/android/src/main/java/com/horcrux/svg/TextLayoutAlgorithm.java @@ -2,964 +2,1299 @@ package com.horcrux.svg; // TODO implement https://www.w3.org/TR/SVG2/text.html#TextLayoutAlgorithm +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.PointF; + +import com.facebook.react.uimanager.ReactShadowNode; + +import java.util.ArrayList; + @SuppressWarnings("unused") public class TextLayoutAlgorithm { - @SuppressWarnings("EmptyMethod") - void layoutText() { -/* + class CharacterInformation { + int index; + double x = 0; + double y = 0; + double advance; + char character; + double rotate = 0; + TextShadowNode element; + boolean hidden = false; + boolean middle = false; + boolean resolved = false; + boolean xSpecified = false; + boolean ySpecified = false; + boolean addressable = true; + boolean anchoredChunk = false; + boolean rotateSpecified = false; + boolean firstCharacterInResolvedDescendant = false; + CharacterInformation(int index, char c) { + this.index = index; + this.character = c; + } + } + + class LayoutInput { + TextShadowNode text; + boolean horizontal; + } + + private void getSubTreeTypographicCharacterPositions( + ArrayList inTextPath, + ArrayList subtree, + StringBuilder line, + ReactShadowNode node, + TextPathShadowNode textPath + ) { + if (node instanceof TSpanShadowNode) { + final TSpanShadowNode tSpanShadowNode = (TSpanShadowNode) node; + String content = tSpanShadowNode.mContent; + if (content == null) { + for (int i = 0; i < node.getChildCount(); i++) { + getSubTreeTypographicCharacterPositions(inTextPath, subtree, line, node.getChildAt(i), textPath); + } + } else { + for (int i = 0; i < content.length(); i++) { + subtree.add(tSpanShadowNode); + inTextPath.add(textPath); + } + line.append(content); + } + } else { + textPath = node instanceof TextPathShadowNode ? (TextPathShadowNode) node : textPath; + for (int i = 0; i < node.getChildCount(); i++) { + getSubTreeTypographicCharacterPositions(inTextPath, subtree, line, node.getChildAt(i), textPath); + } + } + } + + CharacterInformation[] layoutText(LayoutInput layoutInput) { +/* Setup + Let root be the result of generating + typographic character positions for the + ‘text’ element and its subtree, laid out as if it + were an absolutely positioned element. - Let root be the result of generating - typographic character positions for the - ‘text’ element and its subtree, laid out as if it - were an absolutely positioned element. - - This will be a single line of text unless the - white-space property causes line breaks. - - - - Let count be the number of DOM characters - within the ‘text’ element's subtree. - - - Let result be an array of length count - whose entries contain the per-character information described - above. Each entry is initialized as follows: - - its global index number equal to its position in the array, - its "x" coordinate set to "unspecified", - its "y" coordinate set to "unspecified", - its "rotate" coordinate set to "unspecified", - its "hidden" flag is false, - its "addressable" flag is true, - its "middle" flag is false, - its "anchored chunk" flag is false. - - If result is empty, then return result. - - - Let CSS_positions be an array of length - count whose entries will be filled with the - x and y positions of the corresponding - typographic character in root. The array - entries are initialized to (0, 0). - - - Let "horizontal" be a flag, true if the writing mode of ‘text’ - is horizontal, false otherwise. - + This will be a single line of text unless the + white-space property causes line breaks. +*/ + TextShadowNode text = layoutInput.text; + StringBuilder line = new StringBuilder(); + ArrayList subtree = new ArrayList<>(); + ArrayList inTextPath = new ArrayList<>(); + getSubTreeTypographicCharacterPositions(inTextPath, subtree, line, text, null); + final char[] root = line.toString().toCharArray(); +/* + Let count be the number of DOM characters + within the ‘text’ element's subtree. +*/ + int count = root.length; +/* + Let result be an array of length count + whose entries contain the per-character information described + above. Each entry is initialized as follows: + its global index number equal to its position in the array, + its "x" coordinate set to "unspecified", + its "y" coordinate set to "unspecified", + its "rotate" coordinate set to "unspecified", + its "hidden" flag is false, + its "addressable" flag is true, + its "middle" flag is false, + its "anchored chunk" flag is false. +*/ + final CharacterInformation[] result = new CharacterInformation[count]; + for (int i = 0; i < count; i++) { + result[i] = new CharacterInformation(i, root[i]); + } +/* + If result is empty, then return result. +*/ + if (count == 0) { + return result; + } +/* + Let CSS_positions be an array of length + count whose entries will be filled with the + x and y positions of the corresponding + typographic character in root. The array + entries are initialized to (0, 0). +*/ + PointF[] CSS_positions = new PointF[count]; + for (int i = 0; i < count; i++) { + CSS_positions[i] = new PointF(0, 0); + } +/* + Let "horizontal" be a flag, true if the writing mode of ‘text’ + is horizontal, false otherwise. +*/ + final boolean horizontal = true; +/* Set flags and assign initial positions - For each array element with index i in - result: - - - - Set addressable to false if the character at index i was: - + For each array element with index i in + result: +*/ + for (int i = 0; i < count; i++) { +/* + TODO Set addressable to false if the character at index i was: part of the text content of a non-rendered element - - discarded during layout due to being a - collapsed - white space character, a soft hyphen character, or a - bidi control character; or + discarded during layout due to being a + collapsed + white space character, a soft hyphen character, or a + bidi control character; or - discarded during layout due to being a - collapsed - segment break; or + discarded during layout due to being a + collapsed + segment break; or - trimmed - from the start or end of a line. + trimmed + from the start or end of a line. + Since there is collapsible white space not addressable by glyph + positioning attributes in the following ‘text’ element + (with a standard font), the "B" glyph will be placed at x=300. + + A + B + + This is because the white space before the "A", and all but one white space + character between the "A" and "B", is collapsed away or trimmed. - Since there is collapsible white space not addressable by glyph - positioning attributes in the following ‘text’ element - (with a standard font), the "B" glyph will be placed at x=300. +*/ + result[i].addressable = true; +/* - - A - B - + Set middle to true if the character at index i + TODO is the second or later character that corresponds to a typographic character. +*/ + result[i].middle = false; +/* - This is because the white space before the "A", and all but one white space - character between the "A" and "B", is collapsed away or trimmed. - - - - - Set middle to true if the character at index i - is the second or later character that corresponds to a typographic character. - - - If the character at index i corresponds to a typographic character at the beginning of a line, then set the "anchored - chunk" flag of result[i] to true. - - This ensures chunks shifted by text-anchor do not - span multiple lines. - - - - If addressable is true and middle is false then - set CSS_positions[i] to the position of the - corresponding typographic character as determined by the CSS - renderer. Otherwise, if i > 0, then set - CSS_positions[i] = - CSS_positions[i − 1] + TODO If the character at index i corresponds to a typographic character at the beginning of a line, then set the "anchored + chunk" flag of result[i] to true. + This ensures chunks shifted by text-anchor do not + span multiple lines. +*/ + result[i].anchoredChunk = i == 0; +/* + If addressable is true and middle is false then + set CSS_positions[i] to the position of the + TODO corresponding typographic character as determined by the CSS + renderer. Otherwise, if i > 0, then set + CSS_positions[i] = + CSS_positions[i − 1] +*/ + if (result[i].addressable && !result[i].middle) { + CSS_positions[i].set(0, 0); + } else if (i > 0) { + CSS_positions[i].set(CSS_positions[i - 1]); + } + } +/* Resolve character positioning - Position adjustments (e.g values in a ‘x’ attribute) - specified by a node apply to all characters in that node including - characters in the node's descendants. Adjustments specified in - descendant nodes, however, override adjustments from ancestor - nodes. This section resolves which adjustments are to be applied to - which characters. It also directly sets the rotate coordinate - of result. - - - - Set up: - - - Let resolve_x, resolve_y, - resolve_dx, and resolve_dy be arrays of - length count whose entries are all initialized - to "unspecified". - - - Set "in_text_path" flag false. - - This flag will allow ‘y’ (‘x’) - attribute values to be ignored for horizontal (vertical) - text inside ‘textPath’ elements. - - - - Call the following procedure with the ‘text’ element node. - - - - - Procedure: resolve character - positioning: - - A recursive procedure that takes as input a node and - whose steps are as follows: - - - - If node is a ‘text’ or ‘tspan’ node: - - - Let index equal the "global index number" of the - first character in the node. - - - Let x, y, dx, dy - and rotate be the lists of values from the - corresponding attributes on node, or empty - lists if the corresponding attribute was not specified - or was invalid. - - - If "in_text_path" flag is false: - - - Let new_chunk_count - = max(length of x, length of y). - - - Else: - - - If the "horizontal" flag is true: - - - Let new_chunk_count = length of x. - - - - - Else: - - - Let new_chunk_count = length of y. - - - - - - - Let length be the number of DOM characters in the - subtree rooted at node. - - - Let i = 0 and j = 0. - - i is an index of addressable characters in the node; - j is an index of all characters in the node. - - - - While j < length, do: - - This loop applies the ‘x’, ‘y’, - ‘dx’, ‘dy’ and ‘rotate’ - attributes to the content inside node. - - - - If the "addressable" flag of result[index + - j] is true, then: - - - If i < new_check_count, then - set the "anchored chunk" flag of - result[index + j] to - true. Else set the flag to false. - - Setting the flag to false ensures that ‘x’ - and ‘y’ attributes set in a ‘text’ - element don't create anchored chunk in a ‘textPath’ - element when they should not. - - - - If i < length of x, - then set resolve_x[index - + j] to x[i]. - - - If "in_text_path" flag is true and the "horizontal" - flag is false, unset - resolve_x[index]. - - The ‘x’ attribute is ignored for - vertical text on a path. - - - - If i < length of y, - then set resolve_y[index - + j] to y[i]. - - - If "in_text_path" flag is true and the "horizontal" - flag is true, unset - resolve_y[index]. - - The ‘y’ attribute is ignored for - horizontal text on a path. - - - - If i < length of dx, - then set resolve_dx[index - + j] to dy[i]. - - - If i < length of dy, - then set resolve_dy[index - + j] to dy[i]. - - - If i < length of rotate, - then set the angle value of result[index - + j] to rotate[i]. - Otherwise, if rotate is not empty, then - set result[index + j] - to result[index + j − 1]. - - - Set i = i + 1. - - - - - Set j = j + 1. - - - - - - - If node is a ‘textPath’ node: - - - Let index equal the global index number of the - first character in the node (including descendant nodes). - - - Set the "anchored chunk" flag of result[index] - to true. - - A ‘textPath’ element always creates an anchored chunk. - - - - Set in_text_path flag true. - - - - - For each child node child of node: - - - Resolve glyph - positioning of child. - - - - - If node is a ‘textPath’ node: - - - Set "in_text_path" flag false. - - - - - - - - + Position adjustments (e.g values in a ‘x’ attribute) + specified by a node apply to all characters in that node including + characters in the node's descendants. Adjustments specified in + descendant nodes, however, override adjustments from ancestor + nodes. This section resolves which adjustments are to be applied to + which characters. It also directly sets the rotate coordinate + of result. + + Set up: + + Let resolve_x, resolve_y, + resolve_dx, and resolve_dy be arrays of + length count whose entries are all initialized + to "unspecified". +*/ + String[] resolve_x = new String[count]; + String[] resolve_y = new String[count]; + String[] resolve_dx = new String[count]; + String[] resolve_dy = new String[count]; +/* + + Set "in_text_path" flag false. + + This flag will allow ‘y’ (‘x’) + attribute values to be ignored for horizontal (vertical) + text inside ‘textPath’ elements. +*/ + boolean in_text_path = false; +/* + Call the following procedure with the ‘text’ element node. + + Procedure: resolve character + positioning: + + A recursive procedure that takes as input a node and + whose steps are as follows: +*/ + class CharacterPositioningResolver { + private int global = 0; + private boolean horizontal = true; + private boolean in_text_path = false; + private CharacterInformation[] result; + private String[] resolve_x; + private String[] resolve_y; + private String[] resolve_dx; + private String[] resolve_dy; + + private CharacterPositioningResolver( + CharacterInformation[] result, + String[] resolve_x, + String[] resolve_y, + String[] resolve_dx, + String[] resolve_dy + ) { + this.result = result; + this.resolve_x = resolve_x; + this.resolve_y = resolve_y; + this.resolve_dx = resolve_dx; + this.resolve_dy = resolve_dy; + } + + private void resolveCharacterPositioning(TextShadowNode node) { +/* + If node is a ‘text’ or ‘tspan’ node: +*/ + if (node.getClass() == TextShadowNode.class || node.getClass() == TSpanShadowNode.class) { +/* + Let index equal the "global index number" of the + first character in the node. +*/ + int index = global; +/* + Let x, y, dx, dy + and rotate be the lists of values from the + TODO corresponding attributes on node, or empty + lists if the corresponding attribute was not specified + or was invalid. +*/ + // https://www.w3.org/TR/SVG/text.html#TSpanElementXAttribute + String[] x = new String[]{}; + + // https://www.w3.org/TR/SVG/text.html#TSpanElementYAttribute + String[] y = new String[]{}; + + // Current SVGLengthList + // https://www.w3.org/TR/SVG/types.html#DataTypeLengths + + // https://www.w3.org/TR/SVG/text.html#TSpanElementDXAttribute + String[] dx = new String[]{}; + + // https://www.w3.org/TR/SVG/text.html#TSpanElementDYAttribute + String[] dy = new String[]{}; + + // Current SVGLengthList + // https://www.w3.org/TR/SVG/types.html#DataTypeNumbers + + // https://www.w3.org/TR/SVG/text.html#TSpanElementRotateAttribute + double[] rotate = new double[]{}; +/* + + If "in_text_path" flag is false: + Let new_chunk_count + = max(length of x, length of y). +*/ + int new_chunk_count; + if (!in_text_path) { + new_chunk_count = Math.max(x.length, y.length); +/* + + Else: +*/ + } else { +/* + If the "horizontal" flag is true: + + Let new_chunk_count = length of x. +*/ + if (horizontal) { + new_chunk_count = x.length; +/* + + Else: + + Let new_chunk_count = length of y. +*/ + } else { + new_chunk_count = y.length; + } + } +/* + + Let length be the number of DOM characters in the + subtree rooted at node. +*/ + String content = ((TSpanShadowNode) node).mContent; + int length = content == null ? 0 : content.length(); +/* + Let i = 0 and j = 0. + + i is an index of addressable characters in the node; + j is an index of all characters in the node. +*/ + int i = 0; + int j = 0; +/* + While j < length, do: +*/ + while (j < length) { +/* + This loop applies the ‘x’, ‘y’, + ‘dx’, ‘dy’ and ‘rotate’ + attributes to the content inside node. + If the "addressable" flag of result[index + + j] is true, then: +*/ + if (result[index + j].addressable) { +/* + If i < TODO new_check_count, then (typo) + set the "anchored chunk" flag of + result[index + j] to + true. Else set the flag to false. + + Setting the flag to false ensures that ‘x’ + and ‘y’ attributes set in a ‘text’ + element don't create anchored chunk in a ‘textPath’ + element when they should not. +*/ + result[index + j].anchoredChunk = i < new_chunk_count; +/* + + If i < length of x, + then set resolve_x[index + + j] to x[i]. +*/ + if (i < x.length) { + resolve_x[index + j] = x[i]; + } +/* + + If "in_text_path" flag is true and the "horizontal" + flag is false, unset + resolve_x[index]. + + The ‘x’ attribute is ignored for + vertical text on a path. +*/ + if (in_text_path && !horizontal) { + resolve_x[index] = ""; + } +/* + + If i < length of y, + then set resolve_y[index + + j] to y[i]. +*/ + if (i < y.length) { + resolve_y[index + j] = y[i]; + } +/* + If "in_text_path" flag is true and the "horizontal" + flag is true, unset + resolve_y[index]. + + The ‘y’ attribute is ignored for + horizontal text on a path. +*/ + if (in_text_path && horizontal) { + resolve_y[index] = ""; + } +/* + If i < length of dx, + then set resolve_dx[index + + j] to TODO dy[i]. (typo) +*/ + if (i < dx.length) { + resolve_dx[index + j] = dx[i]; + } +/* + If i < length of dy, + then set resolve_dy[index + + j] to dy[i]. +*/ + if (i < dy.length) { + resolve_dy[index + j] = dy[i]; + } +/* + If i < length of rotate, + then set the angle value of result[index + + j] to rotate[i]. + Otherwise, if rotate is not empty, then + set result[index + j] + to result[index + j − 1]. +*/ + if (i < rotate.length) { + result[index + j].rotate = rotate[i]; + } else if (rotate.length != 0) { + result[index + j].rotate = result[index + j - 1].rotate; + } +/* + Set i = i + 1. + Set j = j + 1. +*/ + } + i++; + j++; + } +/* + If node is a ‘textPath’ node: + + Let index equal the global index number of the + first character in the node (including descendant nodes). +*/ + } else if (node.getClass() == TextPathShadowNode.class) { + int index = global; +/* + Set the "anchored chunk" flag of result[index] + to true. + + A ‘textPath’ element always creates an anchored chunk. +*/ + result[index].anchoredChunk = true; +/* + Set in_text_path flag true. +*/ + in_text_path = true; +/* + For each child node child of node: + Resolve glyph + positioning of child. +*/ + for (int child = 0; child < node.getChildCount(); child++) { + resolveCharacterPositioning((TextShadowNode) node.getChildAt(child)); + } +/* + If node is a ‘textPath’ node: + + Set "in_text_path" flag false. + +*/ + if (node instanceof TextPathShadowNode) { + in_text_path = false; + } + } + } + } + + CharacterPositioningResolver resolver = new CharacterPositioningResolver( + result, + resolve_x, + resolve_y, + resolve_dx, + resolve_dy + ); +/* Adjust positions: dx, dy - The ‘dx’ and ‘dy’ adjustments are applied - before adjustments due to the ‘textLength’ attribute while - the ‘x’, ‘y’ and ‘rotate’ - adjustments are applied after. - - - - Let shift be the cumulative x and - y shifts due to ‘x’ and ‘y’ - attributes, initialized to (0,0). - - - For each array element with index i in result: - - - If resolve_x[i] is unspecified, set it to 0. - If resolve_y[i] is unspecified, set it to 0. - - - Let shift.x = shift.x + resolve_x[i] - and shift.y = shift.y + resolve_y[i]. - - - Let result[i].x = CSS_positions[i].x + shift.x - and result[i].y = CSS_positions[i].y + shift.y. - - - - - - - Apply ‘textLength’ attribute - - - Set up: - - - Define resolved descendant node as a - descendant of node with a valid ‘textLength’ - attribute that is not itself a descendant node of a - descendant node that has a valid ‘textLength’ - attribute. - - - Call the following procedure with the ‘text’ element - node. - - - - - Procedure: resolve text length: - - A recursive procedure that takes as input - a node and whose steps are as follows: - - - - For each child node child of node: - - - Resolve text length of child. - - Child nodes are adjusted before parent nodes. - - - - - - If node is a ‘text’ or ‘tspan’ node - and if the node has a valid ‘textLength’ attribute value: - - - Let a = +∞ and b = −∞. - - - Let i and j be the global - index of the first character and last characters - in node, respectively. - - - For each index k in the range - [i, j] where the "addressable" flag - of result[k] is true: - - This loop finds the left-(top-) most and - right-(bottom-) most extents of the typographic characters within the node and checks for - forced line breaks. - - - - If the character at k is a linefeed - or carriage return, return. No adjustments due to - ‘textLength’ are made to a node with - a forced line break. - - - Let pos = the x coordinate of the position - in result[k], if the "horizontal" - flag is true, and the y coordinate otherwise. - - - Let advance = the advance of - the typographic character corresponding to - character k. [NOTE: This advance will be - negative for RTL horizontal text.] - - - Set a = - min(a, pos, pos - + advance). - - - Set b = - max(b, pos, pos - + advance). - - - - - If a ≠ +∞ then: - - - Find the distance delta = ‘textLength’ - computed value − (b − a). - - User agents are required to shift the last - typographic character in the node by - delta, in the positive x direction - if the "horizontal" flag is true and if - direction is - lrt, in the - negative x direction if the "horizontal" flag - is true and direction is - rtl, or in the - positive y direction otherwise. User agents - are free to adjust intermediate - typographic characters for optimal - typography. The next steps indicate one way to - adjust typographic characters when - the value of ‘lengthAdjust’ is - spacing. - - - - Find n, the total number of - typographic characters in this node - including any descendant nodes that are not resolved - descendant nodes or within a resolved descendant - node. - - - Let n = n + number of - resolved descendant nodes − 1. - - Each resolved descendant node is treated as if it - were a single - typographic character in this - context. - - - - Find the per-character adjustment δ - = delta/n. - - - Let shift = 0. - - - For each index k in the range [i,j]: - - - Add shift to the x coordinate of the - position in result[k], if the "horizontal" - flag is true, and to the y coordinate - otherwise. - - - If the "middle" flag for result[k] - is not true and k is not a character in - a resolved descendant node other than the first - character then shift = shift - + δ. - - - - - - - - - - - + The ‘dx’ and ‘dy’ adjustments are applied + before adjustments due to the ‘textLength’ attribute while + the ‘x’, ‘y’ and ‘rotate’ + adjustments are applied after. + + Let shift be the cumulative x and + y shifts due to ‘x’ and ‘y’ + attributes, initialized to (0,0). +*/ + PointF shift = new PointF(0, 0); +/* + For each array element with index i in result: +*/ + for (int i = 0; i < count; i++) { +/* + If resolve_x[i] is unspecified, set it to 0. + If resolve_y[i] is unspecified, set it to 0. +*/ + if (resolve_x[i].equals("")) { + resolve_x[i] = "0"; + } + if (resolve_y[i].equals("")) { + resolve_y[i] = "0"; + } +/* + Let shift.x = shift.x + resolve_x[i] + and shift.y = shift.y + resolve_y[i]. +*/ + shift.x = shift.x + Float.parseFloat(resolve_x[i]); + shift.y = shift.y + Float.parseFloat(resolve_y[i]); +/* + Let result[i].x = CSS_positions[i].x + shift.x + and result[i].y = CSS_positions[i].y + shift.y. +*/ + result[i].x = CSS_positions[i].x + shift.x; + result[i].y = CSS_positions[i].y + shift.y; + } +/* + TODO Apply ‘textLength’ attribute + + Set up: + + Define resolved descendant node as a + descendant of node with a valid ‘textLength’ + attribute that is not itself a descendant node of a + descendant node that has a valid ‘textLength’ + attribute. + + Call the following procedure with the ‘text’ element + node. + + Procedure: resolve text length: + + A recursive procedure that takes as input + a node and whose steps are as follows: + For each child node child of node: + + Resolve text length of child. + + Child nodes are adjusted before parent nodes. +*/ + class TextLengthResolver { + int global; + + private void resolveTextLength(TextShadowNode node) { + /* + + If node is a ‘text’ or ‘tspan’ node + and if the node has a valid ‘textLength’ attribute value: +*/ + final Class nodeClass = node.getClass(); + final boolean validTextLength = node.mTextLength != null; + if ( + (nodeClass == TSpanShadowNode.class) + && validTextLength + ) { + /* + Let a = +∞ and b = −∞. +*/ + double a = Double.POSITIVE_INFINITY; + double b = Double.NEGATIVE_INFINITY; +/* + + + Let i and j be the global + index of the first character and last characters + in node, respectively. +*/ + String content = ((TSpanShadowNode) node).mContent; + int i = global; + int j = i + (content == null ? 0 : content.length()); +/* + For each index k in the range + [i, j] where the "addressable" flag + of result[k] is true: + + This loop finds the left-(top-) most and + right-(bottom-) most extents of the typographic characters within the node and checks for + forced line breaks. +*/ + for (int k = i; k <= j; k++) { + if (!result[i].addressable) { + continue; + } +/* + If the character at k is a linefeed + or carriage return, return. No adjustments due to + ‘textLength’ are made to a node with + a forced line break. +*/ + switch (result[i].character) { + case '\n': + case '\r': + return; + } +/* + Let pos = the x coordinate of the position + in result[k], if the "horizontal" + flag is true, and the y coordinate otherwise. +*/ + double pos = horizontal ? result[k].x : result[k].y; +/* + Let advance = the advance of + the typographic character corresponding to + character k. [NOTE: This advance will be + negative for RTL horizontal text.] +*/ + double advance = result[k].advance; +/* + Set a = + min(a, pos, pos + + advance). + + + Set b = + max(b, pos, pos + + advance). +*/ + a = Math.min(a, Math.min(pos, pos + advance)); + b = Math.max(b, Math.max(pos, pos + advance)); + } +/* + + If a ≠ +∞ then: + +*/ + if (a != Double.POSITIVE_INFINITY) { +/* + + Find the distance delta = ‘textLength’ + computed value − (b − a). +*/ + double delta = Double.parseDouble(node.mTextLength) - (b - a); +/* + + User agents are required to shift the last + typographic character in the node by + delta, in the positive x direction + if the "horizontal" flag is true and if + direction is + lrt, in the + negative x direction if the "horizontal" flag + is true and direction is + rtl, or in the + positive y direction otherwise. User agents + are free to adjust intermediate + typographic characters for optimal + typography. The next steps indicate one way to + adjust typographic characters when + the value of ‘lengthAdjust’ is + spacing. + + Find n, the total number of + typographic characters in this node + TODO including any descendant nodes that are not resolved + descendant nodes or within a resolved descendant + node. +*/ + int n = 0; + int resolvedDescendantNodes = 0; + for (int c = 0; c < node.getChildCount(); c++) { + if (((TextPathShadowNode) node.getChildAt(c)).mTextLength == null) { + String ccontent = ((TSpanShadowNode) node).mContent; + n += ccontent == null ? 0 : ccontent.length(); + } else { + result[n].firstCharacterInResolvedDescendant = true; + resolvedDescendantNodes++; + } + } +/* + Let n = n + number of + resolved descendant nodes − 1. +*/ + n += resolvedDescendantNodes - 1; +/* + Each resolved descendant node is treated as if it + were a single + typographic character in this + context. + + Find the per-character adjustment δ + = delta/n. + + Let shift = 0. +*/ + double perCharacterAdjustment = delta / n; + double shift = 0; +/* + For each index k in the range [i,j]: +*/ + for (int k = i; k <= j; k++) { +/* + Add shift to the x coordinate of the + position in result[k], if the "horizontal" + flag is true, and to the y coordinate + otherwise. +*/ + if (horizontal) { + result[k].x += shift; + } else { + result[k].y += shift; + } +/* + If the "middle" flag for result[k] + is not true and k is not a character in + a resolved descendant node other than the first + character then shift = shift + + δ. + */ + if (!result[k].middle && (!result[k].resolved || result[k].firstCharacterInResolvedDescendant)) { + shift += perCharacterAdjustment; + } + } + } + } + } + } + TextLengthResolver lengthResolver = new TextLengthResolver(); + lengthResolver.resolveTextLength(text); +/* Adjust positions: x, y - This loop applies ‘x’ and ‘y’ values, - and ensures that text-anchor chunks do not start in - the middle of a typographic character. - - - - Let shift be the current adjustment due to - the ‘x’ and ‘y’ attributes, - initialized to (0,0). - - - Set index = 1. - - - While index < count: - - - If resolved_x[index] is set, then let - shift.x = - resolved_x[index] − - result.x[index]. - - - If resolved_y[index] is set, then let - shift.y = - resolved_y[index] − - result.y[index]. - - - Let result.x[index] = - result.x[index] + shift.x - and result.y[index] = - result.y[index] + shift.y. - - - If the "middle" and "anchored chunk" flags - of result[index] are both true, then: - - - Set the "anchored chunk" flag - of result[index] to false. - - - If index + 1 < count, then set - the "anchored chunk" flag - of result[index + 1] to true. - - - - - Set index to index + 1. - - + This loop applies ‘x’ and ‘y’ values, + and ensures that text-anchor chunks do not start in + the middle of a typographic character. + Let shift be the current adjustment due to + the ‘x’ and ‘y’ attributes, + initialized to (0,0). + Set index = 1. +*/ + shift.set(0, 0); + int index = 1; +/* + While index < count: +*/ + while (index < count) { +/* + TODO If resolved_x[index] is set, then let (typo) + shift.x = + resolved_x[index] − + result.x[index]. +*/ + if (resolve_x[index] != null) { + shift.x = (float) (Double.parseDouble(resolve_x[index]) - result[index].x); + } +/* + TODO If resolved_y[index] is set, then let (typo) + shift.y = + resolved_y[index] − + result.y[index]. +*/ + if (resolve_y[index] != null) { + shift.y = (float) (Double.parseDouble(resolve_y[index]) - result[index].y); + } +/* + Let result.x[index] = + result.x[index] + shift.x + and result.y[index] = + result.y[index] + shift.y. +*/ + result[index].x += shift.x; + result[index].y += shift.y; +/* + If the "middle" and "anchored chunk" flags + of result[index] are both true, then: +*/ + if (result[index].middle && result[index].anchoredChunk) { +/* + Set the "anchored chunk" flag + of result[index] to false. +*/ + result[index].anchoredChunk = false; + } +/* + If index + 1 < count, then set + the "anchored chunk" flag + of result[index + 1] to true. +*/ + if (index + 1 < count) { + result[index + 1].anchoredChunk = true; + } +/* + Set index to index + 1. +*/ + index++; + } +/* Apply anchoring + TODO For each slice result[i..j] + (inclusive of both i and j), where: - For each slice result[i..j] - (inclusive of both i and j), where: + the "anchored chunk" flag of result[i] + is true, + the "anchored chunk" flags + of result[k] where i + < k ≤ j are false, and - the "anchored chunk" flag of result[i] - is true, + j = count − 1 or the "anchored + chunk" flag of result[j + 1] is + true; + do: + This loops over each anchored chunk. - the "anchored chunk" flags - of result[k] where i - < k ≤ j are false, and + Let a = +∞ and b = −∞. + For each index k in the range + [i, j] where the "addressable" flag + of result[k] is true: - j = count − 1 or the "anchored - chunk" flag of result[j + 1] is - true; + This loop finds the left-(top-) most and + right-(bottom-) most extents of the typographic character within the anchored chunk. +*/ + int i = 0; + double a = Double.POSITIVE_INFINITY; + double b = Double.NEGATIVE_INFINITY; + double prevA = Double.POSITIVE_INFINITY; + double prevB = Double.NEGATIVE_INFINITY; + for (int k = 0; k < count; k++) { + if (!result[k].addressable) { + continue; + } + if (result[k].anchoredChunk) { + prevA = a; + prevB = b; + a = Double.POSITIVE_INFINITY; + b = Double.NEGATIVE_INFINITY; + } +/* + Let pos = the x coordinate of the position + in result[k], if the "horizontal" flag + is true, and the y coordinate otherwise. + Let advance = the advance of + the typographic character corresponding to + character k. [NOTE: This advance will be + negative for RTL horizontal text.] - do: + Set a = + min(a, pos, pos + + advance). - This loops over each anchored chunk. + Set b = + max(b, pos, pos + + advance). +*/ + double pos = horizontal ? result[k].x : result[k].y; + double advance = result[k].advance; + a = Math.min(a, Math.min(pos, pos + advance)); + b = Math.max(b, Math.max(pos, pos + advance)); +/* + If a ≠ +∞, then: + Here we perform the text anchoring. + Let shift be the x coordinate of + result[i], if the "horizontal" flag + is true, and the y coordinate otherwise. - Let a = +∞ and b = −∞. - - - For each index k in the range - [i, j] where the "addressable" flag - of result[k] is true: - - This loop finds the left-(top-) most and - right-(bottom-) most extents of the typographic character within the anchored chunk. - - - - Let pos = the x coordinate of the position - in result[k], if the "horizontal" flag - is true, and the y coordinate otherwise. - - - Let advance = the advance of - the typographic character corresponding to - character k. [NOTE: This advance will be - negative for RTL horizontal text.] - - - Set a = - min(a, pos, pos - + advance). - - - Set b = - max(b, pos, pos - + advance). - - - - - If a ≠ +∞, then: - - Here we perform the text anchoring. - - - - Let shift be the x coordinate of - result[i], if the "horizontal" flag - is true, and the y coordinate otherwise. - - - Adjust shift based on the value of text-anchor - and direction of the element the character at - index i is in: - - (start, ltr) or (end, rtl) - Set shift = shift − a. - (start, rtl) or (end, ltr) - Set shift = shift − b. - (middle, ltr) or (middle, rtl) - Set shift = shift − (a + b) / 2. - - - - For each index k in the range [i, j]: - - - Add shift to the x coordinate of the position - in result[k], if the "horizontal" - flag is true, and to the y coordinate otherwise. - - + TODO Adjust shift based on the value of text-anchor + TODO and direction of the element the character at + index i is in: + (start, ltr) or (end, rtl) + Set shift = shift − a. + (start, rtl) or (end, ltr) + Set shift = shift − b. + (middle, ltr) or (middle, rtl) + Set shift = shift − (a + b) / 2. +*/ + if ((k > 0 && result[k].anchoredChunk && prevA != Double.POSITIVE_INFINITY) || k == count - 1) { + TextAnchor anchor = TextAnchor.start; + Direction direction = Direction.ltr; + if (k == count - 1) { + prevA = a; + prevB = b; + } + double anchorShift = horizontal ? result[i].x : result[i].y; + switch (anchor) { + case start: + if (direction == Direction.ltr) { + anchorShift = anchorShift - prevA; + } else { + anchorShift = anchorShift - prevB; + } + break; + case middle: + if (direction == Direction.ltr) { + anchorShift = anchorShift - (prevA + prevB) / 2; + } else { + anchorShift = anchorShift - (prevA + prevB) / 2; + } + break; + case end: + if (direction == Direction.ltr) { + anchorShift = anchorShift - prevB; + } else { + anchorShift = anchorShift - prevA; + } + break; + } +/* + For each index k in the range [i, j]: + Add shift to the x coordinate of the position + in result[k], if the "horizontal" + flag is true, and to the y coordinate otherwise. +*/ + int j = k == count - 1 ? k : k - 1; + for (int r = i; r <= j; r++) { + if (horizontal) { + result[r].x += anchorShift; + } else { + result[r].y += anchorShift; + } + } + i = k; + } + } +/* Position on path - - Set index = 0. - - - Set the "in path" flag to false. - - - Set the "after path" flag to false. - - - Let path_end be an offset for characters that follow - a ‘textPath’ element. Set path_end to (0,0). - - - While index < count: - - - If the character at index i is within a - ‘textPath’ element and corresponds to a typographic character, then: - - - Set "in path" flag to true. - - - If the "middle" flag of - result[index] is false, then: - - Here we apply ‘textPath’ positioning. - - - - Let path be the equivalent path of - the basic shape element referenced by - the ‘textPath’ element, or an empty path if - the reference is invalid. - - - If the ‘side’ attribute of - the ‘textPath’ element is - 'right', then - reverse path. - - - Let length be the length - of path. - - - Let offset be the value of the - ‘textPath’ element's - ‘startOffset’ attribute, adjusted - due to any ‘pathLength’ attribute on the - referenced element (if the referenced element is - a ‘path’ element). - - - Let advance = the advance of - the typographic character corresponding - to character k. [NOTE: This advance will - be negative for RTL horizontal text.] - - - Let (x, y) - and angle be the position and angle - in result[index]. - - - Let mid be a coordinate value depending - on the value of the "horizontal" flag: - - true - mid is x + advance / 2 - + offset - false - mid is y + advance / 2 - + offset - - - The user agent is free to make any additional adjustments to - mid necessary to ensure high quality typesetting - due to a ‘spacing’ value of - 'auto' or a - ‘method’ value of - 'stretch'. - - - - If path is not a closed subpath and - mid < 0 or mid > length, - set the "hidden" flag of result[index] to true. - - - If path is a closed subpath depending on - the values of text-anchor and direction of - the element the character at index is in: - - This implements the special wrapping criteria for single - closed subpaths. - - - (start, ltr) or (end, rtl) - - If mid−offset < 0 - or mid−offset > length, - set the "hidden" flag of result[index] to true. - - (middle, ltr) or (middle, rtl) - - If - If mid−offset < −length/2 - or mid−offset > length/2, - set the "hidden" flag of result[index] to true. - - (start, rtl) or (end, ltr) - - If mid−offset < −length - or mid−offset > 0, - set the "hidden" flag of result[index] to true. - - - - Set mid = mid mod length. - - - If the hidden flag is false: - - - Let point be the position and - t be the unit vector tangent to - the point mid distance - along path. - - - If the "horizontal" flag is - - true - - - - Let n be the normal unit vector - pointing in the direction t + 90°. - - - Let o be the horizontal distance from the - vertical center line of the glyph to the alignment point. - - - Then set the position in - result[index] to - point - - o×t + - y×n. - - - Let r be the angle from - the positive x-axis to the tangent. - - - Set the angle value - in result[index] - to angle + r. - - - - false - - - - Let n be the normal unit vector - pointing in the direction t - 90°. - - - Let o be the vertical distance from the - horizontal center line of the glyph to the alignment point. - - - Then set the position in - result[index] to - point - - o×t + - x×n. - - - Let r be the angle from - the positive y-axis to the tangent. - - - Set the angle value - in result[index] - to angle + r. - - - - - - - - - - - Otherwise, the "middle" flag - of result[index] is true: - - - Set the position and angle values - of result[index] to those - in result[index − 1]. - - - - - - - If the character at index i is not within a - ‘textPath’ element and corresponds to a typographic character, then: - - This sets the starting point for rendering any characters that - occur after a ‘textPath’ element to the end of the path. - - - If the "in path" flag is true: - - - Set the "in path" flag to false. - - - Set the "after path" flag to true. - - - Set path_end equal to the end point of the path - referenced by ‘textPath’ − the position of - result[index]. - - - - - If the "after path" is true. - - - If anchored chunk of - result[index] is true, set the - "after path" flag to false. - - - Else, - let result.x[index] = - result.x[index] + path_end.x - and result.y[index] = - result.y[index] + path_end.y. - - - - - - - Set index = index + 1. - - - - - - + Set index = 0. + + Set the "in path" flag to false. + + Set the "after path" flag to false. + + Let path_end be an offset for characters that follow + a ‘textPath’ element. Set path_end to (0,0). + + While index < count: +*/ + index = 0; + boolean inPath = false; + boolean afterPath = false; + PointF path_end = new PointF(0, 0); + Path textPath = null; + PathMeasure pm = new PathMeasure(); + while (index < count) { +/* + If the character at index i is within a + ‘textPath’ element and corresponds to a typographic character, then: + + Set "in path" flag to true. +*/ + final TextPathShadowNode textPathShadowNode = inTextPath.get(index); + if (textPathShadowNode != null && result[index].addressable) { + textPath = textPathShadowNode.getPath(); + inPath = true; +/* + + If the "middle" flag of + result[index] is false, then: +*/ + if (!result[index].middle) { +/* + Here we apply ‘textPath’ positioning. + + Let path be the equivalent path of + the basic shape element referenced by + the ‘textPath’ element, or an empty path if + the reference is invalid. + + If the ‘side’ attribute of + the ‘textPath’ element is + 'right', then + TODO reverse path. +*/ + Path path = textPath; + if (textPathShadowNode.getSide() == TextPathSide.right) { + + } +/* + Let length be the length + of path. +*/ + pm.setPath(path, false); + double length = pm.getLength(); +/* + Let offset be the value of the + ‘textPath’ element's + ‘startOffset’ attribute, adjusted + due to any ‘pathLength’ attribute on the + referenced element (if the referenced element is + a ‘path’ element). +*/ + double offset = Double.parseDouble(textPathShadowNode.getStartOffset()); +/* + Let advance = the advance of + the typographic character corresponding + to character TODO k. (typo) [NOTE: This advance will + be negative for RTL horizontal text.] +*/ + double advance = result[index].advance; +/* + Let (x, y) + and angle be the position and angle + in result[index]. +*/ + double x = result[index].x; + double y = result[index].y; + double angle = result[index].rotate; +/* + + Let mid be a coordinate value depending + on the value of the "horizontal" flag: + + true + mid is x + advance / 2 + + offset + false + mid is y + advance / 2 + + offset +*/ + double mid = (horizontal ? x : y) + advance / 2 + offset; +/* + + The user agent is free to make any additional adjustments to + mid necessary to ensure high quality typesetting + TODO due to a ‘spacing’ value of + 'auto' or a + ‘method’ value of + 'stretch'. + + If path is not a closed subpath and + mid < 0 or mid > length, + set the "hidden" flag of result[index] to true. +*/ + if (!pm.isClosed() && (mid < 0 || mid > length)) { + result[index].hidden = true; + } +/* + If path is a closed subpath depending on + the values of text-anchor and direction of + the element the character at index is in: +*/ + if (pm.isClosed()) { +/* + This implements the special wrapping criteria for single + closed subpaths. + + (start, ltr) or (end, rtl) + + If mid−offset < 0 + or mid−offset > length, + set the "hidden" flag of result[index] to true. + + (middle, ltr) or (middle, rtl) + + If + If mid−offset < −length/2 + or mid−offset > length/2, + set the "hidden" flag of result[index] to true. + + (start, rtl) or (end, ltr) + + If mid−offset < −length + or mid−offset > 0, + set the "hidden" flag of result[index] to true. +*/ + TextAnchor anchor = TextAnchor.start; + Direction direction = Direction.ltr; + + double anchorShift = horizontal ? result[i].x : result[i].y; + switch (anchor) { + case start: + if (direction == Direction.ltr) { + if (mid < 0 || mid > length) { + result[index].hidden = true; + } + } else { + if (mid < -length || mid > 0) { + result[index].hidden = true; + } + } + break; + + case middle: + if (mid < -length / 2 || mid > length / 2) { + result[index].hidden = true; + } + break; + + case end: + if (direction == Direction.ltr) { + if (mid < -length || mid > 0) { + result[index].hidden = true; + } + } else { + if (mid < 0 || mid > length) { + result[index].hidden = true; + } + } + break; + } + } +/* + Set mid = mid mod length. +*/ + mid %= length; +/* + If the hidden flag is false: +*/ + if (!result[index].hidden) { +/* + Let point be the position and + t be the unit vector tangent to + the point mid distance + along path. +*/ + float[] point = new float[2]; + float[] t = new float[2]; + pm.getPosTan((float) mid, point, t); + final double tau = 2 * Math.PI; + final double radToDeg = 360 / tau; + final double r = Math.atan2(t[1], t[0]) * radToDeg; +/* + If the "horizontal" flag is +*/ + if (horizontal) { +/* + true + + Let n be the normal unit vector + pointing in the direction t + 90°. +*/ + double normAngle = r + 90; + double[] n = new double[]{Math.cos(normAngle), Math.sin(normAngle)}; +/* + Let o be the horizontal distance from the + TODO vertical center line of the glyph to the alignment point. +*/ + double o = 0; +/* + Then set the position in + result[index] to + point - + o×t + + y×n. + + Let r be the angle from + the positive x-axis to the tangent. + + Set the angle value + in result[index] + to angle + r. +*/ + result[index].rotate += r; + } else { +/* + false + + Let n be the normal unit vector + pointing in the direction t - 90°. +*/ + double normAngle = r - 90; + double[] n = new double[]{Math.cos(normAngle), Math.sin(normAngle)}; +/* + Let o be the vertical distance from the + TODO horizontal center line of the glyph to the alignment point. +*/ + double o = 0; +/* + + Then set the position in + result[index] to + point - + o×t + + x×n. + + Let r be the angle from + the positive y-axis to the tangent. + + Set the angle value + in result[index] + to angle + r. +*/ + result[index].rotate += r; + } + } +/* + + Otherwise, the "middle" flag + of result[index] is true: + + Set the position and angle values + of result[index] to those + in result[index − 1]. +*/ + } else { + result[index].x = result[index - 1].x; + result[index].y = result[index - 1].y; + result[index].rotate = result[index - 1].rotate; + } + + } +/* + If the character at index i is not within a + ‘textPath’ element and corresponds to a typographic character, then: + + This sets the starting point for rendering any characters that + occur after a ‘textPath’ element to the end of the path. +*/ + if (textPathShadowNode == null && result[index].addressable) { +/* + If the "in path" flag is true: + + Set the "in path" flag to false. + + Set the "after path" flag to true. + + Set path_end equal to the end point of the path + referenced by ‘textPath’ − the position of + result[index]. +*/ + if (inPath) { + inPath = false; + afterPath = true; + pm.setPath(textPath, false); + float[] pos = new float[2]; + pm.getPosTan(pm.getLength(), pos, null); + path_end.set(pos[0], pos[1]); + } +/* + + If the "after path" is true. + + If anchored chunk of + result[index] is true, set the + "after path" flag to false. + + Else, + let result.x[index] = + result.x[index] + path_end.x + and result.y[index] = + result.y[index] + path_end.y. +*/ + if (afterPath) { + if (result[index].anchoredChunk) { + afterPath = false; + } else { + result[index].x += path_end.x; + result[index].y += path_end.y; + } + } + } +/* + + Set index = index + 1. +*/ + index++; + } +/* Return result - - */ +*/ + return result; } }