213 lines
7.1 KiB
Python
213 lines
7.1 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
"""
|
||
|
scenegraph.element.text
|
||
|
"""
|
||
|
|
||
|
|
||
|
# imports ####################################################################
|
||
|
|
||
|
from math import hypot, degrees, atan2, modf, log
|
||
|
|
||
|
from ...font import Face
|
||
|
# from ...opengl.utils import create_texture
|
||
|
|
||
|
from .._common import _u
|
||
|
from ..transform import Translate, Rotate, Scale
|
||
|
from ..paint import Color
|
||
|
from .rectangle import Rectangle
|
||
|
from .path import Path
|
||
|
from .group import Group
|
||
|
from .use import Use
|
||
|
from . import Element
|
||
|
|
||
|
|
||
|
# text #######################################################################
|
||
|
|
||
|
|
||
|
_ANGLE_STEPS = 360
|
||
|
_SCALE_DOUBLE_STEPS = 128
|
||
|
_SUBPIXEL_STEPS = 64
|
||
|
|
||
|
|
||
|
class Text(Element):
|
||
|
tag = "text"
|
||
|
|
||
|
_state_attributes = Element._state_attributes + [
|
||
|
"font_family", "font_weight", "font_style", "font_size", "text"
|
||
|
]
|
||
|
|
||
|
_VECTOR_L = 30
|
||
|
_letters_cache = {}
|
||
|
_faces_cache = {}
|
||
|
|
||
|
def __init__(self, text,
|
||
|
**attributes):
|
||
|
super(Text, self).__init__(**attributes)
|
||
|
|
||
|
self._text_bbox = Rectangle()
|
||
|
self._width = None
|
||
|
self._text = None
|
||
|
self._ws = []
|
||
|
|
||
|
self.text = text
|
||
|
|
||
|
# TODO: handle list of coordinates
|
||
|
if isinstance(self.x, list):
|
||
|
self.x = self.x[0]
|
||
|
if isinstance(self.y, list):
|
||
|
self.y = self.y[0]
|
||
|
|
||
|
def _update_text_bbox(self):
|
||
|
(x, y), (width, height) = self.font_face.get_bbox(self.text)
|
||
|
self._width = width
|
||
|
self._text_bbox.x, self._text_bbox.width = x, width
|
||
|
self._text_bbox.y, self._text_bbox.height = y, height
|
||
|
self._text_bbox._paths() # force bbox update
|
||
|
|
||
|
@property
|
||
|
def font_face(self):
|
||
|
key = (self.font_family, self.font_weight, self.font_style,
|
||
|
int(self.font_size))
|
||
|
try:
|
||
|
font_face = self._faces_cache[key]
|
||
|
except KeyError:
|
||
|
font_face = self._faces_cache[key] = Face(*key)
|
||
|
return font_face
|
||
|
|
||
|
def get_text(self):
|
||
|
return _u(self._text)
|
||
|
|
||
|
def set_text(self, text):
|
||
|
self._text = text.strip()
|
||
|
text = property(get_text, set_text)
|
||
|
|
||
|
def _anchor(self):
|
||
|
self._update_text_bbox()
|
||
|
return {
|
||
|
'start': 0.,
|
||
|
'middle': -self._width / 2., # pylint: disable=invalid-unary-operand-type
|
||
|
'end': -self._width, # pylint: disable=invalid-unary-operand-type
|
||
|
}[self.text_anchor]
|
||
|
|
||
|
def _aabbox(self, transform, inheriteds):
|
||
|
return self._text_bbox.aabbox(transform * Translate(self._anchor()), inheriteds)
|
||
|
|
||
|
def _render(self, transform, inheriteds, context):
|
||
|
font_size = self.font_size
|
||
|
font_face = self.font_face
|
||
|
|
||
|
_, (cosa, sina), _, (sx, sy) = transform.params()
|
||
|
a, b = cosa * sx, sina * sy
|
||
|
c, d = -b, a
|
||
|
scale = hypot(a, b)
|
||
|
angle = degrees(atan2(b, a))
|
||
|
|
||
|
vector = font_size * scale > self._VECTOR_L
|
||
|
vector = vector or (self.stroke is not None) or (self.fill is None)
|
||
|
|
||
|
x_anchor = self._anchor()
|
||
|
X0, Y0 = transform.project(x_anchor)
|
||
|
untransform = transform.inverse()
|
||
|
|
||
|
if vector:
|
||
|
X, Y = 0., 0.
|
||
|
else:
|
||
|
(X, X0), (Y, Y0) = modf(X0), modf(Y0)
|
||
|
|
||
|
letters = Group(
|
||
|
transform=[Translate(x_anchor), Rotate(-angle), Scale(1 / scale), Translate(-X, -Y)],
|
||
|
stroke_width=self.stroke_width * scale,
|
||
|
)
|
||
|
self._ws = [0]
|
||
|
|
||
|
up = None
|
||
|
for uc in self.text:
|
||
|
X += font_face.get_hkerning(up, uc)
|
||
|
up = uc
|
||
|
|
||
|
if vector:
|
||
|
(Xf, Xi), (Yf, Yi) = (0., X), (0., Y)
|
||
|
else:
|
||
|
(Xf, Xi), (Yf, Yi) = modf(X), modf(Y)
|
||
|
if Xf < 0:
|
||
|
Xf, Xi = Xf + 1, Xi - 1
|
||
|
if Yf < 0:
|
||
|
Yf, Yi = Yf + 1, Yi - 1
|
||
|
key = (font_face, uc, vector,
|
||
|
int(round(angle * _ANGLE_STEPS / 360.)),
|
||
|
int(log(scale, 2.) * _SCALE_DOUBLE_STEPS),
|
||
|
int(Xf * _SUBPIXEL_STEPS), int(Yf * _SUBPIXEL_STEPS))
|
||
|
try:
|
||
|
letter, (Xc, Yc), (W, H), (dX, dY) = self._letters_cache[key]
|
||
|
except KeyError:
|
||
|
font_face.set_transform(a, b, c, d, Xf, Yf)
|
||
|
if vector:
|
||
|
(Xc, Yc), (W, H), (dX, dY), outline = font_face.outline(uc)
|
||
|
letter = Path(d=outline)
|
||
|
else:
|
||
|
assert False # code requires opengl
|
||
|
# (Xc, Yc), (W, H), (dX, dY), data = font_face.bitmap(uc)
|
||
|
# letter = Rectangle(x=Xc, y=Yc, width=W, height=H, fill=_Texture(create_texture(W, H, data)))
|
||
|
self._letters_cache[key] = letter, (Xc, Yc), (W, H), (dX, dY)
|
||
|
|
||
|
if W > 0 and H > 0:
|
||
|
letters.add_child(Use(letter, x=Xi, y=Yi))
|
||
|
|
||
|
X += dX
|
||
|
Y += dY
|
||
|
x, _ = untransform.project(X + X0, Y + Y0)
|
||
|
self._ws.append(x - x_anchor)
|
||
|
|
||
|
if all(type(c) in [type(None), Color] for c in [self.fill, self.stroke]):
|
||
|
# single pass rendering
|
||
|
if not vector:
|
||
|
for letter in letters.children:
|
||
|
letter.element.fill.rgb = self.fill.rgb
|
||
|
letters.render(transform, inheriteds, context)
|
||
|
|
||
|
else:
|
||
|
# multi-pass rendering if a gradient or pattern is used
|
||
|
filler_x, filler_y = self.x, self.y - self._text_bbox.height
|
||
|
bbox_x = self._text_bbox.x - self.stroke_width / 2. + x_anchor
|
||
|
bbox_y = self._text_bbox.y - self.stroke_width / 2.
|
||
|
width = self._text_bbox.width + self.stroke_width,
|
||
|
height = self._text_bbox.height + self.stroke_width
|
||
|
|
||
|
mask = Use(letters,
|
||
|
transform=[Translate(filler_x - bbox_x, filler_y - bbox_y)],
|
||
|
fill_opacity=1., stroke_opacity=1.,)
|
||
|
|
||
|
filler = Rectangle(
|
||
|
x=filler_x, y=filler_y, width=width, height=height,
|
||
|
transform=[Translate(bbox_x - filler_x, bbox_y - filler_y)],
|
||
|
stroke=None,
|
||
|
mask=mask,)
|
||
|
|
||
|
for filler_fill, filler_opacity, masking in [
|
||
|
(self.fill, self.fill_opacity, (Color.white, None)),
|
||
|
(self.stroke, self.stroke_opacity, (None, Color.white))
|
||
|
]:
|
||
|
if filler_fill:
|
||
|
filler.fill, filler.fill_opacity = filler_fill, filler_opacity
|
||
|
mask.fill, mask.stroke = masking
|
||
|
filler.render(transform, inheriteds, context)
|
||
|
|
||
|
def index(self, x, y=0):
|
||
|
"""index of the char at x (local coordinates)."""
|
||
|
for i, w in enumerate(self._ws):
|
||
|
if x < w:
|
||
|
break
|
||
|
return i - 1
|
||
|
|
||
|
def _hit_test(self, x, y, transform):
|
||
|
return self._text_bbox.pick(x, y, transform * Translate(self._anchor()))
|
||
|
|
||
|
def _xml_content(self, defs):
|
||
|
text = self.text
|
||
|
for old, new in [('&', '&'),
|
||
|
('<', '<'),
|
||
|
('>', '>')]:
|
||
|
text = text.replace(old, new)
|
||
|
return text
|