Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for textLength and lengthAdjust in SVG text elements #1922

Merged
merged 7 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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