Skip to content

Commit

Permalink
Merge pull request #1922 from oshmoun/master
Browse files Browse the repository at this point in the history
Add support for textLength and lengthAdjust in SVG text elements
  • Loading branch information
liZe authored Aug 19, 2023
2 parents 5ae1aab + 166ec93 commit 4e9ad95
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 14 deletions.
70 changes: 70 additions & 0 deletions tests/draw/svg/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,73 @@ def test_text_rotate(assert_pixels):
rotate="180" letter-spacing="2">abc</text>
</svg>
''')


@assert_no_logs
def test_text_text_length(assert_pixels):
assert_pixels('''
__RRRRRR____________
__RRRRRR____________
__BB__BB__BB________
__BB__BB__BB________
''', '''
<style>
@font-face { src: url(weasyprint.otf); font-family: weasyprint }
@page { size: 20px 4px }
svg { display: block }
</style>
<svg width="20px" height="4px" xmlns="http://www.w3.org/2000/svg">
<text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="red">
abc
</text>
<text x="2" y="3.5" font-family="weasyprint" font-size="2" fill="blue"
textLength="10">abc</text>
</svg>
''')


@assert_no_logs
def test_text_length_adjust_glyphs_only(assert_pixels):
assert_pixels('''
__RRRRRR____________
__RRRRRR____________
__BBBBBBBBBBBB______
__BBBBBBBBBBBB______
''', '''
<style>
@font-face { src: url(weasyprint.otf); font-family: weasyprint }
@page { size: 20px 4px }
svg { display: block }
</style>
<svg width="20px" height="4px" xmlns="http://www.w3.org/2000/svg">
<text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="red">
abc
</text>
<text x="2" y="3.5" font-family="weasyprint" font-size="2" fill="blue"
textLength="12" lengthAdjust="spacingAndGlyphs">abc</text>
</svg>
''')


@assert_no_logs
def test_text_length_adjust_spacing_and_glyphs(assert_pixels):
assert_pixels('''
__RR_RR_RR__________
__RR_RR_RR__________
__BBBB__BBBB__BBBB__
__BBBB__BBBB__BBBB__
''', '''
<style>
@font-face { src: url(weasyprint.otf); font-family: weasyprint }
@page { size: 20px 4px }
svg { display: block }
</style>
<svg width="20px" height="4px" xmlns="http://www.w3.org/2000/svg">
<text x="2" y="1.5" font-family="weasyprint" font-size="2" fill="red"
letter-spacing="1">abc</text>
<text x="2" y="3.5" font-family="weasyprint" font-size="2" fill="blue"
letter-spacing="1" textLength="16" lengthAdjust="spacingAndGlyphs">
abc
</text>
</svg>
''')
13 changes: 3 additions & 10 deletions weasyprint/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import operator
from colorsys import hsv_to_rgb, rgb_to_hsv
from io import BytesIO
from math import ceil, cos, floor, pi, sin, sqrt, tan
from math import ceil, floor, pi, sqrt, tan
from xml.etree import ElementTree

from PIL import Image
Expand Down Expand Up @@ -1073,7 +1073,7 @@ def draw_text(stream, textbox, offset_x, text_overflow, block_ellipsis):
textbox.pango_layout.reactivate(textbox.style)
stream.begin_text()
emojis = draw_first_line(
stream, textbox, text_overflow, block_ellipsis, x, y)
stream, textbox, text_overflow, block_ellipsis, Matrix(d=-1, e=x, f=y))
stream.end_text()

draw_emojis(stream, textbox.style['font_size'], x, y, emojis)
Expand All @@ -1097,8 +1097,7 @@ def draw_emojis(stream, font_size, x, y, emojis):
stream.pop_state()


def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y,
angle=0):
def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
"""Draw the given ``textbox`` line to the document ``stream``."""
# Don’t draw lines with only invisible characters
if not textbox.text.strip():
Expand Down Expand Up @@ -1152,12 +1151,6 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, x, y,

utf8_text = textbox.pango_layout.text.encode()
previous_utf8_position = 0

matrix = Matrix(1, 0, 0, -1, x, y)
if angle:
a, c = cos(angle), sin(angle)
b, d = -c, a
matrix = Matrix(a, b, c, d) @ matrix
stream.text_matrix(*matrix.values)
last_font = None
string = ''
Expand Down
33 changes: 29 additions & 4 deletions weasyprint/svg/text.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Draw text."""

from math import inf, radians
from math import cos, inf, radians, sin

from ..matrix import Matrix
from .bounding_box import EMPTY_BOUNDING_BOX, extend_bounding_box
from .utils import normalize, size

Expand Down Expand Up @@ -68,9 +69,28 @@ def text(svg, node, font_size):
([pl.pop(0) if pl else None for pl in (x, y, dx, dy, rotate)], char)
for char in node.text]

letter_spacing = svg.length(node.get('letter-spacing'), font_size)
text_length = svg.length(node.get('textLength'), font_size)
scale_x = 1
if text_length and node.text:
# calculate the number of spaces to be considered for the text
spaces_count = len(node.text) - 1
if normalize(node.attrib.get('lengthAdjust')) == 'spacingAndGlyphs':
# scale letter_spacing up/down to textLength
width_with_spacing = width + spaces_count * letter_spacing
letter_spacing *= text_length / width_with_spacing
# calculate the glyphs scaling factor by:
# - deducting the scaled letter_spacing from textLength
# - dividing the calculated value by the original width
spaceless_text_length = text_length - spaces_count * letter_spacing
scale_x = spaceless_text_length / width
elif spaces_count:
# adjust letter spacing to fit textLength
letter_spacing = (text_length - width) / spaces_count
width = text_length

# Align text box horizontally
x_align = 0
letter_spacing = svg.length(node.get('letter-spacing'), font_size)
text_anchor = node.get('text-anchor')
# TODO: use real values
ascent, descent = font_size * .8, font_size * .2
Expand Down Expand Up @@ -134,8 +154,10 @@ def text(svg, node, font_size):
letter, style, svg.context, inf, 0)
x = svg.cursor_position[0] if x is None else x
y = svg.cursor_position[1] if y is None else y
width *= scale_x
if i:
x += letter_spacing

x_position = x + svg.cursor_d_position[0] + x_align
y_position = y + svg.cursor_d_position[1] + y_align
cursor_position = x + width, y
Expand All @@ -150,9 +172,12 @@ def text(svg, node, font_size):

layout.reactivate(style)
svg.fill_stroke(node, font_size, text=True)
matrix = Matrix(a=scale_x, d=-1, e=x_position, f=y_position)
if angle:
a, c = cos(angle), sin(angle)
matrix = Matrix(a, -c, c, a) @ matrix
emojis = draw_first_line(
svg.stream, TextBox(layout, style), 'none', 'none',
x_position, y_position, angle)
svg.stream, TextBox(layout, style), 'none', 'none', matrix)
emoji_lines.append((font_size, x, y, emojis))
svg.cursor_position = cursor_position

Expand Down

0 comments on commit 4e9ad95

Please sign in to comment.