2022-02-14 09:11:30 +03:00
|
|
|
|
"""Draw text."""
|
2021-04-12 17:56:37 +03:00
|
|
|
|
|
2023-07-25 15:46:18 +03:00
|
|
|
|
from math import cos, inf, radians, sin
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
2023-07-25 15:46:18 +03:00
|
|
|
|
from ..matrix import Matrix
|
2024-01-23 01:10:10 +03:00
|
|
|
|
from .bounding_box import extend_bounding_box
|
2021-04-26 12:20:39 +03:00
|
|
|
|
from .utils import normalize, size
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TextBox:
|
2021-04-12 17:56:37 +03:00
|
|
|
|
"""Dummy text box used to draw text."""
|
2021-04-10 23:15:43 +03:00
|
|
|
|
def __init__(self, pango_layout, style):
|
|
|
|
|
self.pango_layout = pango_layout
|
|
|
|
|
self.style = style
|
|
|
|
|
|
2023-03-21 16:58:09 +03:00
|
|
|
|
@property
|
|
|
|
|
def text(self):
|
|
|
|
|
return self.pango_layout.text
|
|
|
|
|
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
|
|
|
|
def text(svg, node, font_size):
|
2021-04-12 17:56:37 +03:00
|
|
|
|
"""Draw text node."""
|
2021-04-10 23:15:43 +03:00
|
|
|
|
from ..css.properties import INITIAL_VALUES
|
2024-06-09 01:56:43 +03:00
|
|
|
|
from ..draw.text import draw_emojis, draw_first_line
|
2021-04-10 23:15:43 +03:00
|
|
|
|
from ..text.line_break import split_first_line
|
|
|
|
|
|
2021-04-12 17:56:37 +03:00
|
|
|
|
# TODO: use real computed values
|
2021-04-10 23:15:43 +03:00
|
|
|
|
style = INITIAL_VALUES.copy()
|
2021-07-12 11:54:11 +03:00
|
|
|
|
style['font_family'] = [
|
|
|
|
|
font.strip('"\'') for font in
|
|
|
|
|
node.get('font-family', 'sans-serif').split(',')]
|
2021-04-10 23:15:43 +03:00
|
|
|
|
style['font_style'] = node.get('font-style', 'normal')
|
|
|
|
|
style['font_weight'] = node.get('font-weight', 400)
|
|
|
|
|
style['font_size'] = font_size
|
|
|
|
|
if style['font_weight'] == 'normal':
|
|
|
|
|
style['font_weight'] = 400
|
|
|
|
|
elif style['font_weight'] == 'bold':
|
|
|
|
|
style['font_weight'] = 700
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
style['font_weight'] = int(style['font_weight'])
|
|
|
|
|
except ValueError:
|
|
|
|
|
style['font_weight'] = 400
|
|
|
|
|
|
2021-08-22 13:10:48 +03:00
|
|
|
|
layout, _, _, width, height, _ = split_first_line(
|
2022-01-24 13:34:10 +03:00
|
|
|
|
node.text, style, svg.context, inf, 0)
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
2021-04-12 17:56:37 +03:00
|
|
|
|
# Get rotations and translations
|
2021-04-10 23:15:43 +03:00
|
|
|
|
x, y, dx, dy, rotate = [], [], [], [], [0]
|
2021-04-17 19:15:26 +03:00
|
|
|
|
if 'x' in node.attrib:
|
2021-07-31 09:29:23 +03:00
|
|
|
|
x = [size(i, font_size, svg.inner_width)
|
2021-04-17 19:15:26 +03:00
|
|
|
|
for i in normalize(node.attrib['x']).strip().split(' ')]
|
|
|
|
|
if 'y' in node.attrib:
|
2021-07-31 09:29:23 +03:00
|
|
|
|
y = [size(i, font_size, svg.inner_height)
|
2021-04-17 19:15:26 +03:00
|
|
|
|
for i in normalize(node.attrib['y']).strip().split(' ')]
|
|
|
|
|
if 'dx' in node.attrib:
|
2021-07-31 09:29:23 +03:00
|
|
|
|
dx = [size(i, font_size, svg.inner_width)
|
2021-04-17 19:15:26 +03:00
|
|
|
|
for i in normalize(node.attrib['dx']).strip().split(' ')]
|
|
|
|
|
if 'dy' in node.attrib:
|
2021-07-31 09:29:23 +03:00
|
|
|
|
dy = [size(i, font_size, svg.inner_height)
|
2021-04-17 19:15:26 +03:00
|
|
|
|
for i in normalize(node.attrib['dy']).strip().split(' ')]
|
2021-04-20 18:21:40 +03:00
|
|
|
|
if 'rotate' in node.attrib:
|
2021-04-10 23:15:43 +03:00
|
|
|
|
rotate = [radians(float(i)) if i else 0
|
2021-04-20 18:21:40 +03:00
|
|
|
|
for i in normalize(node.attrib['rotate']).strip().split(' ')]
|
2021-04-10 23:15:43 +03:00
|
|
|
|
last_r = rotate[-1]
|
2021-04-12 17:56:37 +03:00
|
|
|
|
letters_positions = [
|
|
|
|
|
([pl.pop(0) if pl else None for pl in (x, y, dx, dy, rotate)], char)
|
|
|
|
|
for char in node.text]
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
2023-07-25 01:49:43 +03:00
|
|
|
|
letter_spacing = svg.length(node.get('letter-spacing'), font_size)
|
2023-07-25 16:41:33 +03:00
|
|
|
|
text_length = svg.length(node.get('textLength'), font_size)
|
2023-07-25 01:49:43 +03:00
|
|
|
|
scale_x = 1
|
2023-07-25 16:41:33 +03:00
|
|
|
|
if text_length and node.text:
|
2023-07-25 01:49:43 +03:00
|
|
|
|
# calculate the number of spaces to be considered for the text
|
2023-07-25 16:41:33 +03:00
|
|
|
|
spaces_count = len(node.text) - 1
|
|
|
|
|
if normalize(node.attrib.get('lengthAdjust')) == 'spacingAndGlyphs':
|
2023-07-25 01:49:43 +03:00
|
|
|
|
# scale letter_spacing up/down to textLength
|
|
|
|
|
width_with_spacing = width + spaces_count * letter_spacing
|
2023-07-25 14:10:28 +03:00
|
|
|
|
letter_spacing *= text_length / width_with_spacing
|
2023-07-25 01:49:43 +03:00
|
|
|
|
# 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
|
2023-07-25 14:07:52 +03:00
|
|
|
|
scale_x = spaceless_text_length / width
|
2023-07-25 16:41:33 +03:00
|
|
|
|
elif spaces_count:
|
|
|
|
|
# adjust letter spacing to fit textLength
|
|
|
|
|
letter_spacing = (text_length - width) / spaces_count
|
|
|
|
|
width = text_length
|
2023-07-25 01:49:43 +03:00
|
|
|
|
|
2021-04-12 17:56:37 +03:00
|
|
|
|
# TODO: use real values
|
2021-11-04 11:31:27 +03:00
|
|
|
|
ascent, descent = font_size * .8, font_size * .2
|
2021-04-10 23:15:43 +03:00
|
|
|
|
|
2021-04-12 17:56:37 +03:00
|
|
|
|
# Align text box vertically
|
2021-04-11 17:13:59 +03:00
|
|
|
|
# TODO: This is a hack. Other baseline alignment tags are not supported.
|
|
|
|
|
# See https://www.w3.org/TR/SVG2/text.html#TextPropertiesSVG
|
2021-04-12 17:56:37 +03:00
|
|
|
|
y_align = 0
|
2021-04-10 23:15:43 +03:00
|
|
|
|
display_anchor = node.get('display-anchor')
|
2021-04-12 17:56:37 +03:00
|
|
|
|
alignment_baseline = node.get(
|
|
|
|
|
'dominant-baseline', node.get('alignment-baseline'))
|
2021-04-10 23:15:43 +03:00
|
|
|
|
if display_anchor == 'middle':
|
2023-10-16 19:59:42 +03:00
|
|
|
|
y_align = -height / 2
|
2021-04-10 23:15:43 +03:00
|
|
|
|
elif display_anchor == 'top':
|
2023-10-16 19:59:42 +03:00
|
|
|
|
pass
|
2021-04-10 23:15:43 +03:00
|
|
|
|
elif display_anchor == 'bottom':
|
2023-10-16 19:59:42 +03:00
|
|
|
|
y_align = -height
|
2021-07-18 11:08:50 +03:00
|
|
|
|
elif alignment_baseline in ('central', 'middle'):
|
2021-04-10 23:15:43 +03:00
|
|
|
|
# TODO: This is wrong, we use font top-to-bottom
|
|
|
|
|
y_align = (ascent + descent) / 2 - descent
|
2021-07-18 11:08:50 +03:00
|
|
|
|
elif alignment_baseline in (
|
|
|
|
|
'text-before-edge', 'before_edge', 'top', 'hanging', 'text-top'):
|
2021-04-10 23:15:43 +03:00
|
|
|
|
y_align = ascent
|
2021-07-18 11:08:50 +03:00
|
|
|
|
elif alignment_baseline in (
|
|
|
|
|
'text-after-edge', 'after_edge', 'bottom', 'text-bottom'):
|
2021-04-10 23:15:43 +03:00
|
|
|
|
y_align = -descent
|
|
|
|
|
|
2021-04-12 17:56:37 +03:00
|
|
|
|
# Return early when there’s no text
|
|
|
|
|
if not node.text:
|
2021-04-10 23:15:43 +03:00
|
|
|
|
x = x[0] if x else svg.cursor_position[0]
|
|
|
|
|
y = y[0] if y else svg.cursor_position[1]
|
|
|
|
|
dx = dx[0] if dx else 0
|
|
|
|
|
dy = dy[0] if dy else 0
|
|
|
|
|
svg.cursor_position = (x + dx, y + dy)
|
2021-04-12 17:56:37 +03:00
|
|
|
|
return
|
|
|
|
|
|
2022-01-27 04:18:46 +03:00
|
|
|
|
svg.stream.push_state()
|
2021-04-14 17:55:14 +03:00
|
|
|
|
svg.stream.begin_text()
|
2021-08-30 12:10:12 +03:00
|
|
|
|
emoji_lines = []
|
2021-04-14 17:55:14 +03:00
|
|
|
|
|
2021-04-12 17:56:37 +03:00
|
|
|
|
# Draw letters
|
|
|
|
|
for i, ((x, y, dx, dy, r), letter) in enumerate(letters_positions):
|
|
|
|
|
if x:
|
|
|
|
|
svg.cursor_d_position[0] = 0
|
|
|
|
|
if y:
|
|
|
|
|
svg.cursor_d_position[1] = 0
|
|
|
|
|
svg.cursor_d_position[0] += dx or 0
|
|
|
|
|
svg.cursor_d_position[1] += dy or 0
|
|
|
|
|
layout, _, _, width, height, _ = split_first_line(
|
2022-01-24 13:34:10 +03:00
|
|
|
|
letter, style, svg.context, inf, 0)
|
2021-04-17 19:30:18 +03:00
|
|
|
|
x = svg.cursor_position[0] if x is None else x
|
|
|
|
|
y = svg.cursor_position[1] if y is None else y
|
2023-07-25 14:11:31 +03:00
|
|
|
|
width *= scale_x
|
2021-04-17 19:30:18 +03:00
|
|
|
|
if i:
|
|
|
|
|
x += letter_spacing
|
2023-10-16 19:59:42 +03:00
|
|
|
|
svg.cursor_position = x + width, y
|
2023-07-25 01:49:43 +03:00
|
|
|
|
|
2023-10-16 19:59:42 +03:00
|
|
|
|
x_position = x + svg.cursor_d_position[0]
|
2021-04-20 18:21:40 +03:00
|
|
|
|
y_position = y + svg.cursor_d_position[1] + y_align
|
2021-04-17 19:30:18 +03:00
|
|
|
|
angle = last_r if r is None else r
|
|
|
|
|
points = (
|
2023-10-16 19:59:42 +03:00
|
|
|
|
(x_position, y_position),
|
|
|
|
|
(x_position + width, y_position - height))
|
2022-04-28 19:57:01 +03:00
|
|
|
|
node.text_bounding_box = extend_bounding_box(
|
|
|
|
|
node.text_bounding_box, points)
|
2021-04-12 17:56:37 +03:00
|
|
|
|
|
|
|
|
|
layout.reactivate(style)
|
2021-04-20 18:15:17 +03:00
|
|
|
|
svg.fill_stroke(node, font_size, text=True)
|
2023-07-25 15:46:18 +03:00
|
|
|
|
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
|
2021-08-30 12:10:12 +03:00
|
|
|
|
emojis = draw_first_line(
|
2023-07-25 15:46:18 +03:00
|
|
|
|
svg.stream, TextBox(layout, style), 'none', 'none', matrix)
|
2021-08-30 12:10:12 +03:00
|
|
|
|
emoji_lines.append((font_size, x, y, emojis))
|
2021-04-14 17:55:14 +03:00
|
|
|
|
|
|
|
|
|
svg.stream.end_text()
|
2022-01-27 04:18:46 +03:00
|
|
|
|
svg.stream.pop_state()
|
2021-08-29 20:34:13 +03:00
|
|
|
|
|
2021-08-30 12:10:12 +03:00
|
|
|
|
for font_size, x, y, emojis in emoji_lines:
|
|
|
|
|
draw_emojis(svg.stream, font_size, x, y, emojis)
|