diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py
index f7e9df4477..efbf2c4cbf 100644
--- a/mathics/builtin/drawing/graphics3d.py
+++ b/mathics/builtin/drawing/graphics3d.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
-Graphics (3D)
+Three-Dimensional Graphics
"""
@@ -778,9 +778,6 @@ def __init__(self, content, evaluation, neg_y=False):
def extent(self, completely_visible_only=False):
return total_extent_3d([element.extent() for element in self.elements])
- def to_svg(self):
- return "\n".join(element.to_svg() for element in self.elements)
-
def to_asy(self):
return "\n".join([element.to_asy() for element in self.elements])
diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py
index fc0c9a0302..f5b0125923 100644
--- a/mathics/builtin/graphics.py
+++ b/mathics/builtin/graphics.py
@@ -7,7 +7,6 @@
from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp
-import json
import base64
from itertools import chain
@@ -33,6 +32,8 @@
system_symbols_dict,
from_python,
)
+from mathics.core.formatter import lookup_method
+
from mathics.builtin.drawing.colors import convert as convert_color
from mathics.core.numbers import machine_epsilon
@@ -323,7 +324,11 @@ def _extract_graphics(graphics, format, evaluation):
if format == "asy":
code = "\n".join(element.to_asy() for element in elements.elements)
elif format == "svg":
- code = elements.to_svg()
+ format_fn = lookup_method(elements, "svg")
+ if format_fn is not None:
+ code = format_fn(elements)
+ else:
+ code = elements.to_svg()
else:
raise NotImplementedError
@@ -1171,20 +1176,27 @@ class Disk(Builtin):
class Circle(Builtin):
"""
- - 'Circle[{$cx$, $cy$}, $r$]'
-
- draws a circle with center '($cx$, $cy$)' and radius $r$.
-
- 'Circle[{$cx$, $cy$}, {$rx$, $ry$}]'
-
- draws an ellipse.
-
- 'Circle[{$cx$, $cy$}]'
-
- chooses radius 1.
-
- 'Circle[]'
-
- chooses center '(0, 0)' and radius 1.
+
- 'Circle[{$cx$, $cy$}, $r$]'
+
- draws a circle with center '($cx$, $cy$)' and radius $r$.
+
+
- 'Circle[{$cx$, $cy$}, {$rx$, $ry$}]'
+
- draws an ellipse.
+
+
- 'Circle[{$cx$, $cy$}]'
+
- chooses radius 1.
+
+
- 'Circle[]'
+
- chooses center '(0, 0)' and radius 1.
>> Graphics[{Red, Circle[{0, 0}, {2, 1}]}]
= -Graphics-
>> Graphics[{Circle[], Disk[{0, 0}, {1, 1}, {0, 2.1}]}]
= -Graphics-
+
+ Target practice:
+ >> Graphics[Circle[], Axes-> True]
+ = -Graphics-
"""
rules = {"Circle[]": "Circle[{0, 0}]"}
@@ -1231,26 +1243,6 @@ def extent(self):
)
return result
- def to_svg(self, offset=None):
- l = self.style.get_line_width(face_element=True)
- x1, y1 = self.p1.pos()
- x2, y2 = self.p2.pos()
- 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,
- style,
- )
-
def to_asy(self):
l = self.style.get_line_width(face_element=True)
x1, y1 = self.p1.pos()
@@ -1302,21 +1294,6 @@ def extent(self):
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()
- rx -= x
- ry = 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,
- style,
- )
-
def to_asy(self):
x, y = self.c.pos()
rx, ry = self.r.pos()
@@ -1389,27 +1366,33 @@ def _arc_params(self):
return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc
- def to_svg(self, offset=None):
- if self.arc is None:
- return super(_ArcBox, self).to_svg(offset)
+ # FIXME: Why do we need this? If so,
+ # figure out how to put in svg.py
+ # --------------------------------
+ # def to_svg(self, offset=None):
+ # if self.arc is None:
+ # return super(_ArcBox, self).to_svg(offset)
- 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()
- def path(closed):
- if closed:
- yield "M %f,%f" % (x, y)
- yield "L %f,%f" % (sx, sy)
- else:
- yield "M %f,%f" % (sx, sy)
+ # format_fn = lookup_method(self, "svg")
+ # if format_fn is not None:
+ # return format_fn(self, offset)
+ # def path(closed):
+ # if closed:
+ # yield "M %f,%f" % (x, y)
+ # yield "L %f,%f" % (sx, sy)
+ # else:
+ # yield "M %f,%f" % (sx, sy)
- yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey)
+ # yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey)
- if closed:
- yield "Z"
+ # if closed:
+ # yield "Z"
- l = self.style.get_line_width(face_element=self.face_element)
- style = create_css(self.edge_color, self.face_color, stroke_width=l)
- return '' % (" ".join(path(self.face_element)), style)
+ # l = self.style.get_line_width(face_element=self.face_element)
+ # style = create_css(self.edge_color, self.face_color, stroke_width=l)
+ # return '' % (" ".join(path(self.face_element)), style)
def to_asy(self):
if self.arc is None:
@@ -1522,26 +1505,6 @@ def init(self, graphics, style, item=None):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
- 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()
-
- 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,
- style,
- )
- return svg
-
def to_asy(self):
pen = create_pens(face_color=self.face_color, is_face_element=False)
@@ -1586,17 +1549,6 @@ def init(self, graphics, style, item=None, lines=None):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
- 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:
- svg += '' % (
- " ".join(["%f,%f" % coords.pos() for coords in line]),
- style,
- )
- return svg
-
def to_asy(self):
l = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
@@ -1741,16 +1693,6 @@ def init(self, graphics, style, item, options):
raise BoxConstructError
self.spline_degree = spline_degree.get_int_value()
- def to_svg(self, offset=None):
- 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])))
- svg += '' % (s, style)
- return svg
-
def to_asy(self):
l = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
@@ -1828,22 +1770,6 @@ def parse_component(segments):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
- 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
- )
-
- def components():
- for component in self.components:
- transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
- yield " ".join(_svg_bezier(*transformed)) + " Z"
-
- return '' % (
- " ".join(components()),
- style,
- )
-
def to_asy(self):
l = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=l)
@@ -1933,32 +1859,6 @@ def process_option(self, name, value):
else:
raise BoxConstructError
- def to_svg(self, offset=None):
- l = self.style.get_line_width(face_element=True)
- if self.vertex_colors is None:
- face_color = self.face_color
- else:
- face_color = None
- style = create_css(
- edge_color=self.edge_color, face_color=face_color, stroke_width=l
- )
- svg = ""
- if self.vertex_colors is not 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])
- ]
- mesh.append(data)
- svg += '' % json.dumps(mesh)
- for line in self.lines:
- svg += '' % (
- " ".join("%f,%f" % coords.pos() for coords in line),
- style,
- )
- return svg
-
def to_asy(self):
l = self.style.get_line_width(face_element=True)
if self.vertex_colors is None:
@@ -2514,23 +2414,6 @@ def draw(px, py, vx, vy, t1, s):
return make
- def to_svg(self, offset=None):
- 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)
-
- arrow_style = create_css(face_color=self.edge_color, stroke_width=width)
-
- def polygon(points):
- yield '' % arrow_style
-
- extent = self.graphics.view_width or 0
- default_arrow = self._default_arrow(polygon)
- custom_arrow = self._custom_arrow("svg", _SVGTransform)
- return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
-
def to_asy(self):
width = self.style.get_line_width(face_element=False)
pen = create_pens(edge_color=self.edge_color, stroke_width=width)
@@ -2617,36 +2500,6 @@ def extent(self):
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]
-
- if hasattr(self.content, "to_svg"):
- content = self.content.to_svg(noheader=True, offset=(x, y))
- svg = "\n" + content + "\n"
- else:
- css_style = create_css(
- font_color=self.color,
- edge_color=self.color,
- face_color=self.color,
- opacity=self.opacity,
- )
- text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"'
- # FIXME: don't hard code text_style_opts, but allow these to be adjustable.
- text_style_opts = "text-anchor:middle; dominant-baseline:middle;"
- content = self.content.boxes_to_text(evaluation=self.graphics.evaluation)
- svg = f'{content}'
-
- # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation)
- # style = create_css(font_color=self.color)
- # svg = (
- # ''
- # "")
-
- return svg
-
def to_asy(self):
x, y = self.pos.pos()
content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation)
@@ -2960,9 +2813,6 @@ def extent(self, completely_visible_only=False):
ymax *= 2
return xmin, xmax, ymin, ymax
- def to_svg(self, offset=None):
- return "\n".join(element.to_svg(offset) for element in self.elements)
-
def to_asy(self):
return "\n".join(element.to_asy() for element in self.elements)
@@ -3250,54 +3100,7 @@ def boxes_to_tex(self, leaves=None, **options):
return tex
- def to_svg(self, leaves=None, **options):
- if not leaves:
- leaves = self._leaves
-
- data = options.get("data", None)
- if data:
- elements, xmin, xmax, ymin, ymax, w, h, width, height = data
- else:
- elements, calc_dimensions = self._prepare_elements(
- leaves, options, neg_y=True
- )
- xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
-
- elements.view_width = w
-
- svg = elements.to_svg(offset=options.get("offset", None))
-
- if self.background_color is not None:
- svg = '%s' % (
- xmin,
- ymin,
- w,
- h,
- self.background_color.to_css()[0],
- svg,
- )
-
- xmin -= 1
- ymin -= 1
- w += 2
- h += 2
-
- if options.get("noheader", False):
- return svg
- svg_xml = """
-
- """ % (
- " ".join("%f" % t for t in (xmin, ymin, w, h)),
- svg,
- )
- return svg_xml # , width, height
-
- def boxes_to_mathml(self, leaves=None, **options):
+ def boxes_to_mathml(self, leaves=None, **options) -> str:
if not leaves:
leaves = self._leaves
@@ -3305,7 +3108,11 @@ def boxes_to_mathml(self, leaves=None, **options):
xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
data = (elements, xmin, xmax, ymin, ymax, w, h, width, height)
- svg_xml = self.to_svg(leaves, data=data, **options)
+ # FIXME: SVG is the only thing we can convert MathML into.
+ # Handle other graphics formats.
+ format_fn = lookup_method(self, "svg")
+ svg_main = format_fn(self, leaves, data=data, **options)
+
# mglyph, which is what we have been using, is bad because MathML standard changed.
# metext does not work because the way in which we produce the svg images is also based on this outdated mglyph behaviour.
# template = '
'
@@ -3318,9 +3125,10 @@ def boxes_to_mathml(self, leaves=None, **options):
# int(height),
int(width),
int(height),
- base64.b64encode(svg_xml.encode("utf8")).decode("utf8"),
+ base64.b64encode(svg_main.encode("utf8")).decode("utf8"),
)
+ # FIXME: this isn't always properly align with overlaid SVG plots
def axis_ticks(self, xmin, xmax):
def round_to_zero(value):
if value == 0:
diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py
index 9930eacb70..adf4b2ce6d 100644
--- a/mathics/builtin/inout.py
+++ b/mathics/builtin/inout.py
@@ -2063,25 +2063,25 @@ def apply_mathml(self, expr, evaluation) -> Expression:
boxes = MakeBoxes(expr).evaluate(evaluation)
try:
- xml = boxes.boxes_to_mathml(evaluation=evaluation)
+ mathml = boxes.boxes_to_mathml(evaluation=evaluation)
except BoxError:
evaluation.message(
"General",
"notboxes",
Expression("FullForm", boxes).evaluate(evaluation),
)
- xml = ""
- is_a_picture = xml[:6] == " Callable:
+ """
+ Find a conversion method for `format` in self's class method resolution order.
+ """
+ for cls in inspect.getmro(type(self)):
+ format_fn = format2fn.get((format, cls), None)
+ if format_fn is not None:
+ # print(f"format function: {format_fn.__name__} for {type(self).__name__}")
+ return format_fn
+ raise RuntimeError(
+ f"Can't find formatter {format_fn.__name__} for {type(self).__name__}"
+ )
+
+
+def add_conversion_fn(cls) -> None:
+ """Add to `format2fn` a mapping from a conversion type and builtin-class
+ to a conversion method.
+
+ The conversion type is determined form the module name.
+ For example, in module mathics.formatter.svg the conversion
+ type is "svg".
+
+ The conversion method is assumed to be a method in the caller's
+ module, and is derived from lowercasing `cls`.
+
+ For example function arrowbox in module mathics.formatter.svg would be
+ the SVG conversion routine for class ArrowBox.
+
+ We use frame introspection to get all of this done.
+ """
+ fr = inspect.currentframe().f_back
+ module_dict = fr.f_globals
+
+ # The last part of the module name is expected to be the conversion routine.
+ conversion_type = module_dict["__name__"].split(".")[-1]
+
+ # Derive the conversion function from the passed-in class argument.
+ module_fn_name = cls.__name__.lower()
+
+ # Finally register the mapping: (Builtin-class, conversion name) -> conversion_function.
+ format2fn[(conversion_type, cls)] = module_dict[module_fn_name]
diff --git a/mathics/formatter/__init__.py b/mathics/formatter/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mathics/formatter/svg.py b/mathics/formatter/svg.py
new file mode 100644
index 0000000000..395c2cae74
--- /dev/null
+++ b/mathics/formatter/svg.py
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+import json
+
+"""
+Format a Mathics object as an SVG string
+"""
+
+from mathics.builtin.drawing.graphics3d import Graphics3DElements
+from mathics.builtin.graphics import (
+ ArrowBox,
+ BezierCurveBox,
+ FilledCurveBox,
+ GraphicsBox,
+ GraphicsElements,
+ InsetBox,
+ LineBox,
+ PointBox,
+ PointSize,
+ PolygonBox,
+ RectangleBox,
+ _RoundBox,
+ _svg_bezier,
+ _SVGTransform,
+)
+
+from mathics.core.formatter import lookup_method, add_conversion_fn
+
+
+def create_css(
+ edge_color=None, face_color=None, stroke_width=None, font_color=None, opacity=1.0
+) -> str:
+ """
+ Return a string suitable for CSS inclusion setting the various parameters passed.
+ """
+ css = []
+ if edge_color is not None:
+ color, stroke_opacity = edge_color.to_css()
+ css.append("stroke: %s" % color)
+ css.append("stroke-opacity: %s" % stroke_opacity)
+ else:
+ css.append("stroke: none")
+ if stroke_width is not None:
+ css.append("stroke-width: %fpx" % stroke_width)
+ if face_color is not None:
+ color, fill_opacity = face_color.to_css()
+ css.append("fill: %s" % color)
+ css.append("fill-opacity: %s" % fill_opacity)
+ else:
+ css.append("fill: none")
+ if font_color is not None:
+ color, _ = font_color.to_css()
+ css.append("color: %s" % color)
+ css.append("opacity: %s" % opacity)
+ return "; ".join(css)
+
+
+def arrowbox(self, offset=None):
+ 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)
+
+ arrow_style = create_css(face_color=self.edge_color, stroke_width=width)
+
+ def polygon(points):
+ yield '' % arrow_style
+
+ extent = self.graphics.view_width or 0
+ default_arrow = self._default_arrow(polygon)
+ custom_arrow = self._custom_arrow("svg", _SVGTransform)
+ return "".join(self._draw(polyline, default_arrow, custom_arrow, extent))
+
+
+add_conversion_fn(ArrowBox)
+
+
+def beziercurvebox(self, offset=None):
+ 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])))
+ svg += '' % (s, style)
+ # print("XXX bezier", svg)
+ return svg
+
+
+add_conversion_fn(BezierCurveBox)
+
+
+def filledcurvebox(self, offset=None):
+ 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
+ )
+
+ def components():
+ for component in self.components:
+ transformed = [(k, [xy.pos() for xy in p]) for k, p in component]
+ yield " ".join(_svg_bezier(*transformed)) + " Z"
+
+ # print("XXX FilledcurveBox", components)
+ return '' % (
+ " ".join(components()),
+ style,
+ )
+
+
+add_conversion_fn(FilledCurveBox)
+
+# FIXME figure out how we can add this.
+def graphicsbox(self, leaves=None, **options) -> str:
+ if not leaves:
+ leaves = self._leaves
+
+ data = options.get("data", None)
+ if data:
+ elements, xmin, xmax, ymin, ymax, w, h, width, height = data
+ else:
+ elements, calc_dimensions = self._prepare_elements(
+ leaves, options, neg_y=True
+ )
+ xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions()
+
+ elements.view_width = w
+
+ format_fn = lookup_method(elements, "svg")
+ if format_fn is not None:
+ svg_body = format_fn(elements, offset=options.get("offset", None))
+ else:
+ svg_body = elements.to_svg(offset=options.get("offset", None))
+
+ if self.background_color is not None:
+ # Wrap svg_elements in a rectangle
+ svg_body = '%s' % (
+ xmin,
+ ymin,
+ w,
+ h,
+ self.background_color.to_css()[0],
+ svg_body,
+ )
+
+ xmin -= 1
+ ymin -= 1
+ w += 2
+ h += 2
+
+ if options.get("noheader", False):
+ return svg_body
+ svg_main = """
+
+ """ % (
+ " ".join("%f" % t for t in (xmin, ymin, w, h)),
+ svg_body,
+ )
+ return svg_main # , width, height
+
+
+add_conversion_fn(GraphicsBox)
+
+
+def graphicselements(self, offset=None):
+ result = []
+ for element in self.elements:
+ format_fn = lookup_method(element, "svg")
+ if format_fn is None:
+ result.append(element.to_svg(offset))
+ else:
+ result.append(format_fn(element, offset))
+
+ return "\n".join(result)
+
+
+add_conversion_fn(GraphicsElements)
+graphics3delements = graphicselements
+
+add_conversion_fn(Graphics3DElements)
+
+
+def insetbox(self, offset=None):
+ x, y = self.pos.pos()
+ if offset:
+ x = x + offset[0]
+ y = y + offset[1]
+
+ if hasattr(self.content, "to_svg"):
+ content = self.content.to_svg(noheader=True, offset=(x, y))
+ svg = "\n" + content + "\n"
+ else:
+ css_style = create_css(
+ font_color=self.color,
+ edge_color=self.color,
+ face_color=self.color,
+ opacity=self.opacity,
+ )
+ text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"'
+ # FIXME: don't hard code text_style_opts, but allow these to be adjustable.
+ text_style_opts = "text-anchor:middle; dominant-baseline:middle;"
+ content = self.content.boxes_to_text(evaluation=self.graphics.evaluation)
+ svg = f'{content}'
+
+ # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation)
+ # style = create_css(font_color=self.color)
+ # svg = (
+ # ''
+ # "")
+
+ return svg
+
+
+add_conversion_fn(InsetBox)
+
+
+def linebox(self, offset=None):
+ 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:
+ svg += '' % (
+ " ".join(["%f,%f" % coords.pos() for coords in line]),
+ style,
+ )
+ # print("XXX linebox", svg)
+ return svg
+
+
+add_conversion_fn(LineBox)
+
+
+def pointbox(self, offset=None):
+ 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()
+
+ 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,
+ style,
+ )
+ # print("XXX PointBox", svg)
+ return svg
+
+
+add_conversion_fn(PointBox)
+
+
+def polygonbox(self, offset=None):
+ l = self.style.get_line_width(face_element=True)
+ if self.vertex_colors is None:
+ face_color = self.face_color
+ else:
+ face_color = None
+ style = create_css(
+ edge_color=self.edge_color, face_color=face_color, stroke_width=l
+ )
+ svg = ""
+ if self.vertex_colors is not 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])
+ ]
+ mesh.append(data)
+ svg += '' % json.dumps(mesh)
+ for line in self.lines:
+ svg += '' % (
+ " ".join("%f,%f" % coords.pos() for coords in line),
+ style,
+ )
+ # print("XXX PolygonBox", svg)
+ return svg
+
+
+add_conversion_fn(PolygonBox)
+
+
+def rectanglebox(self, offset=None):
+ l = self.style.get_line_width(face_element=True)
+ x1, y1 = self.p1.pos()
+ x2, y2 = self.p2.pos()
+ 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,
+ style,
+ )
+ "\n".join(element.to_svg() for element in self.elements)
+
+
+add_conversion_fn(RectangleBox)
+
+
+def _roundbox(self, offset=None):
+ x, y = self.c.pos()
+ rx, ry = self.r.pos()
+ rx -= x
+ ry = 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,
+ style,
+ )
+
+
+add_conversion_fn(_RoundBox)
diff --git a/setup.py b/setup.py
index 6dfc62621e..bd5c73e12b 100644
--- a/setup.py
+++ b/setup.py
@@ -134,6 +134,7 @@ def subdirs(root, file="*.*", depth=10):
"mathics.builtin.pympler",
"mathics.builtin.specialfns",
"mathics.doc",
+ "mathics.formatter",
],
install_requires=INSTALL_REQUIRES,
dependency_links=DEPENDENCY_LINKS,
diff --git a/test/test_formatter.py b/test/test_formatter.py
new file mode 100644
index 0000000000..998141a9b0
--- /dev/null
+++ b/test/test_formatter.py
@@ -0,0 +1,86 @@
+import re
+from mathics.core.expression import Symbol, Integer0, Expression
+from mathics.core.evaluation import Evaluation
+from mathics.session import MathicsSession
+from mathics.builtin.inout import MakeBoxes
+from mathics.core.formatter import lookup_method
+
+session = MathicsSession(add_builtin=True, catch_interrupt=False)
+evaluation = Evaluation(session.definitions)
+
+GraphicsSymbol = Symbol("Graphics")
+ListSymbol = Symbol("List")
+
+svg_wrapper_pat = r"""^\s*
+\s*', inner_svg
+ )
+ assert matches
+ assert matches.group(1) == matches.group(2) == matches.group(3)
+
+def test_svg_point():
+ expression = Expression(
+ GraphicsSymbol,
+ Expression("Point", Expression(ListSymbol, Integer0, Integer0)),
+ )
+
+ svg = get_svg(expression)
+ inner_svg = extract_svg_body(svg)
+
+ # Circles are implemented as ellipses with equal major and minor axes.
+ # Check for that.
+ print(inner_svg)
+ matches = re.match(
+ r'^', inner_svg
+ )
+ assert matches
+ assert matches.group(1) == matches.group(2)
+
+
+if __name__ == "__main__":
+ test_svg_point()