Proposals/Text layout

From SVG

Text layout with positioned glyphs, anchoring and bidirectionality

I want to make a proposal for defining text layout behaviour in these cases. Comments and suggestions (particularly any suggested simplifications) are welcome.

Use cases

There are three particular uses of SVG text I think we should be mindful to support (or keep supporting):

  • We should support the authoring use case of painting a horizontal text string with its begin/end edge or middle at a given point.
  • We should support the authoring use case of providing absolute glyphs positions for text strings. This includes defining reasonable behaviour if not all positions are given, and if only a single absolute glyph position is given.
  • We should support existing content that is expecting a <text> with child <tspan>s where each <tspan> is given a single x="" value to be treated as separate chunks for positioning and anchoring. (But not for bidi resolution.)

Observations

It seems like x="" on <text> and <tspan> has two purposes currently: one is to give the text anchoring position, and the other is to be a list of absolute glyph position x-coordinates. These two purposes are sometimes in conflict. When you write <text x="10" direction="ltr">BIDI text</text>, 10 cannot be both the position of the first logical character of the text and be the text anchor position (the left edge). (In cases where the first logical character of the text is the same direction as <text> element’s base direction, then there’s no problem.)

Conflicting meaning of the x="" attribute: which should it be?

If we want to support the two authoring use cases above in the presence of bidi text, in particular in the cases where the first logical character is not the leftmost or rightmost character visually, then I think we need to distinguish them in the document somehow. The positioned glyph use case is the less common one, and more likely to be generated by tools than by hand, so I think this is the case that should require the extra indication that the x="" value is indeed the position of the glyph and shouldn’t be altered by text-anchor.

Proposal

The proposal is as follows:

Assume horizontal text. (For vertical text, x="" would be swapped with y="". Mixed horizontal and vertical text probably requires more thought.) x="", rotate="", etc. all refer to characters in document order.

We no longer chunk text when we encounter an x="" value. The entire <text> element is one chunk with regards to bidi resolution, ligature formation (modulo rules around the 'letter-spacing' and 'kerning' properties) and white space collapsing. The recommended way for authors to manually line break text is to use separate <text> elements, and implementations will be required to allow text selection across <text> elements (as some already do).

 Why text for linebreaking and not tspan? --Erik Dahlström 15:34, 15 June 2011 (UTC)
 Inkscape uses tspan for all line breaking. --Tavmjong Bah 19 June 2011

When a <text> has an x="" with a single value or is not specified, and none its text content descendant elements has an x="" specified, and there are no descendant <textPath> elements, then text-anchor values of start/middle/end will result in the text being translated so that the x="" value is the appropriate start/middle/end of the entire <text> element.

When a <text> has an x="" with a single value or is not specified, and

  • one or more of its text content descendant elements have an x="" with a single value, and/or
  • there is one or more <textPath> descendants,

then the entire text content of the <text> element is partitioned, where the starts of each partition are:

  • index 0 (the start of the text),
  • the first character of each descendant text content element that has a single x="" value specified,
  • the first character of each descendant <textPath> element, and
  • the first character following each descendant <textPath> element.

Each of these substrings gets translated to honour text-anchor. For the purpose of deciding whether start/end map to left/right or vice versa, only the <text> element’s base direction is used. The value of the text-anchor property on the element that begins the partitioned substring provides the anchoring for that substring.

A substring that is anchored may not have contiguously laid out glyphs. The left and right edges of the substring are taken to be the leftmost edge of all the glyphs and the rightmost edge of all the glyphs. This also gives the length of the substring to be used when text-anchor is set to middle. (This length could well be different from the sum of the advances of all the glyphs.)

 the terms 'rightmost edge' and 'leftmost edge' need to be defined in more detail --Erik Dahlström 15:54, 15 June 2011 (UTC)

When a text content element has an x="" attribute with more than one value, then text anchoring is disabled for the entire <text> element.

We introduce a new value of text-anchor: none (or some other value name) to explicitly disable text anchoring. This will need to be used to have anchoring disabled when x="" attributes have at most one value.

In all cases, if a particular glyph has not been given an absolute position, then it is laid out relative to the nearest, previous absolutely positioned glyph. (See the examples below for how this works exactly.)

If a <textPath> element is used, it behaves for following non-positioned glyphs as if the characters within it were all absolutely positioned. If all of the glyphs were able to be placed on the path, then the following non-positioned glyph will be placed, horizontally (for horizontal text) at the position on the path that would be where the next glyph would have been placed. If this point would fall off the end of the path, or if not all of the glyphs for the <textPath>’s characters did fit on the path, then the end point of the path is the following non-positioned glyph’s position.

dx="" values are all applied after the initial layout of the text has been performed, but before any translation due to anchoring has been applied.

Consequences

The above should mean that:

  • unidirectional text with anchoring like the following continues to work:
<text x="50" text-anchor="start">abcdef</text>
<text x="50" text-anchor="middle">abcdef</text>
<text x="50" text-anchor="end">abcdef</text>
  • unidirectional text with multiple span anchoring like the following continues to work:
<text x="50" text-anchor="middle">abcdefghi
  <tspan x="70" y="30">jklmno</tspan></text>
  • unidirectional text with all glyphs positioned like the following continues to work:
<text x="50 60 70 80 90 100">abcdef</text>
  • unidirectional text with some glyphs positioned like the following continues to work:
<text x="50 60">abcdef</text>
  • bidirectional text with anchoring like the following will be defined to work sensibly:
<text x="50" text-anchor="middle">ABCdef</text>
  • bidirectional text with all glyphs positioned like the following will be defined to work sensibly:
<text x="50 40 30 50 60 70">ABCdef</text>
  • bidirectional text with some glyphs positioned like the following will have moderately sensible behaviour:
<text x="50 40">ABCdef</text>
  • simple uses of unidirectional or bidirectional text on a path like the following will continue to work:
<text><textPath xlink:href="#a">ABCdef</text></text>
  • the position of characters following a <textPath> like in the following will be defined to do something moderately reasonable:
<text><textPath xlink:href="#a">ABC</textPath>def</text>
  • all text layout behaviour is well defined.

Examples

Here are some examples, which are probably easier to follow than the proposal text above. We just show x positions, except for the examples involving <textPath>. In all of these examples, glyph positions for LTR glyphs identify the left edge of the glyph and for RTL glyphs the right edge of the glyph. All glyphs have an advance of 10.


<text x="10" direction="ltr">ABCdef</text>

x = 10 is the left edge of the text string. So we first lay out the text with the “A” glyph at x = 10:

A:10 B:0 C:-10 d:10 e:20 f:30

And then translate them all so that the left edge of the “C” glyph is at x = 10:

A:40 B:30 C:20 d:40 e:50 f:60




<text x="10" direction="ltr" text-anchor="none">ABCdef</text>

x = 10 is the right edge of the “A” glyph and no translation for anchoring is done. The remaining glyphs are laid out relative to “A”:

A:10 B:0 C:-10 d:10 e:20 f:30




<text x="30 15" direction="ltr">ABCdef</text>

Since we have two values in x="", anchoring is disabled. First, we do the layout assuming we only have a single value:

A:30 B:20 C:10 d:30 e:40 f:50

Then we compute the offsets between each glyph:

A:- B:-10 C:-10 d:20 e:10 f:10

And then we lay out the glyphs using the explicit glyph positions for the first two glyphs and using the offsets for the remaining glyphs:

A:30 B:15 C:5 d:25 e:35 f:45




<text x="30" direction="ltr">AB<tspan x="5 100">Cd</tspan>ef</text>

Here, the “A”, “C” and “d” glyphs are absolute positioned. First, do the layout assuming a single position:

A:30 B:20 C:10 d:30 e:40 f:50

Then compute the offsets between each glyph:

A:- B:-10 C:-10 d:20 e:10 f:10

Now lay out the glyphs using the explicit glyph positions for “A”, “C” and “d” and usingthe offsets for the remaining glyphs:

A:30 B:20 C:5 d:100 e:110 f:120




<text x="30" direction="ltr" text-anchor="end">ABCdef</text>

The base direction of the “paragraph” is LTR, so x = 30 will be used to align the text’s right edge. First, lay out the test with x = 30 for the first logical character:

A:30 B:20 C:10 d:30 e:40 f:50

Then translate the whole thing due to the text anchor:

A:0 B:-10 C:-20 d:0 e:10 f:20




<text x="30" dx="100 50" direction="ltr" text-anchor="end">ABCdef</text>

First, lay out the test with x = 30 for the first logical character:

A:30 B:20 C:10 d:30 e:40 f:50

Convert it to offsets:

A:- B:-10 C:-10 d:20 e:10 f:10

Position the first two glyphs absolutely,

A:30 B:20

apply dx to them,

A:130 B:170

and use the offsets for the remaining glyphs:

A:130 B:170 C:160 d:180 e:190 f:200

The extents of the text are [120,210], so align the right edge with x = 30 we translate the whole thing by -180 for the text anchoring:

A:-50 B:-10 C:-20 d:0 e:10 f:20




<text x="30" text-anchor="end">a<tspan x="100">BCdef</tspan></text>

Bidi resolution is done over the entire <text>, so in visual order the text is “aCBdef”. The base directionality of the paragraph is LTR, so text-anchor:end maps to the right edge for all anchored substrings in the text.

The initial layout will be:

a:30 B:100 C:90 d:100 e:110 f:120

We divide this into two parts:

a:30   and   B:100 C:90 d:100 e:110 f:120

The left/right extents of the first part is [30,40] and for the second part is [80,130]. We translate the first part so that its right edge is at x = 30 and the second part so that its right edge is at x = 100.

a:20   and   B:70 C:60 d:70 e:80 f:90




<text x="30" direction="ltr">AB<tspan x="100">CDef</tspan>gh</text>

Do an initial layout with “A” at x = 30:

A:30 B:20 C:10 D:0 e:30 f:40 g:50 h:60

Determine offsets:

A:- B:-10 C:-10 D:-10 e:30 f:10 g:10 h:10

Lay out using the absolute positions and filling in the rest with the offsets:

A:30 B:20 C:100 D:90 e:120 f:130 g:140 h:150

Split into the two anchored substrings:

A:30 B:20   and   C:100 D:90 e:120 f:130 g:140 h:150

Then translate each substring to the anchor position:

A:50 B:40   and   C:120 D:110 e:140 f:150 g:160 h:170




<text x="30">abc<tspan text-anchor="end">def</tspan></text>

There is no x="" attribute on the <tspan>, so there’s only a single anchored substring.

a:30 b:40 c:50 d:60 e:70 f:80




<text x="30">ab<tspan x="30" text-anchor="end">de</tspan>fg</text>

There are two anchored substrings, “ab” anchored start and “defgh” anchored end.

a:30 b:40 d:-10 e:0 f:10 g:20




<path id="a" d="M150,150 v100"/>
<text x="100" y="100" direction="ltr">
  AB<textPath xlink:href="#a">CDef</textPath>gh
</text>

Do the initial layout with “A” at x = 100:

A:100 B:90 C:80 D:70 e:100 f:110 g:120 h:130

Compute the offsets:

A:- B:-10 C:-10 D:-10 e:30 f:10 g:10 h:10

The first substring is:

A:100 B:90

The anchored first substring is:

A:120 B:110

For the second substring, we consider the first character in the <textPath> to be absolutely positioned at x = 0 in path space.

C:0 D:-10 e:20 f:30 [following:40]

Do the anchoring while in path space:

C:20 D:10 e:40 f:50 [following:60]

Then place them along the path:

C:150,170 D:150,160 e:150,190 f:150,200 [following:150,210]

We haven’t fallen off the end of the path, so the third substring gets laid out as if its first character is absolutely positioned at (150,210).

g:150,210 h:160,210

Anchoring the third substring doesn’t require any translation.




<path id="b" d="M0,100 h100"/>
<text text-anchor="middle">
  <textPath xlink:href="#b" text-anchor="end">ab</textPath>cd
</text>

Two anchored substrings. Do an initial layout at x = 0:

a:0 b:10 c:20 d:30

Offsets (simple, since there’s no bidi):

a:- b:10 c:10 d:10

The first substring in path space:

a:0 b:10 [following:20]

then anchored:

a:80 b:90 [following:100]

Note that the anchoring doesn’t include the following “c” glyph when determining the extent of the substring. The second substring is first laid out as:

c:100 d:110

and then translated for the anchoring at x = 100:

c:90 d:100