diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py
index f7e9df4477..4d0a970a3c 100644
--- a/mathics/builtin/drawing/graphics3d.py
+++ b/mathics/builtin/drawing/graphics3d.py
@@ -764,8 +764,6 @@ def total_extent_3d(extents):
class Graphics3DElements(_GraphicsElements):
- coords = Coords3D
-
def __init__(self, content, evaluation, neg_y=False):
super(Graphics3DElements, self).__init__(content, evaluation)
self.neg_y = neg_y
@@ -774,6 +772,10 @@ def __init__(self, content, evaluation, neg_y=False):
) = (
self.pixel_width
) = self.pixel_height = self.extent_width = self.extent_height = None
+ self.local_to_screen = None
+
+ def make_coords(self, points):
+ return [Coords3D(self, p) for p in points]
def extent(self, completely_visible_only=False):
return total_extent_3d([element.extent() for element in self.elements])
@@ -835,7 +837,11 @@ def to_asy(self):
return "".join(
"path3 g={0}--cycle;dot(g, {1});".format(
- "--".join("(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line), pen
+ "--".join(
+ "(%.5g,%.5g,%.5g)" % tuple(float(x) for x in coords.pos()[0])
+ for coords in line
+ ),
+ pen,
)
for line in self.lines
)
diff --git a/mathics/builtin/drawing/image.py b/mathics/builtin/drawing/image.py
index 861156c409..c97af88cd4 100644
--- a/mathics/builtin/drawing/image.py
+++ b/mathics/builtin/drawing/image.py
@@ -25,6 +25,7 @@
convert as convert_color,
colorspaces as known_colorspaces,
)
+from mathics.layout.client import WebEngineError
import base64
import functools
@@ -2561,3 +2562,48 @@ def color_func(
image = wc.to_image()
return Image(numpy.array(image), "RGB")
+
+
+class Rasterize(Builtin):
+ requires = _image_requires
+
+ options = {
+ "RasterSize": "300",
+ }
+
+ def apply(self, expr, evaluation, options):
+ "Rasterize[expr_, OptionsPattern[%(name)s]]"
+
+ raster_size = self.get_option(options, "RasterSize", evaluation)
+ if isinstance(raster_size, Integer):
+ s = raster_size.get_int_value()
+ py_raster_size = (s, s)
+ elif raster_size.has_form("List", 2) and all(
+ isinstance(s, Integer) for s in raster_size.leaves
+ ):
+ py_raster_size = tuple(s.get_int_value for s in raster_size.leaves)
+ else:
+ return
+
+ mathml = evaluation.format_output(expr, "xml")
+ try:
+ svg = evaluation.output.mathml_to_svg(mathml)
+ png = evaluation.output.rasterize(svg, py_raster_size)
+
+ stream = BytesIO()
+ stream.write(png)
+ stream.seek(0)
+ im = PIL.Image.open(stream)
+ # note that we need to get these pixels as long as stream is still open,
+ # otherwise PIL will generate an IO error.
+ pixels = numpy.array(im)
+ stream.close()
+
+ return Image(pixels, "RGB")
+ except WebEngineError as e:
+ evaluation.message(
+ "General",
+ "nowebeng",
+ "Rasterize[] did not succeed: " + str(e),
+ once=True,
+ )
diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py
index 0c93460549..bf58c0207e 100644
--- a/mathics/builtin/drawing/plot.py
+++ b/mathics/builtin/drawing/plot.py
@@ -40,6 +40,7 @@
except ImportError:
has_compile = False
+
def gradient_palette(color_function, n, evaluation): # always returns RGB values
if isinstance(color_function, String):
color_data = Expression("ColorData", color_function).evaluate(evaluation)
@@ -696,6 +697,15 @@ def find_excl(excl):
Expression("Point", Expression(SymbolList, *meshpoints))
)
+ # We need the PrecomputeTransformations option here. To understand why, try Plot[1+x*0.000001, {x, 0, 1}]
+ # without it. in Graphics[], we set up a transformation that scales a very tiny area to a very large area.
+ # unfortunately, most browsers seem to have problems with scaling stroke width properly. since we scale a
+ # very tiny area, we specify a very small stroke width (e.g. 1e-6) which is then scaled. but most browsers
+ # simply round this stroke width to 0 before scaling, so we end up with an empty plot. in order to fix this,
+ # Transformation -> Precomputed simply gets rid of the SVG transformations and passes the scaled coordinates
+ # into the SVG. this also has the advantage that we can precompute with arbitrary precision using mpmath.
+ options["System`Transformation"] = String("Precomputed")
+
return Expression(
"Graphics", Expression(SymbolList, *graphics), *options_to_rules(options)
)
diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py
index fc0c9a0302..6fb6645d96 100644
--- a/mathics/builtin/graphics.py
+++ b/mathics/builtin/graphics.py
@@ -6,10 +6,12 @@
"""
-from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp
+from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, radians, exp
+import re
import json
import base64
from itertools import chain
+from sympy.matrices import Matrix
from mathics.version import __version__ # noqa used in loading to check consistency.
from mathics.builtin.base import (
@@ -19,7 +21,9 @@
BoxConstructError,
)
from mathics.builtin.options import options_to_rules
+from mathics.layout.client import WebEngineUnavailable
from mathics.core.expression import (
+ BoxError,
Expression,
Integer,
Rational,
@@ -36,6 +40,7 @@
from mathics.builtin.drawing.colors import convert as convert_color
from mathics.core.numbers import machine_epsilon
+
GRAPHICS_OPTIONS = {
"AspectRatio": "Automatic",
"Axes": "False",
@@ -47,6 +52,10 @@
"PlotRangePadding": "Automatic",
"TicksStyle": "{}",
"$OptionSyntax": "Ignore",
+ "Transformation": "Automatic", # Mathics specific; used internally to enable stuff like
+ # Plot[x + 1e-20 * x, {x, 0, 1}] that without precomputing transformations inside Mathics
+ # will hit SVGs numerical accuracy abilities in some browsers as strokes with width < 1e-6
+ # will get rounded to 0 and thus won't get scale transformed in SVG and vanish.
}
@@ -71,51 +80,28 @@ def get_class(name):
# like return globals().get(name)
-def coords(value):
- if value.has_form("List", 2):
- x, y = value.leaves[0].round_to_float(), value.leaves[1].round_to_float()
- if x is None or y is None:
- raise CoordinatesError
- return (x, y)
- raise CoordinatesError
-
+def expr_to_coords(value):
+ if not value.has_form("List", 2):
+ raise CoordinatesError
+ x, y = value.leaves[0].to_mpmath(), value.leaves[1].to_mpmath()
+ if x is None or y is None:
+ raise CoordinatesError
+ return x, y
-class Coords(object):
- def __init__(self, graphics, expr=None, pos=None, d=None):
- self.graphics = graphics
- self.p = pos
- self.d = d
- if expr is not None:
- if expr.has_form("Offset", 1, 2):
- self.d = coords(expr.leaves[0])
- if len(expr.leaves) > 1:
- self.p = coords(expr.leaves[1])
- else:
- self.p = None
- else:
- self.p = coords(expr)
-
- def pos(self):
- p = self.graphics.translate(self.p)
- p = (cut(p[0]), cut(p[1]))
- if self.d is not None:
- d = self.graphics.translate_absolute(self.d)
- return (p[0] + d[0], p[1] + d[1])
- return p
- def add(self, x, y):
- p = (self.p[0] + x, self.p[1] + y)
- return Coords(self.graphics, pos=p, d=self.d)
+def add_coords(a, b):
+ x1, y1 = a
+ x2, y2 = b
+ return x1 + x2, y1 + y2
-def cut(value):
- "Cut values in graphics primitives (not displayed otherwise in SVG)"
- border = 10 ** 8
- if value < -border:
- value = -border
- elif value > border:
- value = border
- return value
+def axis_coords(graphics, pos, d=None):
+ p = graphics.translate(pos)
+ if d is not None:
+ d = graphics.translate_absolute_in_pixels(d)
+ return p[0] + d[0], p[1] + d[1]
+ else:
+ return p
def create_css(
@@ -148,10 +134,13 @@ def asy_number(value):
def _to_float(x):
- x = x.round_to_float()
- if x is None:
- raise BoxConstructError
- return x
+ if isinstance(x, Integer):
+ return x.get_int_value()
+ else:
+ y = x.round_to_float()
+ if y is None:
+ raise BoxConstructError
+ return y
def create_pens(
@@ -296,96 +285,153 @@ def _CMC_distance(lab1, lab2, l, c):
def _extract_graphics(graphics, format, evaluation):
graphics_box = Expression(SymbolMakeBoxes, graphics).evaluate(evaluation)
- # builtin = GraphicsBox(expression=False)
elements, calc_dimensions = graphics_box._prepare_elements(
- graphics_box.leaves, {"evaluation": evaluation}, neg_y=True
+ graphics_box._leaves, {"evaluation": evaluation}, neg_y=True
)
- xmin, xmax, ymin, ymax, _, _, _, _ = calc_dimensions()
-
- # xmin, xmax have always been moved to 0 here. the untransformed
- # and unscaled bounds are found in elements.xmin, elements.ymin,
- # elements.extent_width, elements.extent_height.
-
- # now compute the position of origin (0, 0) in the transformed
- # coordinate space.
- ex = elements.extent_width
- ey = elements.extent_height
+ if not isinstance(elements.elements[0], GeometricTransformationBox):
+ raise ValueError("expected GeometricTransformationBox")
- sx = (xmax - xmin) / ex
- sy = (ymax - ymin) / ey
-
- ox = -elements.xmin * sx + xmin
- oy = -elements.ymin * sy + ymin
+ # contents = elements.elements[0].contents
# generate code for svg or asy.
if format == "asy":
- code = "\n".join(element.to_asy() for element in elements.elements)
+ code = "\n".join(element.to_asy(None) for element in elements.elements)
elif format == "svg":
code = elements.to_svg()
else:
raise NotImplementedError
- return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code
+ return code
-class _SVGTransform:
- def __init__(self):
- self.transforms = []
+class _Transform:
+ def __init__(self, f):
+ if not isinstance(f, Expression):
+ self.matrix = f
+ return
- def matrix(self, a, b, c, d, e, f):
- # a c e
- # b d f
- # 0 0 1
- self.transforms.append("matrix(%f, %f, %f, %f, %f, %f)" % (a, b, c, d, e, f))
+ if f.get_head_name() != "System`TransformationFunction":
+ raise BoxConstructError
- def translate(self, x, y):
- self.transforms.append("translate(%f, %f)" % (x, y))
+ if len(f.leaves) != 1 or f.leaves[0].get_head_name() != "System`List":
+ raise BoxConstructError
- def scale(self, x, y):
- self.transforms.append("scale(%f, %f)" % (x, y))
+ rows = f.leaves[0].leaves
+ if len(rows) != 3:
+ raise BoxConstructError
+ if any(row.get_head_name() != "System`List" for row in rows):
+ raise BoxConstructError
+ if any(len(row.leaves) != 3 for row in rows):
+ raise BoxConstructError
- def rotate(self, x):
- self.transforms.append("rotate(%f)" % x)
+ self.matrix = [[_to_float(x) for x in row.leaves] for row in rows]
- def apply(self, svg):
- return '%s' % (" ".join(self.transforms), svg)
+ def combine(self, transform0):
+ if isinstance(transform0, _Transform):
+ return self.multiply(transform0)
+ else:
+ t = self
+ def combined(*p, w=1):
+ return transform0(*t(*p, w=w), w=w)
-class _ASYTransform:
- _template = """
- add(%s * (new picture() {
- picture saved = currentpicture;
- picture transformed = new picture;
- currentpicture = transformed;
- %s
- currentpicture = saved;
- return transformed;
- })());
- """
+ return combined
+
+ def inverse(self):
+ return _Transform(Matrix(self.matrix).inv().tolist())
+
+ def multiply(self, other):
+ a = self.matrix
+ b = other.matrix
+ return _Transform(
+ [
+ [sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)]
+ for i in range(3)
+ ]
+ )
- def __init__(self):
- self.transforms = []
+ def __call__(self, *p, w=1):
+ m = self.matrix
+
+ m11 = m[0][0]
+ m12 = m[0][1]
+ m13 = m[0][2]
+
+ m21 = m[1][0]
+ m22 = m[1][1]
+ m23 = m[1][2]
+
+ if w == 1:
+ for x, y in p:
+ yield m11 * x + m12 * y + m13, m21 * x + m22 * y + m23
+ elif w == 0:
+ for x, y in p:
+ yield m11 * x + m12 * y, m21 * x + m22 * y
+ else:
+ raise NotImplementedError("w not in (0, 1)")
+
+ def to_svg(self, svg):
+ m = self.matrix
+
+ a = m[0][0]
+ b = m[1][0]
+ c = m[0][1]
+ d = m[1][1]
+ e = m[0][2]
+ f = m[1][2]
+
+ if m[2][0] != 0.0 or m[2][1] != 0.0 or m[2][2] != 1.0:
+ raise BoxConstructError
- def matrix(self, a, b, c, d, e, f):
# a c e
# b d f
# 0 0 1
- # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms
- self.transforms.append("(%f, %f, %f, %f, %f, %f)" % (e, f, a, c, b, d))
- def translate(self, x, y):
- self.transforms.append("shift(%f, %f)" % (x, y))
+ t = "matrix(%s, %s, %s, %s, %s, %s)" % (
+ str(a),
+ str(b),
+ str(c),
+ str(d),
+ str(e),
+ str(f),
+ )
+ return '%s' % (t, svg)
+
+ def to_asy(self, asy):
+ m = self.matrix
+
+ a = m[0][0]
+ b = m[1][0]
+ c = m[0][1]
+ d = m[1][1]
+ e = m[0][2]
+ f = m[1][2]
+
+ if m[2][0] != 0.0 or m[2][1] != 0.0 or m[2][2] != 1.0:
+ raise BoxConstructError
- def scale(self, x, y):
- self.transforms.append("scale(%f, %f)" % (x, y))
+ # a c e
+ # b d f
+ # 0 0 1
+ # see http://asymptote.sourceforge.net/doc/Transforms.html#Transforms
+ t = ",".join(map(asy_number, (e, f, a, c, b, d)))
+
+ return "".join(
+ (
+ "add((",
+ t,
+ ")*(new picture(){",
+ "picture s=currentpicture,t=new picture;currentpicture=t;",
+ asy,
+ "currentpicture=s;return t;})());",
+ )
+ )
- def rotate(self, x):
- self.transforms.append("rotate(%f)" % x)
- def apply(self, asy):
- return self._template % (" * ".join(self.transforms), asy)
+def _no_transform(*p, w=None):
+ return p
class Show(Builtin):
@@ -411,7 +457,7 @@ def apply(self, graphics, evaluation, options):
new_leaves = []
options_set = set(options.keys())
for leaf in graphics.leaves:
- new_leaf = leaf
+ # new_leaf = leaf
leaf_name = leaf.get_head_name()
if leaf_name == "System`Rule" and str(leaf.leaves[0]) in options_set:
continue
@@ -464,9 +510,13 @@ class Graphics(Builtin):
. \begin{asy}
. usepackage("amsmath");
. size(5.8556cm, 5.8333cm);
- . draw(ellipse((175,175),175,175), rgb(0, 0, 0)+linewidth(0.66667));
+ . add((175,175,175,0,0,175)*(new picture(){picture s=currentpicture,t=new picture;currentpicture=t;draw(ellipse((0,0),1,1), rgb(0, 0, 0)+linewidth(0.0038095));currentpicture=s;return t;})());
. clip(box((-0.33333,0.33333), (350.33,349.67)));
. \end{asy}
+
+ Invalid graphics directives yield invalid box structures:
+ >> Graphics[Circle[{a, b}]]
+ : GraphicsBox[CircleBox[List[a, b]], Rule[$OptionSyntax, Ignore], Rule[AspectRatio, Automatic], Rule[Axes, False], Rule[AxesStyle, List[]], Rule[Background, Automatic], Rule[ImageSize, Automatic], Rule[LabelStyle, List[]], Rule[PlotRange, Automatic], Rule[PlotRangePadding, Automatic], Rule[TicksStyle, List[]], Rule[Transformation, Automatic]] is not a valid box structure.
"""
options = GRAPHICS_OPTIONS
@@ -482,11 +532,17 @@ def convert(content):
if head == "System`List":
return Expression(
- SymbolList, *[convert(item) for item in content.leaves]
+ SymbolList, *[convert(item) for item in content._leaves]
)
elif head == "System`Style":
return Expression(
- "StyleBox", *[convert(item) for item in content.leaves]
+ "StyleBox", *[convert(item) for item in content._leaves]
+ )
+ elif head == "System`GeometricTransformation" and len(content._leaves) == 2:
+ return Expression(
+ "GeometricTransformationBox",
+ convert(content._leaves[0]),
+ content._leaves[1],
)
if head in element_heads:
@@ -909,7 +965,7 @@ def apply(self, c1, c2, evaluation, options):
# If numpy is not installed, 100 * c1.to_color_space returns
# a list of 100 x 3 elements, instead of doing elementwise multiplication
try:
- import numpy as np
+ import numpy as np # noqa just for check that the library is installed...
except:
raise RuntimeError("NumPy needs to be installed for ColorDistance")
@@ -1100,7 +1156,7 @@ class PointSize(_Size):
"""
def get_size(self):
- return self.graphics.view_width * self.value
+ return self.graphics.extent_width * self.value
class FontColor(Builtin):
@@ -1114,6 +1170,56 @@ class FontColor(Builtin):
pass
+class FontSize(_GraphicsElement):
+ """
+
+ - 'FontSize[$s$]'
+
- sets the font size to $s$ printer's points.
+
+ """
+
+ def init(self, graphics, item=None, value=None):
+ super(FontSize, self).init(graphics, item)
+
+ self.scaled = False
+ if item is not None and len(item.leaves) == 1:
+ if item.leaves[0].get_head_name() == "System`Scaled":
+ scaled = item.leaves[0]
+ if len(scaled.leaves) == 1:
+ self.scaled = True
+ self.value = scaled.leaves[0].round_to_float()
+
+ if self.scaled:
+ pass
+ elif item is not None:
+ self.value = item.leaves[0].round_to_float()
+ elif value is not None:
+ self.value = value
+ else:
+ raise BoxConstructError
+
+ if self.value < 0:
+ raise BoxConstructError
+
+ def get_size(self):
+ if self.scaled:
+ if self.graphics.extent_width is None:
+ return 1.0
+ else:
+ return self.graphics.extent_width * self.value
+ else:
+ if self.graphics.extent_width is None or self.graphics.pixel_width is None:
+ return 1.0
+ else:
+ return (96.0 / 72.0) * (
+ self.value * self.graphics.extent_width / self.graphics.pixel_width
+ )
+
+
+class Scaled(Builtin):
+ pass
+
+
class Offset(Builtin):
pass
@@ -1214,58 +1320,64 @@ def init(self, graphics, style, item):
super(RectangleBox, self).init(graphics, item, style)
if len(item.leaves) not in (1, 2):
raise BoxConstructError
+
self.edge_color, self.face_color = style.get_style(_Color, face_element=True)
- self.p1 = Coords(graphics, item.leaves[0])
+ self.p1 = expr_to_coords(item._leaves[0])
if len(item.leaves) == 1:
- self.p2 = self.p1.add(1, 1)
+ self.p2 = add_coords(self.p1, (1, 1))
elif len(item.leaves) == 2:
- self.p2 = Coords(graphics, item.leaves[1])
+ self.p2 = expr_to_coords(item.leaves[1])
def extent(self):
l = self.style.get_line_width(face_element=True) / 2
result = []
- for p in [self.p1, self.p2]:
- x, y = p.pos()
- result.extend(
- [(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
- )
+
+ tx1, ty1 = self.p1
+ tx2, ty2 = self.p2
+
+ x1 = min(tx1, tx2) - l
+ x2 = max(tx1, tx2) + l
+ y1 = min(ty1, ty2) - l
+ y2 = max(ty1, ty2) + l
+
+ result.extend([(x1, y1), (x1, y2), (x2, y1), (x2, y2)])
+
return result
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=True)
- x1, y1 = self.p1.pos()
- x2, y2 = self.p2.pos()
+ p1, p2 = transform(self.p1, self.p2)
+ x1, y1 = p1
+ x2, y2 = p2
xmin = min(x1, x2)
ymin = min(y1, y2)
w = max(x1, x2) - xmin
h = max(y1, y2) - ymin
- if offset:
- x1, x2 = x1 + offset[0], x2 + offset[0]
- y1, y2 = y1 + offset[1], y2 + offset[1]
style = create_css(self.edge_color, self.face_color, l)
- return '' % (
- xmin,
- ymin,
- w,
- h,
+ return '' % (
+ str(xmin),
+ str(ymin),
+ str(w),
+ str(h),
style,
)
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=True)
- x1, y1 = self.p1.pos()
- x2, y2 = self.p2.pos()
+ p1, p2 = transform(self.p1, self.p2)
+ x1, y1 = p1
+ x2, y2 = p2
pens = create_pens(self.edge_color, self.face_color, l, is_face_element=True)
x1, x2, y1, y2 = asy_number(x1), asy_number(x2), asy_number(y1), asy_number(y2)
return "filldraw((%s,%s)--(%s,%s)--(%s,%s)--(%s,%s)--cycle, %s);" % (
- x1,
- y1,
- x2,
- y1,
- x2,
- y2,
- x1,
- y2,
+ str(x1),
+ str(y1),
+ str(x2),
+ str(y1),
+ str(x2),
+ str(y2),
+ str(x1),
+ str(y2),
pens,
)
@@ -1280,7 +1392,7 @@ def init(self, graphics, style, item):
self.edge_color, self.face_color = style.get_style(
_Color, face_element=self.face_element
)
- self.c = Coords(graphics, item.leaves[0])
+ self.c = expr_to_coords(item.leaves[0])
if len(item.leaves) == 1:
rx = ry = 1
elif len(item.leaves) == 2:
@@ -1290,38 +1402,43 @@ def init(self, graphics, style, item):
ry = r.leaves[1].round_to_float()
else:
rx = ry = r.round_to_float()
- self.r = self.c.add(rx, ry)
+ self.r = add_coords(self.c, (rx, ry))
def extent(self):
l = self.style.get_line_width(face_element=self.face_element) / 2
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ x, y = self.c
+ rx, ry = self.r
rx -= x
ry = y - ry
rx += l
ry += l
return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)]
- def to_svg(self, offset=None):
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ def to_svg(self, transform):
+ c, r = transform(self.c, self.r)
+ x, y = c
+ rx, ry = r
rx -= x
- ry = y - ry
+ ry = abs(y - ry)
l = self.style.get_line_width(face_element=self.face_element)
style = create_css(self.edge_color, self.face_color, stroke_width=l)
- return '' % (
- x,
- y,
- rx,
- ry,
+ return '' % (
+ str(x),
+ str(y),
+ str(rx),
+ str(ry),
style,
)
- def to_asy(self):
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ def to_asy(self, transform):
+ if transform:
+ c, r = transform(self.c, self.r)
+ else:
+ c, r = self.c, self.r
+ x, y = c
+ rx, ry = r
rx -= x
- ry -= y
+ ry = abs(ry - y)
l = self.style.get_line_width(face_element=self.face_element)
pen = create_pens(
edge_color=self.edge_color,
@@ -1367,9 +1484,10 @@ def init(self, graphics, style, item):
self.arc = None
super(_ArcBox, self).init(graphics, style, item)
- def _arc_params(self):
- x, y = self.c.pos()
- rx, ry = self.r.pos()
+ def _arc_params(self, transform):
+ c, r = transform(self.c, self.r)
+ x, y = c
+ rx, ry = r
rx -= x
ry -= y
@@ -1389,20 +1507,26 @@ def _arc_params(self):
return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
if self.arc is None:
- return super(_ArcBox, self).to_svg(offset)
+ return super(_ArcBox, self).to_svg(transform)
- x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params()
+ x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params(transform)
def path(closed):
if closed:
- yield "M %f,%f" % (x, y)
- yield "L %f,%f" % (sx, sy)
+ yield "M %s,%s" % (str(x), str(y))
+ yield "L %s,%s" % (str(sx), str(sy))
else:
- yield "M %f,%f" % (sx, sy)
-
- yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey)
+ yield "M %s,%s" % (str(sx), str(sy))
+
+ yield "A %s,%s,0,%d,0,%s,%s" % (
+ str(rx),
+ str(ry),
+ large_arc,
+ str(ex),
+ str(ey),
+ )
if closed:
yield "Z"
@@ -1411,11 +1535,11 @@ def path(closed):
style = create_css(self.edge_color, self.face_color, stroke_width=l)
return '' % (" ".join(path(self.face_element)), style)
- def to_asy(self):
+ def to_asy(self, transform):
if self.arc is None:
- return super(_ArcBox, self).to_asy()
+ return super(_ArcBox, self).to_asy(transform)
- x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params()
+ x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params(transform)
def path(closed):
if closed:
@@ -1469,16 +1593,15 @@ def do_init(self, graphics, points):
lines.append(leaf.leaves)
else:
raise BoxConstructError
- self.lines = [
- [graphics.coords(graphics, point) for point in line] for line in lines
- ]
+ make_coords = graphics.make_coords # for Graphics and Graphics3D support
+ self.lines = [make_coords(line) for line in lines]
def extent(self):
l = self.style.get_line_width(face_element=False)
result = []
for line in self.lines:
for c in line:
- x, y = c.pos()
+ x, y = c
result.extend(
[(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
)
@@ -1522,33 +1645,42 @@ def init(self, graphics, style, item=None):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
point_size, _ = self.style.get_style(PointSize, face_element=False)
if point_size is None:
point_size = PointSize(self.graphics, value=0.005)
size = point_size.get_size()
+ graphics = self.graphics
+ size_x = size
+ size_y = (
+ size_x
+ * (graphics.extent_height / graphics.extent_width)
+ * (graphics.pixel_width / graphics.pixel_height)
+ )
+
style = create_css(
edge_color=self.edge_color, stroke_width=0, face_color=self.face_color
)
svg = ""
for line in self.lines:
- for coords in line:
- svg += '' % (
- coords.pos()[0],
- coords.pos()[1],
- size,
+ for x, y in transform(*line):
+ svg += '' % (
+ float(x),
+ float(y),
+ float(size_x),
+ float(size_y),
style,
)
return svg
- def to_asy(self):
+ def to_asy(self, transform):
pen = create_pens(face_color=self.face_color, is_face_element=False)
asy = ""
for line in self.lines:
- for coords in line:
- asy += "dot(%s, %s);" % (coords.pos(), pen)
+ for x, y in transform(*line):
+ asy += "dot(%s, %s);" % ((float(x), float(y)), pen)
return asy
@@ -1586,24 +1718,32 @@ def init(self, graphics, style, item=None, lines=None):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=False)
+ l = list(transform((l, l), w=0))[0][0]
style = create_css(edge_color=self.edge_color, stroke_width=l)
+
svg = ""
for line in self.lines:
- svg += '' % (
- " ".join(["%f,%f" % coords.pos() for coords in line]),
- style,
+ path = " ".join(
+ ["%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)]
)
+ svg += '' % (path, style)
+
return svg
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=False)
+ l = list(transform((l, l), w=0))[0][0]
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
+
asy = ""
for line in self.lines:
- path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line])
+ path = "--".join(
+ ["(%.5g,%5g)" % tuple(float(cc) for cc in c) for c in transform(*line)]
+ )
asy += "draw(%s, %s);" % (path, pen)
+
return asy
@@ -1625,11 +1765,13 @@ def path(max_degree, p):
n = min(max_degree, len(p)) # 1, 2, or 3
if n < 1:
raise BoxConstructError
- yield forms[n - 1] + " ".join("%f,%f" % xy for xy in p[:n])
+ yield forms[n - 1] + " ".join(
+ "%f,%f" % tuple(float(cc) for cc in xy) for xy in p[:n]
+ )
p = p[n:]
k, p = segments[0]
- yield "M%f,%f" % p[0]
+ yield "M%f,%f" % tuple(float(cc) for cc in p[0])
for s in path(k, p[1:]):
yield s
@@ -1741,25 +1883,23 @@ def init(self, graphics, style, item, options):
raise BoxConstructError
self.spline_degree = spline_degree.get_int_value()
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=False)
style = create_css(edge_color=self.edge_color, stroke_width=l)
svg = ""
for line in self.lines:
- s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line])))
+ s = " ".join(_svg_bezier((self.spline_degree, transform(*line))))
svg += '' % (s, style)
return svg
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
asy = ""
for line in self.lines:
- for path in _asy_bezier((self.spline_degree, [xy.pos() for xy in line])):
- if path[:2] == "..":
- path = "(0.,0.)" + path
+ for path in _asy_bezier((self.spline_degree, transform(*line))):
asy += "draw(%s, %s);" % (path, pen)
return asy
@@ -1810,16 +1950,13 @@ def parse_component(segments):
else:
raise BoxConstructError
- coords = []
-
+ c = []
for part in parts:
if part.get_head_name() != "System`List":
raise BoxConstructError
- coords.extend(
- [graphics.coords(graphics, xy) for xy in part.leaves]
- )
+ c.extend([expr_to_coords(xy) for xy in part.leaves])
- yield k, coords
+ yield k, c
if all(x.get_head_name() == "System`List" for x in leaves):
self.components = [list(parse_component(x)) for x in leaves]
@@ -1828,7 +1965,7 @@ def parse_component(segments):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=False)
style = create_css(
edge_color=self.edge_color, face_color=self.face_color, stroke_width=l
@@ -1836,7 +1973,7 @@ def to_svg(self, offset=None):
def components():
for component in self.components:
- transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
+ transformed = [(k, transform(*p)) for k, p in component]
yield " ".join(_svg_bezier(*transformed)) + " Z"
return '' % (
@@ -1844,7 +1981,7 @@ def components():
style,
)
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
@@ -1853,7 +1990,7 @@ def to_asy(self):
def components():
for component in self.components:
- transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
+ transformed = [(k, transform(*p)) for k, p in component]
yield "fill(%s--cycle, %s);" % ("".join(_asy_bezier(*transformed)), pen)
return "".join(components())
@@ -1864,7 +2001,7 @@ def extent(self):
for component in self.components:
for _, points in component:
for p in points:
- x, y = p.pos()
+ x, y = p
result.extend(
[(x - l, y - l), (x - l, y + l), (x + l, y - l), (x + l, y + l)]
)
@@ -1933,7 +2070,7 @@ def process_option(self, name, value):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
l = self.style.get_line_width(face_element=True)
if self.vertex_colors is None:
face_color = self.face_color
@@ -1947,19 +2084,23 @@ def to_svg(self, offset=None):
mesh = []
for index, line in enumerate(self.lines):
data = [
- [coords.pos(), color.to_js()]
- for coords, color in zip(line, self.vertex_colors[index])
+ [[float(c) for c in coords], color.to_js()]
+ for coords, color in zip(
+ transform(*line), self.vertex_colors[index]
+ )
]
mesh.append(data)
svg += '' % json.dumps(mesh)
for line in self.lines:
svg += '' % (
- " ".join("%f,%f" % coords.pos() for coords in line),
+ " ".join(
+ "%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)
+ ),
style,
)
return svg
- def to_asy(self):
+ def to_asy(self, transform):
l = self.style.get_line_width(face_element=True)
if self.vertex_colors is None:
face_color = self.face_color
@@ -1978,8 +2119,7 @@ def to_asy(self):
edges = []
for index, line in enumerate(self.lines):
paths.append(
- "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line])
- + "--cycle"
+ "--".join(["(%.5g,%.5g)" % c for c in transform(*line)]) + "--cycle"
)
# ignore opacity
@@ -1999,8 +2139,7 @@ def to_asy(self):
if pens and pens != "nullpen":
for line in self.lines:
path = (
- "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line])
- + "--cycle"
+ "--".join(["(%.5g,%.5g)" % c for c in transform(*line)]) + "--cycle"
)
asy += "filldraw(%s, %s);" % (path, pens)
return asy
@@ -2460,9 +2599,9 @@ def shrink_one_end(line, s):
while s > 0.0:
if len(line) < 2:
return []
- xy, length = setback(line[0].p, line[1].p, s)
+ xy, length = setback(line[0], line[1], s)
if xy is not None:
- line[0] = line[0].add(*xy)
+ line[0] = add_coords(line[0], xy)
else:
line = line[1:]
s -= length
@@ -2482,7 +2621,7 @@ def shrink(line, s1, s2):
# note that shrinking needs to happen in the Graphics[] coordinate space, whereas the
# subsequent position calculation needs to happen in pixel space.
- transformed_points = [xy.pos() for xy in shrink(line, *self.setback)]
+ transformed_points = shrink(line, *self.setback)
for s in polyline(transformed_points):
yield s
@@ -2490,31 +2629,40 @@ def shrink(line, s1, s2):
for s in self.curve.arrows(transformed_points, heads):
yield s
- def _custom_arrow(self, format, format_transform):
+ def _custom_arrow(self, format, transform):
def make(graphics):
- xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics(
- graphics, format, self.graphics.evaluation
- )
- boxw = xmax - xmin
- boxh = ymax - ymin
+ code = _extract_graphics(graphics, format, self.graphics.evaluation)
+
+ half_pi = pi / 2.0
def draw(px, py, vx, vy, t1, s):
t0 = t1
- cx = px + t0 * vx
- cy = py + t0 * vy
- transform = format_transform()
- transform.translate(cx, cy)
- transform.scale(-s / boxw * ex, -s / boxh * ey)
- transform.rotate(90 + degrees(atan2(vy, vx)))
- transform.translate(-ox, -oy)
- yield transform.apply(code)
+ tx = px + t0 * vx
+ ty = py + t0 * vy
+
+ r = half_pi + atan2(vy, vx)
+
+ s = -s
+
+ cos_r = cos(r)
+ sin_r = sin(r)
+
+ # see TranslationTransform[{tx,ty}].ScalingTransform[{s,s}].RotationTransform[r]
+ yield transform(
+ [
+ [s * cos_r, -s * sin_r, tx],
+ [s * sin_r, s * cos_r, ty],
+ [0, 0, 1],
+ ],
+ code,
+ )
return draw
return make
- def to_svg(self, offset=None):
+ def to_svg(self, transform):
width = self.style.get_line_width(face_element=False)
style = create_css(edge_color=self.edge_color, stroke_width=width)
polyline = self.curve.make_draw_svg(style)
@@ -2523,15 +2671,18 @@ def to_svg(self, offset=None):
def polygon(points):
yield '' % arrow_style
- extent = self.graphics.view_width or 0
+ def svg_transform(m, code):
+ return _Transform(m).to_svg(code)
+
+ extent = self.graphics.extent_width or 0
default_arrow = self._default_arrow(polygon)
- custom_arrow = self._custom_arrow("svg", _SVGTransform)
+ custom_arrow = self._custom_arrow("svg", svg_transform)
return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
- def to_asy(self):
+ def to_asy(self, transform):
width = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=width)
polyline = self.curve.make_draw_asy(pen)
@@ -2540,12 +2691,15 @@ def to_asy(self):
def polygon(points):
yield "filldraw("
- yield "--".join(["(%.5g,%5g)" % xy for xy in points])
+ yield "--".join(["(%.5g,%5g)" % xy for xy in transform(*points)])
yield "--cycle, % s);" % arrow_pen
- extent = self.graphics.view_width or 0
+ def asy_transform(m, code):
+ return _Transform(m).to_asy(code)
+
+ extent = self.graphics.extent_width or 0
default_arrow = self._default_arrow(polygon)
- custom_arrow = self._custom_arrow("asy", _ASYTransform)
+ custom_arrow = self._custom_arrow("asy", asy_transform)
return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
def extent(self):
@@ -2569,6 +2723,183 @@ def default_arrow(px, py, vx, vy, t1, s):
return list(self._draw(polyline, default_arrow, None, 0))
+class TransformationFunction(Builtin):
+ """
+ >> RotationTransform[Pi].TranslationTransform[{1, -1}]
+ = TransformationFunction[{{-1, 0, -1}, {0, -1, 1}, {0, 0, 1}}]
+
+ >> TranslationTransform[{1, -1}].RotationTransform[Pi]
+ = TransformationFunction[{{-1, 0, 1}, {0, -1, -1}, {0, 0, 1}}]
+ """
+
+ rules = {
+ "Dot[TransformationFunction[a_], TransformationFunction[b_]]": "TransformationFunction[a . b]",
+ "TransformationFunction[m_][v_]": "Take[m . Join[v, {0}], Length[v]]",
+ }
+
+
+class TranslationTransform(Builtin):
+ """
+
+ - 'TranslationTransform[v]'
+
- gives the translation by the vector $v$.
+
+
+ >> TranslationTransform[{1, 2}]
+ = TransformationFunction[{{1, 0, 1}, {0, 1, 2}, {0, 0, 1}}]
+ """
+
+ rules = {
+ "TranslationTransform[v_]": "TransformationFunction[IdentityMatrix[Length[v] + 1] + "
+ "(Join[ConstantArray[0, Length[v]], {#}]& /@ Join[v, {0}])]",
+ }
+
+
+class RotationTransform(Builtin):
+ rules = {
+ "RotationTransform[phi_]": "TransformationFunction[{{Cos[phi], -Sin[phi], 0}, {Sin[phi], Cos[phi], 0}, {0, 0, 1}}]",
+ "RotationTransform[phi_, p_]": "TranslationTransform[-p] . RotationTransform[phi] . TranslationTransform[p]",
+ }
+
+
+class ScalingTransform(Builtin):
+ rules = {
+ "ScalingTransform[v_]": "TransformationFunction[DiagonalMatrix[Join[v, {1}]]]",
+ "ScalingTransform[v_, p_]": "TranslationTransform[-p] . ScalingTransform[v] . TranslationTransform[p]",
+ }
+
+
+class Translate(Builtin):
+ """
+
+ - 'Translate[g, {x, y}]'
+
- translates an object by the specified amount.
+
- 'Translate[g, {{x1, y1}, {x2, y2}, ...}]'
+
- creates multiple instances of object translated by the specified amounts.
+
+
+ >> Graphics[{Circle[], Translate[Circle[], {1, 0}]}]
+ = -Graphics-
+ """
+
+ rules = {
+ "Translate[g_, v_?(Depth[#] > 2&)]": "GeometricTransformation[g, TranslationTransform /@ v]",
+ "Translate[g_, v_?(Depth[#] == 2&)]": "GeometricTransformation[g, TranslationTransform[v]]",
+ }
+
+
+class Rotate(Builtin):
+ """
+
+ - 'Rotate[g, phi]'
+
- rotates an object by the specified amount.
+
+
+ >> Graphics[Rotate[Rectangle[], Pi / 3]]
+ = -Graphics-
+
+ >> Graphics[{Rotate[Rectangle[{0, 0}, {0.2, 0.2}], 1.2, {0.1, 0.1}], Red, Disk[{0.1, 0.1}, 0.05]}]
+ = -Graphics-
+
+ >> Graphics[Table[Rotate[Scale[{RGBColor[i,1-i,1],Rectangle[],Black,Text["ABC",{0.5,0.5}]},1-i],Pi*i], {i,0,1,0.2}]]
+ = -Graphics-
+ """
+
+ rules = {
+ "Rotate[g_, phi_]": "GeometricTransformation[g, RotationTransform[phi]]",
+ "Rotate[g_, phi_, p_]": "GeometricTransformation[g, RotationTransform[phi, p]]",
+ }
+
+
+class Scale(Builtin):
+ """
+
+ - 'Scale[g, phi]'
+
- scales an object by the specified amount.
+
+
+ >> Graphics[Rotate[Rectangle[], Pi / 3]]
+ = -Graphics-
+
+ >> Graphics[{Scale[Rectangle[{0, 0}, {0.2, 0.2}], 3, {0.1, 0.1}], Red, Disk[{0.1, 0.1}, 0.05]}]
+ = -Graphics-
+ """
+
+ rules = {
+ "Scale[g_, s_?ListQ]": "GeometricTransformation[g, ScalingTransform[s]]",
+ "Scale[g_, s_]": "GeometricTransformation[g, ScalingTransform[{s, s}]]",
+ "Scale[g_, s_?ListQ, p_]": "GeometricTransformation[g, ScalingTransform[s, p]]",
+ "Scale[g_, s_, p_]": "GeometricTransformation[g, ScalingTransform[{s, s}, p]]",
+ }
+
+
+class GeometricTransformation(Builtin):
+ """
+
+ - 'GeometricTransformation[$g$, $tfm$]'
+
- transforms an object $g$ with the transformation $tfm$.
+
+ """
+
+ pass
+
+
+class GeometricTransformationBox(_GraphicsElement):
+ def init(self, graphics, style, contents, transform):
+ super(GeometricTransformationBox, self).init(graphics, None, style)
+ self.contents = contents
+ if transform.get_head_name() == "System`List":
+ functions = transform.leaves
+ else:
+ functions = [transform]
+ evaluation = graphics.evaluation
+ self.transforms = [
+ _Transform(Expression("N", f).evaluate(evaluation)) for f in functions
+ ]
+ self.precompute = graphics.precompute_transformations
+
+ def patch_transforms(self, transforms):
+ self.transforms = transforms
+
+ def extent(self):
+ def points():
+ for content in self.contents:
+ p = content.extent()
+ for transform in self.transforms:
+ for q in transform(*p):
+ yield q
+
+ return list(points())
+
+ def to_svg(self, transform0):
+ if self.precompute:
+
+ def instances():
+ for transform in self.transforms:
+ t = transform.combine(transform0)
+ for content in self.contents:
+ yield content.to_svg(t)
+
+ else:
+
+ def instances():
+ for content in self.contents:
+ content_svg = content.to_svg(transform0)
+ for transform in self.transforms:
+ yield transform.to_svg(content_svg)
+
+ return "".join(instances())
+
+ def to_asy(self, transform0):
+ def instances():
+ for content in self.contents:
+ content_asy = content.to_asy(transform0)
+ for transform in self.transforms:
+ yield transform.to_asy(content_asy)
+
+ return "".join(instances())
+
+
class InsetBox(_GraphicsElement):
def init(
self,
@@ -2579,6 +2910,8 @@ def init(
pos=None,
opos=(0, 0),
opacity=1.0,
+ font_size=None,
+ is_absolute=False,
):
super(InsetBox, self).init(graphics, item, style)
@@ -2587,58 +2920,177 @@ def init(
self.color, _ = style.get_style(_Color, face_element=False)
self.opacity = opacity
+ if font_size is not None:
+ self.font_size = FontSize(self.graphics, value=font_size)
+ else:
+ self.font_size, _ = self.style.get_style(FontSize, face_element=False)
+ if self.font_size is None:
+ self.font_size = FontSize(self.graphics, value=10.0)
+
if item is not None:
if len(item.leaves) not in (1, 2, 3):
raise BoxConstructError
content = item.leaves[0]
self.content = content.format(graphics.evaluation, "TraditionalForm")
if len(item.leaves) > 1:
- self.pos = Coords(graphics, item.leaves[1])
+ self.pos = expr_to_coords(item.leaves[1])
else:
- self.pos = Coords(graphics, pos=(0, 0))
+ self.pos = (0, 0)
if len(item.leaves) > 2:
- self.opos = coords(item.leaves[2])
+ self.opos = expr_to_coords(item.leaves[2])
else:
self.opos = (0, 0)
else:
self.content = content
self.pos = pos
self.opos = opos
- self.content_text = self.content.boxes_to_text(
- evaluation=self.graphics.evaluation
- )
+
+ self.is_absolute = is_absolute
+
+ try:
+ self._prepare_text_svg()
+ except WebEngineUnavailable as e:
+ self.svg = None
+
+ self.content_text = self.content.boxes_to_text(
+ evaluation=self.graphics.evaluation
+ )
+
+ if self.graphics.evaluation.output.warn_about_web_engine():
+ self.graphics.evaluation.message(
+ "General", "nowebeng", str(e), once=True
+ )
+ except Exception as e:
+ self.svg = None
+
+ self.graphics.evaluation.message("General", "nowebeng", str(e), once=True)
def extent(self):
- p = self.pos.pos()
- h = 25
- w = len(self.content_text) * 7 # rough approximation by numbers of characters
+ p = self.pos
+
+ if not self.svg:
+ h = 25
+ w = (
+ len(self.content_text) * 7
+ ) # rough approximation by numbers of characters
+ else:
+ _, w, h = self.svg
+ scale = self._text_svg_scale(h)
+ w *= scale
+ h *= scale
+
opos = self.opos
x = p[0] - w / 2.0 - opos[0] * w / 2.0
y = p[1] - h / 2.0 + opos[1] * h / 2.0
return [(x, y), (x + w, y + h)]
- def to_svg(self, offset=None):
- x, y = self.pos.pos()
- if offset:
- x = x + offset[0]
- y = y + offset[1]
+ def _prepare_text_svg(self):
+ self.graphics.evaluation.output.assume_web_engine()
+
+ content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation)
+
+ svg = self.graphics.evaluation.output.mathml_to_svg("" % content)
+
+ svg = svg.replace("style", "data-style", 1) # HACK
+
+ # we could parse the svg and edit it. using regexps here should be
+ # a lot faster though.
+
+ def extract_dimension(svg, name):
+ values = [0.0]
+
+ def replace(m):
+ value = m.group(1)
+ values.append(float(value))
+ return '%s="%s"' % (name, value)
+
+ svg = re.sub(name + r'="([0-9\.]+)ex"', replace, svg, 1)
+ return svg, values[-1]
+
+ svg, width = extract_dimension(svg, "width")
+ svg, height = extract_dimension(svg, "height")
- if hasattr(self.content, "to_svg"):
- content = self.content.to_svg(noheader=True, offset=(x, y))
- svg = "\n" + content + "\n"
+ self.svg = (svg, width, height)
+
+ def _text_svg_scale(self, height):
+ size = self.font_size.get_size()
+ return size / height
+
+ def _text_svg_xml(self, style, x, y, absolute):
+ svg, width, height = self.svg
+ svg = re.sub(r"