From 90aac8ca6045578241aca4bc8e052c7f64aeab4b Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 30 Aug 2016 16:10:32 +0200 Subject: [PATCH 01/48] basic LayoutEngine working --- mathics/builtin/graphics.py | 174 ++++++++++++++++++++++++++---------- mathics/builtin/inout.py | 12 ++- mathics/core/evaluation.py | 25 ++++++ mathics/layout/__init__.py | 2 + mathics/layout/client.py | 95 ++++++++++++++++++++ mathics/layout/server.js | 44 +++++++++ mathics/server.py | 8 ++ mathics/settings.py | 4 + mathics/web/views.py | 4 +- setup.py | 4 +- 10 files changed, 321 insertions(+), 51 deletions(-) create mode 100644 mathics/layout/__init__.py create mode 100644 mathics/layout/client.py create mode 100644 mathics/layout/server.js diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 76c259b843..c752279dfe 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -7,6 +7,7 @@ from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp +import re import json import base64 from itertools import chain @@ -172,48 +173,48 @@ def _euclidean_distance(a, b): def _component_distance(a, b, i): return abs(a[i] - b[i]) - + def _cie2000_distance(lab1, lab2): #reference: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 e = machine_epsilon kL = kC = kH = 1 #common values - + L1, L2 = lab1[0], lab2[0] a1, a2 = lab1[1], lab2[1] b1, b2 = lab1[2], lab2[2] - + dL = L2 - L1 Lm = (L1 + L2)/2 C1 = sqrt(a1**2 + b1**2) C2 = sqrt(a2**2 + b2**2) Cm = (C1 + C2)/2; - + a1 = a1 * (1 + (1 - sqrt(Cm**7/(Cm**7 + 25**7)))/2) a2 = a2 * (1 + (1 - sqrt(Cm**7/(Cm**7 + 25**7)))/2) - + C1 = sqrt(a1**2 + b1**2) C2 = sqrt(a2**2 + b2**2) Cm = (C1 + C2)/2 dC = C2 - C1 - + h1 = (180 * atan2(b1, a1 + e))/pi % 360 h2 = (180 * atan2(b2, a2 + e))/pi % 360 if abs(h2 - h1) <= 180: - dh = h2 - h1 + dh = h2 - h1 elif abs(h2 - h1) > 180 and h2 <= h1: dh = h2 - h1 + 360 elif abs(h2 - h1) > 180 and h2 > h1: dh = h2 - h1 - 360 - + dH = 2*sqrt(C1*C2)*sin(radians(dh)/2) - + Hm = (h1 + h2)/2 if abs(h2 - h1) <= 180 else (h1 + h2 + 360)/2 T = 1 - 0.17*cos(radians(Hm - 30)) + 0.24*cos(radians(2*Hm)) + 0.32*cos(radians(3*Hm + 6)) - 0.2*cos(radians(4*Hm - 63)) - + SL = 1 + (0.015*(Lm - 50)**2)/sqrt(20 + (Lm - 50)**2) SC = 1 + 0.045*Cm SH = 1 + 0.015*Cm*T - + rT = -2 * sqrt(Cm**7/(Cm**7 + 25**7))*sin(radians(60*exp(-((Hm - 275)**2 / 25**2)))) return sqrt((dL/(SL*kL))**2 + (dC/(SC*kC))**2 + (dH/(SH*kH))**2 + rT*(dC/(SC*kC))*(dH/(SH*kH))) @@ -223,19 +224,19 @@ def _CMC_distance(lab1, lab2, l, c): L1, L2 = lab1[0], lab2[0] a1, a2 = lab1[1], lab2[1] b1, b2 = lab1[2], lab2[2] - + dL, da, db = L2-L1, a2-a1, b2-b1 e = machine_epsilon - + C1 = sqrt(a1**2 + b1**2); C2 = sqrt(a2**2 + b2**2); - + h1 = (180 * atan2(b1, a1 + e))/pi % 360; dC = C2 - C1; dH2 = da**2 + db**2 - dC**2; F = C1**2/sqrt(C1**4 + 1900); T = 0.56 + abs(0.2*cos(radians(h1 + 168))) if (164 <= h1 and h1 <= 345) else 0.36 + abs(0.4*cos(radians(h1 + 35))); - + SL = 0.511 if L1 < 16 else (0.040975*L1)/(1 + 0.01765*L1); SC = (0.0638*C1)/(1 + 0.0131*C1) + 0.638; SH = SC*(F*T + 1 - F); @@ -739,7 +740,7 @@ class ColorDistance(Builtin): = 0.557976 #> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)] = 0.542917 - + """ options = { @@ -750,17 +751,17 @@ class ColorDistance(Builtin): 'invdist': '`1` is not Automatic or a valid distance specification.', 'invarg': '`1` and `2` should be two colors or a color and a lists of colors or ' + 'two lists of colors of the same length.' - + } - - # the docs say LABColor's colorspace corresponds to the CIE 1976 L^* a^* b^* color space + + # the docs say LABColor's colorspace corresponds to the CIE 1976 L^* a^* b^* color space # with {l,a,b}={L^*,a^*,b^*}/100. Corrections factors are put accordingly. - + _distances = { "CIE76": lambda c1, c2: _euclidean_distance(c1.to_color_space('LAB')[:3], c2.to_color_space('LAB')[:3]), "CIE94": lambda c1, c2: _euclidean_distance(c1.to_color_space('LCH')[:3], c2.to_color_space('LCH')[:3]), "CIE2000": lambda c1, c2: _cie2000_distance(100*c1.to_color_space('LAB')[:3], 100*c2.to_color_space('LAB')[:3])/100, - "CIEDE2000": lambda c1, c2: _cie2000_distance(100*c1.to_color_space('LAB')[:3], 100*c2.to_color_space('LAB')[:3])/100, + "CIEDE2000": lambda c1, c2: _cie2000_distance(100*c1.to_color_space('LAB')[:3], 100*c2.to_color_space('LAB')[:3])/100, "DeltaL": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 0), "DeltaC": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 1), "DeltaH": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 2), @@ -792,7 +793,7 @@ def apply(self, c1, c2, evaluation, options): 100*c2.to_color_space('LAB')[:3], 2, 1)/100 elif distance_function.leaves[1].get_string_value() == 'Perceptibility': compute = ColorDistance._distances.get("CMC") - + elif distance_function.leaves[1].has_form('List', 2): if (isinstance(distance_function.leaves[1].leaves[0], Integer) and isinstance(distance_function.leaves[1].leaves[1], Integer)): @@ -2319,13 +2320,15 @@ def default_arrow(px, py, vx, vy, t1, s): class InsetBox(_GraphicsElement): def init(self, graphics, style, item=None, content=None, pos=None, - opos=(0, 0)): + opos=(0, 0), font_size=None): super(InsetBox, self).init(graphics, item, style) self.color = self.style.get_option('System`FontColor') if self.color is None: self.color, _ = style.get_style(_Color, face_element=False) + self.font_size = font_size + if item is not None: if len(item.leaves) not in (1, 2, 3): raise BoxConstructError @@ -2344,29 +2347,101 @@ def init(self, graphics, style, item=None, content=None, pos=None, self.content = content self.pos = pos self.opos = opos - self.content_text = self.content.boxes_to_text( - evaluation=self.graphics.evaluation) + + if self.graphics.evaluation.output.svgify(): + self._prepare_text_svg() + else: + self.svg = None + + self.content_text = self.content.boxes_to_text( + evaluation=self.graphics.evaluation) def extent(self): p = self.pos.pos() - h = 25 - w = len(self.content_text) * \ - 7 # rough approximation by numbers of characters + + 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() + 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 _prepare_text_svg(self): + content = self.content.boxes_to_xml( + evaluation=self.graphics.evaluation) + + svg = self.graphics.evaluation.output.mathml_to_svg( + '%s' % content) + + # we could parse the svg and edit it. using regexps here should be + # a lot faster though. + + def extract_dimension(svg, name): + values = [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') + + self.svg = (svg, width, height) + + def _text_svg_scale(self): + svg, width, height = self.svg + + x, y = self.pos.pos() + x2, y2 = self.pos.add(width, height).pos() + target_height = abs(y2 - y) + + if self.font_size is None: + font_size = 0.5 + return font_size * target_height / height + else: + return self.font_size / height # absolute coords + + def _text_svg_xml(self, style, x, y): + svg, width, height = self.svg + svg = re.sub(r'%s' % ( + x, + y, + scale, + -width / 2 - ox * width / 2, + -height / 2 + oy * height / 2, + svg) + def to_svg(self): + evaluation = self.graphics.evaluation x, y = self.pos.pos() content = self.content.boxes_to_xml( - evaluation=self.graphics.evaluation) + evaluation=evaluation) style = create_css(font_color=self.color) - svg = ( - '' - '%s') % ( - x, y, self.opos[0], self.opos[1], style, content) - return svg + + if not self.svg: + return ( + '' + '%s') % ( + x, y, self.opos[0], self.opos[1], style, content) + else: + return self._text_svg_xml(style, x, y) def to_asy(self): x, y = self.pos.pos() @@ -2932,19 +3007,23 @@ def boxes_to_xml(self, leaves, **options): w += 2 h += 2 - svg_xml = ''' - - %s - - ''' % (' '.join('%f' % t for t in (xmin, ymin, w, h)), svg) + svgify = options['evaluation'].output.svgify() + + if not svgify: + params = '' + else: + params = 'width="%dpx" height="%dpx"' % (width, height) + + svg_xml = '%s' % (' '.join('%f' % t for t in (xmin, ymin, w, h)), params, svg) - return '' % ( - int(width), - int(height), - base64.b64encode(svg_xml.encode('utf8')).decode('utf8')) + if not svgify: + return '' % ( + int(width), + int(height), + base64.b64encode(svg_xml.encode('utf8')).decode('utf8')) + else: + return svg_xml def axis_ticks(self, xmin, xmax): def round_to_zero(value): @@ -3064,6 +3143,7 @@ def add_element(element): ticks_lines = [] tick_label_style = ticks_style[index].clone() tick_label_style.extend(label_style) + font_size = tick_large_size * 3. for x in ticks: ticks_lines.append([Coords(elements, pos=p_origin(x)), Coords(elements, pos=p_origin(x), @@ -3078,7 +3158,7 @@ def add_element(element): elements, tick_label_style, content=content, pos=Coords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1))) + d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) for x in ticks_small: pos = p_origin(x) ticks_lines.append([Coords(elements, pos=pos), diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 0e91dc5c3e..8fb5816a97 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1897,8 +1897,16 @@ def apply_mathml(self, expr, evaluation) -> Expression: xml = '' # mathml = '%s' % xml # #convert_box(boxes) - mathml = '%s' % xml # convert_box(boxes) - return Expression('RowBox', Expression('List', String(mathml))) + + if not evaluation.output.svgify(): + result = '%s' % xml + else: + if xml.startswith('%s' % xml) + + return Expression('RowBox', Expression('List', String(result))) class TeXForm(Builtin): diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index f99ee8a636..57800c167e 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -177,6 +177,15 @@ def get_data(self): class Output(object): + def __init__(self, layout_engine=None): + self.layout_engine = layout_engine + + def svgify(self): + # True if the MathML output should be instantly rendered into SVG + # in the backend, i.e. the browser will only see SVG. False for the + # classic mode, i.e. Mathics gives tags to the browser. + return False + def max_stored_size(self, settings) -> int: return settings.MAX_STORED_SIZE @@ -189,6 +198,22 @@ def clear(self, wait): def display(self, data, metadata): raise NotImplementedError + def mathml_to_svg(self, mathml): + svg = None + if self.layout_engine: + svg = self.layout_engine.mathml_to_svg(mathml) + if svg is None: + raise RuntimeError("LayoutEngine is unavailable") + return svg + + def rasterize(self, svg): + png = None + if self.layout_engine: + png = self.layout_engine.rasterize(svg) + if png is None: + raise RuntimeError("LayoutEngine is unavailable") + return png + class Evaluation(object): def __init__(self, definitions=None, diff --git a/mathics/layout/__init__.py b/mathics/layout/__init__.py new file mode 100644 index 0000000000..faa18be5bb --- /dev/null +++ b/mathics/layout/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- diff --git a/mathics/layout/client.py b/mathics/layout/client.py new file mode 100644 index 0000000000..7131c8c286 --- /dev/null +++ b/mathics/layout/client.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# In order to use LayoutEngine, you need the "zerorpc" in your Python installation, and nodejs in your path. +# you may use "NODE" in settings.py to specify a custom node binary, and you may use NODE_PATH in settings.py +# to specify a custom node_modules path (that has the necessary node modules mathjax-node, zerorpc, ...). +# +# The zerorpc package from https://github.com/0rpc/zerorpc-python. needs to be installed manually using +# "/your/python setup.py install". For Python 3, you need to use the zerorpc 3.4 branch. + +# Your installation of nodejs with the following packages: mathjax-node zerorpc svg2png (install them using +# npm). + +# Some tips for installing nodejs and zmq on OS X: +# see https://gist.github.com/DanHerbert/9520689 +# https://github.com/JustinTulloss/zeromq.node/issues/283 +# brew install zmq && npm install zmq +# export NODE_PATH=/your/path/to/homebrew/bin/node_modules:$NODE_PATH + +import subprocess +from subprocess import Popen +import os + +from mathics import settings + +try: + import zerorpc + supported = True +except ImportError: + supported = False + + +def warn(s): + print(s) + + +class LayoutEngine(object): + def __init__(self): + if not supported: + self.process = None + warn('Web layout engine is disabled as zerorpc is not installed.') + else: + try: + popen_env = os.environ.copy() + if settings.NODE_PATH: + popen_env["NODE_PATH"] = settings.NODE_PATH + + base = os.path.dirname(os.path.realpath(__file__)) + + self.process = Popen( + [settings.NODE, base + "/server.js"], + stdout=subprocess.PIPE, + env=popen_env) + + status = self.process.stdout.readline().decode('utf8').strip() + if status != 'OK': + error = '' + while True: + line = self.process.stdout.readline().decode('utf8') + if not line: + break + error += ' ' + line + warn('Node.js failed to start web layout engine:') + warn(error) + warn('Check necessary node.js modules and that NODE_PATH is set correctly.') + self.process.terminate() + self.process = None + except OSError as e: + warn('Failed to start web layout engine:') + warn(str(e)) + self.process = None + + if self.process is None: + self.client = None + else: + try: + self.client = zerorpc.Client() + self.client.connect("tcp://127.0.0.1:4241") + except Exception as e: + self.client = None + warn('node.js failed to start web layout engine:') + warn(str(e)) + warn('Probably you are missing node.js modules.') + + def mathml_to_svg(self, mathml): + if self.client: + return self.client.mathml_to_svg(mathml) + + def rasterize(self, svg): + if self.client: + return self.client.rasterize(svg) + + def terminate(self): + if self.process: + self.process.terminate() diff --git a/mathics/layout/server.js b/mathics/layout/server.js new file mode 100644 index 0000000000..94bdc12d58 --- /dev/null +++ b/mathics/layout/server.js @@ -0,0 +1,44 @@ +// to install: npm install mathjax-node zerorpc svg2png + +try { + var zerorpc = require("zerorpc"); + + var mathjax = require("mathjax-node/lib/mj-single.js"); + mathjax.config({ + MathJax: { + // traditional MathJax configuration + } + }); + mathjax.start(); + + var svg2png = require("svg2png"); + + var server = new zerorpc.Server({ + mathml_to_svg: function(mathml, reply) { + mathjax.typeset({ + math: mathml, + format: "MathML", + svg: true, + }, function (data) { + if (!data.errors) { + reply(null, data.svg); + } + }); + }, + rasterize: function(svg, reply) { + svg2png(Buffer.from(svg, 'utf8'), { + width: 300, + height: 400 + }) + .then(buffer => reply(null, buffer)) + .catch(e => console.error(e)); + } + }); + + console.log('OK') + + server.bind("tcp://0.0.0.0:4241"); +} catch (ex) { + console.log('FAIL') + console.log(ex) +} diff --git a/mathics/server.py b/mathics/server.py index 48c949e2f0..903a46e51a 100644 --- a/mathics/server.py +++ b/mathics/server.py @@ -12,6 +12,9 @@ import mathics from mathics import server_version_string, license_string from mathics import settings as mathics_settings # Prevents UnboundLocalError +from mathics.layout.client import LayoutEngine + +layout_engine = None def check_database(): @@ -85,6 +88,9 @@ def launch_app(args): else: addr = '127.0.0.1' + global layout_engine + layout_engine = LayoutEngine() + try: from django.core.servers.basehttp import ( run, get_internal_wsgi_application) @@ -102,10 +108,12 @@ def launch_app(args): except KeyError: error_text = str(e) sys.stderr.write("Error: %s" % error_text + '\n') + layout_engine.terminate() # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: print("\nGoodbye!\n") + layout_engine.terminate() sys.exit(0) diff --git a/mathics/settings.py b/mathics/settings.py index 3720b9db0f..5904065bf3 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -11,6 +11,10 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG +# node.js based layout engine +NODE = 'node' # path to node binary; default 'node' assumes it is in PATH +NODE_PATH = None # overrides NODE_PATH environment variable if not None + # set only to True in DEBUG mode DEBUG_MAIL = True PROPAGATE_EXCEPTIONS = True diff --git a/mathics/web/views.py b/mathics/web/views.py index 118d43a29d..335e762a14 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -97,9 +97,11 @@ def query(request): ) query_log.save() + from mathics.server import layout_engine + user_definitions = request.session.get('definitions') definitions.set_user_definitions(user_definitions) - evaluation = Evaluation(definitions, format='xml', output=WebOutput()) + evaluation = Evaluation(definitions, format='xml', output=WebOutput(layout_engine)) feeder = MultiLineFeeder(input, '') results = [] try: diff --git a/setup.py b/setup.py index 4ce6a13cfe..22e4bc5b29 100644 --- a/setup.py +++ b/setup.py @@ -159,7 +159,8 @@ def run(self): 'mathics.builtin', 'mathics.builtin.pymimesniffer', 'mathics.builtin.numpy_utils', 'mathics.builtin.pympler', 'mathics.builtin.compile', 'mathics.doc', - 'mathics.web', 'mathics.web.templatetags', 'mathics.web.migrations' + 'mathics.web', 'mathics.web.templatetags', 'mathics.web.migrations', + 'mathics.layout', ], install_requires=INSTALL_REQUIRES, @@ -179,6 +180,7 @@ def run(self): 'media/js/three/Detector.js', 'media/js/*.js', 'templates/*.html', 'templates/doc/*.html'] + mathjax_files, 'mathics.builtin.pymimesniffer': ['mimetypes.xml'], + 'mathics.layout': ['server.js'], }, entry_points={ From a253dd73597a3ea332f64e2b9a82c31903f484af Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 30 Aug 2016 17:27:00 +0200 Subject: [PATCH 02/48] adds FontSize --- mathics/builtin/graphics.py | 74 ++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index c752279dfe..a35d536cfb 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -942,6 +942,54 @@ 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.view_width is None: + return 1. + else: + return self.graphics.view_width * self.value + else: + if self.graphics.view_width is None or self.graphics.pixel_width is None: + return 1. + else: + return (96. / 72.) * (self.value * self.graphics.pixel_width) / self.graphics.view_width + + +class Scaled(Builtin): + pass + + class Offset(Builtin): pass @@ -2327,7 +2375,12 @@ def init(self, graphics, style, item=None, content=None, pos=None, if self.color is None: self.color, _ = style.get_style(_Color, face_element=False) - self.font_size = font_size + 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.) if item is not None: if len(item.leaves) not in (1, 2, 3): @@ -2401,17 +2454,9 @@ def replace(m): self.svg = (svg, width, height) def _text_svg_scale(self): - svg, width, height = self.svg - - x, y = self.pos.pos() - x2, y2 = self.pos.add(width, height).pos() - target_height = abs(y2 - y) - - if self.font_size is None: - font_size = 0.5 - return font_size * target_height / height - else: - return self.font_size / height # absolute coords + size = self.font_size.get_size() + # multiplying with 0.5 makes FontSize[] and FontSize[Scaled[]] work as expected + return size * 0.5 def _text_svg_xml(self, style, x, y): svg, width, height = self.svg @@ -2600,6 +2645,7 @@ class _GraphicsElements(object): def __init__(self, content, evaluation): self.evaluation = evaluation self.elements = [] + self.view_width = None builtins = evaluation.definitions.builtin def get_options(name): @@ -3123,6 +3169,8 @@ def add_element(element): tick_large_size = 5 tick_label_d = 2 + font_size = tick_large_size * 2. + ticks_x_int = all(floor(x) == x for x in ticks_x) ticks_y_int = all(floor(x) == x for x in ticks_y) @@ -3143,7 +3191,6 @@ def add_element(element): ticks_lines = [] tick_label_style = ticks_style[index].clone() tick_label_style.extend(label_style) - font_size = tick_large_size * 3. for x in ticks: ticks_lines.append([Coords(elements, pos=p_origin(x)), Coords(elements, pos=p_origin(x), @@ -3570,6 +3617,7 @@ class Large(Builtin): 'Thick': Thick, 'Thin': Thin, 'PointSize': PointSize, + 'FontSize': FontSize, 'Arrowheads': Arrowheads, }) From a15648132d1d954d94163e1838fe5668cb9faa57 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 08:08:59 +0200 Subject: [PATCH 03/48] added --svg-math parameter to enable layout engine; not used by default --- mathics/server.py | 12 +++++++++--- mathics/web/views.py | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mathics/server.py b/mathics/server.py index 903a46e51a..8459299914 100644 --- a/mathics/server.py +++ b/mathics/server.py @@ -65,6 +65,9 @@ def parse_args(): argparser.add_argument( "--external", "-e", dest="external", action="store_true", help="allow external access to server") + argparser.add_argument( + '--svg-math', '-s', dest="svg_math", action='store_true', + help='prerender all math using svg (experimental)') return argparser.parse_args() @@ -89,7 +92,8 @@ def launch_app(args): addr = '127.0.0.1' global layout_engine - layout_engine = LayoutEngine() + if args.svg_math: + layout_engine = LayoutEngine() try: from django.core.servers.basehttp import ( @@ -108,12 +112,14 @@ def launch_app(args): except KeyError: error_text = str(e) sys.stderr.write("Error: %s" % error_text + '\n') - layout_engine.terminate() + if layout_engine is not None: + layout_engine.terminate() # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: print("\nGoodbye!\n") - layout_engine.terminate() + if layout_engine is not None: + layout_engine.terminate() sys.exit(0) diff --git a/mathics/web/views.py b/mathics/web/views.py index 335e762a14..90906b2c21 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -38,7 +38,8 @@ def __init__(self, result={}): class WebOutput(Output): - pass + def svgify(self): + return layout_engine is not None def require_ajax_login(func): From 50dbf6475ccbd5a819cf400db8160cdbcbe44aaa Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 16:27:23 +0200 Subject: [PATCH 04/48] LayoutEngine constructor now raises Exceptions --- mathics/layout/client.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 7131c8c286..3f3e7766bf 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -30,15 +30,10 @@ supported = False -def warn(s): - print(s) - - class LayoutEngine(object): def __init__(self): if not supported: - self.process = None - warn('Web layout engine is disabled as zerorpc is not installed.') + raise RuntimeError('Web layout engine is disabled as zerorpc is not installed.') else: try: popen_env = os.environ.copy() @@ -54,21 +49,20 @@ def __init__(self): status = self.process.stdout.readline().decode('utf8').strip() if status != 'OK': + self.process.terminate() + error = '' while True: line = self.process.stdout.readline().decode('utf8') if not line: break error += ' ' + line - warn('Node.js failed to start web layout engine:') - warn(error) - warn('Check necessary node.js modules and that NODE_PATH is set correctly.') - self.process.terminate() - self.process = None + + raise RuntimeError( + 'Node.js failed to start web layout engine:\n' + error + '\n' + + 'Check necessary node.js modules and that NODE_PATH is set correctly.') except OSError as e: - warn('Failed to start web layout engine:') - warn(str(e)) - self.process = None + raise RuntimeError('Failed to start web layout engine: ' + str(e)) if self.process is None: self.client = None @@ -78,9 +72,10 @@ def __init__(self): self.client.connect("tcp://127.0.0.1:4241") except Exception as e: self.client = None - warn('node.js failed to start web layout engine:') - warn(str(e)) - warn('Probably you are missing node.js modules.') + self.process.terminate() + raise RuntimeError( + 'node.js failed to start web layout engine: \n' + str(e) + '\n' + + 'Probably you are missing node.js modules.') def mathml_to_svg(self, mathml): if self.client: From bd08c19a8a41a6f7b6d0bac832f675b894c98f05 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 18:46:51 +0200 Subject: [PATCH 05/48] fixes error reporting --- mathics/layout/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 3f3e7766bf..50279dad0e 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -49,8 +49,6 @@ def __init__(self): status = self.process.stdout.readline().decode('utf8').strip() if status != 'OK': - self.process.terminate() - error = '' while True: line = self.process.stdout.readline().decode('utf8') @@ -58,6 +56,8 @@ def __init__(self): break error += ' ' + line + self.process.terminate() + raise RuntimeError( 'Node.js failed to start web layout engine:\n' + error + '\n' + 'Check necessary node.js modules and that NODE_PATH is set correctly.') From aaf113f5d0edf39e3cca0c00350cf7a42272f154 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 18:53:25 +0200 Subject: [PATCH 06/48] error handling cosmetics --- mathics/layout/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 50279dad0e..a0e8659006 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -40,10 +40,14 @@ def __init__(self): if settings.NODE_PATH: popen_env["NODE_PATH"] = settings.NODE_PATH - base = os.path.dirname(os.path.realpath(__file__)) + server_path = os.path.dirname(os.path.realpath(__file__)) + "/server.js" + + def abort(message): + error_text = 'Node.js failed to start %s:\n' % server_path + raise RuntimeError(error_text + message) self.process = Popen( - [settings.NODE, base + "/server.js"], + [settings.NODE, server_path], stdout=subprocess.PIPE, env=popen_env) @@ -58,11 +62,9 @@ def __init__(self): self.process.terminate() - raise RuntimeError( - 'Node.js failed to start web layout engine:\n' + error + '\n' + - 'Check necessary node.js modules and that NODE_PATH is set correctly.') + abort(error + '\nCheck Node.js modules and that NODE_PATH.') except OSError as e: - raise RuntimeError('Failed to start web layout engine: ' + str(e)) + abort(str(e)) if self.process is None: self.client = None @@ -73,9 +75,7 @@ def __init__(self): except Exception as e: self.client = None self.process.terminate() - raise RuntimeError( - 'node.js failed to start web layout engine: \n' + str(e) + '\n' + - 'Probably you are missing node.js modules.') + abort(str(e)) def mathml_to_svg(self, mathml): if self.client: From 6a3ec6b17c79ef70892b4ba032a6747e41ee70af Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 1 Sep 2016 13:51:06 +0200 Subject: [PATCH 07/48] got rid of zerorpc since it is such a dependency hell --- mathics/layout/client.py | 133 +++++++++++++++++++++++++-------------- mathics/layout/server.js | 73 ++++++++++++++++++--- 2 files changed, 151 insertions(+), 55 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index a0e8659006..b5828944de 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -1,14 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# In order to use LayoutEngine, you need the "zerorpc" in your Python installation, and nodejs in your path. # you may use "NODE" in settings.py to specify a custom node binary, and you may use NODE_PATH in settings.py -# to specify a custom node_modules path (that has the necessary node modules mathjax-node, zerorpc, ...). +# to specify a custom node_modules path (that has the necessary node modules mathjax-node, ...). # -# The zerorpc package from https://github.com/0rpc/zerorpc-python. needs to be installed manually using -# "/your/python setup.py install". For Python 3, you need to use the zerorpc 3.4 branch. - -# Your installation of nodejs with the following packages: mathjax-node zerorpc svg2png (install them using +# Your installation of nodejs with the following packages: mathjax-node svg2png (install them using # npm). # Some tips for installing nodejs and zmq on OS X: @@ -23,55 +19,100 @@ from mathics import settings -try: - import zerorpc - supported = True -except ImportError: - supported = False +import socket +import json +import struct + +# the following three functions are taken from +# http://stackoverflow.com/questions/17667903/python-socket-receive-large-amount-of-data + +def send_msg(sock, msg): + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + sock.sendall(msg) + + +def recv_msg(sock): + # Read message length and unpack it into an integer + raw_msglen = recvall(sock, 4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + + +def recvall(sock, n): + # Helper function to recv n bytes or return None if EOF is hit + data = b'' + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + return data + + +class RemoteMethod: + def __init__(self, socket, name): + self.socket = socket + self.name = name + + def __call__(self, data): + send_msg(self.socket, json.dumps({'call': self.name, 'data': data}).encode('utf8')) + return json.loads(recv_msg(self.socket).decode('utf8')) + + +class Client: + def __init__(self, ip, port): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((ip, port)) + + def __getattr__(self, name): + return RemoteMethod(self.socket, name) + + def close(self): + return self.socket.close() class LayoutEngine(object): def __init__(self): - if not supported: - raise RuntimeError('Web layout engine is disabled as zerorpc is not installed.') - else: - try: - popen_env = os.environ.copy() - if settings.NODE_PATH: - popen_env["NODE_PATH"] = settings.NODE_PATH - - server_path = os.path.dirname(os.path.realpath(__file__)) + "/server.js" - - def abort(message): - error_text = 'Node.js failed to start %s:\n' % server_path - raise RuntimeError(error_text + message) - - self.process = Popen( - [settings.NODE, server_path], - stdout=subprocess.PIPE, - env=popen_env) - - status = self.process.stdout.readline().decode('utf8').strip() - if status != 'OK': - error = '' - while True: - line = self.process.stdout.readline().decode('utf8') - if not line: - break - error += ' ' + line - - self.process.terminate() - - abort(error + '\nCheck Node.js modules and that NODE_PATH.') - except OSError as e: - abort(str(e)) + try: + popen_env = os.environ.copy() + if settings.NODE_PATH: + popen_env["NODE_PATH"] = settings.NODE_PATH + + server_path = os.path.dirname(os.path.realpath(__file__)) + "/server.js" + + def abort(message): + error_text = 'Node.js failed to start %s:\n' % server_path + raise RuntimeError(error_text + message) + + self.process = Popen( + [settings.NODE, server_path], + stdout=subprocess.PIPE, + env=popen_env) + + status = self.process.stdout.readline().decode('utf8').strip() + if status != 'OK': + error = '' + while True: + line = self.process.stdout.readline().decode('utf8') + if not line: + break + error += ' ' + line + + self.process.terminate() + + abort(error + '\nCheck Node.js modules and that NODE_PATH.') + except OSError as e: + abort(str(e)) if self.process is None: self.client = None else: try: - self.client = zerorpc.Client() - self.client.connect("tcp://127.0.0.1:4241") + self.client = Client('127.0.0.1', 5000) except Exception as e: self.client = None self.process.terminate() diff --git a/mathics/layout/server.js b/mathics/layout/server.js index 94bdc12d58..87b1eeeb71 100644 --- a/mathics/layout/server.js +++ b/mathics/layout/server.js @@ -1,7 +1,66 @@ -// to install: npm install mathjax-node zerorpc svg2png +// to install: npm install mathjax-node svg2png try { - var zerorpc = require("zerorpc"); + function server(methods) { + net = require('net'); + + var uint32 = { + parse: function(buffer) { + return (buffer[0] << 24) | + (buffer[1] << 16) | + (buffer[2] << 8) | + (buffer[3] << 0); + }, + make: function(x) { + var buffer = new Buffer(4); + buffer[0] = x >> 24; + buffer[1] = x >> 16; + buffer[2] = x >> 8; + buffer[3] = x >> 0; + return buffer; + } + }; + + var server = net.createServer(function (socket) { + function write(data) { + var json = JSON.stringify(data); + var size = json.length; + socket.write(Buffer.concat([uint32.make(size), new Buffer(json)])); + } + + var state = { + buffer: new Buffer(0) + }; + + function rpc(size) { + var json = JSON.parse(state.buffer.slice(4, size + 4)); + state.buffer = state.buffer.slice(size + 4) + var method = methods[json.call]; + if (method) { + method(json.data, write); + } + } + + socket.on('data', function(data) { + state.buffer = Buffer.concat( + [state.buffer, data]); + + if (state.buffer.length >= 4) { + var buffer = state.buffer; + var size = uint32.parse(buffer); + if (buffer.length >= size + 4) { + rpc(size); + } + } + }); + }); + + server.on('listening', function() { + console.log('OK'); + }); + + server.listen(5000); + } var mathjax = require("mathjax-node/lib/mj-single.js"); mathjax.config({ @@ -13,7 +72,7 @@ try { var svg2png = require("svg2png"); - var server = new zerorpc.Server({ + server({ mathml_to_svg: function(mathml, reply) { mathjax.typeset({ math: mathml, @@ -21,7 +80,7 @@ try { svg: true, }, function (data) { if (!data.errors) { - reply(null, data.svg); + reply(data.svg); } }); }, @@ -30,14 +89,10 @@ try { width: 300, height: 400 }) - .then(buffer => reply(null, buffer)) + .then(buffer => reply(buffer)) .catch(e => console.error(e)); } }); - - console.log('OK') - - server.bind("tcp://0.0.0.0:4241"); } catch (ex) { console.log('FAIL') console.log(ex) From f95027f240d676bf3718cbd4b443280f50cdb3a0 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 1 Sep 2016 15:16:14 +0200 Subject: [PATCH 08/48] allow environment variables in node paths --- mathics/layout/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index b5828944de..a828e7ed53 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -79,8 +79,8 @@ class LayoutEngine(object): def __init__(self): try: popen_env = os.environ.copy() - if settings.NODE_PATH: - popen_env["NODE_PATH"] = settings.NODE_PATH + if settings.NODE_MODULES: + popen_env["NODE_PATH"] = os.path.expandvars(settings.NODE_MODULES) server_path = os.path.dirname(os.path.realpath(__file__)) + "/server.js" @@ -89,7 +89,7 @@ def abort(message): raise RuntimeError(error_text + message) self.process = Popen( - [settings.NODE, server_path], + [os.path.expandvars(settings.NODE), server_path], stdout=subprocess.PIPE, env=popen_env) From f0900eede1bbc2c532b50930327d97e718319aba Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 1 Sep 2016 15:21:40 +0200 Subject: [PATCH 09/48] fixes NODE_MODULES --- mathics/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/settings.py b/mathics/settings.py index 5904065bf3..6ae191968a 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -13,7 +13,7 @@ # node.js based layout engine NODE = 'node' # path to node binary; default 'node' assumes it is in PATH -NODE_PATH = None # overrides NODE_PATH environment variable if not None +NODE_MODULES = None # overrides NODE_PATH environment variable if not None # set only to True in DEBUG mode DEBUG_MAIL = True From 24edc953a9bbb92ca6f8a9b5924349c6b840c394 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 3 Sep 2016 07:16:16 +0200 Subject: [PATCH 10/48] work in progress: mix mglyph and svg-only approach --- mathics/builtin/graphics.py | 31 +++++++++++-------------------- mathics/builtin/inout.py | 8 +------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index a35d536cfb..24090121e8 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2418,7 +2418,7 @@ def extent(self): 7 # rough approximation by numbers of characters else: _, w, h = self.svg - scale = self._text_svg_scale() + scale = self._text_svg_scale(h) w *= scale h *= scale @@ -2434,6 +2434,8 @@ def _prepare_text_svg(self): svg = self.graphics.evaluation.output.mathml_to_svg( '%s' % 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. @@ -2453,16 +2455,15 @@ def replace(m): self.svg = (svg, width, height) - def _text_svg_scale(self): + def _text_svg_scale(self, height): size = self.font_size.get_size() - # multiplying with 0.5 makes FontSize[] and FontSize[Scaled[]] work as expected - return size * 0.5 + return size / height def _text_svg_xml(self, style, x, y): svg, width, height = self.svg svg = re.sub(r'%s' % ( @@ -3053,23 +3054,13 @@ def boxes_to_xml(self, leaves, **options): w += 2 h += 2 - svgify = options['evaluation'].output.svgify() - - if not svgify: - params = '' - else: - params = 'width="%dpx" height="%dpx"' % (width, height) - svg_xml = '%s' % (' '.join('%f' % t for t in (xmin, ymin, w, h)), params, svg) + 'version="1.1" viewBox="%s">%s' % (' '.join('%f' % t for t in (xmin, ymin, w, h)), svg) - if not svgify: - return '' % ( - int(width), - int(height), - base64.b64encode(svg_xml.encode('utf8')).decode('utf8')) - else: - return svg_xml + return '' % ( + int(width), + int(height), + base64.b64encode(svg_xml.encode('utf8')).decode('utf8')) def axis_ticks(self, xmin, xmax): def round_to_zero(value): diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 8fb5816a97..247b9ba993 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1898,13 +1898,7 @@ def apply_mathml(self, expr, evaluation) -> Expression: # mathml = '%s' % xml # #convert_box(boxes) - if not evaluation.output.svgify(): - result = '%s' % xml - else: - if xml.startswith('%s' % xml) + result = '%s' % xml return Expression('RowBox', Expression('List', String(result))) From c30595d8e45533b57f7eeef57696101a6b101c8b Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 4 Sep 2016 16:31:13 +0200 Subject: [PATCH 11/48] several small improvement for more stability --- mathics/layout/client.py | 16 ++++++++-------- mathics/layout/server.js | 13 +++++++++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index a828e7ed53..0c644668b1 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -7,10 +7,8 @@ # Your installation of nodejs with the following packages: mathjax-node svg2png (install them using # npm). -# Some tips for installing nodejs and zmq on OS X: +# Tips for installing nodejs on OS X: # see https://gist.github.com/DanHerbert/9520689 -# https://github.com/JustinTulloss/zeromq.node/issues/283 -# brew install zmq && npm install zmq # export NODE_PATH=/your/path/to/homebrew/bin/node_modules:$NODE_PATH import subprocess @@ -82,10 +80,11 @@ def __init__(self): if settings.NODE_MODULES: popen_env["NODE_PATH"] = os.path.expandvars(settings.NODE_MODULES) - server_path = os.path.dirname(os.path.realpath(__file__)) + "/server.js" + server_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'server.js') def abort(message): - error_text = 'Node.js failed to start %s:\n' % server_path + error_text = 'Node.js failed to startup %s:\n\n' % server_path raise RuntimeError(error_text + message) self.process = Popen( @@ -94,7 +93,7 @@ def abort(message): env=popen_env) status = self.process.stdout.readline().decode('utf8').strip() - if status != 'OK': + if not status.startswith('HELLO:'): error = '' while True: line = self.process.stdout.readline().decode('utf8') @@ -103,8 +102,9 @@ def abort(message): error += ' ' + line self.process.terminate() + abort(error + '\nPlease check Node.js modules and NODE_PATH.') - abort(error + '\nCheck Node.js modules and that NODE_PATH.') + port = int(status[len('HELLO:'):]) except OSError as e: abort(str(e)) @@ -112,7 +112,7 @@ def abort(message): self.client = None else: try: - self.client = Client('127.0.0.1', 5000) + self.client = Client('127.0.0.1', port) except Exception as e: self.client = None self.process.terminate() diff --git a/mathics/layout/server.js b/mathics/layout/server.js index 87b1eeeb71..b50810f98f 100644 --- a/mathics/layout/server.js +++ b/mathics/layout/server.js @@ -41,6 +41,11 @@ try { } } + socket.on('close', function() { + // means our Python client has lost us. quit. + process.exit(); + }); + socket.on('data', function(data) { state.buffer = Buffer.concat( [state.buffer, data]); @@ -56,10 +61,11 @@ try { }); server.on('listening', function() { - console.log('OK'); + var port = server.address().port; + process.stdout.write('HELLO:' + port.toString() + '\n'); }); - server.listen(5000); + server.listen(0); // pick a free port } var mathjax = require("mathjax-node/lib/mj-single.js"); @@ -94,6 +100,5 @@ try { } }); } catch (ex) { - console.log('FAIL') - console.log(ex) + process.stdout.write('FAIL.' + '\n' + ex.toString() + '\n'); } From be7106e23d7384e4079a0aac12fe905db0e82e11 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 4 Sep 2016 19:00:55 +0200 Subject: [PATCH 12/48] major cleanup --- mathics/builtin/graphics.py | 13 +++- mathics/core/evaluation.py | 27 ++------ mathics/layout/client.py | 135 +++++++++++++++++++++++------------- mathics/server.py | 20 +++--- mathics/web/views.py | 3 +- 5 files changed, 113 insertions(+), 85 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 24090121e8..3a7d0556cd 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -15,6 +15,7 @@ from mathics.builtin.base import ( Builtin, InstancableBuiltin, BoxConstruct, BoxConstructError) from mathics.builtin.options import options_to_rules +from mathics.layout.client import WebEngineUnavailable from mathics.core.expression import ( Expression, Integer, Rational, Real, String, Symbol, strip_context, system_symbols, system_symbols_dict, from_python) @@ -2401,14 +2402,19 @@ def init(self, graphics, style, item=None, content=None, pos=None, self.pos = pos self.opos = opos - if self.graphics.evaluation.output.svgify(): + try: self._prepare_text_svg() - else: + except WebEngineUnavailable as e: self.svg = None self.content_text = self.content.boxes_to_text( evaluation=self.graphics.evaluation) + if not self.graphics.web_engine_warning_issued: + self.graphics.evaluation.message( + 'General', 'nowebeng', str(e)) + self.graphics.web_engine_warning_issued = True + def extent(self): p = self.pos.pos() @@ -2428,6 +2434,8 @@ def extent(self): return [(x, y), (x + w, y + h)] def _prepare_text_svg(self): + self.graphics.evaluation.output.assume_web_engine() + content = self.content.boxes_to_xml( evaluation=self.graphics.evaluation) @@ -2647,6 +2655,7 @@ def __init__(self, content, evaluation): self.evaluation = evaluation self.elements = [] self.view_width = None + self.web_engine_warning_issued = False builtins = evaluation.definitions.builtin def get_options(name): diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 57800c167e..b5ccfe93a9 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -177,14 +177,8 @@ def get_data(self): class Output(object): - def __init__(self, layout_engine=None): - self.layout_engine = layout_engine - - def svgify(self): - # True if the MathML output should be instantly rendered into SVG - # in the backend, i.e. the browser will only see SVG. False for the - # classic mode, i.e. Mathics gives tags to the browser. - return False + def __init__(self, web_engine=None): + self.web_engine = web_engine def max_stored_size(self, settings) -> int: return settings.MAX_STORED_SIZE @@ -198,21 +192,14 @@ def clear(self, wait): def display(self, data, metadata): raise NotImplementedError + def is_web_engine_available(self): + return self.web_engine.is_available() + def mathml_to_svg(self, mathml): - svg = None - if self.layout_engine: - svg = self.layout_engine.mathml_to_svg(mathml) - if svg is None: - raise RuntimeError("LayoutEngine is unavailable") - return svg + return self.web_engine.mathml_to_svg(mathml) def rasterize(self, svg): - png = None - if self.layout_engine: - png = self.layout_engine.rasterize(svg) - if png is None: - raise RuntimeError("LayoutEngine is unavailable") - return png + return self.web_engine.rasterize(svg) class Evaluation(object): diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 0c644668b1..67993fd6ab 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -21,44 +21,49 @@ import json import struct -# the following three functions are taken from -# http://stackoverflow.com/questions/17667903/python-socket-receive-large-amount-of-data - -def send_msg(sock, msg): - # Prefix each message with a 4-byte length (network byte order) - msg = struct.pack('>I', len(msg)) + msg - sock.sendall(msg) - - -def recv_msg(sock): - # Read message length and unpack it into an integer - raw_msglen = recvall(sock, 4) - if not raw_msglen: - return None - msglen = struct.unpack('>I', raw_msglen)[0] - # Read the message data - return recvall(sock, msglen) - - -def recvall(sock, n): - # Helper function to recv n bytes or return None if EOF is hit - data = b'' - while len(data) < n: - packet = sock.recv(n - len(data)) - if not packet: + +class Pipe: + def __init__(self, sock): + self.sock = sock + + # the following three functions are taken from + # http://stackoverflow.com/questions/17667903/python-socket-receive-large-amount-of-data + + def _recvall(self, n): + # Helper function to recv n bytes or return None if EOF is hit + data = b'' + sock = self.sock + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + return data + + def put(self, msg): + msg = json.dumps(msg).encode('utf8') + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + self.sock.sendall(msg) + + def get(self): + # Read message length and unpack it into an integer + raw_msglen = self.recvall(4) + if not raw_msglen: return None - data += packet - return data + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return json.loads(self.recvall(msglen).decode('utf8')) class RemoteMethod: def __init__(self, socket, name): - self.socket = socket + self.pipe = Pipe(socket) self.name = name def __call__(self, data): - send_msg(self.socket, json.dumps({'call': self.name, 'data': data}).encode('utf8')) - return json.loads(recv_msg(self.socket).decode('utf8')) + self.pipe.put({'call': self.name, 'data': data}) + return self.pipe.get() class Client: @@ -73,8 +78,22 @@ def close(self): return self.socket.close() -class LayoutEngine(object): +# Why WebEngine? Well, QT calls its class for similar stuff "web engine", an engine +# that "provides functionality for rendering regions of dynamic web content". This +# is not about web servers but layout (http://doc.qt.io/qt-5/qtwebengine-index.html). + + +class WebEngineUnavailable(RuntimeError): + pass + + +class WebEngine(object): def __init__(self): + self.process = None + self.client = None + self.unavailable = None + + def _create_client(self): try: popen_env = os.environ.copy() if settings.NODE_MODULES: @@ -85,47 +104,65 @@ def __init__(self): def abort(message): error_text = 'Node.js failed to startup %s:\n\n' % server_path - raise RuntimeError(error_text + message) + raise WebEngineUnavailable(error_text + message) - self.process = Popen( + process = Popen( [os.path.expandvars(settings.NODE), server_path], stdout=subprocess.PIPE, env=popen_env) - status = self.process.stdout.readline().decode('utf8').strip() - if not status.startswith('HELLO:'): + hello = 'HELLO:' # agreed upon "all ok" hello message. + + status = process.stdout.readline().decode('utf8').strip() + if not status.startswith(hello): error = '' while True: - line = self.process.stdout.readline().decode('utf8') + line = process.stdout.readline().decode('utf8') if not line: break error += ' ' + line - self.process.terminate() + process.terminate() abort(error + '\nPlease check Node.js modules and NODE_PATH.') - port = int(status[len('HELLO:'):]) + port = int(status[len(hello):]) except OSError as e: abort(str(e)) - if self.process is None: + try: + self.client = Client('127.0.0.1', port) + self.process = process + except Exception as e: self.client = None - else: + self.process = None + process.terminate() + abort(str(e)) + + def _ensure_client(self): + if not self.client: + if self.unavailable is not None: + raise WebEngineUnavailable(self.unavailable) try: - self.client = Client('127.0.0.1', port) - except Exception as e: - self.client = None - self.process.terminate() - abort(str(e)) + self._create_client() + except WebEngineUnavailable as e: + self.unavailable = str(e) + raise e + + return self.client + + def assume_is_available(self): + if self.unavailable is not None: + raise WebEngineUnavailable(self.unavailable) def mathml_to_svg(self, mathml): - if self.client: - return self.client.mathml_to_svg(mathml) + return self._ensure_client().mathml_to_svg(mathml) def rasterize(self, svg): - if self.client: - return self.client.rasterize(svg) + return self._ensure_client().rasterize(svg) def terminate(self): if self.process: self.process.terminate() + self.process = None + self.client = None + diff --git a/mathics/server.py b/mathics/server.py index 8459299914..3537d2cf3b 100644 --- a/mathics/server.py +++ b/mathics/server.py @@ -12,9 +12,9 @@ import mathics from mathics import server_version_string, license_string from mathics import settings as mathics_settings # Prevents UnboundLocalError -from mathics.layout.client import LayoutEngine +from mathics.layout.client import WebEngine -layout_engine = None +web_engine = None def check_database(): @@ -65,9 +65,6 @@ def parse_args(): argparser.add_argument( "--external", "-e", dest="external", action="store_true", help="allow external access to server") - argparser.add_argument( - '--svg-math', '-s', dest="svg_math", action='store_true', - help='prerender all math using svg (experimental)') return argparser.parse_args() @@ -91,9 +88,8 @@ def launch_app(args): else: addr = '127.0.0.1' - global layout_engine - if args.svg_math: - layout_engine = LayoutEngine() + global web_engine + web_engine = WebEngine() try: from django.core.servers.basehttp import ( @@ -112,14 +108,14 @@ def launch_app(args): except KeyError: error_text = str(e) sys.stderr.write("Error: %s" % error_text + '\n') - if layout_engine is not None: - layout_engine.terminate() + if web_engine is not None: + web_engine.terminate() # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: print("\nGoodbye!\n") - if layout_engine is not None: - layout_engine.terminate() + if web_engine is not None: + web_engine.terminate() sys.exit(0) diff --git a/mathics/web/views.py b/mathics/web/views.py index 90906b2c21..335e762a14 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -38,8 +38,7 @@ def __init__(self, result={}): class WebOutput(Output): - def svgify(self): - return layout_engine is not None + pass def require_ajax_login(func): From f5c5e6d260af9b72589cbade8cd7b02c004d33fc Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 4 Sep 2016 22:55:44 +0200 Subject: [PATCH 13/48] fixes node.js output; better warnings --- mathics/builtin/graphics.py | 5 ++--- mathics/core/evaluation.py | 15 ++++++++++++--- mathics/layout/client.py | 6 +++--- mathics/web/views.py | 4 ++-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 3a7d0556cd..8392eeef8c 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2410,10 +2410,9 @@ def init(self, graphics, style, item=None, content=None, pos=None, self.content_text = self.content.boxes_to_text( evaluation=self.graphics.evaluation) - if not self.graphics.web_engine_warning_issued: + if self.graphics.evaluation.output.warn_about_web_engine(): self.graphics.evaluation.message( - 'General', 'nowebeng', str(e)) - self.graphics.web_engine_warning_issued = True + 'General', 'nowebeng', str(e), once=True) def extent(self): p = self.pos.pos() diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index b5ccfe93a9..3c07982ece 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -192,8 +192,11 @@ def clear(self, wait): def display(self, data, metadata): raise NotImplementedError - def is_web_engine_available(self): - return self.web_engine.is_available() + def warn_about_web_engine(self): + return False + + def assume_web_engine(self): + return self.web_engine.assume_is_available() def mathml_to_svg(self, mathml): return self.web_engine.mathml_to_svg(mathml) @@ -222,6 +225,7 @@ def __init__(self, definitions=None, self.quiet_all = False self.format = format self.catch_interrupt = catch_interrupt + self.once_messages = set() def parse(self, query): 'Parse a single expression and print the messages.' @@ -417,7 +421,7 @@ def get_quiet_messages(self): return [] return value.leaves - def message(self, symbol, tag, *args) -> None: + def message(self, symbol, tag, *args, once=False) -> None: from mathics.core.expression import (String, Symbol, Expression, from_python) @@ -428,6 +432,11 @@ def message(self, symbol, tag, *args) -> None: pattern = Expression('MessageName', Symbol(symbol), String(tag)) + if once: + if pattern in self.once_messages: + return + self.once_messages.add(pattern) + if pattern in quiet_messages or self.quiet_all: return diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 67993fd6ab..2844dbca5b 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -48,12 +48,12 @@ def put(self, msg): def get(self): # Read message length and unpack it into an integer - raw_msglen = self.recvall(4) + raw_msglen = self._recvall(4) if not raw_msglen: return None msglen = struct.unpack('>I', raw_msglen)[0] # Read the message data - return json.loads(self.recvall(msglen).decode('utf8')) + return json.loads(self._recvall(msglen).decode('utf8')) class RemoteMethod: @@ -123,7 +123,7 @@ def abort(message): error += ' ' + line process.terminate() - abort(error + '\nPlease check Node.js modules and NODE_PATH.') + abort(error + '\nPlease check Node.js modules and NODE_PATH') port = int(status[len(hello):]) except OSError as e: diff --git a/mathics/web/views.py b/mathics/web/views.py index 335e762a14..6cd33a5ce7 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -97,11 +97,11 @@ def query(request): ) query_log.save() - from mathics.server import layout_engine + from mathics.server import web_engine user_definitions = request.session.get('definitions') definitions.set_user_definitions(user_definitions) - evaluation = Evaluation(definitions, format='xml', output=WebOutput(layout_engine)) + evaluation = Evaluation(definitions, format='xml', output=WebOutput(web_engine)) feeder = MultiLineFeeder(input, '') results = [] try: From ef52066337cda012e3ec3031e2642687a3ac3fd2 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 5 Sep 2016 10:58:19 +0200 Subject: [PATCH 14/48] basic version of Rasterize[], fixes for the classic Mathics frontend --- mathics/builtin/image.py | 47 ++++++ mathics/core/evaluation.py | 7 +- mathics/layout/client.py | 21 ++- mathics/layout/server.js | 23 ++- mathics/web/media/js/mathics.js | 287 +++++++++++++++++--------------- 5 files changed, 239 insertions(+), 146 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 2b34aa6fdf..a06475bf9d 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -9,6 +9,7 @@ from mathics.core.expression import ( Atom, Expression, Integer, Rational, Real, MachineReal, Symbol, from_python) from mathics.builtin.colors import convert as convert_color, colorspaces as known_colorspaces +from mathics.layout.client import WebEngineUnavailable import base64 import functools @@ -2463,3 +2464,49 @@ def color_func(word, font_size, position, orientation, random_state=None, **kwar image = wc.to_image() return Image(numpy.array(image), 'RGB') + + +class Rasterize(Builtin): + requires = _image_requires + + options = { + 'RasterSize': '300', + } + + messages = { + 'err': 'Rasterize[] failed: `1`', + } + + 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) + reply = evaluation.output.rasterize(svg, py_raster_size) + buffer = reply.get('buffer') + + if buffer: + stream = BytesIO() + stream.write(bytearray(buffer['data'])) + stream.seek(0) + pixels = skimage.io.imread(stream) + stream.close() + + return Image(pixels, 'RGB') + else: + error = reply.get('error', 'could not identify the reason for the error') + evaluation.message('Rasterize', 'err', error) + + except WebEngineUnavailable as e: + evaluation.message( + 'General', 'nowebeng', 'Rasterize[] is not available: ' + str(e), once=True) diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 3c07982ece..ead66c2291 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -12,6 +12,7 @@ from mathics import settings from mathics.core.expression import ensure_context, KeyComparable +from mathics.layout.client import NoWebEngine FORMATS = ['StandardForm', 'FullForm', 'TraditionalForm', 'OutputForm', 'InputForm', @@ -177,7 +178,7 @@ def get_data(self): class Output(object): - def __init__(self, web_engine=None): + def __init__(self, web_engine=NoWebEngine()): self.web_engine = web_engine def max_stored_size(self, settings) -> int: @@ -201,8 +202,8 @@ def assume_web_engine(self): def mathml_to_svg(self, mathml): return self.web_engine.mathml_to_svg(mathml) - def rasterize(self, svg): - return self.web_engine.rasterize(svg) + def rasterize(self, svg, *args, **kwargs): + return self.web_engine.rasterize(svg, *args, **kwargs) class Evaluation(object): diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 2844dbca5b..93ad5df197 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -61,8 +61,8 @@ def __init__(self, socket, name): self.pipe = Pipe(socket) self.name = name - def __call__(self, data): - self.pipe.put({'call': self.name, 'data': data}) + def __call__(self, *args): + self.pipe.put({'call': self.name, 'args': args}) return self.pipe.get() @@ -87,7 +87,18 @@ class WebEngineUnavailable(RuntimeError): pass -class WebEngine(object): +class NoWebEngine: + def assume_is_available(self): + raise WebEngineUnavailable + + def mathml_to_svg(self, mathml): + raise WebEngineUnavailable + + def rasterize(self, svg, *args, **kwargs): + raise WebEngineUnavailable + + +class WebEngine: def __init__(self): self.process = None self.client = None @@ -157,8 +168,8 @@ def assume_is_available(self): def mathml_to_svg(self, mathml): return self._ensure_client().mathml_to_svg(mathml) - def rasterize(self, svg): - return self._ensure_client().rasterize(svg) + def rasterize(self, svg, size): + return self._ensure_client().rasterize(svg, size) def terminate(self): if self.process: diff --git a/mathics/layout/server.js b/mathics/layout/server.js index b50810f98f..ff73165548 100644 --- a/mathics/layout/server.js +++ b/mathics/layout/server.js @@ -1,6 +1,12 @@ // to install: npm install mathjax-node svg2png try { + var fs = require('fs'); + function debug(s) { + fs.writeFile((process.env.HOME || process.env.USERPROFILE) + "/debug.txt", s, function(err) { + }); + } + function server(methods) { net = require('net'); @@ -37,7 +43,7 @@ try { state.buffer = state.buffer.slice(size + 4) var method = methods[json.call]; if (method) { - method(json.data, write); + method.apply(null, json.args.concat([write])); } } @@ -86,17 +92,22 @@ try { svg: true, }, function (data) { if (!data.errors) { + debug(data.svg); reply(data.svg); } }); }, - rasterize: function(svg, reply) { + rasterize: function(svg, size, reply) { svg2png(Buffer.from(svg, 'utf8'), { - width: 300, - height: 400 + width: size[0], + height: size[1] }) - .then(buffer => reply(buffer)) - .catch(e => console.error(e)); + .then(function(buffer) { + reply({buffer: buffer}); + }) + .catch(function(e) { + reply({error: e.toString()}); + }); } }); } catch (ex) { diff --git a/mathics/web/media/js/mathics.js b/mathics/web/media/js/mathics.js index 08fa3f1cd2..1a52284297 100644 --- a/mathics/web/media/js/mathics.js +++ b/mathics/web/media/js/mathics.js @@ -142,138 +142,159 @@ var objectsCount = 0; var objects = {}; function translateDOMElement(element, svg) { - if (element.nodeType == 3) { - var text = element.nodeValue; - return $T(text); - } - var dom = null; - var nodeName = element.nodeName; - if (nodeName != 'meshgradient' && nodeName != 'graphics3d') { - dom = createMathNode(element.nodeName); - for (var i = 0; i < element.attributes.length; ++i) { - var attr = element.attributes[i]; - if (attr.nodeName != 'ox' && attr.nodeName != 'oy') - dom.setAttribute(attr.nodeName, attr.nodeValue); - } - } - if (nodeName == 'foreignObject') { - dom.setAttribute('width', svg.getAttribute('width')); - dom.setAttribute('height', svg.getAttribute('height')); - dom.setAttribute('style', dom.getAttribute('style') + '; text-align: left; padding-left: 2px; padding-right: 2px;'); - var ox = parseFloat(element.getAttribute('ox')); - var oy = parseFloat(element.getAttribute('oy')); - dom.setAttribute('ox', ox); - dom.setAttribute('oy', oy); - } - if (nodeName == 'mo') { - var op = element.childNodes[0].nodeValue; - if (op == '[' || op == ']' || op == '{' || op == '}' || op == String.fromCharCode(12314) || op == String.fromCharCode(12315)) - dom.setAttribute('maxsize', '3'); - } - if (nodeName == 'meshgradient') { - if (!MathJax.Hub.Browser.isOpera) { - var data = element.getAttribute('data').evalJSON(); - var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); - var foreign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); - foreign.setAttribute('width', svg.getAttribute('width')); - foreign.setAttribute('height', svg.getAttribute('height')); - foreign.setAttribute('x', '0px'); - foreign.setAttribute('y', '0px'); - foreign.appendChild(div); - - var canvas = createMathNode('canvas'); - canvas.setAttribute('width', svg.getAttribute('width')); - canvas.setAttribute('height', svg.getAttribute('height')); - div.appendChild(canvas); - - var ctx = canvas.getContext('2d'); - for (var index = 0; index < data.length; ++index) { - var points = data[index]; - if (points.length == 3) { - drawMeshGradient(ctx, points); - } - } - - dom = foreign; - } - } - var object = null; - if (nodeName == 'graphics3d') { - var data = element.getAttribute('data').evalJSON(); - var div = document.createElement('div'); - drawGraphics3D(div, data); - dom = div; - } - if (nodeName == 'svg' || nodeName == 'graphics3d' || nodeName.toLowerCase() == 'img') { - // create that will contain the graphics - object = createMathNode('mspace'); - var width, height; - if (nodeName == 'svg' || nodeName.toLowerCase() == 'img') { - width = dom.getAttribute('width'); - height = dom.getAttribute('height'); - } else { - // TODO: calculate appropriate height and recalculate on every view change - width = height = '400'; + if (element.nodeType == 3) { + var text = element.nodeValue; + return $T(text); + } + + if (svg && element.nodeName == 'svg') { + // leave s embedded in s alone, if they are + // not > > . this fixes the + // node.js web engine svg rendering, which embeds text + // as in the Graphics . + var node = element; + var ok = false; + while (node != svg && node.parentNode) { + if (node.nodeName == 'foreignObject') { + ok = true; + break; + } + node = node.parentNode; + } + if (!ok) { + return element; + } + } + + var dom = null; + var nodeName = element.nodeName; + if (nodeName != 'meshgradient' && nodeName != 'graphics3d') { + dom = createMathNode(element.nodeName); + for (var i = 0; i < element.attributes.length; ++i) { + var attr = element.attributes[i]; + if (attr.nodeName != 'ox' && attr.nodeName != 'oy') + dom.setAttribute(attr.nodeName, attr.nodeValue); + } + } + if (nodeName == 'foreignObject') { + dom.setAttribute('width', svg.getAttribute('width')); + dom.setAttribute('height', svg.getAttribute('height')); + dom.setAttribute('style', dom.getAttribute('style') + '; text-align: left; padding-left: 2px; padding-right: 2px;'); + var ox = parseFloat(element.getAttribute('ox')); + var oy = parseFloat(element.getAttribute('oy')); + dom.setAttribute('ox', ox); + dom.setAttribute('oy', oy); + } + if (nodeName == 'mo') { + var op = element.childNodes[0].nodeValue; + if (op == '[' || op == ']' || op == '{' || op == '}' || op == String.fromCharCode(12314) || op == String.fromCharCode(12315)) + dom.setAttribute('maxsize', '3'); + } + if (nodeName == 'meshgradient') { + if (!MathJax.Hub.Browser.isOpera) { + var data = element.getAttribute('data').evalJSON(); + var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); + var foreign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); + foreign.setAttribute('width', svg.getAttribute('width')); + foreign.setAttribute('height', svg.getAttribute('height')); + foreign.setAttribute('x', '0px'); + foreign.setAttribute('y', '0px'); + foreign.appendChild(div); + + var canvas = createMathNode('canvas'); + canvas.setAttribute('width', svg.getAttribute('width')); + canvas.setAttribute('height', svg.getAttribute('height')); + div.appendChild(canvas); + + var ctx = canvas.getContext('2d'); + for (var index = 0; index < data.length; ++index) { + var points = data[index]; + if (points.length == 3) { + drawMeshGradient(ctx, points); + } + } + + dom = foreign; + } + } + var object = null; + if (nodeName == 'graphics3d') { + var data = element.getAttribute('data').evalJSON(); + var div = document.createElement('div'); + drawGraphics3D(div, data); + dom = div; + } + if (nodeName == 'svg' || nodeName == 'graphics3d' || nodeName.toLowerCase() == 'img') { + // create that will contain the graphics + object = createMathNode('mspace'); + var width, height; + if (nodeName == 'svg' || nodeName.toLowerCase() == 'img') { + width = dom.getAttribute('width'); + height = dom.getAttribute('height'); + } else { + // TODO: calculate appropriate height and recalculate on every view change + width = height = '400'; + } + object.setAttribute('width', width + 'px'); + object.setAttribute('height', height + 'px'); + } + if (nodeName == 'svg') + svg = dom; + var rows = [[]]; + $A(element.childNodes).each(function(child) { + if (child.nodeName == 'mspace' && child.getAttribute('linebreak') == 'newline') + rows.push([]); + else + rows[rows.length - 1].push(child); + }); + var childParent = dom; + if (nodeName == 'math') { + var mstyle = createMathNode('mstyle'); + mstyle.setAttribute('displaystyle', 'true'); + dom.appendChild(mstyle); + childParent = mstyle; + } + if (rows.length > 1) { + var mtable = createMathNode('mtable'); + mtable.setAttribute('rowspacing', '0'); + mtable.setAttribute('columnalign', 'left'); + var nospace = 'cell-spacing: 0; cell-padding: 0; row-spacing: 0; row-padding: 0; border-spacing: 0; padding: 0; margin: 0'; + mtable.setAttribute('style', nospace); + rows.each(function(row) { + var mtr = createMathNode('mtr'); + mtr.setAttribute('style', nospace); + var mtd = createMathNode('mtd'); + mtd.setAttribute('style', nospace); + row.each(function(element) { + var elmt = translateDOMElement(element, svg); + if (nodeName == 'mtext') { + // wrap element in mtext + var outer = createMathNode('mtext'); + outer.appendChild(elmt); + elmt = outer; + } + mtd.appendChild(elmt); + }); + mtr.appendChild(mtd); + mtable.appendChild(mtr); + }); + if (nodeName == 'mtext') { + // no mtable inside mtext, but mtable instead of mtext + dom = mtable; + } else + childParent.appendChild(mtable); + } else { + rows[0].each(function(element) { + childParent.appendChild(translateDOMElement(element, svg)); + }); } - object.setAttribute('width', width + 'px'); - object.setAttribute('height', height + 'px'); - } - if (nodeName == 'svg') - svg = dom; - var rows = [[]]; - $A(element.childNodes).each(function(child) { - if (child.nodeName == 'mspace' && child.getAttribute('linebreak') == 'newline') - rows.push([]); - else - rows[rows.length - 1].push(child); - }); - var childParent = dom; - if (nodeName == 'math') { - var mstyle = createMathNode('mstyle'); - mstyle.setAttribute('displaystyle', 'true'); - dom.appendChild(mstyle); - childParent = mstyle; - } - if (rows.length > 1) { - var mtable = createMathNode('mtable'); - mtable.setAttribute('rowspacing', '0'); - mtable.setAttribute('columnalign', 'left'); - var nospace = 'cell-spacing: 0; cell-padding: 0; row-spacing: 0; row-padding: 0; border-spacing: 0; padding: 0; margin: 0'; - mtable.setAttribute('style', nospace); - rows.each(function(row) { - var mtr = createMathNode('mtr'); - mtr.setAttribute('style', nospace); - var mtd = createMathNode('mtd'); - mtd.setAttribute('style', nospace); - row.each(function(element) { - var elmt = translateDOMElement(element, svg); - if (nodeName == 'mtext') { - // wrap element in mtext - var outer = createMathNode('mtext'); - outer.appendChild(elmt); - elmt = outer; - } - mtd.appendChild(elmt); - }); - mtr.appendChild(mtd); - mtable.appendChild(mtr); - }); - if (nodeName == 'mtext') { - // no mtable inside mtext, but mtable instead of mtext - dom = mtable; - } else - childParent.appendChild(mtable); - } else - rows[0].each(function(element) { - childParent.appendChild(translateDOMElement(element, svg)); - }); - if (object) { - var id = objectsCount++; - object.setAttribute('id', objectsPrefix + id); - objects[id] = dom; - return object; - } - return dom; + if (object) { + var id = objectsCount++; + object.setAttribute('id', objectsPrefix + id); + objects[id] = dom; + return object; + } + return dom; } function convertMathGlyphs(dom) { @@ -287,17 +308,19 @@ function convertMathGlyphs(dom) { var src = glyph.getAttribute('src'); if (src.startsWith('data:image/svg+xml;base64,')) { var svgText = atob(src.substring(src.indexOf(",") + 1)); - var mtable =document.createElementNS(MML, "mtable"); + var mtable = document.createElementNS(MML, "mtable"); mtable.innerHTML = '' + svgText + ''; var svg = mtable.getElementsByTagNameNS("*", "svg")[0]; svg.setAttribute('width', glyph.getAttribute('width')); svg.setAttribute('height', glyph.getAttribute('height')); + svg.setAttribute('data-mathics', 'format'); glyph.parentNode.replaceChild(mtable, glyph); } else if (src.startsWith('data:image/')) { var img = document.createElement('img'); img.setAttribute('src', src) img.setAttribute('width', glyph.getAttribute('width')); img.setAttribute('height', glyph.getAttribute('height')); + img.setAttribute('data-mathics', 'format'); glyph.parentNode.replaceChild(img, glyph); } } From dcda65a939be46493d100abf87b7fdbe008b2dcf Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 5 Sep 2016 12:31:31 +0200 Subject: [PATCH 15/48] better error handling --- mathics/builtin/image.py | 30 ++++++++++-------------------- mathics/layout/client.py | 24 +++++++++++++++++------- mathics/layout/server.js | 25 ++++++++++++------------- 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index a06475bf9d..041cb46325 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -9,7 +9,7 @@ from mathics.core.expression import ( Atom, Expression, Integer, Rational, Real, MachineReal, Symbol, from_python) from mathics.builtin.colors import convert as convert_color, colorspaces as known_colorspaces -from mathics.layout.client import WebEngineUnavailable +from mathics.layout.client import WebEngineError import base64 import functools @@ -2473,10 +2473,6 @@ class Rasterize(Builtin): 'RasterSize': '300', } - messages = { - 'err': 'Rasterize[] failed: `1`', - } - def apply(self, expr, evaluation, options): 'Rasterize[expr_, OptionsPattern[%(name)s]]' @@ -2492,21 +2488,15 @@ def apply(self, expr, evaluation, options): mathml = evaluation.format_output(expr, 'xml') try: svg = evaluation.output.mathml_to_svg(mathml) - reply = evaluation.output.rasterize(svg, py_raster_size) - buffer = reply.get('buffer') - - if buffer: - stream = BytesIO() - stream.write(bytearray(buffer['data'])) - stream.seek(0) - pixels = skimage.io.imread(stream) - stream.close() + png = evaluation.output.rasterize(svg, py_raster_size) - return Image(pixels, 'RGB') - else: - error = reply.get('error', 'could not identify the reason for the error') - evaluation.message('Rasterize', 'err', error) + stream = BytesIO() + stream.write(png) + stream.seek(0) + pixels = skimage.io.imread(stream) + stream.close() - except WebEngineUnavailable as e: + return Image(pixels, 'RGB') + except WebEngineError as e: evaluation.message( - 'General', 'nowebeng', 'Rasterize[] is not available: ' + str(e), once=True) + 'General', 'nowebeng', 'Rasterize[] did not succeed: ' + str(e), once=True) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 93ad5df197..96fd2d982a 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -22,6 +22,14 @@ import struct +class WebEngineError(RuntimeError): + pass + + +class WebEngineUnavailable(WebEngineError): + pass + + class Pipe: def __init__(self, sock): self.sock = sock @@ -63,7 +71,13 @@ def __init__(self, socket, name): def __call__(self, *args): self.pipe.put({'call': self.name, 'args': args}) - return self.pipe.get() + reply = self.pipe.get() + + error = reply.get('error') + if error: + raise WebEngineError(str(error)) + else: + return reply.get('data') class Client: @@ -82,11 +96,6 @@ def close(self): # that "provides functionality for rendering regions of dynamic web content". This # is not about web servers but layout (http://doc.qt.io/qt-5/qtwebengine-index.html). - -class WebEngineUnavailable(RuntimeError): - pass - - class NoWebEngine: def assume_is_available(self): raise WebEngineUnavailable @@ -169,7 +178,8 @@ def mathml_to_svg(self, mathml): return self._ensure_client().mathml_to_svg(mathml) def rasterize(self, svg, size): - return self._ensure_client().rasterize(svg, size) + buffer = self._ensure_client().rasterize(svg, size) + return bytearray(buffer['data']) def terminate(self): if self.process: diff --git a/mathics/layout/server.js b/mathics/layout/server.js index ff73165548..bedfdd8f54 100644 --- a/mathics/layout/server.js +++ b/mathics/layout/server.js @@ -1,12 +1,6 @@ // to install: npm install mathjax-node svg2png try { - var fs = require('fs'); - function debug(s) { - fs.writeFile((process.env.HOME || process.env.USERPROFILE) + "/debug.txt", s, function(err) { - }); - } - function server(methods) { net = require('net'); @@ -43,7 +37,11 @@ try { state.buffer = state.buffer.slice(size + 4) var method = methods[json.call]; if (method) { - method.apply(null, json.args.concat([write])); + try { + method.apply(null, json.args.concat([write])); + } catch(e) { + write({error: e.toString() + '; ' + e.stack}); + } } } @@ -82,28 +80,29 @@ try { }); mathjax.start(); - var svg2png = require("svg2png"); - server({ mathml_to_svg: function(mathml, reply) { mathjax.typeset({ math: mathml, format: "MathML", - svg: true, + svg: true }, function (data) { if (!data.errors) { - debug(data.svg); - reply(data.svg); + reply({data: data.svg}); + } else { + reply({error: data.errors}); } }); }, rasterize: function(svg, size, reply) { + var svg2png = require("svg2png"); + svg2png(Buffer.from(svg, 'utf8'), { width: size[0], height: size[1] }) .then(function(buffer) { - reply({buffer: buffer}); + reply({data: buffer}); }) .catch(function(e) { reply({error: e.toString()}); From 4d725a3ee985c635d2cb772975b8ff5d2c3abb1f Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 10 Sep 2016 09:14:35 +0200 Subject: [PATCH 16/48] fixes Python 2 syntax error --- mathics/core/evaluation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index ead66c2291..340779f936 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -422,7 +422,7 @@ def get_quiet_messages(self): return [] return value.leaves - def message(self, symbol, tag, *args, once=False) -> None: + def message(self, symbol, tag, *args, **kwargs) -> None: from mathics.core.expression import (String, Symbol, Expression, from_python) @@ -433,7 +433,7 @@ def message(self, symbol, tag, *args, once=False) -> None: pattern = Expression('MessageName', Symbol(symbol), String(tag)) - if once: + if kwargs.get('once', False): if pattern in self.once_messages: return self.once_messages.add(pattern) From 01164774e7c1cb1e1a6adca4a4e1c7f2ae5377d2 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 14 Sep 2016 18:41:28 +0200 Subject: [PATCH 17/48] added nowebeng message --- mathics/builtin/inout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 247b9ba993..cd5a2bd8dd 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1756,6 +1756,7 @@ class General(Builtin): 'notboxes': "`1` is not a valid box structure.", 'pyimport': "`1`[] is not available. Your Python installation misses the \"`2`\" module.", + 'nowebeng': "Web Engine is not available: `1`", } From c7c54c7c90e7bce991d3919741e3a0fd45901a44 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 14 Sep 2016 19:55:02 +0200 Subject: [PATCH 18/48] got rid of NODE and NODE_MODULES in settings.py; the cleanest way to deal with this is to setup PATH and NODE_PATH prior to running mathics/jupyter in a script --- mathics/layout/client.py | 9 +-------- mathics/settings.py | 4 ---- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 96fd2d982a..97bfe60ee6 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -1,9 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# you may use "NODE" in settings.py to specify a custom node binary, and you may use NODE_PATH in settings.py -# to specify a custom node_modules path (that has the necessary node modules mathjax-node, ...). -# # Your installation of nodejs with the following packages: mathjax-node svg2png (install them using # npm). @@ -15,8 +12,6 @@ from subprocess import Popen import os -from mathics import settings - import socket import json import struct @@ -116,8 +111,6 @@ def __init__(self): def _create_client(self): try: popen_env = os.environ.copy() - if settings.NODE_MODULES: - popen_env["NODE_PATH"] = os.path.expandvars(settings.NODE_MODULES) server_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'server.js') @@ -127,7 +120,7 @@ def abort(message): raise WebEngineUnavailable(error_text + message) process = Popen( - [os.path.expandvars(settings.NODE), server_path], + ['node', server_path], stdout=subprocess.PIPE, env=popen_env) diff --git a/mathics/settings.py b/mathics/settings.py index 6ae191968a..3720b9db0f 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -11,10 +11,6 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG -# node.js based layout engine -NODE = 'node' # path to node binary; default 'node' assumes it is in PATH -NODE_MODULES = None # overrides NODE_PATH environment variable if not None - # set only to True in DEBUG mode DEBUG_MAIL = True PROPAGATE_EXCEPTIONS = True From f9144b29afc84ac9023e0bea51096d8fa6857d11 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 15 Sep 2016 15:25:27 +0200 Subject: [PATCH 19/48] Rasterize[] changed to use PIL --- mathics/builtin/image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 041cb46325..7d79ff7807 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -2493,7 +2493,10 @@ def apply(self, expr, evaluation, options): stream = BytesIO() stream.write(png) stream.seek(0) - pixels = skimage.io.imread(stream) + 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') From ea0b17a9e6f14a6e08b1c52e717b4266c6954bcb Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 15 Sep 2016 16:22:10 +0200 Subject: [PATCH 20/48] fixes Rasterize[]: avoid embedding svgs in svgs --- mathics/layout/client.py | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 97bfe60ee6..68d63b233f 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -102,6 +102,53 @@ def rasterize(self, svg, *args, **kwargs): raise WebEngineUnavailable +def _normalize_svg(svg): + import xml.etree.ElementTree as ET + import base64 + import re + + ET.register_namespace('', 'http://www.w3.org/2000/svg') + root = ET.fromstring(svg) + prefix = 'data:image/svg+xml;base64,' + + def rewrite(up): + changes = [] + + for i, node in enumerate(up): + if node.tag == '{http://www.w3.org/2000/svg}image': + src = node.attrib.get('src', '') + if src.startswith(prefix): + attrib = node.attrib + + if 'width' in attrib and 'height' in attrib: + target_width = float(attrib['width']) + target_height = float(attrib['height']) + target_transform = attrib.get('transform', '') + + image_svg = _normalize_svg(base64.b64decode(src[len(prefix):])) + root = ET.fromstring(image_svg) + + view_box = re.split('\s+', root.attrib.get('viewBox', '')) + + if len(view_box) == 4: + x, y, w, h = (float(t) for t in view_box) + root.tag = '{http://www.w3.org/2000/svg}g' + root.attrib = {'transform': '%s scale(%f, %f) translate(%f, %f)' % ( + target_transform, target_width / w, target_height / h, -x, -y)} + + changes.append((i, node, root)) + else: + rewrite(node) + + for i, node, new_node in reversed(changes): + up.remove(node) + up.insert(i, new_node) + + rewrite(root) + + return ET.tostring(root, 'utf8').decode('utf8') + + class WebEngine: def __init__(self): self.process = None @@ -171,7 +218,7 @@ def mathml_to_svg(self, mathml): return self._ensure_client().mathml_to_svg(mathml) def rasterize(self, svg, size): - buffer = self._ensure_client().rasterize(svg, size) + buffer = self._ensure_client().rasterize(_normalize_svg(svg), size) return bytearray(buffer['data']) def terminate(self): From 6e694d8706a41e208a075f789342615646d14013 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 15 Sep 2016 21:05:08 +0200 Subject: [PATCH 21/48] fixes a bug in TerminalOutput --- mathics/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mathics/main.py b/mathics/main.py index 4a3cd4beda..bae320457c 100644 --- a/mathics/main.py +++ b/mathics/main.py @@ -159,6 +159,7 @@ def max_stored_size(self, settings): return None def __init__(self, shell): + super(TerminalOutput, self).__init__() self.shell = shell def out(self, out): From e6bbb139a7484074b588c3c878698f3de9b7c8ef Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 18 Oct 2016 15:35:00 +0200 Subject: [PATCH 22/48] better node config/startup --- mathics/builtin/graphics.py | 5 +++++ mathics/layout/client.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 8392eeef8c..6dac46edb9 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2413,6 +2413,11 @@ def init(self, graphics, style, item=None, content=None, pos=None, 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() diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 68d63b233f..a40512b988 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -162,6 +162,15 @@ def _create_client(self): server_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'server.js') + if True: + # fixes problems on Windows network drives + import tempfile + fd, copied_path = tempfile.mkstemp(suffix='js') + with open(server_path, 'rb') as f: + os.write(fd, f.read()) + os.fsync(fd) + server_path = copied_path + def abort(message): error_text = 'Node.js failed to startup %s:\n\n' % server_path raise WebEngineUnavailable(error_text + message) From e51b5a63643fc627e3e9e5525a019e1ba05ad8e1 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:29:57 +0200 Subject: [PATCH 23/48] merge with transforms --- mathics/builtin/graphics.py | 146 +++++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 6dac46edb9..ae8e124934 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -337,6 +337,137 @@ def apply(self, asy): return self._template % (' * '.join(self.transforms), asy) +def _to_float(x): + if isinstance(x, Integer): + return x.get_int_value() + else: + y = x.round_to_float() + if y is None: + raise BoxConstructError + return y + + +class _Transform(): + def __init__(self, f): + if not isinstance(f, Expression): + self.matrix = f + return + + if f.get_head_name() != 'System`TransformationFunction': + raise BoxConstructError + + if len(f.leaves) != 1 or f.leaves[0].get_head_name() != 'System`List': + raise BoxConstructError + + 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 + + self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] + + def scaled(self, x, y): + # we compute AB, where A is the scale matrix (x, y, 1) and B is + # self.matrix + m = self.matrix + return _Transform([[t * x for t in m[0]], [t * y for t in m[1]], m[2]]) + + def transform(self, p): + 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] + + for x, y in p: + yield m11 * x + m12 * y + m13, m21 * x + m22 * y + m23 + + 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. or m[2][1] != 0. or m[2][2] != 1.: + raise BoxConstructError + + # a c e + # b d f + # 0 0 1 + + t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a, b, c, d, e, f) + return '%s' % (t, svg) + + +class _SVGTransform(): + def __init__(self): + self.transforms = [] + + 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)) + + def translate(self, x, y): + self.transforms.append('translate(%f, %f)' % (x, y)) + + def scale(self, x, y): + self.transforms.append('scale(%f, %f)' % (x, y)) + + def rotate(self, x): + self.transforms.append('rotate(%f)' % x) + + def apply(self, svg): + return '%s' % (' '.join(self.transforms), svg) + + +class _ASYTransform(): + _template = """ + add(%s * (new picture() { + picture saved = currentpicture; + picture transformed = new picture; + currentpicture = transformed; + %s + currentpicture = saved; + return transformed; + })()); + """ + + def __init__(self): + self.transforms = [] + + 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)) + + def scale(self, x, y): + self.transforms.append('scale(%f, %f)' % (x, y)) + + def rotate(self, x): + self.transforms.append('rotate(%f)' % x) + + def apply(self, asy): + return self._template % (' * '.join(self.transforms), asy) + + class Graphics(Builtin): r"""
@@ -397,6 +528,8 @@ def convert(content): return Expression('List', *[convert(item) for item in content.leaves]) elif head == 'System`Style': return Expression('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: if head == 'System`Text': @@ -2657,7 +2790,6 @@ def _flatten(leaves): class _GraphicsElements(object): def __init__(self, content, evaluation): self.evaluation = evaluation - self.elements = [] self.view_width = None self.web_engine_warning_issued = False @@ -2706,6 +2838,8 @@ def convert(content, style): raise BoxConstructError for element in convert(item.leaves[0], stylebox_style(style, item.leaves[1:])): yield element + elif head == 'System`GeometricTransformationBox': + yield GeometricTransformationBox(self, style, list(convert(item.leaves[0], style)), item.leaves[1]) elif head[-3:] == 'Box': # and head[:-3] in element_heads: element_class = get_class(head) if element_class is not None: @@ -2754,6 +2888,16 @@ def __init__(self, content, evaluation, neg_y=False): self.pixel_height = self.extent_width = self.extent_height = None self.view_width = None + def fix_transform(self, transform): + if self.pixel_width is not None: + w = self.extent_width if self.extent_width > 0 else 1 + h = self.extent_height if self.extent_height > 0 else 1 + x = self.pixel_width / w + y = self.pixel_height / h + return transform.scaled(x, y) + else: + return transform + def translate(self, coords): if self.pixel_width is not None: w = self.extent_width if self.extent_width > 0 else 1 From 44adcfd2e2f212b87e9741b36f51820cda34b236 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:30:55 +0200 Subject: [PATCH 24/48] merge transforms --- mathics/builtin/graphics.py | 386 ++++++++++++++++++++++++++---------- mathics/builtin/tensors.py | 97 +++++++++ 2 files changed, 374 insertions(+), 109 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index ae8e124934..8c3d3d45ea 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -54,10 +54,9 @@ def coords(value): class Coords(object): - def __init__(self, graphics, expr=None, pos=None, d=None): + def __init__(self, graphics, expr=None, pos=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]) @@ -69,11 +68,8 @@ def __init__(self, graphics, expr=None, pos=None, d=None): self.p = coords(expr) def pos(self): - p = self.graphics.translate(self.p) + p = 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): @@ -81,6 +77,22 @@ def add(self, x, y): return Coords(self.graphics, pos=p, d=self.d) +class AxisCoords(Coords): + def __init__(self, graphics, expr=None, pos=None, d=None): + super(AxisCoords, self).__init__(graphics, expr=expr, pos=pos) + self.d = d + + def pos(self): + p = self.p + p = self.graphics.translate(p) + p = (cut(p[0]), cut(p[1])) + if self.d is not None: + d = self.graphics.translate_absolute_in_pixels(self.d) + return p[0] + d[0], p[1] + d[1] + else: + return p + + def cut(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" border = 10 ** 8 @@ -369,11 +381,10 @@ def __init__(self, f): self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] - def scaled(self, x, y): - # we compute AB, where A is the scale matrix (x, y, 1) and B is - # self.matrix - m = self.matrix - return _Transform([[t * x for t in m[0]], [t * y for t in m[1]], m[2]]) + 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 transform(self, p): m = self.matrix @@ -409,63 +420,28 @@ def to_svg(self, svg): t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a, b, c, d, e, f) return '%s' % (t, svg) + def to_asy(self, asy): + m = self.matrix -class _SVGTransform(): - def __init__(self): - self.transforms = [] - - 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)) - - def translate(self, x, y): - self.transforms.append('translate(%f, %f)' % (x, y)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, svg): - return '%s' % (' '.join(self.transforms), svg) - - -class _ASYTransform(): - _template = """ - add(%s * (new picture() { - picture saved = currentpicture; - picture transformed = new picture; - currentpicture = transformed; - %s - currentpicture = saved; - return transformed; - })()); - """ + a = m[0][0] + b = m[1][0] + c = m[0][1] + d = m[1][1] + e = m[0][2] + f = m[1][2] - def __init__(self): - self.transforms = [] + if m[2][0] != 0. or m[2][1] != 0. or m[2][2] != 1.: + 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)) + t = ','.join(map(asy_number, (e, f, a, c, b, d))) - def translate(self, x, y): - self.transforms.append('shift(%f, %f)' % (x, y)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, asy): - return self._template % (' * '.join(self.transforms), asy) + return ''.join(("add((", t, ")*(new picture(){", + "picture s=currentpicture,t=new picture;currentpicture=t;", asy, + "currentpicture=s;return t;})());")) class Graphics(Builtin): @@ -494,7 +470,7 @@ class Graphics(Builtin): = . \begin{asy} . 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} @@ -1241,10 +1217,17 @@ def init(self, graphics, style, item): 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.pos() + tx2, ty2 = self.p2.pos() + + 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): @@ -1307,7 +1290,7 @@ def to_svg(self): x, y = self.c.pos() rx, ry = self.r.pos() 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 '' % ( @@ -1317,7 +1300,7 @@ def to_asy(self): x, y = self.c.pos() rx, ry = self.r.pos() 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, face_color=self.face_color, stroke_width=l, @@ -2500,6 +2483,169 @@ 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- + """ + + 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] + + def patch_transforms(self, transforms): + self.transforms = transforms + + def extent(self): + def points(): + for content in self.contents: + for transform in self.transforms: + p = content.extent() + for q in transform.transform(p): + yield q + return list(points()) + + def to_svg(self): + def instances(): + for content in self.contents: + content_svg = content.to_svg() + for transform in self.transforms: + yield transform.to_svg(content_svg) + return ''.join(instances()) + + def to_asy(self): + def instances(): + # graphics = self.graphics + for content in self.contents: + content_asy = content.to_asy() + for transform in self.transforms: + yield transform.to_asy(content_asy) + return ''.join(instances()) + + class InsetBox(_GraphicsElement): def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0), font_size=None): @@ -2764,7 +2910,7 @@ def get_option(self, name): return self.options.get(name, None) def get_line_width(self, face_element=True): - if self.graphics.pixel_width is None: + if self.graphics.local_to_world is None: return 0 edge_style, _ = self.get_style( _Thickness, default_to_faces=face_element, @@ -2887,41 +3033,67 @@ def __init__(self, content, evaluation, neg_y=False): self.xmin = self.ymin = self.pixel_width = None self.pixel_height = self.extent_width = self.extent_height = None self.view_width = None + self.local_to_world = None + + def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height): + self.pixel_width = pixel_width + self.extent_width = extent_width + + tx = -xmin + ty = -ymin + + w = extent_width if extent_width > 0 else 1 + h = extent_height if extent_height > 0 else 1 + + sx = pixel_width / w + sy = pixel_height / h - def fix_transform(self, transform): - if self.pixel_width is not None: - w = self.extent_width if self.extent_width > 0 else 1 - h = self.extent_height if self.extent_height > 0 else 1 - x = self.pixel_width / w - y = self.pixel_height / h - return transform.scaled(x, y) + qx = 0 + if self.neg_y: + sy = -sy + qy = pixel_height else: - return transform + qy = 0 + + # now build a transform matrix that mimics what used to happen in GraphicsElements.translate(). + # m = TranslationTransform[{qx, qy}].ScalingTransform[{sx, sy}].TranslationTransform[{tx, ty}] + + m = [[sx, 0, sx * tx + qx], [0, sy, sy * ty + qy], [0, 0, 1]] + transform = _Transform(m) + + # update the GeometricTransformationBox, that always has to be the root element. + + self.elements[0].patch_transforms([transform]) + self.local_to_world = transform + + def add_axis_element(self, e): + # axis elements are added after the GeometricTransformationBox and are thus not + # subject to the transformation from local to pixel space. + self.elements.append(e) def translate(self, coords): - if self.pixel_width is not None: - w = self.extent_width if self.extent_width > 0 else 1 - h = self.extent_height if self.extent_height > 0 else 1 - result = [(coords[0] - self.xmin) * self.pixel_width / w, - (coords[1] - self.ymin) * self.pixel_height / h] - if self.neg_y: - result[1] = self.pixel_height - result[1] - return tuple(result) + if self.local_to_world: + return list(self.local_to_world.transform([coords]))[0] else: - return (coords[0], coords[1]) + return coords[0], coords[1] def translate_absolute(self, d): - if self.pixel_width is None: - return (0, 0) + s = self.extent_width / self.pixel_width + x, y = self.translate_absolute_in_pixels(d) + return x * s, y * s + + def translate_absolute_in_pixels(self, d): + if self.local_to_world is None: + return 0, 0 else: - l = 96.0 / 72 - return (d[0] * l, (-1 if self.neg_y else 1) * d[1] * l) + l = 96.0 / 72 # d is measured in printer's points + return d[0] * l, (-1 if self.neg_y else 1) * d[1] * l def translate_relative(self, x): - if self.pixel_width is None: + if self.local_to_world is None: return 0 else: - return x * self.pixel_width + return x * self.extent_width def extent(self, completely_visible_only=False): if completely_visible_only: @@ -2944,13 +3116,6 @@ def to_svg(self): def to_asy(self): return '\n'.join(element.to_asy() for element in self.elements) - def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, - pixel_height): - - self.xmin, self.ymin = xmin, ymin - self.extent_width, self.extent_height = extent_width, extent_height - self.pixel_width, self.pixel_height = pixel_width, pixel_height - class GraphicsBox(BoxConstruct): options = Graphics.options @@ -3030,7 +3195,12 @@ def _prepare_elements(self, leaves, options, neg_y=False, max_width=None): if not isinstance(plot_range, list) or len(plot_range) != 2: raise BoxConstructError - elements = GraphicsElements(leaves[0], options['evaluation'], neg_y) + transformation = Expression('System`TransformationFunction', [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + + elements = GraphicsElements( + Expression('System`GeometricTransformationBox', leaves[0], transformation), + options['evaluation'], neg_y) + axes = [] # to be filled further down def calc_dimensions(final_pass=True): @@ -3307,7 +3477,7 @@ def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax): def add_element(element): element.is_completely_visible = True - elements.elements.append(element) + elements.add_axis_element(element) ticks_x, ticks_x_small, origin_x = self.axis_ticks(xmin, xmax) ticks_y, ticks_y_small, origin_y = self.axis_ticks(ymin, ymax) @@ -3332,16 +3502,14 @@ def add_element(element): if axes[index]: add_element(LineBox( elements, axes_style[index], - lines=[[Coords(elements, pos=p_origin(min), - d=p_other0(-axes_extra)), - Coords(elements, pos=p_origin(max), - d=p_other0(axes_extra))]])) + lines=[[AxisCoords(elements, pos=p_origin(min), d=p_other0(-axes_extra)), + AxisCoords(elements, pos=p_origin(max), d=p_other0(axes_extra))]])) ticks_lines = [] tick_label_style = ticks_style[index].clone() tick_label_style.extend(label_style) for x in ticks: - ticks_lines.append([Coords(elements, pos=p_origin(x)), - Coords(elements, pos=p_origin(x), + ticks_lines.append([AxisCoords(elements, pos=p_origin(x)), + AxisCoords(elements, pos=p_origin(x), d=p_self0(tick_large_size))]) if ticks_int: content = String(str(int(x))) @@ -3352,12 +3520,12 @@ def add_element(element): add_element(InsetBox( elements, tick_label_style, content=content, - pos=Coords(elements, pos=p_origin(x), + pos=AxisCoords(elements, pos=p_origin(x), d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) for x in ticks_small: pos = p_origin(x) - ticks_lines.append([Coords(elements, pos=pos), - Coords(elements, pos=pos, + ticks_lines.append([AxisCoords(elements, pos=pos), + AxisCoords(elements, pos=pos, d=p_self0(tick_small_size))]) add_element(LineBox(elements, axes_style[0], lines=ticks_lines)) diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py index 0a892e5718..4a5f2c4701 100644 --- a/mathics/builtin/tensors.py +++ b/mathics/builtin/tensors.py @@ -481,3 +481,100 @@ def is_boolean(x): return 'ColorDistance' return None + + + +class TransformationFunction(Builtin): + """ +
+
'TransformationFunction[$m$]' +
represents a transformation. +
+ + >> 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, {1}], 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): + """ +
+
'RotationTransform[$phi$]' +
gives a rotation by $phi$. +
'RotationTransform[$phi$, $p$]' +
gives a rotation by $phi$ around the point $p$. +
+ """ + + 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): + """ +
+
'ScalingTransform[$v$]' +
gives a scaling transform of $v$. $v$ may be a scalar or a vector. +
'ScalingTransform[$phi$, $p$]' +
gives a scaling transform of $v$ that is centered at the point $p$. +
+ """ + + rules = { + 'ScalingTransform[v_]': + 'TransformationFunction[DiagonalMatrix[Join[v, {1}]]]', + 'ScalingTransform[v_, p_]': + 'TranslationTransform[p] . ScalingTransform[v] . TranslationTransform[-p]', + } + + +class ShearingTransform(Builtin): + """ +
+
'ShearingTransform[$phi$, {1, 0}, {0, 1}]' +
gives a horizontal shear by the angle $phi$. +
'ShearingTransform[$phi$, {0, 1}, {1, 0}]' +
gives a vertical shear by the angle $phi$. +
'ShearingTransform[$phi$, $u$, $u$, $p$]' +
gives a shear centered at the point $p$. +
+ """ + + rules = { + 'ShearingTransform[phi_, {1, 0}, {0, 1}]': + 'TransformationFunction[{{1, Tan[phi], 0}, {0, 1, 0}, {0, 0, 1}}]', + 'ShearingTransform[phi_, {0, 1}, {1, 0}]': + 'TransformationFunction[{{1, 0, 0}, {Tan[phi], 1, 0}, {0, 0, 1}}]', + 'ShearingTransform[phi_, u_, v_, p_]': + 'TranslationTransform[p] . ShearingTransform[phi, u, v] . TranslationTransform[-p]', + } From d9407ed45a75a488c487ac5db357a71d0b851fc3 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:32:58 +0200 Subject: [PATCH 25/48] merge transforms --- mathics/builtin/graphics.py | 41 +++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 8c3d3d45ea..482ed58c3b 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -11,6 +11,8 @@ import json import base64 from itertools import chain +from math import sin, cos, pi +from sympy.matrices import Matrix from mathics.builtin.base import ( Builtin, InstancableBuiltin, BoxConstruct, BoxConstructError) @@ -74,7 +76,7 @@ def pos(self): def add(self, x, y): p = (self.p[0] + x, self.p[1] + y) - return Coords(self.graphics, pos=p, d=self.d) + return Coords(self.graphics, pos=p) class AxisCoords(Coords): @@ -92,6 +94,9 @@ def pos(self): else: return p + def add(self, x, y): + raise NotImplementedError + def cut(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" @@ -381,6 +386,9 @@ def __init__(self, f): self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] + def inverse(self): + return _Transform(Matrix(self.matrix).inv().tolist()) + def multiply(self, other): a = self.matrix b = other.matrix @@ -467,7 +475,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); @@ -2648,12 +2656,13 @@ def instances(): class InsetBox(_GraphicsElement): def init(self, graphics, style, item=None, content=None, pos=None, - opos=(0, 0), font_size=None): + opos=(0, 0), font_size=None, absolute_coordinates=False): super(InsetBox, self).init(graphics, item, style) self.color = self.style.get_option('System`FontColor') if self.color is None: self.color, _ = style.get_style(_Color, face_element=False) + self.absolute_coordinates = absolute_coordinates if font_size is not None: self.font_size = FontSize(self.graphics, value=font_size) @@ -2773,12 +2782,17 @@ def to_svg(self): style = create_css(font_color=self.color) if not self.svg: - return ( + svg = ( '' '%s') % ( x, y, self.opos[0], self.opos[1], style, content) else: - return self._text_svg_xml(style, x, y) + svg = self._text_svg_xml(style, x, y) + + if not self.absolute_coordinates: + svg = self.graphics.text_matrix.to_svg(svg) + + return svg def to_asy(self): x, y = self.pos.pos() @@ -2910,7 +2924,7 @@ def get_option(self, name): return self.options.get(name, None) def get_line_width(self, face_element=True): - if self.graphics.local_to_world is None: + if self.graphics.local_to_screen is None: return 0 edge_style, _ = self.get_style( _Thickness, default_to_faces=face_element, @@ -3033,7 +3047,7 @@ def __init__(self, content, evaluation, neg_y=False): self.xmin = self.ymin = self.pixel_width = None self.pixel_height = self.extent_width = self.extent_height = None self.view_width = None - self.local_to_world = None + self.local_to_screen = None def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height): self.pixel_width = pixel_width @@ -3064,7 +3078,8 @@ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_h # update the GeometricTransformationBox, that always has to be the root element. self.elements[0].patch_transforms([transform]) - self.local_to_world = transform + self.local_to_screen = transform + self.text_matrix = _Transform([[1. / sx, 0, 0], [0, 1. / sy, 0], [0, 0, 1]]) def add_axis_element(self, e): # axis elements are added after the GeometricTransformationBox and are thus not @@ -3072,8 +3087,8 @@ def add_axis_element(self, e): self.elements.append(e) def translate(self, coords): - if self.local_to_world: - return list(self.local_to_world.transform([coords]))[0] + if self.local_to_screen: + return list(self.local_to_screen.transform([coords]))[0] else: return coords[0], coords[1] @@ -3083,14 +3098,14 @@ def translate_absolute(self, d): return x * s, y * s def translate_absolute_in_pixels(self, d): - if self.local_to_world is None: + if self.local_to_screen is None: return 0, 0 else: l = 96.0 / 72 # d is measured in printer's points return d[0] * l, (-1 if self.neg_y else 1) * d[1] * l def translate_relative(self, x): - if self.local_to_world is None: + if self.local_to_screen is None: return 0 else: return x * self.extent_width @@ -3521,7 +3536,7 @@ def add_element(element): elements, tick_label_style, content=content, pos=AxisCoords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) + d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size, absolute_coordinates=True)) for x in ticks_small: pos = p_origin(x) ticks_lines.append([AxisCoords(elements, pos=pos), From 72abc7ae725a03ca666441bcb6b2e259586813ce Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:34:21 +0200 Subject: [PATCH 26/48] merge transforms --- mathics/builtin/graphics.py | 146 ++++++++++++------------------------ 1 file changed, 46 insertions(+), 100 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 482ed58c3b..c8495d1751 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -141,6 +141,7 @@ def _to_float(x): raise BoxConstructError return x + def create_pens(edge_color=None, face_color=None, stroke_width=None, is_face_element=False): result = [] @@ -264,94 +265,25 @@ def _CMC_distance(lab1, lab2, l, c): def _extract_graphics(graphics, format, evaluation): graphics_box = Expression('MakeBoxes', graphics).evaluate(evaluation) builtin = GraphicsBox(expression=False) + elements, calc_dimensions = builtin._prepare_elements( 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 - sx = (xmax - xmin) / ex - sy = (ymax - ymin) / ey + if not isinstance(elements.elements[0], GeometricTransformationBox): + raise ValueError('expected GeometricTransformationBox') - 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() for element in contents) elif format == 'svg': - code = elements.to_svg() + code = ''.join(element.to_svg() for element in contents) else: raise NotImplementedError - return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code - - -class _SVGTransform(): - def __init__(self): - self.transforms = [] - - 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)) - - def translate(self, x, y): - self.transforms.append('translate(%f, %f)' % (x, y)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, svg): - return '%s' % (' '.join(self.transforms), svg) - - -class _ASYTransform(): - _template = """ - add(%s * (new picture() { - picture saved = currentpicture; - picture transformed = new picture; - currentpicture = transformed; - %s - currentpicture = saved; - return transformed; - })()); - """ - - def __init__(self): - self.transforms = [] - - 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)) - - def scale(self, x, y): - self.transforms.append('scale(%f, %f)' % (x, y)) - - def rotate(self, x): - self.transforms.append('rotate(%f)' % x) - - def apply(self, asy): - return self._template % (' * '.join(self.transforms), asy) + return code def _to_float(x): @@ -1047,7 +979,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): @@ -2413,24 +2345,28 @@ 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( + code = _extract_graphics( graphics, format, self.graphics.evaluation) - boxw = xmax - xmin - boxh = ymax - ymin + + half_pi = pi / 2. 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 @@ -2448,9 +2384,12 @@ def polygon(points): yield ' '.join('%f,%f' % xy for xy in points) yield '" style="%s" />' % 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): @@ -2465,9 +2404,12 @@ def polygon(points): yield '--'.join(['(%.5g,%5g)' % xy for xy in 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): @@ -2573,6 +2515,9 @@ class Rotate(Builtin): >> 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 = { @@ -2781,6 +2726,9 @@ def to_svg(self): evaluation=evaluation) style = create_css(font_color=self.color) + if not self.absolute_coordinates: + x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] + if not self.svg: svg = ( '' @@ -2790,7 +2738,7 @@ def to_svg(self): svg = self._text_svg_xml(style, x, y) if not self.absolute_coordinates: - svg = self.graphics.text_matrix.to_svg(svg) + svg = self.graphics.inverse_local_to_screen.to_svg(svg) return svg @@ -3044,14 +2992,14 @@ class GraphicsElements(_GraphicsElements): def __init__(self, content, evaluation, neg_y=False): super(GraphicsElements, self).__init__(content, evaluation) self.neg_y = neg_y - self.xmin = self.ymin = self.pixel_width = None - self.pixel_height = self.extent_width = self.extent_height = None - self.view_width = None + self.pixel_width = None + self.extent_width = self.extent_height = None self.local_to_screen = None def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height): self.pixel_width = pixel_width self.extent_width = extent_width + self.extent_height = extent_height tx = -xmin ty = -ymin @@ -3079,7 +3027,7 @@ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_h self.elements[0].patch_transforms([transform]) self.local_to_screen = transform - self.text_matrix = _Transform([[1. / sx, 0, 0], [0, 1. / sy, 0], [0, 0, 1]]) + self.inverse_local_to_screen = transform.inverse() def add_axis_element(self, e): # axis elements are added after the GeometricTransformationBox and are thus not @@ -3343,7 +3291,6 @@ def boxes_to_tex(self, leaves, **options): leaves, options, max_width=450) xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - elements.view_width = w asy_completely_visible = '\n'.join( element.to_asy() for element in elements.elements @@ -3383,7 +3330,6 @@ def boxes_to_xml(self, leaves, **options): leaves, options, neg_y=True) xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - elements.view_width = w svg = elements.to_svg() From cb915dcab68004328bc949d3e65e930343a64ffb Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:35:46 +0200 Subject: [PATCH 27/48] merge transforms --- mathics/builtin/graphics.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index c8495d1751..25791b2985 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -78,6 +78,9 @@ def add(self, x, y): p = (self.p[0] + x, self.p[1] + y) return Coords(self.graphics, pos=p) + def is_absolute(self): + return False + class AxisCoords(Coords): def __init__(self, graphics, expr=None, pos=None, d=None): @@ -97,6 +100,9 @@ def pos(self): def add(self, x, y): raise NotImplementedError + def is_absolute(self): + return True + def cut(value): "Cut values in graphics primitives (not displayed otherwise in SVG)" @@ -407,7 +413,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); @@ -2600,14 +2606,12 @@ def instances(): class InsetBox(_GraphicsElement): - def init(self, graphics, style, item=None, content=None, pos=None, - opos=(0, 0), font_size=None, absolute_coordinates=False): + def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0), font_size=None): super(InsetBox, self).init(graphics, item, style) self.color = self.style.get_option('System`FontColor') if self.color is None: self.color, _ = style.get_style(_Color, face_element=False) - self.absolute_coordinates = absolute_coordinates if font_size is not None: self.font_size = FontSize(self.graphics, value=font_size) @@ -2722,11 +2726,13 @@ def _text_svg_xml(self, style, x, y): def to_svg(self): evaluation = self.graphics.evaluation x, y = self.pos.pos() + absolute = self.pos.is_absolute() + content = self.content.boxes_to_xml( evaluation=evaluation) style = create_css(font_color=self.color) - if not self.absolute_coordinates: + if not absolute: x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] if not self.svg: @@ -2737,7 +2743,7 @@ def to_svg(self): else: svg = self._text_svg_xml(style, x, y) - if not self.absolute_coordinates: + if not absolute: svg = self.graphics.inverse_local_to_screen.to_svg(svg) return svg @@ -3482,7 +3488,7 @@ def add_element(element): elements, tick_label_style, content=content, pos=AxisCoords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size, absolute_coordinates=True)) + d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) for x in ticks_small: pos = p_origin(x) ticks_lines.append([AxisCoords(elements, pos=pos), From 6729d0cf0fa5ee050f7c9fcd6ea27b6ef30aade7 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:36:04 +0200 Subject: [PATCH 28/48] merge transforms --- mathics/builtin/graphics.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 25791b2985..0e8fc30982 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2887,6 +2887,35 @@ def get_line_width(self, face_element=True): return 0 return edge_style.get_thickness() + def to_axis_style(self): + return AxisStyle(self) + + +class AxisStyle(object): + # used exclusively for graphics generated inside GraphicsBox.create_axes(). + # wraps a Style instance for graphics and does not operate on local but on + # screen space, i.e. has to apply "local_to_screen" to all widths, sizes, ... + + def __init__(self, style): + self.base = Style(style.graphics) + self.base.extend(style) + self.sx = style.graphics.local_to_screen.matrix[0][0] + + def extend(self, style, pre=True): + self.base.extend(style.base, pre) + + def clone(self): + return AxisStyle(self.base.clone()) + + def get_style(self, *args, **kwargs): + return self.base.get_style(*args, **kwargs) + + def get_option(self, name): + return self.base.get_option(name) + + def get_line_width(self, face_element=True): + return self.base.get_line_width(face_element) * self.sx + def _flatten(leaves): for leaf in leaves: @@ -3439,6 +3468,11 @@ def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax): ticks_style = [elements.create_style(s) for s in ticks_style] axes_style = [elements.create_style(s) for s in axes_style] label_style = elements.create_style(label_style) + + ticks_style = [s.to_axis_style() for s in ticks_style] + axes_style = [s.to_axis_style() for s in axes_style] + label_style = label_style.to_axis_style() + ticks_style[0].extend(axes_style[0]) ticks_style[1].extend(axes_style[1]) From 941a839ddd1c198222576198f65d4578507a9abc Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 10:44:43 +0200 Subject: [PATCH 29/48] various fixes regarding text and points --- mathics/builtin/graphics.py | 89 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 0e8fc30982..d64378d1fb 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -11,7 +11,6 @@ import json import base64 from itertools import chain -from math import sin, cos, pi from sympy.matrices import Matrix from mathics.builtin.base import ( @@ -413,7 +412,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); @@ -530,8 +529,9 @@ def init(self, item=None, components=None): # become RGBColor[0, 0, 0, 1]. does not seem the right thing # to do in this general context. poke1024 - if len(components) < 3: - components.extend(self.default_components[len(components):]) + # if len(components) < len(self.default_components): + # components.extend(self.default_components[ + # len(components):]) self.components = components else: @@ -1031,15 +1031,15 @@ def init(self, graphics, item=None, value=None): def get_size(self): if self.scaled: - if self.graphics.view_width is None: + if self.graphics.extent_width is None: return 1. else: - return self.graphics.view_width * self.value + return self.graphics.extent_width * self.value else: - if self.graphics.view_width is None or self.graphics.pixel_width is None: + if self.graphics.extent_width is None or self.graphics.pixel_width is None: return 1. else: - return (96. / 72.) * (self.value * self.graphics.pixel_width) / self.graphics.view_width + return (96. / 72.) * (self.value * self.graphics.extent_width / self.graphics.pixel_width) class Scaled(Builtin): @@ -1437,13 +1437,19 @@ def to_svg(self): 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, style) + svg += '' % ( + coords.pos()[0], coords.pos()[1], size_x, size_y, style) return svg def to_asy(self): @@ -2606,7 +2612,8 @@ def instances(): class InsetBox(_GraphicsElement): - def init(self, graphics, style, item=None, content=None, pos=None, opos=(0, 0), font_size=None): + def init(self, graphics, style, item=None, content=None, pos=None, + opos=(0, 0), font_size=None): super(InsetBox, self).init(graphics, item, style) self.color = self.style.get_option('System`FontColor') @@ -2708,17 +2715,23 @@ def _text_svg_scale(self, height): size = self.font_size.get_size() return size / height - def _text_svg_xml(self, style, x, y): + def _text_svg_xml(self, style, x, y, absolute): svg, width, height = self.svg svg = re.sub(r'%s' % ( + if absolute: + tx, ty = (1., 1.) + else: + tx, ty = self.graphics.text_rescale + + return '%s' % ( x, y, - scale, + scale * tx, + scale * ty, -width / 2 - ox * width / 2, -height / 2 + oy * height / 2, svg) @@ -2732,19 +2745,19 @@ def to_svg(self): evaluation=evaluation) style = create_css(font_color=self.color) - if not absolute: - x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] - if not self.svg: + if not absolute: + x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] + svg = ( '' '%s') % ( x, y, self.opos[0], self.opos[1], style, content) - else: - svg = self._text_svg_xml(style, x, y) - if not absolute: - svg = self.graphics.inverse_local_to_screen.to_svg(svg) + if not absolute: + svg = self.graphics.inverse_local_to_screen.to_svg(svg) + else: + svg = self._text_svg_xml(style, x, y, absolute) return svg @@ -2891,30 +2904,14 @@ def to_axis_style(self): return AxisStyle(self) -class AxisStyle(object): - # used exclusively for graphics generated inside GraphicsBox.create_axes(). - # wraps a Style instance for graphics and does not operate on local but on - # screen space, i.e. has to apply "local_to_screen" to all widths, sizes, ... - +class AxisStyle(Style): def __init__(self, style): - self.base = Style(style.graphics) - self.base.extend(style) - self.sx = style.graphics.local_to_screen.matrix[0][0] - - def extend(self, style, pre=True): - self.base.extend(style.base, pre) - - def clone(self): - return AxisStyle(self.base.clone()) - - def get_style(self, *args, **kwargs): - return self.base.get_style(*args, **kwargs) - - def get_option(self, name): - return self.base.get_option(name) + super(AxisStyle, self).__init__(style.graphics, style.edge, style.face) + self.styles = style.styles + self.options = style.options def get_line_width(self, face_element=True): - return self.base.get_line_width(face_element) * self.sx + return 0.5 def _flatten(leaves): @@ -2933,7 +2930,6 @@ def _flatten(leaves): class _GraphicsElements(object): def __init__(self, content, evaluation): self.evaluation = evaluation - self.view_width = None self.web_engine_warning_issued = False builtins = evaluation.definitions.builtin @@ -3033,6 +3029,7 @@ def __init__(self, content, evaluation, neg_y=False): def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height): self.pixel_width = pixel_width + self.pixel_height = pixel_height self.extent_width = extent_width self.extent_height = extent_height @@ -3063,6 +3060,7 @@ def set_size(self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_h self.elements[0].patch_transforms([transform]) self.local_to_screen = transform self.inverse_local_to_screen = transform.inverse() + self.text_rescale = (1., -1. if self.neg_y else 1.) def add_axis_element(self, e): # axis elements are added after the GeometricTransformationBox and are thus not @@ -3488,7 +3486,8 @@ def add_element(element): tick_large_size = 5 tick_label_d = 2 - font_size = tick_large_size * 2. + # hack: work around the local to screen scaling in class FontSize + font_size = tick_large_size * 2. / (elements.extent_width / elements.pixel_width) ticks_x_int = all(floor(x) == x for x in ticks_x) ticks_y_int = all(floor(x) == x for x in ticks_y) @@ -3522,7 +3521,7 @@ def add_element(element): elements, tick_label_style, content=content, pos=AxisCoords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) + d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) for x in ticks_small: pos = p_origin(x) ticks_lines.append([AxisCoords(elements, pos=pos), From 6c705216711ece826907f55dce02a946e6abf8da Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 21 Oct 2016 17:26:55 +0200 Subject: [PATCH 30/48] fixes test case --- mathics/builtin/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index d64378d1fb..3762aadafb 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -412,7 +412,7 @@ class Graphics(Builtin): In 'TeXForm', 'Graphics' produces Asymptote figures: >> Graphics[Circle[]] // TeXForm - = + = . \begin{asy} . size(5.8556cm, 5.8333cm); . 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;})()); From d7aeb056a67756d3db09611071ecc914dc5d63c5 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 5 Nov 2016 16:48:24 +0100 Subject: [PATCH 31/48] preparations for Mathics computed transforms --- mathics/builtin/graphics.py | 288 ++++++++++++++++-------------------- 1 file changed, 129 insertions(+), 159 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 3762aadafb..baab764814 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -46,71 +46,33 @@ def get_class(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 + 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): - self.graphics = graphics - self.p = pos - 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.p - p = (cut(p[0]), cut(p[1])) - return p - - def add(self, x, y): - p = (self.p[0] + x, self.p[1] + y) - return Coords(self.graphics, pos=p) - - def is_absolute(self): - return False - - -class AxisCoords(Coords): - def __init__(self, graphics, expr=None, pos=None, d=None): - super(AxisCoords, self).__init__(graphics, expr=expr, pos=pos) - self.d = d - - def pos(self): - p = self.p - p = self.graphics.translate(p) - p = (cut(p[0]), cut(p[1])) - if self.d is not None: - d = self.graphics.translate_absolute_in_pixels(self.d) - return p[0] + d[0], p[1] + d[1] - else: - return p - - def add(self, x, y): - raise NotImplementedError +def add_coords(a, b): + x1, y1 = a + x2, y2 = b + return x1 + x2, y1 + y2 - def is_absolute(self): - return True - -def cut(value): +def cut_coords(xy): "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 + return min(max(xy[0], -border), border), min(max(xy[1], -border), border) + + +def axis_coords(graphics, pos, d=None): + p = cut_coords(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(edge_color=None, face_color=None, stroke_width=None, @@ -301,7 +263,7 @@ def _to_float(x): return y -class _Transform(): +class _Transform: def __init__(self, f): if not isinstance(f, Expression): self.matrix = f @@ -1154,18 +1116,19 @@ def init(self, graphics, style, item): raise BoxConstructError self.edge_color, self.face_color = style.get_style( _Color, face_element=True) - self.p1 = Coords(graphics, item.leaves[0]) + + self.p1 = 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 = coords(item.leaves[1]) def extent(self): l = self.style.get_line_width(face_element=True) / 2 result = [] - tx1, ty1 = self.p1.pos() - tx2, ty2 = self.p2.pos() + tx1, ty1 = self.p1 + tx2, ty2 = self.p2 x1 = min(tx1, tx2) - l x2 = max(tx1, tx2) + l @@ -1176,10 +1139,11 @@ def extent(self): return result - def to_svg(self): + 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 @@ -1188,10 +1152,11 @@ def to_svg(self): return '' % ( xmin, ymin, w, 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( @@ -1209,7 +1174,7 @@ def init(self, graphics, style, item): raise BoxConstructError self.edge_color, self.face_color = style.get_style( _Color, face_element=self.face_element) - self.c = Coords(graphics, item.leaves[0]) + self.c = coords(item.leaves[0]) if len(item.leaves) == 1: rx = ry = 1 elif len(item.leaves) == 2: @@ -1219,12 +1184,12 @@ 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 @@ -1232,9 +1197,10 @@ def extent(self): return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] - def to_svg(self): - 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 = abs(y - ry) l = self.style.get_line_width(face_element=self.face_element) @@ -1242,9 +1208,10 @@ def to_svg(self): return '' % ( x, y, rx, ry, style) - def to_asy(self): - x, y = self.c.pos() - rx, ry = self.r.pos() + def to_asy(self, transform): + c, r = transform(self.c, self.r) + x, y = c + rx, ry = r rx -= x ry = abs(ry - y) l = self.style.get_line_width(face_element=self.face_element) @@ -1284,9 +1251,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 @@ -1306,11 +1274,11 @@ def _arc_params(self): return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc - def to_svg(self): + def to_svg(self, transform): if self.arc is None: - return super(_ArcBox, self).to_svg() + 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: @@ -1328,11 +1296,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() - 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: @@ -1379,15 +1347,14 @@ 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] + self.lines = [[coords(point) for point in 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)]) return result @@ -1431,7 +1398,7 @@ def init(self, graphics, style, item=None): else: raise BoxConstructError - def to_svg(self): + 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) @@ -1441,24 +1408,22 @@ def to_svg(self): 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: + for x, y in transform(*line): svg += '' % ( - coords.pos()[0], coords.pos()[1], size_x, size_y, style) + x, y, size_x, 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);' % ((x, y), pen) return asy @@ -1495,21 +1460,22 @@ def init(self, graphics, style, item=None, lines=None): else: raise BoxConstructError - def to_svg(self): + 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: - svg += '' % ( - ' '.join(['%f,%f' % coords.pos() for coords in line]), style) + path = ' '.join(['%f,%f' % 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) 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)' % c for c in transform(*line)]) asy += 'draw(%s, %s);' % (path, pen) return asy @@ -1614,6 +1580,7 @@ class BezierFunction(Builtin): 'BezierFunction[p_]': 'Function[x, Total[p * BernsteinBasis[Length[p] - 1, Range[0, Length[p] - 1], x]]]', } + class BezierCurve(Builtin): """
@@ -1646,23 +1613,23 @@ def init(self, graphics, style, item, options): raise BoxConstructError self.spline_degree = spline_degree.get_int_value() - def to_svg(self): + 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])): + for path in _asy_bezier((self.spline_degree, transform(*line))): asy += 'draw(%s, %s);' % (path, pen) return asy @@ -1712,14 +1679,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([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] @@ -1728,18 +1694,18 @@ def parse_component(segments): else: raise BoxConstructError - def to_svg(self): + 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) 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 '' % (' '.join(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) @@ -1748,7 +1714,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()) @@ -1759,7 +1725,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)]) return result @@ -1827,7 +1793,7 @@ def process_option(self, name, value): else: raise BoxConstructError - def to_svg(self): + 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 @@ -1839,16 +1805,16 @@ def to_svg(self): 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])] + data = [[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), style) + ' '.join('%f,%f' % 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 @@ -1863,7 +1829,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') + '(%.5g,%.5g)' % c for c in transform(*line)]) + '--cycle') # ignore opacity colors.append(','.join([ @@ -1877,7 +1843,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' + ['(%.5g,%.5g)' % c for c in transform(*line)]) + '--cycle' asy += 'filldraw(%s, %s);' % (path, pens) return asy @@ -2349,7 +2315,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 @@ -2384,7 +2350,7 @@ def draw(px, py, vx, vy, t1, s): return make - def to_svg(self): + 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) @@ -2393,7 +2359,7 @@ def to_svg(self): def polygon(points): yield '' % arrow_style def svg_transform(m, code): @@ -2404,7 +2370,7 @@ def svg_transform(m, code): 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) @@ -2413,7 +2379,7 @@ 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 def asy_transform(m, code): @@ -2593,19 +2559,18 @@ def points(): yield q return list(points()) - def to_svg(self): + def to_svg(self, outer_transform): def instances(): for content in self.contents: - content_svg = content.to_svg() + content_svg = content.to_svg(outer_transform) for transform in self.transforms: yield transform.to_svg(content_svg) return ''.join(instances()) - def to_asy(self): + def to_asy(self, outer_transform): def instances(): - # graphics = self.graphics for content in self.contents: - content_asy = content.to_asy() + content_asy = content.to_asy(outer_transform) for transform in self.transforms: yield transform.to_asy(content_asy) return ''.join(instances()) @@ -2613,7 +2578,7 @@ def instances(): class InsetBox(_GraphicsElement): def init(self, graphics, style, item=None, content=None, pos=None, - opos=(0, 0), font_size=None): + opos=(0, 0), font_size=None, is_absolute=False): super(InsetBox, self).init(graphics, item, style) self.color = self.style.get_option('System`FontColor') @@ -2634,9 +2599,9 @@ def init(self, graphics, style, item=None, content=None, pos=None, self.content = content.format( graphics.evaluation, 'TraditionalForm') if len(item.leaves) > 1: - self.pos = Coords(graphics, item.leaves[1]) + self.pos = 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]) else: @@ -2646,6 +2611,8 @@ def init(self, graphics, style, item=None, content=None, pos=None, self.pos = pos self.opos = opos + self.is_absolute = is_absolute + try: self._prepare_text_svg() except WebEngineUnavailable as e: @@ -2664,7 +2631,7 @@ def init(self, graphics, style, item=None, content=None, pos=None, 'General', 'nowebeng', str(e), once=True) def extent(self): - p = self.pos.pos() + p = self.pos if not self.svg: h = 25 @@ -2736,17 +2703,18 @@ def _text_svg_xml(self, style, x, y, absolute): -height / 2 + oy * height / 2, svg) - def to_svg(self): + def to_svg(self, transform): evaluation = self.graphics.evaluation - x, y = self.pos.pos() - absolute = self.pos.is_absolute() + x, y = transform(self.pos)[0] content = self.content.boxes_to_xml( evaluation=evaluation) style = create_css(font_color=self.color) + is_absolute = self.is_absolute + if not self.svg: - if not absolute: + if not is_absolute: x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] svg = ( @@ -2754,15 +2722,15 @@ def to_svg(self): '%s') % ( x, y, self.opos[0], self.opos[1], style, content) - if not absolute: + if not is_absolute: svg = self.graphics.inverse_local_to_screen.to_svg(svg) else: - svg = self._text_svg_xml(style, x, y, absolute) + svg = self._text_svg_xml(style, x, y, is_absolute) return svg - def to_asy(self): - x, y = self.pos.pos() + def to_asy(self, transform): + x, y = transform(self.pos)[0] content = self.content.boxes_to_tex( evaluation=self.graphics.evaluation) pen = create_pens(edge_color=self.color) @@ -3018,8 +2986,6 @@ def get_style_class(self): class GraphicsElements(_GraphicsElements): - coords = Coords - def __init__(self, content, evaluation, neg_y=False): super(GraphicsElements, self).__init__(content, evaluation) self.neg_y = neg_y @@ -3107,10 +3073,14 @@ def extent(self, completely_visible_only=False): return xmin, xmax, ymin, ymax def to_svg(self): - return '\n'.join(element.to_svg() for element in self.elements) + def cut(*p): + return [cut_coords(q) for q in p] + return '\n'.join(element.to_svg(cut) for element in self.elements) def to_asy(self): - return '\n'.join(element.to_asy() for element in self.elements) + def no_transform(*p): + return p + return '\n'.join(element.to_asy(no_transform) for element in self.elements) class GraphicsBox(BoxConstruct): @@ -3502,14 +3472,14 @@ def add_element(element): if axes[index]: add_element(LineBox( elements, axes_style[index], - lines=[[AxisCoords(elements, pos=p_origin(min), d=p_other0(-axes_extra)), - AxisCoords(elements, pos=p_origin(max), d=p_other0(axes_extra))]])) + lines=[[axis_coords(elements, pos=p_origin(min), d=p_other0(-axes_extra)), + axis_coords(elements, pos=p_origin(max), d=p_other0(axes_extra))]])) ticks_lines = [] tick_label_style = ticks_style[index].clone() tick_label_style.extend(label_style) for x in ticks: - ticks_lines.append([AxisCoords(elements, pos=p_origin(x)), - AxisCoords(elements, pos=p_origin(x), + ticks_lines.append([axis_coords(elements, pos=p_origin(x)), + axis_coords(elements, pos=p_origin(x), d=p_self0(tick_large_size))]) if ticks_int: content = String(str(int(x))) @@ -3520,12 +3490,12 @@ def add_element(element): add_element(InsetBox( elements, tick_label_style, content=content, - pos=AxisCoords(elements, pos=p_origin(x), - d=p_self0(-tick_label_d)), opos=p_self0(1), font_size=font_size)) + pos=axis_coords(elements, pos=p_origin(x), d=p_self0(-tick_label_d)), + opos=p_self0(1), font_size=font_size, is_absolute=True)) for x in ticks_small: pos = p_origin(x) - ticks_lines.append([AxisCoords(elements, pos=pos), - AxisCoords(elements, pos=pos, + ticks_lines.append([axis_coords(elements, pos=pos), + axis_coords(elements, pos=pos, d=p_self0(tick_small_size))]) add_element(LineBox(elements, axes_style[0], lines=ticks_lines)) From 7bff167b738df6163474a8715013dacf49dfc7d5 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 5 Nov 2016 17:28:28 +0100 Subject: [PATCH 32/48] fixes Plot[1+x*0.000001, {x, 0, 1}] --- mathics/builtin/graphics.py | 70 ++++++++++++++++++++++++++----------- mathics/builtin/plot.py | 9 +++++ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index baab764814..f94ce122fb 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -60,14 +60,8 @@ def add_coords(a, b): return x1 + x2, y1 + y2 -def cut_coords(xy): - "Cut values in graphics primitives (not displayed otherwise in SVG)" - border = 10 ** 8 - return min(max(xy[0], -border), border), min(max(xy[1], -border), border) - - def axis_coords(graphics, pos, d=None): - p = cut_coords(graphics.translate(pos)) + 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] @@ -285,6 +279,13 @@ def __init__(self, f): self.matrix = [[_to_float(x) for x in row.leaves] for row in rows] + def combine(self, transform0): + if isinstance(transform0, _Transform): + return self.multiply(transform0) + else: + t = self + return lambda *p: transform0(*t(p)) + def inverse(self): return _Transform(Matrix(self.matrix).inv().tolist()) @@ -293,7 +294,7 @@ def multiply(self, other): 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 transform(self, p): + def __call__(self, p): m = self.matrix m11 = m[0][0] @@ -396,6 +397,10 @@ class Graphics(Builtin): 'PlotRangePadding': 'Automatic', 'ImageSize': 'Automatic', 'Background': 'Automatic', + + 'PrecomputeTransformations': 'False', # 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 (strokes with width < 1e-6 won't get transformed in SVG). } box_suffix = 'Box' @@ -1462,21 +1467,26 @@ def init(self, graphics, style, item=None, lines=None): def to_svg(self, transform): l = self.style.get_line_width(face_element=False) + l = list(transform((l, l)))[0][0] # assume pure scaling transform style = create_css(edge_color=self.edge_color, stroke_width=l) svg = '' for line in self.lines: path = ' '.join(['%f,%f' % c for c in transform(*line)]) svg += '' % (path, style) + return svg def to_asy(self, transform): l = self.style.get_line_width(face_element=False) + l = list(transform((l, l)))[0][0] # assume pure scaling transform pen = create_pens(edge_color=self.edge_color, stroke_width=l) + asy = '' for line in self.lines: path = '--'.join(['(%.5g,%5g)' % c for c in transform(*line)]) asy += 'draw(%s, %s);' % (path, pen) + return asy @@ -2546,6 +2556,7 @@ def init(self, graphics, style, contents, transform): 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 @@ -2553,24 +2564,31 @@ def patch_transforms(self, transforms): def extent(self): def points(): for content in self.contents: + p = content.extent() for transform in self.transforms: - p = content.extent() - for q in transform.transform(p): + for q in transform(p): yield q return list(points()) - def to_svg(self, outer_transform): - def instances(): - for content in self.contents: - content_svg = content.to_svg(outer_transform) + def to_svg(self, transform0): + if self.precompute: + def instances(): for transform in self.transforms: - yield transform.to_svg(content_svg) + 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, outer_transform): + def to_asy(self, transform0): def instances(): for content in self.contents: - content_asy = content.to_asy(outer_transform) + content_asy = content.to_asy(transform0) for transform in self.transforms: yield transform.to_asy(content_asy) return ''.join(instances()) @@ -2715,7 +2733,7 @@ def to_svg(self, transform): if not self.svg: if not is_absolute: - x, y = list(self.graphics.local_to_screen.transform([(x, y)]))[0] + x, y = list(self.graphics.local_to_screen([(x, y)]))[0] svg = ( '' @@ -2986,7 +3004,8 @@ def get_style_class(self): class GraphicsElements(_GraphicsElements): - def __init__(self, content, evaluation, neg_y=False): + def __init__(self, content, evaluation, neg_y=False, precompute_transformations=False): + self.precompute_transformations = precompute_transformations super(GraphicsElements, self).__init__(content, evaluation) self.neg_y = neg_y self.pixel_width = None @@ -3035,7 +3054,7 @@ def add_axis_element(self, e): def translate(self, coords): if self.local_to_screen: - return list(self.local_to_screen.transform([coords]))[0] + return list(self.local_to_screen([coords]))[0] else: return coords[0], coords[1] @@ -3073,8 +3092,15 @@ def extent(self, completely_visible_only=False): return xmin, xmax, ymin, ymax def to_svg(self): + border = 10 ** 8 + + def cut_coords(xy): + "Cut values in graphics primitives (not displayed otherwise in SVG)" + return min(max(xy[0], -border), border), min(max(xy[1], -border), border) + def cut(*p): return [cut_coords(q) for q in p] + return '\n'.join(element.to_svg(cut) for element in self.elements) def to_asy(self): @@ -3163,9 +3189,11 @@ def _prepare_elements(self, leaves, options, neg_y=False, max_width=None): transformation = Expression('System`TransformationFunction', [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + precompute_transformations = graphics_options['System`PrecomputeTransformations'].is_true() + elements = GraphicsElements( Expression('System`GeometricTransformationBox', leaves[0], transformation), - options['evaluation'], neg_y) + options['evaluation'], neg_y, precompute_transformations) axes = [] # to be filled further down diff --git a/mathics/builtin/plot.py b/mathics/builtin/plot.py index d7cc9f1809..0cd4e5fe58 100644 --- a/mathics/builtin/plot.py +++ b/mathics/builtin/plot.py @@ -597,6 +597,15 @@ def find_excl(excl): graphics.append(Expression( 'Point', Expression('List', *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 with 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 to we end up with an empty plot. in order to fix this, + # PrecomputeTransformations 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`PrecomputeTransformations'] = Symbol('True') + return Expression('Graphics', Expression('List', *graphics), *options_to_rules(options)) From 86d4f1c06adafdb19779bfaf9a74e111630587f6 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 5 Nov 2016 19:55:24 +0100 Subject: [PATCH 33/48] various cleanups and fixes --- mathics/builtin/graphics.py | 78 +++++++++++++++++++++-------------- mathics/builtin/graphics3d.py | 5 ++- mathics/builtin/plot.py | 8 ++-- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index f94ce122fb..dbadfd7e02 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -45,7 +45,7 @@ def get_class(name): # like return globals().get(name) -def coords(value): +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() @@ -284,7 +284,10 @@ def combine(self, transform0): return self.multiply(transform0) else: t = self - return lambda *p: transform0(*t(p)) + + def combined(*p, w=1): + return transform0(*t(*p, w=w), w=w) + return combined def inverse(self): return _Transform(Matrix(self.matrix).inv().tolist()) @@ -294,7 +297,7 @@ def multiply(self, other): 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 __call__(self, p): + def __call__(self, *p, w=1): m = self.matrix m11 = m[0][0] @@ -305,8 +308,14 @@ def __call__(self, p): m22 = m[1][1] m23 = m[1][2] - for x, y in p: - yield m11 * x + m12 * y + m13, m21 * x + m22 * y + m23 + 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 @@ -352,6 +361,10 @@ def to_asy(self, asy): "currentpicture=s;return t;})());")) +def _no_transform(*p, w=None): + return p + + class Graphics(Builtin): r"""
@@ -384,7 +397,7 @@ class Graphics(Builtin): Invalid graphics directives yield invalid box structures: >> Graphics[Circle[{a, b}]] - : GraphicsBox[CircleBox[List[a, b]], 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[]]] is not a valid box structure. + : GraphicsBox[CircleBox[List[a, b]], 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 = { @@ -398,9 +411,10 @@ class Graphics(Builtin): 'ImageSize': 'Automatic', 'Background': 'Automatic', - 'PrecomputeTransformations': 'False', # 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 (strokes with width < 1e-6 won't get transformed in SVG). + '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. } box_suffix = 'Box' @@ -1122,11 +1136,11 @@ def init(self, graphics, style, item): self.edge_color, self.face_color = style.get_style( _Color, face_element=True) - self.p1 = coords(item.leaves[0]) + self.p1 = expr_to_coords(item.leaves[0]) if len(item.leaves) == 1: self.p2 = add_coords(self.p1, (1, 1)) elif len(item.leaves) == 2: - self.p2 = coords(item.leaves[1]) + self.p2 = expr_to_coords(item.leaves[1]) def extent(self): l = self.style.get_line_width(face_element=True) / 2 @@ -1179,7 +1193,7 @@ def init(self, graphics, style, item): raise BoxConstructError self.edge_color, self.face_color = style.get_style( _Color, face_element=self.face_element) - self.c = coords(item.leaves[0]) + self.c = expr_to_coords(item.leaves[0]) if len(item.leaves) == 1: rx = ry = 1 elif len(item.leaves) == 2: @@ -1303,7 +1317,7 @@ def path(closed): 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(transform) @@ -1352,7 +1366,8 @@ def do_init(self, graphics, points): lines.append(leaf.leaves) else: raise BoxConstructError - self.lines = [[coords(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) @@ -1467,7 +1482,7 @@ def init(self, graphics, style, item=None, lines=None): def to_svg(self, transform): l = self.style.get_line_width(face_element=False) - l = list(transform((l, l)))[0][0] # assume pure scaling transform + l = list(transform((l, l), w=0))[0][0] style = create_css(edge_color=self.edge_color, stroke_width=l) svg = '' @@ -1479,7 +1494,7 @@ def to_svg(self, transform): def to_asy(self, transform): l = self.style.get_line_width(face_element=False) - l = list(transform((l, l)))[0][0] # assume pure scaling transform + l = list(transform((l, l), w=0))[0][0] pen = create_pens(edge_color=self.edge_color, stroke_width=l) asy = '' @@ -1693,7 +1708,7 @@ def parse_component(segments): for part in parts: if part.get_head_name() != 'System`List': raise BoxConstructError - c.extend([coords(xy) for xy in part.leaves]) + c.extend([expr_to_coords(xy) for xy in part.leaves]) yield k, c @@ -2306,9 +2321,9 @@ def shrink_one_end(line, s): while s > 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 @@ -2566,7 +2581,7 @@ def points(): for content in self.contents: p = content.extent() for transform in self.transforms: - for q in transform(p): + for q in transform(*p): yield q return list(points()) @@ -2617,11 +2632,11 @@ def init(self, graphics, style, item=None, content=None, pos=None, self.content = content.format( graphics.evaluation, 'TraditionalForm') if len(item.leaves) > 1: - self.pos = coords(item.leaves[1]) + self.pos = expr_to_coords(item.leaves[1]) else: 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: @@ -2733,7 +2748,7 @@ def to_svg(self, transform): if not self.svg: if not is_absolute: - x, y = list(self.graphics.local_to_screen([(x, y)]))[0] + x, y = list(self.graphics.local_to_screen((x, y)))[0] svg = ( '' @@ -2986,6 +3001,9 @@ def convert(content, style): self.elements = list(convert(content, self.get_style_class()(self))) + def make_coords(self, points): # overriden by Graphics3DElements + return [expr_to_coords(p) for p in points] + def create_style(self, expr): style = self.get_style_class()(self) @@ -3054,7 +3072,7 @@ def add_axis_element(self, e): def translate(self, coords): if self.local_to_screen: - return list(self.local_to_screen([coords]))[0] + return list(self.local_to_screen(coords))[0] else: return coords[0], coords[1] @@ -3098,15 +3116,13 @@ def cut_coords(xy): "Cut values in graphics primitives (not displayed otherwise in SVG)" return min(max(xy[0], -border), border), min(max(xy[1], -border), border) - def cut(*p): + def cut(*p, w=None): return [cut_coords(q) for q in p] return '\n'.join(element.to_svg(cut) for element in self.elements) def to_asy(self): - def no_transform(*p): - return p - return '\n'.join(element.to_asy(no_transform) for element in self.elements) + return '\n'.join(element.to_asy(_no_transform) for element in self.elements) class GraphicsBox(BoxConstruct): @@ -3189,7 +3205,7 @@ def _prepare_elements(self, leaves, options, neg_y=False, max_width=None): transformation = Expression('System`TransformationFunction', [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) - precompute_transformations = graphics_options['System`PrecomputeTransformations'].is_true() + precompute_transformations = graphics_options['System`Transformation'].get_string_value() == 'Precomputed' elements = GraphicsElements( Expression('System`GeometricTransformationBox', leaves[0], transformation), @@ -3324,11 +3340,11 @@ def boxes_to_tex(self, leaves, **options): xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() asy_completely_visible = '\n'.join( - element.to_asy() for element in elements.elements + element.to_asy(_no_transform) for element in elements.elements if element.is_completely_visible) asy_regular = '\n'.join( - element.to_asy() for element in elements.elements + element.to_asy(_no_transform) for element in elements.elements if not element.is_completely_visible) asy_box = 'box((%s,%s), (%s,%s))' % (asy_number(xmin), asy_number(ymin), asy_number(xmax), asy_number(ymax)) diff --git a/mathics/builtin/graphics3d.py b/mathics/builtin/graphics3d.py index 85e1785f2e..a179f76762 100644 --- a/mathics/builtin/graphics3d.py +++ b/mathics/builtin/graphics3d.py @@ -661,14 +661,15 @@ 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 self.xmin = self.ymin = self.pixel_width = self.pixel_height = \ self.extent_width = self.extent_height = 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]) diff --git a/mathics/builtin/plot.py b/mathics/builtin/plot.py index 0cd4e5fe58..796ecd206f 100644 --- a/mathics/builtin/plot.py +++ b/mathics/builtin/plot.py @@ -599,12 +599,12 @@ def find_excl(excl): # 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 with properly. since we scale a + # 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 to we end up with an empty plot. in order to fix this, - # PrecomputeTransformations simply gets rid of the SVG transformations and passes the scaled coordinates + # 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`PrecomputeTransformations'] = Symbol('True') + options['System`Transformation'] = String('Precomputed') return Expression('Graphics', Expression('List', *graphics), *options_to_rules(options)) From fb2d1db14e0b98ad8d08645dbccee1e635f56fff Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 5 Nov 2016 22:40:05 +0100 Subject: [PATCH 34/48] fixes test case --- mathics/builtin/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index dbadfd7e02..b0e556789f 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -3887,7 +3887,7 @@ class Automatic(Builtin): graphical options: >> Cases[Options[Plot], HoldPattern[_ :> Automatic]] - = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic} + = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic, Transformation :> Automatic} ''' From e85bef5c738462d0b31fedda8273cc105c643de6 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 30 Aug 2020 11:54:03 +0200 Subject: [PATCH 35/48] fix node.js server startup --- mathics/layout/client.py | 17 +++++++++++------ mathics/layout/server.js | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index a40512b988..776bf763ca 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -4,9 +4,9 @@ # Your installation of nodejs with the following packages: mathjax-node svg2png (install them using # npm). -# Tips for installing nodejs on OS X: -# see https://gist.github.com/DanHerbert/9520689 -# export NODE_PATH=/your/path/to/homebrew/bin/node_modules:$NODE_PATH +# Tips for troubleshooting: +# https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally +# export NODE_PATH=$(npm root --quiet -g) import subprocess from subprocess import Popen @@ -162,7 +162,7 @@ def _create_client(self): server_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'server.js') - if True: + if False: # fixes problems on Windows network drives import tempfile fd, copied_path = tempfile.mkstemp(suffix='js') @@ -172,7 +172,12 @@ def _create_client(self): server_path = copied_path def abort(message): - error_text = 'Node.js failed to startup %s:\n\n' % server_path + error_text = '\n'.join([ + '', + 'Node.js failed to start Mathics server.', + 'You might need to run: npm install -g mathjax-node svg2png', + '', + '']) raise WebEngineUnavailable(error_text + message) process = Popen( @@ -192,7 +197,7 @@ def abort(message): error += ' ' + line process.terminate() - abort(error + '\nPlease check Node.js modules and NODE_PATH') + abort(error) port = int(status[len(hello):]) except OSError as e: diff --git a/mathics/layout/server.js b/mathics/layout/server.js index bedfdd8f54..5a33f4be63 100644 --- a/mathics/layout/server.js +++ b/mathics/layout/server.js @@ -1,4 +1,4 @@ -// to install: npm install mathjax-node svg2png +// to install: npm install -g mathjax-node svg2png try { function server(methods) { @@ -72,7 +72,7 @@ try { server.listen(0); // pick a free port } - var mathjax = require("mathjax-node/lib/mj-single.js"); + var mathjax = require("mathjax-node"); mathjax.config({ MathJax: { // traditional MathJax configuration From 386767f145a271ebaf125c9ff106838b0674df6a Mon Sep 17 00:00:00 2001 From: mmatera Date: Thu, 13 May 2021 12:27:53 -0300 Subject: [PATCH 36/48] fix --- mathics/builtin/drawing/plot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 8a8e9d69e7..7d7d945bd1 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -1064,6 +1064,9 @@ def rectangles(): ) last_x1 = x1 + yield Expression( + "Line", Expression(SymbolList, vector2(0, 0), vector2(last_x1, 0)) + ) # 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. @@ -1074,9 +1077,6 @@ def rectangles(): # into the SVG. this also has the advantage that we can precompute with arbitrary precision using mpmath. options['System`Transformation'] = String('Precomputed') - yield Expression( - "Line", Expression(SymbolList, vector2(0, 0), vector2(last_x1, 0)) - ) def axes(): yield Expression("FaceForm", Symbol("Black")) From 1d1979f3ca408b8c7ea8c8281036bfdb68a7602d Mon Sep 17 00:00:00 2001 From: mmatera Date: Thu, 13 May 2021 14:35:35 -0300 Subject: [PATCH 37/48] fix3 --- mathics/builtin/graphics.py | 36 ++++++++++++++++++++++++++---------- mathics/builtin/optiondoc.py | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 9f4fe71225..b32238eba3 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -3473,8 +3473,12 @@ def __new__(cls, *leaves, **kwargs): def boxes_to_text(self, leaves=None, **options): if not leaves: leaves = self._leaves - - self._prepare_elements(leaves, options) # to test for Box errors + try: + self._prepare_elements(leaves, options) # to test for Box errors + except Exception as e: + if self.evaluation: + self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) + return return "-Graphics-" def _get_image_size(self, options, graphics_options, max_width): @@ -3699,10 +3703,14 @@ def get_range(min, max): def boxes_to_tex(self, leaves=None, **options): if not leaves: leaves = self._leaves - elements, calc_dimensions = self._prepare_elements( - leaves, options, max_width=450 - ) - + try: + elements, calc_dimensions = self._prepare_elements( + leaves, options, max_width=450 + ) + except: + if self.evaluation: + self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) + return xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() asy_completely_visible = "\n".join( @@ -3758,9 +3766,13 @@ def to_svg(self, leaves=None, **options): if data: elements, xmin, xmax, ymin, ymax, w, h, width, height = data else: - elements, calc_dimensions = self._prepare_elements( - leaves, options, neg_y=True - ) + try: + elements, calc_dimensions = self._prepare_elements( + leaves, options, neg_y=True + ) + except Exception as e: + print(e) + return xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() elements.view_width = w @@ -3800,8 +3812,12 @@ def to_svg(self, leaves=None, **options): def boxes_to_mathml(self, leaves=None, **options): if not leaves: leaves = self._leaves + try: + elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) + except: + if self.evaluation: + self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) - elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() data = (elements, xmin, xmax, ymin, ymax, w, h, width, height) diff --git a/mathics/builtin/optiondoc.py b/mathics/builtin/optiondoc.py index 8f3cf10df4..487fe225d8 100644 --- a/mathics/builtin/optiondoc.py +++ b/mathics/builtin/optiondoc.py @@ -30,7 +30,7 @@ class Automatic(Builtin): graphical options: >> Cases[Options[Plot], HoldPattern[_ :> Automatic]] - = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic} + = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic, Transformation :> Automatic} """ class Axes(Builtin): From 346dadfbaa9c7a07c9038de2aac289dc279698be Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 14 May 2021 06:33:56 -0300 Subject: [PATCH 38/48] fix6 --- mathics/builtin/graphics.py | 96 ++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index b32238eba3..bb53c21e68 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -389,7 +389,7 @@ def to_svg(self, svg): # b d f # 0 0 1 - t = "matrix(%f, %f, %f, %f, %f, %f)" % (a, b, c, d, e, f) + 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): @@ -1347,11 +1347,11 @@ def to_svg(self, transform): w = max(x1, x2) - xmin h = max(y1, y2) - ymin 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, ) @@ -1363,14 +1363,14 @@ def to_asy(self, transform): 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, ) @@ -1415,11 +1415,11 @@ def to_svg(self, transform): 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, ) @@ -1505,12 +1505,12 @@ def to_svg(self, 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 "M %s,%s" % (str(sx), str(sy)) - yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey) + yield "A %s,%s,0,%d,0,%s,%s" % (str(rx), str(ry), large_arc, str(ex), str(ey)) if closed: yield "Z" @@ -1650,10 +1650,10 @@ def to_svg(self, transform): for line in self.lines: for x, y in transform(*line): svg += '' % ( - x, - y, - size_x, - size_y, + float(x), + float(y), + float(size_x), + float(size_y), style, ) return svg @@ -1709,7 +1709,7 @@ def to_svg(self, transform): svg = "" for line in self.lines: - path = " ".join(["%f,%f" % c for c in transform(*line)]) + path = " ".join(["%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)]) svg += '' % (path, style) return svg @@ -1721,7 +1721,7 @@ def to_asy(self, transform): asy = "" for line in self.lines: - path = "--".join(["(%.5g,%5g)" % c for c in transform(*line)]) + path = "--".join(["(%.5g,%5g)" % (str(cc) for cc in c) for c in transform(*line)]) asy += "draw(%s, %s);" % (path, pen) return asy @@ -1745,11 +1745,11 @@ 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 @@ -2071,7 +2071,7 @@ def to_svg(self, transform): svg += '' % json.dumps(mesh) for line in self.lines: svg += '' % ( - " ".join("%f,%f" % c for c in transform(*line)), + " ".join("%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)), style, ) return svg @@ -2963,7 +2963,7 @@ def extent(self): def _prepare_text_svg(self): self.graphics.evaluation.output.assume_web_engine() - content = self.content.boxes_to_xml(evaluation=self.graphics.evaluation) + content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation) svg = self.graphics.evaluation.output.mathml_to_svg("%s" % content) @@ -3007,12 +3007,12 @@ def _text_svg_xml(self, style, x, y, absolute): return ( '%s' % ( - x, - y, - scale * tx, - scale * ty, - -width / 2 - ox * width / 2, - -height / 2 + oy * height / 2, + float(x), + float(y), + float(scale * tx), + float(scale * ty), + float(-width / 2 - ox * width / 2), + float(-height / 2 + oy * height / 2), svg, ) ) @@ -3036,7 +3036,7 @@ def to_svg(self, transform): svg = ( '' "%s" - ) % (x, y, self.opos[0], self.opos[1], style, content) + ) % (float(x), float(y), float(self.opos[0]), float(self.opos[1]), style, content) if not is_absolute: svg = self.graphics.inverse_local_to_screen.to_svg(svg) @@ -3074,8 +3074,8 @@ def to_asy(self, transform): pen = create_pens(edge_color=self.color) asy = 'label("$%s$", (%s,%s), (%s,%s), %s);' % ( content, - x, - y, + float(x), + float(y), -self.opos[0], -self.opos[1], pen, @@ -3777,14 +3777,14 @@ def to_svg(self, leaves=None, **options): elements.view_width = w - svg = elements.to_svg(offset=options.get("offset", None)) + svg = elements.to_svg() if self.background_color is not None: svg = '%s' % ( - xmin, - ymin, - w, - h, + float(xmin), + float(ymin), + float(w), + float(h), self.background_color.to_css()[0], svg, ) @@ -3804,7 +3804,7 @@ def to_svg(self, leaves=None, **options): %s """ % ( - " ".join("%f" % t for t in (xmin, ymin, w, h)), + " ".join("%f" % float(t) for t in (xmin, ymin, w, h)), svg, ) return svg_xml # , width, height From b5ecad4252e10614e83175e69fe0273f5d744fab Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 14 May 2021 06:51:09 -0300 Subject: [PATCH 39/48] fix test --- mathics/builtin/graphics.py | 2 +- mathics/server.py | 130 ------ mathics/web/media/js/mathics.js | 766 -------------------------------- mathics/web/views.py | 470 -------------------- 4 files changed, 1 insertion(+), 1367 deletions(-) delete mode 100755 mathics/server.py delete mode 100644 mathics/web/media/js/mathics.js delete mode 100644 mathics/web/views.py diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index bb53c21e68..8a1c44727b 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -509,7 +509,7 @@ class Graphics(Builtin): Invalid graphics directives yield invalid box structures: >> Graphics[Circle[{a, b}]] - : GraphicsBox[CircleBox[List[a, b]], 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. + : GraphicsBox[{CircleBox[{a, b}], $OptionSyntax -> Ignore, AspectRatio -> Automatic, Axes -> False, AxesStyle -> {}, Background -> Automatic, ImageSize -> Automatic, LabelStyle -> {}, PlotRange -> Automatic, PlotRangePadding -> Automatic, TicksStyle -> {}, Transformation -> Automatic}] is not a valid box structure. """ options = GRAPHICS_OPTIONS diff --git a/mathics/server.py b/mathics/server.py deleted file mode 100755 index adab9b4b91..0000000000 --- a/mathics/server.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import sys -import os -import argparse -import socket -import errno -import subprocess - -import mathics -from mathics import server_version_string, license_string -from mathics import settings as mathics_settings # Prevents UnboundLocalError -from mathics.layout.client import WebEngine - -web_engine = None - - -def check_database(): - # Check for the database - database_file = mathics_settings.DATABASES['default']['NAME'] - - if not os.path.exists(database_file): - print("warning: database file %s not found\n" % database_file) - if not os.path.exists(mathics_settings.DATA_DIR): - print("Creating data directory %s" % mathics_settings.DATA_DIR) - os.makedirs(mathics_settings.DATA_DIR) - - print("Migrating database %s" % database_file) - manage_file = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "manage.py") - try: - subprocess.check_call( - [sys.executable, manage_file, 'migrate', '--noinput']) - print("\ndatabase initialized sucessfully") - except subprocess.CalledProcessError: - print("error: failed to create database") - sys.exit(1) - - -def parse_args(): - argparser = argparse.ArgumentParser( - prog='mathicsserver', - usage='%(prog)s [options]', - add_help=False, - description="""Mathics server for the graphical user interface in a - web browser. It is not intended for production use on a public Web - server!""", - epilog="""Please feel encouraged to contribute to Mathics! Create - your own fork, make the desired changes, commit, and make a pull - request.""") - - argparser.add_argument( - '--help', '-h', help='show this help message and exit', action='help') - argparser.add_argument( - '--quiet', '-q', help='don\'t print message at startup', - action='store_true') - argparser.add_argument( - '--version', '-v', action='version', - version='%(prog)s ' + mathics.__version__) - argparser.add_argument( - "--port", "-p", dest="port", metavar="PORT", default=8000, type=int, - help="use PORT as server port") - argparser.add_argument( - "--external", "-e", dest="external", action="store_true", - help="allow external access to server") - - return argparser.parse_args() - - -def launch_app(args): - quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' - port = args.port - - if not args.quiet: - print() - print(server_version_string) - print() - print(license_string) - print() - print("Quit by pressing %s\n" % quit_command) - print("""Open the graphical user interface at -http://localhost:%d\nin Firefox, Chrome, or Safari to use Mathics\n""" % port) - - if args.external: - addr = '0.0.0.0' - else: - addr = '127.0.0.1' - - global web_engine - web_engine = WebEngine() - - try: - from django.core.servers.basehttp import ( - run, get_internal_wsgi_application) - handler = get_internal_wsgi_application() - run(addr, port, handler) - except socket.error as e: - # Use helpful error messages instead of ugly tracebacks. - ERRORS = { - errno.EACCES: "You don't have permission to access that port.", - errno.EADDRINUSE: "That port is already in use.", - errno.EADDRNOTAVAIL: "That IP address can't be assigned to.", - } - try: - error_text = ERRORS[e.errno] - except KeyError: - error_text = str(e) - sys.stderr.write("Error: %s" % error_text + '\n') - if web_engine is not None: - web_engine.terminate() - # Need to use an OS exit because sys.exit doesn't work in a thread - os._exit(1) - except KeyboardInterrupt: - print("\nGoodbye!\n") - if web_engine is not None: - web_engine.terminate() - sys.exit(0) - - -def main(): - os.environ['DJANGO_SETTINGS_MODULE'] = 'mathics.settings' - check_database() - args = parse_args() - launch_app(args) - - -if __name__ == '__main__': - main() diff --git a/mathics/web/media/js/mathics.js b/mathics/web/media/js/mathics.js deleted file mode 100644 index 1a52284297..0000000000 --- a/mathics/web/media/js/mathics.js +++ /dev/null @@ -1,766 +0,0 @@ -var deleting; -var blurredElement; - -var movedItem; - -var clickedQuery; - -var lastFocus = null; - -var welcome = true; - -function getLetterWidth(element) { - var letter = $E('span', $T('m')); - letter.setStyle({ - fontFamily: element.getStyle('font-family'), - fontSize: element.getStyle('font-size') - }); - var parent = $$('body')[0]; - parent.appendChild(letter); - var width = letter.getWidth(); - parent.removeChild(letter); - delete letter; - return width; -} - -function refreshInputSize(textarea) { - var letterWidth = getLetterWidth(textarea); - var width = textarea.getWidth() - 15; - var lines = textarea.value.split('\n'); - var lineCount = 0; - for (var index = 0; index < lines.length; ++index) { - var line = lines[index]; - lineCount += Math.ceil(1.0 * (line.length + 1) * letterWidth / width); - } - textarea.rows = lineCount; -} - -function refreshInputSizes() { - $$('textarea.request').each(function(textarea) { - refreshInputSize(textarea); - }); - - $$('#queries ul').each(function(ul) { - afterProcessResult(ul, 'Rerender'); - }); -} - -function inputChange(event) { - refreshInputSize(this); -} - -function isEmpty(textarea) { - return textarea.value.strip() == '' && !textarea.submitted; -} - -function prepareText(text) { - if (text == '') { - text = String.fromCharCode(160); - } - return text; - - /* - // Place ­ between every two characters. - // Problem: Copy & paste yields weird results! - var result = ''; - for (var index = 0; index < text.length; ++index) { - result += text.charAt(index); - if (index < text.length - 1) - result += String.fromCharCode(173); // ­ - } - return result; - */ -} - -function getDimensions(math, callback) { - var all = $('calc_all').cloneNode(true); - all.id = null; - var body = $$('body')[0]; - body.appendChild(all); - var container = all.select('.calc_container')[0]; - container.appendChild(translateDOMElement(math)); - - MathJax.Hub.Queue(["Typeset", MathJax.Hub, container]); - MathJax.Hub.Queue(function() { - var pos = container.cumulativeOffset(); - var next = all.select('.calc_next')[0].cumulativeOffset(); - var below = all.select('.calc_below')[0].cumulativeOffset(); - var width = next.left - pos.left + 4; - var height = below.top - pos.top + 20; - body.removeChild(all); - callback(width, height); - }); -} - -function drawMeshGradient(ctx, points) { - function color(c, a) { - var result = 'rgba(' + Math.round(c[0]*255) + ', ' + Math.round(c[1]*255) + ', ' + - Math.round(c[2]*255) + ', ' + a + ')'; - return result; - } - - var grad1 = ctx.createLinearGradient(0, 0, 0.5, 0.5); - grad1.addColorStop(0, color(points[0][1], 1)); - grad1.addColorStop(1, color(points[0][1], 0)); - var grad2 = ctx.createLinearGradient(1, 0, 0, 0); - grad2.addColorStop(0, color(points[1][1], 1)); - grad2.addColorStop(1, color(points[1][1], 0)); - var grad3 = ctx.createLinearGradient(0, 1, 0, 0); - grad3.addColorStop(0, color(points[2][1], 1)); - grad3.addColorStop(1, color(points[2][1], 0)); - - ctx.save(); - ctx.setTransform(points[1][0][0]-points[0][0][0], points[1][0][1]-points[0][0][1], - points[2][0][0]-points[0][0][0], points[2][0][1]-points[0][0][1], points[0][0][0], points[0][0][1]); - - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(1, 0); - ctx.lineTo(0, 1); - ctx.closePath(); - - ctx.globalCompositeOperation = "lighter"; - ctx.fillStyle = grad1; - ctx.fill(); - ctx.fillStyle = grad2; - ctx.fill(); - ctx.fillStyle = grad3; - ctx.fill(); - ctx.restore(); -} - -function createMathNode(nodeName) { - if (['svg', 'g', 'rect', 'circle', 'polyline', 'polygon', 'path', 'ellipse', 'foreignObject'].include(nodeName)) - return document.createElementNS("http://www.w3.org/2000/svg", nodeName); - else { - return document.createElement(nodeName); - } -} - -var objectsPrefix = 'math_object_'; -var objectsCount = 0; -var objects = {}; - -function translateDOMElement(element, svg) { - if (element.nodeType == 3) { - var text = element.nodeValue; - return $T(text); - } - - if (svg && element.nodeName == 'svg') { - // leave s embedded in s alone, if they are - // not > > . this fixes the - // node.js web engine svg rendering, which embeds text - // as in the Graphics . - var node = element; - var ok = false; - while (node != svg && node.parentNode) { - if (node.nodeName == 'foreignObject') { - ok = true; - break; - } - node = node.parentNode; - } - if (!ok) { - return element; - } - } - - var dom = null; - var nodeName = element.nodeName; - if (nodeName != 'meshgradient' && nodeName != 'graphics3d') { - dom = createMathNode(element.nodeName); - for (var i = 0; i < element.attributes.length; ++i) { - var attr = element.attributes[i]; - if (attr.nodeName != 'ox' && attr.nodeName != 'oy') - dom.setAttribute(attr.nodeName, attr.nodeValue); - } - } - if (nodeName == 'foreignObject') { - dom.setAttribute('width', svg.getAttribute('width')); - dom.setAttribute('height', svg.getAttribute('height')); - dom.setAttribute('style', dom.getAttribute('style') + '; text-align: left; padding-left: 2px; padding-right: 2px;'); - var ox = parseFloat(element.getAttribute('ox')); - var oy = parseFloat(element.getAttribute('oy')); - dom.setAttribute('ox', ox); - dom.setAttribute('oy', oy); - } - if (nodeName == 'mo') { - var op = element.childNodes[0].nodeValue; - if (op == '[' || op == ']' || op == '{' || op == '}' || op == String.fromCharCode(12314) || op == String.fromCharCode(12315)) - dom.setAttribute('maxsize', '3'); - } - if (nodeName == 'meshgradient') { - if (!MathJax.Hub.Browser.isOpera) { - var data = element.getAttribute('data').evalJSON(); - var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); - var foreign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); - foreign.setAttribute('width', svg.getAttribute('width')); - foreign.setAttribute('height', svg.getAttribute('height')); - foreign.setAttribute('x', '0px'); - foreign.setAttribute('y', '0px'); - foreign.appendChild(div); - - var canvas = createMathNode('canvas'); - canvas.setAttribute('width', svg.getAttribute('width')); - canvas.setAttribute('height', svg.getAttribute('height')); - div.appendChild(canvas); - - var ctx = canvas.getContext('2d'); - for (var index = 0; index < data.length; ++index) { - var points = data[index]; - if (points.length == 3) { - drawMeshGradient(ctx, points); - } - } - - dom = foreign; - } - } - var object = null; - if (nodeName == 'graphics3d') { - var data = element.getAttribute('data').evalJSON(); - var div = document.createElement('div'); - drawGraphics3D(div, data); - dom = div; - } - if (nodeName == 'svg' || nodeName == 'graphics3d' || nodeName.toLowerCase() == 'img') { - // create that will contain the graphics - object = createMathNode('mspace'); - var width, height; - if (nodeName == 'svg' || nodeName.toLowerCase() == 'img') { - width = dom.getAttribute('width'); - height = dom.getAttribute('height'); - } else { - // TODO: calculate appropriate height and recalculate on every view change - width = height = '400'; - } - object.setAttribute('width', width + 'px'); - object.setAttribute('height', height + 'px'); - } - if (nodeName == 'svg') - svg = dom; - var rows = [[]]; - $A(element.childNodes).each(function(child) { - if (child.nodeName == 'mspace' && child.getAttribute('linebreak') == 'newline') - rows.push([]); - else - rows[rows.length - 1].push(child); - }); - var childParent = dom; - if (nodeName == 'math') { - var mstyle = createMathNode('mstyle'); - mstyle.setAttribute('displaystyle', 'true'); - dom.appendChild(mstyle); - childParent = mstyle; - } - if (rows.length > 1) { - var mtable = createMathNode('mtable'); - mtable.setAttribute('rowspacing', '0'); - mtable.setAttribute('columnalign', 'left'); - var nospace = 'cell-spacing: 0; cell-padding: 0; row-spacing: 0; row-padding: 0; border-spacing: 0; padding: 0; margin: 0'; - mtable.setAttribute('style', nospace); - rows.each(function(row) { - var mtr = createMathNode('mtr'); - mtr.setAttribute('style', nospace); - var mtd = createMathNode('mtd'); - mtd.setAttribute('style', nospace); - row.each(function(element) { - var elmt = translateDOMElement(element, svg); - if (nodeName == 'mtext') { - // wrap element in mtext - var outer = createMathNode('mtext'); - outer.appendChild(elmt); - elmt = outer; - } - mtd.appendChild(elmt); - }); - mtr.appendChild(mtd); - mtable.appendChild(mtr); - }); - if (nodeName == 'mtext') { - // no mtable inside mtext, but mtable instead of mtext - dom = mtable; - } else - childParent.appendChild(mtable); - } else { - rows[0].each(function(element) { - childParent.appendChild(translateDOMElement(element, svg)); - }); - } - if (object) { - var id = objectsCount++; - object.setAttribute('id', objectsPrefix + id); - objects[id] = dom; - return object; - } - return dom; -} - -function convertMathGlyphs(dom) { - // convert mglyphs to their classic representation ( or ), so the new mglyph logic does not make - // anything worse in the classic Mathics frontend for now. In the long run, this code should vanish. - - var MML = "http://www.w3.org/1998/Math/MathML"; - var glyphs = dom.getElementsByTagName("mglyph"); - for (var i = 0; i < glyphs.length; i++) { - var glyph = glyphs[i]; - var src = glyph.getAttribute('src'); - if (src.startsWith('data:image/svg+xml;base64,')) { - var svgText = atob(src.substring(src.indexOf(",") + 1)); - var mtable = document.createElementNS(MML, "mtable"); - mtable.innerHTML = '' + svgText + ''; - var svg = mtable.getElementsByTagNameNS("*", "svg")[0]; - svg.setAttribute('width', glyph.getAttribute('width')); - svg.setAttribute('height', glyph.getAttribute('height')); - svg.setAttribute('data-mathics', 'format'); - glyph.parentNode.replaceChild(mtable, glyph); - } else if (src.startsWith('data:image/')) { - var img = document.createElement('img'); - img.setAttribute('src', src) - img.setAttribute('width', glyph.getAttribute('width')); - img.setAttribute('height', glyph.getAttribute('height')); - img.setAttribute('data-mathics', 'format'); - glyph.parentNode.replaceChild(img, glyph); - } - } -} - -function createLine(value) { - if (value.startsWith('s - ul.select('.mspace').each(function(mspace) { - var id = mspace.getAttribute('id').substr(objectsPrefix.length); - var object = objects[id]; - mspace.appendChild(object); - }); - }); - if (!MathJax.Hub.Browser.isOpera) { - // Opera 11.01 Build 1190 on Mac OS X 10.5.8 crashes on this call for Plot[x,{x,0,1}] - // => leave inner MathML untouched - MathJax.Hub.Queue(['Typeset', MathJax.Hub, ul]); - } - MathJax.Hub.Queue(function() { - ul.select('foreignObject >span >nobr >span.math').each(function(math) { - var content = math.childNodes[0].childNodes[0].childNodes[0]; - math.removeChild(math.childNodes[0]); - math.insertBefore(content, math.childNodes[0]); - - if (command == 'Typeset') { - // recalculate positions of insets based on ox/oy properties - var foreignObject = math.parentNode.parentNode.parentNode; - var dimensions = math.getDimensions(); - var w = dimensions.width + 4; - var h = dimensions.height + 4; - var x = parseFloat(foreignObject.getAttribute('x').substr()); - var y = parseFloat(foreignObject.getAttribute('y')); - var ox = parseFloat(foreignObject.getAttribute('ox')); - var oy = parseFloat(foreignObject.getAttribute('oy')); - x = x - w/2.0 - ox*w/2.0; - y = y - h/2.0 + oy*h/2.0; - foreignObject.setAttribute('x', x + 'px'); - foreignObject.setAttribute('y', y + 'px'); - } - }); - }); -} - -function setResult(ul, results) { - results.each(function(result) { - var resultUl = $E('ul', {'class': 'out'}); - result.out.each(function(out) { - var li = $E('li', {'class': (out.message ? 'message' : 'print')}); - if (out.message) - li.appendChild($T(out.prefix + ': ')); - li.appendChild(createLine(out.text)); - resultUl.appendChild(li); - }); - if (result.result != null) { - var li = $E('li', {'class': 'result'}, createLine(result.result)); - resultUl.appendChild(li); - } - ul.appendChild($E('li', {'class': 'out'}, resultUl)); - }); - afterProcessResult(ul); -} - -function submitQuery(textarea, onfinish) { - if (welcome) { - $('welcomeContainer').fade({duration: 0.2}); - if ($('hideStartupMsg').checked) localStorage.setItem('hideMathicsStartupMsg', 'true'); - welcome = false; - $('logo').removeClassName('load'); - } - - textarea.li.addClassName('loading'); - $('logo').addClassName('working'); - new Ajax.Request('/ajax/query/', { - method: 'post', - parameters: { - query: textarea.value - }, - onSuccess: function(transport) { - textarea.ul.select('li[class!=request][class!=submitbutton]').invoke('deleteElement'); - if (!transport.responseText) { - // A fatal Python error has occurred, e.g. on 4.4329408320439^43214234345 - // ("Fatal Python error: mp_reallocate failure") - // -> print overflow message - transport.responseText = '{"results": [{"out": [{"prefix": "General::noserver", "message": true, "tag": "noserver", "symbol": "General", "text": "No server running."}]}]}'; - } - var response = transport.responseText.evalJSON(); - setResult(textarea.ul, response.results); - textarea.submitted = true; - textarea.results = response.results; - var next = textarea.li.nextSibling; - if (next) - next.textarea.focus(); - else - createQuery(); - }, - onFailure: function(transport) { - textarea.ul.select('li[class!=request]').invoke('deleteElement'); - var li = $E('li', {'class': 'serverError'}, $T("Sorry, an error occurred while processing your request!")); - textarea.ul.appendChild(li); - textarea.submitted = true; - }, - onComplete: function() { - textarea.li.removeClassName('loading'); - $('logo').removeClassName('working'); - if (onfinish) - onfinish(); - } - }); -} - -function getSelection() { - // TODO -} - -function keyDown(event) { - var textarea = lastFocus; - if (!textarea) - return; - refreshInputSize(textarea); - - if (event.keyCode == Event.KEY_RETURN && (event.shiftKey || event.location == 3)) { - if (!Prototype.Browser.IE) - event.stop(); - - var query = textarea.value.strip(); - if (query) { - submitQuery(textarea); - } - } else if (event.keyCode == Event.KEY_UP) { - if (textarea.selectionStart == 0 && textarea.selectionEnd == 0) { - if (isEmpty(textarea)) { - if (textarea.li.previousSibling) - textarea.li.previousSibling.textarea.focus(); - } else - createQuery(textarea.li); - } - } else if (event.keyCode == Event.KEY_DOWN) { - if (textarea.selectionStart == textarea.value.length && textarea.selectionEnd == textarea.selectionStart) { - if (isEmpty(textarea)) { - if (textarea.li.nextSibling) - textarea.li.nextSibling.textarea.focus(); - } else - createQuery(textarea.li.nextSibling); - } - } else - if (isGlobalKey(event)) - event.stop(); -} - -function deleteMouseDown(event) { - if (event.isLeftClick()) - deleting = true; -} - -function deleteClick(event) { - if (lastFocus == this.li.textarea) - lastFocus = null; - this.li.deleteElement(); - deleting = false; - if (blurredElement) { - blurredElement.focus(); - blurredElement = null; - } - if ($('queries').childElements().length == 0) - createQuery(); - -} - -function moveMouseDown(event) { - movedItem = this.li; - movedItem.addClassName('moving'); -} - -function moveMouseUp(event) { - if (movedItem) { - movedItem.removeClassName('moving'); - movedItem.textarea.focus(); - movedItem = null; - } -} - -function onFocus(event) { - var textarea = this; - textarea.li.addClassName('focused'); - lastFocus = textarea; -} - -function onBlur(event) { - var textarea = this; - blurredElement = textarea; - if (!deleting && textarea.li != movedItem && isEmpty(textarea) && $('queries').childElements().length > 1) { - textarea.li.hide(); - if (textarea == lastFocus) - lastFocus = null; - window.setTimeout(function() { - textarea.li.deleteElement(); - }, 10); - } - textarea.li.removeClassName('focused'); -} - -function createSortable() { - Position.includeScrollOffsets = true; - Sortable.create('queries', { - handle: 'move', - scroll: 'document', - scrollSensitivity: 1 // otherwise strange flying-away of item at top - }); -} - -var queryIndex = 0; - -function createQuery(before, noFocus, updatingAll) { - var ul, textarea, moveHandle, deleteHandle, submitButton; - // Items need id in order for Sortable.onUpdate to work. - var li = $E('li', {'id': 'query_' + queryIndex++, 'class': 'query'}, - ul = $E('ul', {'class': 'query'}, - $E('li', {'class': 'request'}, - textarea = $E('textarea', {'class': 'request', 'spellcheck': 'false'}), - $E('span', {'class': 'submitbutton', 'title': "Evaluate [Shift+Return]"}, - submitButton = $E('span', $T('=')) - ) - ) - ), - moveHandle = $E('span', {'class': 'move'}), - deleteHandle = $E('span', {'class': 'delete', 'title': "Delete"}, $T(String.fromCharCode(215))) - ); - textarea.rows = 1; - textarea.ul = ul; - textarea.li = li; - textarea.submitted = false; - moveHandle.li = li; - deleteHandle.li = li; - li.textarea = textarea; - li.ul = ul; - if (before) - $('queries').insertBefore(li, before); - else - $('queries').appendChild(li); - if (!updatingAll) - refreshInputSize(textarea); - new Form.Element.Observer(textarea, 0.2, inputChange.bindAsEventListener(textarea)); - textarea.observe('focus', onFocus.bindAsEventListener(textarea)); - textarea.observe('blur', onBlur.bindAsEventListener(textarea)); - li.observe('mousedown', queryMouseDown.bindAsEventListener(li)); - deleteHandle.observe('click', deleteClick.bindAsEventListener(deleteHandle)); - deleteHandle.observe('mousedown', deleteMouseDown.bindAsEventListener(deleteHandle)); - moveHandle.observe('mousedown', moveMouseDown.bindAsEventListener(moveHandle)); - moveHandle.observe('mouseup', moveMouseUp.bindAsEventListener(moveHandle)); - $(document).observe('mouseup', moveMouseUp.bindAsEventListener($(document))); - submitButton.observe('mousedown', function() { - if (textarea.value.strip()) - submitQuery(textarea); - else - window.setTimeout(function() { - textarea.focus(); - }, 10); - }); - if (!updatingAll) { - createSortable(); - // calling directly fails in Safari on document loading - //window.setTimeout(createSortable, 10); - } - // Immediately setting focus doesn't work in IE. - if (!noFocus) - window.setTimeout(function() { - textarea.focus(); - }, 10); - return li; -} - -var mouseDownEvent = null; - -function documentMouseDown(event) { - if (event.isLeftClick()) { - if (clickedQuery) { - clickedQuery = null; - mouseDownEvent = null; - return; - } - event.stop(); // strangely, doesn't work otherwise - mouseDownEvent = event; - } -} - -function documentClick(event) { - // In Firefox, mousedown also fires when user clicks scrollbars. - // -> listen to click - event = mouseDownEvent; - if (!event) - return; - if ($('queries').childElements().length == 1 && isEmpty($('queries').childElements()[0].textarea)) { - $('queries').childElements()[0].textarea.focus(); - return; - } - var offset = $('document').cumulativeOffset(); - var y = event.pointerY() - offset.top + $('document').scrollTop; - var element = null; - $('queries').childElements().each(function(li) { - var offset = li.positionedOffset(); // margin-top: 10px - if (offset.top + 20 > y) { - element = li; - throw $break; - } - }); - createQuery(element); -} - -function queryMouseDown(event) { - clickedQuery = this; -} - -function focusLast() { - if (lastFocus) - lastFocus.focus(); - else - createQuery(); -} - -function isGlobalKey(event) { - if (event.ctrlKey) { - switch(event.keyCode) { - case 68: - // case 67: - case 83: - case 79: - return true; - } - } - return false; -} - -function globalKeyUp(event) { - if (!popup && event.ctrlKey) { - switch (event.keyCode) { - case 68: // D - showDoc(); - $('search').select(); - event.stop(); - break; - // case 67: // C - // focusLast(); - // event.stop(); - // break; - case 83: // S - showSave(); - break; - case 79: // O - showOpen(); - break; - } - } -} - -function domLoaded() { - MathJax.Hub.Config({ - "HTML-CSS": { - imageFont: null, - linebreaks: { automatic: true } - }, - MMLorHTML: { - // - // The output jax that is to be preferred when both are possible - // (set to "MML" for native MathML, "HTML" for MathJax's HTML-CSS output jax). - // - prefer: { - MSIE: "HTML", - Firefox: "HTML", - Opera: "HTML", - other: "HTML" - } - } - }); - MathJax.Hub.Configured(); - - if (localStorage.getItem('hideMathicsStartupMsg') === 'true') { - $('welcome').hide(); - } - - if ($('welcomeBrowser')) - if (!(Prototype.Browser.WebKit || Prototype.Browser.MobileSafari || Prototype.Browser.Gecko)) - $('welcomeBrowser').show(); - - $$('body')[0].observe('resize', refreshInputSizes); - - if ($('queriesContainer')) { - $('queriesContainer').appendChild($E('ul', {'id': 'queries'})); - - $('document').observe('mousedown', documentMouseDown.bindAsEventListener($('document'))); - $('document').observe('click', documentClick.bindAsEventListener($('document'))); - - $(document).observe('keydown', keyDown.bindAsEventListener()); - if (Prototype.Browser.IE) { - document.body.addEventListener('keydown', function(event) { - if (event.keyCode == Event.KEY_RETURN && event.shiftKey) { - event.stopPropagation(); - event.preventDefault(); - keyDown(event); - } - }, true); - } - if (Prototype.Browser.Opera || Prototype.Browser.IE) { - // Opera needs another hook so it doesn't insert newlines after Shift+Return - $(document).observe('keypress', function(event) { - if (event.keyCode == Event.KEY_RETURN && event.shiftKey) - event.stop(); - }.bindAsEventListener()); - } - - $(document).observe('keyup', globalKeyUp.bindAsEventListener($('document'))); - - if (!loadLink()) - createQuery(); - } -} - -$(document).observe('dom:loaded', domLoaded); -// Konqueror won't fire dom:loaded, so we still need body.onload. - -window.onresize = refreshInputSizes; diff --git a/mathics/web/views.py b/mathics/web/views.py deleted file mode 100644 index abaec2c809..0000000000 --- a/mathics/web/views.py +++ /dev/null @@ -1,470 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -import traceback - -from django.shortcuts import render -from django.template import RequestContext, loader -from django.http import ( - HttpResponse, - HttpResponseNotFound, - HttpResponseServerError, - Http404, -) -import json -from django.conf import settings -from django.contrib import auth -from django.contrib.auth.models import User - -from django.core.mail import send_mail - -from mathics.core.definitions import Definitions -from mathics.core.evaluation import Message, Result -from mathics.core.expression import Expression - -from mathics.web.models import Query, Worksheet, get_session_evaluation -from mathics.web.forms import LoginForm, SaveForm -from mathics.doc import documentation -from mathics.doc.doc import DocPart, DocChapter, DocSection - -from string import Template - -documentation.load_pymathics_doc() - -if settings.DEBUG: - JSON_CONTENT_TYPE = "text/html" -else: - JSON_CONTENT_TYPE = "application/json" - - -class JsonResponse(HttpResponse): - def __init__(self, result={}): - response = json.dumps(result) - super(JsonResponse, self).__init__(response, content_type=JSON_CONTENT_TYPE) - - -def require_ajax_login(func): - def new_func(request, *args, **kwargs): - if not request.user.is_authenticated(): - return JsonResponse({"requireLogin": True}) - return func(request, *args, **kwargs) - - return new_func - - -from mathics.settings import default_pymathics_modules - -definitions = Definitions(add_builtin=True, extension_modules=default_pymathics_modules) - - -def require_ajax_login(f): - return f - - -def main_view(request): - context = { - "login_form": LoginForm(), - "save_form": SaveForm(), - "require_login": settings.REQUIRE_LOGIN, - } - return render(request, "main.html", context) - - -def error_404_view(request, exception): - t = loader.get_template("404.html") - return HttpResponseNotFound( - t.render( - RequestContext( - request, - { - "title": "Page not found", - "request_path": request.path, - }, - ) - ) - ) - - -def error_500_view(request): - t = loader.get_template("500.html") - return HttpResponseServerError( - t.render( - RequestContext( - request, - { - "title": "Server error", - }, - ) - ) - ) - - -def query(request): - global definitions - from mathics.core.parser import MultiLineFeeder - - input = request.POST.get("query", "") - if settings.DEBUG and not input: - input = request.GET.get("query", "") - - if settings.LOG_QUERIES: - query_log = Query( - query=input, - error=True, - browser=request.META.get("HTTP_USER_AGENT", ""), - remote_user=request.META.get("REMOTE_USER", ""), - remote_addr=request.META.get("REMOTE_ADDR", ""), - remote_host=request.META.get("REMOTE_HOST", ""), - meta=str(request.META), - log="", - ) - query_log.save() - - from mathics.server import web_engine - - user_definitions = request.session.get('definitions') - definitions.set_user_definitions(user_definitions) - evaluation = Evaluation(definitions, format='xml', output=WebOutput(web_engine)) - feeder = MultiLineFeeder(input, '') - results = [] - try: - while not feeder.empty(): - expr = evaluation.parse_feeder(feeder) - if expr is None: - results.append(Result(evaluation.out, None, None)) # syntax errors - evaluation.out = [] - continue - result = evaluation.evaluate(expr, timeout=settings.TIMEOUT) - if result is not None: - results.append(result) - except SystemExit as e: - results = [] - result = None - definitions = Definitions(add_builtin=True, extension_modules=default_pymathics_modules) - evaluation.definitions = definitions - except Exception as exc: - if settings.DEBUG and settings.DISPLAY_EXCEPTIONS: - info = traceback.format_exception(*sys.exc_info()) - info = "\n".join(info) - msg = "Exception raised: %s\n\n%s" % (exc, info) - results.append(Result([Message("System", "exception", msg)], None, None)) - else: - raise - result = { - "results": [result.get_data() for result in results], - } - if settings.LOG_QUERIES: - query_log.timeout = evaluation.timeout - query_log.result = str(result) # evaluation.results - query_log.error = False - query_log.save() - - return JsonResponse(result) - - -# taken from http://code.activestate.com/recipes/410076/ - - -def nicepass(alpha=6, numeric=2): - """ - returns a human-readble password (say rol86din instead of - a difficult to remember K8Yn9muL ) - """ - import string - import random - - vowels = ["a", "e", "i", "o", "u"] - consonants = [a for a in string.ascii_lowercase if a not in vowels] - digits = string.digits - - # utility functions - def a_part(slen): - ret = "" - for i in range(slen): - if i % 2 == 0: - randid = random.randint(0, 20) # number of consonants - ret += consonants[randid] - else: - randid = random.randint(0, 4) # number of vowels - ret += vowels[randid] - return ret - - def n_part(slen): - ret = "" - for i in range(slen): - randid = random.randint(0, 9) # number of digits - ret += digits[randid] - return ret - - fpl = alpha / 2 - if alpha % 2: - fpl = int(alpha / 2) + 1 - lpl = alpha - fpl - - start = a_part(fpl) - mid = n_part(numeric) - end = a_part(lpl) - - return "%s%s%s" % (start, mid, end) - - -def email_user(user, subject, text): - send_mail( - subject, text, "noreply@mathics.net", [user.username], fail_silently=False - ) - - -def login(request): - if settings.DEBUG and not request.POST: - request.POST = request.GET - form = LoginForm(request.POST) - result = "" - general_errors = [] - if form.is_valid(): - email = form.cleaned_data["email"] - password = form.cleaned_data["password"] - if password: - user = auth.authenticate(username=email, password=password) - if user is None: - general_errors = ["Invalid username and/or password."] - else: - result = "ok" - auth.login(request, user) - else: - password = nicepass() - try: - user = User.objects.get(username=email) - result = "reset" - email_user( - user, - "Your password at mathics.net", - ( - """You have reset your password at mathics.net.\n -Your password is: %s\n\nYours,\nThe Mathics team""" - ) - % password, - ) - except User.DoesNotExist: - user = User(username=email, email=email) - result = "created" - email_user( - user, - "New account at mathics.net", - """Welcome to mathics.net!\n -Your password is: %s\n\nYours,\nThe Mathics team""" - % password, - ) - user.set_password(password) - user.save() - - return JsonResponse( - { - "result": result, - "form": form.as_json(general_errors=general_errors), - } - ) - - -def logout(request): - auth.logout(request) - return JsonResponse() - - -@require_ajax_login -def save(request): - if settings.DEBUG and not request.POST: - request.POST = request.GET - if settings.REQUIRE_LOGIN and not request.user.is_authenticated(): - raise Http404 - form = SaveForm(request.POST) - overwrite = request.POST.get("overwrite", False) - result = "" - if form.is_valid(): - content = request.POST.get("content", "") - name = form.cleaned_data["name"] - user = request.user - if not user.is_authenticated(): - user = None - try: - worksheet = Worksheet.objects.get(user=user, name=name) - if overwrite: - worksheet.content = content - else: - result = "overwrite" - except Worksheet.DoesNotExist: - worksheet = Worksheet(user=user, name=name, content=content) - worksheet.save() - - return JsonResponse( - { - "form": form.as_json(), - "result": result, - } - ) - - -def open(request): - if settings.REQUIRE_LOGIN and not request.user.is_authenticated(): - raise Http404 - user = request.user - name = request.POST.get("name", "") - try: - if user.is_authenticated(): - worksheet = user.worksheets.get(name=name) - else: - worksheet = Worksheet.objects.get(user__isnull=True, name=name) - content = worksheet.content - except Worksheet.DoesNotExist: - content = "" - - return JsonResponse( - { - "content": content, - } - ) - - -def get_worksheets(request): - if settings.REQUIRE_LOGIN and not request.user.is_authenticated(): - result = [] - else: - if request.user.is_authenticated(): - result = list(request.user.worksheets.order_by("name").values("name")) - else: - result = list( - Worksheet.objects.filter(user__isnull=True) - .order_by("name") - .values("name") - ) - return JsonResponse( - { - "worksheets": result, - } - ) - - -# auxiliary function - - -def render_doc(request, template_name, context, data=None, ajax=False): - object = context.get("object") - context.update( - { - "ajax": ajax, - "help_base": ("doc/base_ajax.html" if ajax else "doc/base_standalone.html"), - "prev": object.get_prev() if object else None, - "next": object.get_next() if object else None, - } - ) - if not ajax: - context.update( - { - "data": data, - } - ) - - result = render(request, "doc/%s" % template_name, context) - if not ajax: - return result - - result = { - "content": result.getvalue().decode("utf-8"), - } - if data is not None: - result["data"] = data - return JsonResponse(result) - - -def doc(request, ajax=""): - return render_doc( - request, - "overview.html", - { - "title": "Documentation", - "doc": documentation, - }, - ajax=ajax, - ) - - -def doc_part(request, part, ajax=""): - part = documentation.get_part(part) - if not part: - raise Http404 - return render_doc( - request, - "part.html", - { - "title": part.get_title_html(), - "part": part, - "object": part, - }, - ajax=ajax, - ) - - -def doc_chapter(request, part, chapter, ajax=""): - chapter = documentation.get_chapter(part, chapter) - if not chapter: - raise Http404 - return render_doc( - request, - "chapter.html", - { - "title": chapter.get_title_html(), - "chapter": chapter, - "object": chapter, - }, - ajax=ajax, - ) - - -def doc_section(request, part, chapter, section, ajax=""): - section = documentation.get_section(part, chapter, section) - if not section: - raise Http404 - data = section.html_data() - return render_doc( - request, - "section.html", - { - "title": section.get_title_html(), - "title_operator": section.operator, - "section": section, - "object": section, - }, - data=data, - ajax=ajax, - ) - - -def doc_search(request): - query = request.GET.get("query", "") - result = documentation.search(query) - if len([item for exact, item in result if exact]) <= 1: - for exact, item in result: - if exact or len(result) == 1: - if isinstance(item, DocPart): - return doc_part(request, item.slug, ajax=True) - elif isinstance(item, DocChapter): - return doc_chapter(request, item.part.slug, item.slug, ajax=True) - else: - return doc_section( - request, - item.chapter.part.slug, - item.chapter.slug, - item.slug, - ajax=True, - ) - result = [item for exact, item in result] - - return render_doc( - request, - "search.html", - { - "title": "Search documentation", - "result": result, - }, - ajax=True, - ) From ee18ee054d55ecae41e873b13fc8f04ec9fc7082 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 14 May 2021 10:34:09 -0300 Subject: [PATCH 40/48] fix 8 --- mathics/builtin/drawing/colors.py | 3 ++- mathics/builtin/drawing/graphics3d.py | 1 + mathics/builtin/graphics.py | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/drawing/colors.py b/mathics/builtin/drawing/colors.py index 23992c8eff..c9ac141bd6 100644 --- a/mathics/builtin/drawing/colors.py +++ b/mathics/builtin/drawing/colors.py @@ -412,6 +412,7 @@ def lab_to_xyz(l, a, b, *rest): def convert(components, src, dst, preserve_alpha=True): if not preserve_alpha: + print("not preserve alpha") if src == "Grayscale": non_alpha = 1 elif src == "CMYK": @@ -430,7 +431,7 @@ def omit_alpha(*c): path = _paths.get((src, dst), None) if path is None: return None - + print(" components=", components) for s, d in zip(path[:-1], path[1:]): func = conversions.get("%s>%s" % (s, d)) if not func: diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index 9ea754a862..b95ccc6d83 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -772,6 +772,7 @@ 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] diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 8a1c44727b..8f4a9cb436 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -635,8 +635,8 @@ def init(self, item=None, components=None): # become RGBColor[0, 0, 0, 1]. does not seem the right thing # to do in this general context. poke1024 - # if len(components) < 3: - # components.extend(self.default_components[len(components) :]) + if len(components) < 3: + components.extend(self.default_components[len(components) :]) self.components = components else: @@ -683,6 +683,7 @@ def to_rgba(self): return self.to_color_space("RGB") def to_color_space(self, color_space): + print("colorspace=", self.color_space, "components:", self.components) components = convert_color(self.components, self.color_space, color_space) if components is None: raise ValueError( @@ -1721,7 +1722,7 @@ def to_asy(self, transform): asy = "" for line in self.lines: - path = "--".join(["(%.5g,%5g)" % (str(cc) for cc in c) for c in transform(*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 @@ -2062,7 +2063,7 @@ def to_svg(self, transform): mesh = [] for index, line in enumerate(self.lines): data = [ - [coords, color.to_js()] + [[float(c) for c in coords], color.to_js()] for coords, color in zip( transform(*line), self.vertex_colors[index] ) From 82e3ad82ef0360b62430e0eacdf6e59f853d2cf2 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 14 May 2021 20:58:42 -0300 Subject: [PATCH 41/48] fixaa --- mathics/builtin/drawing/colors.py | 2 -- mathics/builtin/graphics.py | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/drawing/colors.py b/mathics/builtin/drawing/colors.py index c9ac141bd6..ed55430fdc 100644 --- a/mathics/builtin/drawing/colors.py +++ b/mathics/builtin/drawing/colors.py @@ -412,7 +412,6 @@ def lab_to_xyz(l, a, b, *rest): def convert(components, src, dst, preserve_alpha=True): if not preserve_alpha: - print("not preserve alpha") if src == "Grayscale": non_alpha = 1 elif src == "CMYK": @@ -431,7 +430,6 @@ def omit_alpha(*c): path = _paths.get((src, dst), None) if path is None: return None - print(" components=", components) for s, d in zip(path[:-1], path[1:]): func = conversions.get("%s>%s" % (s, d)) if not func: diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 8f4a9cb436..82c8652820 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -284,7 +284,6 @@ 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 ) @@ -297,7 +296,7 @@ def _extract_graphics(graphics, format, evaluation): # 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: @@ -1425,7 +1424,10 @@ def to_svg(self, transform): ) def to_asy(self, transform): - c, r = transform(self.c, self.r) + if transform: + c, r = transform(self.c, self.r) + else: + c, r = self.c, self.r x, y = c rx, ry = r rx -= x @@ -3815,6 +3817,7 @@ def boxes_to_mathml(self, leaves=None, **options): leaves = self._leaves try: elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) + print("calc_dimensions:", calc_dimensions) except: if self.evaluation: self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) From e26e1d226d4657b3019ac86ea645189f6d38dc42 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 14 May 2021 22:09:30 -0300 Subject: [PATCH 42/48] fixxxxx --- mathics/builtin/graphics.py | 3 +-- mathics/builtin/inout.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 82c8652820..4a6d9118ac 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -682,7 +682,6 @@ def to_rgba(self): return self.to_color_space("RGB") def to_color_space(self, color_space): - print("colorspace=", self.color_space, "components:", self.components) components = convert_color(self.components, self.color_space, color_space) if components is None: raise ValueError( @@ -3817,10 +3816,10 @@ def boxes_to_mathml(self, leaves=None, **options): leaves = self._leaves try: elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) - print("calc_dimensions:", calc_dimensions) except: if self.evaluation: self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) + return xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() data = (elements, xmin, xmax, ymin, ymax, w, h, width, height) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index e51a02d0fb..476e8737b5 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -37,7 +37,8 @@ PrecisionReal, SymbolList, SymbolMakeBoxes, - SymbolRule + SymbolNull, + SymbolRule, ) from mathics.core.numbers import ( dps, @@ -2101,7 +2102,10 @@ def apply_mathml(self, expr, evaluation) -> Expression: "notboxes", Expression("FullForm", boxes).evaluate(evaluation), ) - xml = "" + xml = None + if xml is None: + return Expression("RowBox", Expression(SymbolList, String(""))) + is_a_picture = xml[:6] == " Expression: boxes = MakeBoxes(expr).evaluate(evaluation) try: tex = boxes.boxes_to_tex(evaluation=evaluation) + if tex is None: + return Expression("RowBox", Expression(SymbolList, String(""))) # Replace multiple newlines by a single one e.g. between asy-blocks tex = MULTI_NEWLINE_RE.sub("\n", tex) From 6448755a383c92f5aa40bf028fd55ed90fbf9155 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 15 May 2021 10:59:29 -0300 Subject: [PATCH 43/48] fixxx --- mathics/builtin/drawing/graphics3d.py | 2 +- mathics/builtin/graphics.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index b95ccc6d83..46e2d27c92 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -837,7 +837,7 @@ 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 in coords.pos()[0]) for coords in line), pen ) for line in self.lines ) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 4a6d9118ac..d1b18d62c2 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -1666,7 +1666,7 @@ def to_asy(self, transform): asy = "" for line in self.lines: for x, y in transform(*line): - asy += "dot(%s, %s);" % ((x, y), pen) + asy += "dot(%f, %f);" % ((float(x), float(y)), pen) return asy From abd24d34cc49a120058213272b5b0e9e1ff53284 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 15 May 2021 11:15:12 -0300 Subject: [PATCH 44/48] merge --- mathics/builtin/drawing/graphics3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index 46e2d27c92..b3961d48e0 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -837,7 +837,7 @@ def to_asy(self): return "".join( "path3 g={0}--cycle;dot(g, {1});".format( - "--".join("(%.5g,%.5g,%.5g)" % tuple(float(x) for in 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 ) From 9bc11284c691deffa320b7ff18d7a9fe6bde2cba Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 15 May 2021 12:02:06 -0300 Subject: [PATCH 45/48] fix plot --- mathics/builtin/drawing/colors.py | 1 + mathics/builtin/drawing/plot.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/mathics/builtin/drawing/colors.py b/mathics/builtin/drawing/colors.py index ed55430fdc..23992c8eff 100644 --- a/mathics/builtin/drawing/colors.py +++ b/mathics/builtin/drawing/colors.py @@ -430,6 +430,7 @@ def omit_alpha(*c): path = _paths.get((src, dst), None) if path is None: return None + for s, d in zip(path[:-1], path[1:]): func = conversions.get("%s>%s" % (s, d)) if not func: diff --git a/mathics/builtin/drawing/plot.py b/mathics/builtin/drawing/plot.py index 7d7d945bd1..687bd102ed 100644 --- a/mathics/builtin/drawing/plot.py +++ b/mathics/builtin/drawing/plot.py @@ -696,6 +696,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) ) @@ -1064,19 +1073,10 @@ def rectangles(): ) last_x1 = x1 - yield Expression( - "Line", Expression(SymbolList, vector2(0, 0), vector2(last_x1, 0)) - ) - - # 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') + yield Expression( + "Line", Expression(SymbolList, vector2(0, 0), vector2(last_x1, 0)) + ) def axes(): yield Expression("FaceForm", Symbol("Black")) From 4327ba8bc6e867db86927b51b3313f514f49442e Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 15 May 2021 12:25:45 -0300 Subject: [PATCH 46/48] tmp --- mathics/builtin/drawing/graphics3d.py | 6 +- mathics/builtin/drawing/image.py | 20 ++++-- mathics/builtin/drawing/plot.py | 3 +- mathics/builtin/graphics.py | 73 ++++++++++++++------ mathics/builtin/inout.py | 13 ++-- mathics/builtin/optiondoc.py | 1 + mathics/builtin/tensors.py | 40 ++++++----- mathics/core/evaluation.py | 10 ++- mathics/layout/client.py | 98 +++++++++++++++------------ 9 files changed, 163 insertions(+), 101 deletions(-) diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index b3961d48e0..4d0a970a3c 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -837,7 +837,11 @@ def to_asy(self): return "".join( "path3 g={0}--cycle;dot(g, {1});".format( - "--".join("(%.5g,%.5g,%.5g)" % tuple(float(x) for x in 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 2f28c78c1c..c97af88cd4 100644 --- a/mathics/builtin/drawing/image.py +++ b/mathics/builtin/drawing/image.py @@ -2568,22 +2568,24 @@ class Rasterize(Builtin): requires = _image_requires options = { - 'RasterSize': '300', + "RasterSize": "300", } def apply(self, expr, evaluation, options): - 'Rasterize[expr_, OptionsPattern[%(name)s]]' + "Rasterize[expr_, OptionsPattern[%(name)s]]" - raster_size = self.get_option(options, 'RasterSize', evaluation) + 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): + 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') + mathml = evaluation.format_output(expr, "xml") try: svg = evaluation.output.mathml_to_svg(mathml) png = evaluation.output.rasterize(svg, py_raster_size) @@ -2597,7 +2599,11 @@ def apply(self, expr, evaluation, options): pixels = numpy.array(im) stream.close() - return Image(pixels, 'RGB') + return Image(pixels, "RGB") except WebEngineError as e: evaluation.message( - 'General', 'nowebeng', 'Rasterize[] did not succeed: ' + str(e), once=True) + "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 687bd102ed..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) @@ -703,7 +704,7 @@ def find_excl(excl): # 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') + 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 d1b18d62c2..6576a0a5c5 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -6,7 +6,7 @@ """ -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 @@ -291,7 +291,7 @@ def _extract_graphics(graphics, format, evaluation): if not isinstance(elements.elements[0], GeometricTransformationBox): raise ValueError("expected GeometricTransformationBox") - contents = elements.elements[0].contents + # contents = elements.elements[0].contents # generate code for svg or asy. @@ -388,7 +388,14 @@ def to_svg(self, svg): # b d f # 0 0 1 - t = "matrix(%s, %s, %s, %s, %s, %s)" % (str(a), str(b), str(c), str(d), str(e), str(f)) + 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): @@ -449,7 +456,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 @@ -957,7 +964,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") @@ -1423,7 +1430,7 @@ def to_svg(self, transform): ) def to_asy(self, transform): - if transform: + if transform: c, r = transform(self.c, self.r) else: c, r = self.c, self.r @@ -1512,7 +1519,13 @@ def path(closed): else: 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)) + yield "A %s,%s,0,%d,0,%s,%s" % ( + str(rx), + str(ry), + large_arc, + str(ex), + str(ey), + ) if closed: yield "Z" @@ -1711,7 +1724,9 @@ def to_svg(self, transform): svg = "" for line in self.lines: - path = " ".join(["%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)]) + path = " ".join( + ["%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)] + ) svg += '' % (path, style) return svg @@ -1723,7 +1738,9 @@ def to_asy(self, transform): asy = "" for line in self.lines: - path = "--".join(["(%.5g,%5g)" % tuple(float(cc) for cc in c) for c in transform(*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 @@ -1747,7 +1764,9 @@ 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" % tuple(float(cc) for cc in 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] @@ -2073,7 +2092,9 @@ def to_svg(self, transform): svg += '' % json.dumps(mesh) for line in self.lines: svg += '' % ( - " ".join("%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line)), + " ".join( + "%f,%f" % tuple(float(cc) for cc in c) for c in transform(*line) + ), style, ) return svg @@ -3038,7 +3059,14 @@ def to_svg(self, transform): svg = ( '' "%s" - ) % (float(x), float(y), float(self.opos[0]), float(self.opos[1]), style, content) + ) % ( + float(x), + float(y), + float(self.opos[0]), + float(self.opos[1]), + style, + content, + ) if not is_absolute: svg = self.graphics.inverse_local_to_screen.to_svg(svg) @@ -3477,9 +3505,11 @@ def boxes_to_text(self, leaves=None, **options): leaves = self._leaves try: self._prepare_elements(leaves, options) # to test for Box errors - except Exception as e: + except Exception: if self.evaluation: - self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) + self.evaluation.message( + "General", "notboxes", Expression("GraphicsBox", self._leaves) + ) return return "-Graphics-" @@ -3711,7 +3741,9 @@ def boxes_to_tex(self, leaves=None, **options): ) except: if self.evaluation: - self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) + self.evaluation.message( + "General", "notboxes", Expression("GraphicsBox", self._leaves) + ) return xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() @@ -3772,8 +3804,7 @@ def to_svg(self, leaves=None, **options): elements, calc_dimensions = self._prepare_elements( leaves, options, neg_y=True ) - except Exception as e: - print(e) + except Exception: return xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() @@ -3815,10 +3846,14 @@ def boxes_to_mathml(self, leaves=None, **options): if not leaves: leaves = self._leaves try: - elements, calc_dimensions = self._prepare_elements(leaves, options, neg_y=True) + elements, calc_dimensions = self._prepare_elements( + leaves, options, neg_y=True + ) except: if self.evaluation: - self.evaluation.message("General", "notboxes", Expression("GraphicsBox", self._leaves)) + self.evaluation.message( + "General", "notboxes", Expression("GraphicsBox", self._leaves) + ) return xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 476e8737b5..2f7f9b819b 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -37,7 +37,6 @@ PrecisionReal, SymbolList, SymbolMakeBoxes, - SymbolNull, SymbolRule, ) from mathics.core.numbers import ( @@ -154,7 +153,7 @@ def make_boxes_infix(leaves, ops, precedence, grouping, form): def real_to_s_exp(expr, n): if expr.is_zero: s = "0" - sign_prefix = "" + # sign_prefix = "" if expr.is_machine_precision(): exp = 0 else: @@ -2103,10 +2102,10 @@ def apply_mathml(self, expr, evaluation) -> Expression: Expression("FullForm", boxes).evaluate(evaluation), ) xml = None - if xml is None: - return Expression("RowBox", Expression(SymbolList, String(""))) +# if xml is None: +# return Expression("RowBox", Expression(SymbolList, String(""))) - is_a_picture = xml[:6] == " 6 and xml[:6] == " Expression: boxes = MakeBoxes(expr).evaluate(evaluation) try: tex = boxes.boxes_to_tex(evaluation=evaluation) - if tex is None: - return Expression("RowBox", Expression(SymbolList, String(""))) +# if tex is None: +# return Expression("RowBox", Expression(SymbolList, String(""))) # Replace multiple newlines by a single one e.g. between asy-blocks tex = MULTI_NEWLINE_RE.sub("\n", tex) diff --git a/mathics/builtin/optiondoc.py b/mathics/builtin/optiondoc.py index 487fe225d8..f80b83b744 100644 --- a/mathics/builtin/optiondoc.py +++ b/mathics/builtin/optiondoc.py @@ -33,6 +33,7 @@ class Automatic(Builtin): = {Background :> Automatic, Exclusions :> Automatic, ImageSize :> Automatic, MaxRecursion :> Automatic, PlotRange :> Automatic, PlotRangePadding :> Automatic, Transformation :> Automatic} """ + class Axes(Builtin): """
diff --git a/mathics/builtin/tensors.py b/mathics/builtin/tensors.py index bef6c264e9..72ecfcb261 100644 --- a/mathics/builtin/tensors.py +++ b/mathics/builtin/tensors.py @@ -7,7 +7,14 @@ from mathics.version import __version__ # noqa used in loading to check consistency. from mathics.builtin.base import Builtin, BinaryOperator -from mathics.core.expression import Expression, Integer, Integer0, String, SymbolTrue, SymbolFalse +from mathics.core.expression import ( + Expression, + Integer, + Integer0, + String, + SymbolTrue, + SymbolFalse, +) from mathics.core.rules import Pattern from mathics.builtin.lists import get_part @@ -488,7 +495,6 @@ def is_boolean(x): return None - class TransformationFunction(Builtin): """
@@ -504,8 +510,8 @@ class TransformationFunction(Builtin): """ rules = { - 'Dot[TransformationFunction[a_], TransformationFunction[b_]]': 'TransformationFunction[a . b]', - 'TransformationFunction[m_][v_]': 'Take[m . Join[v, {1}], Length[v]]', + "Dot[TransformationFunction[a_], TransformationFunction[b_]]": "TransformationFunction[a . b]", + "TransformationFunction[m_][v_]": "Take[m . Join[v, {1}], Length[v]]", } @@ -521,9 +527,8 @@ class TranslationTransform(Builtin): """ rules = { - 'TranslationTransform[v_]': - 'TransformationFunction[IdentityMatrix[Length[v] + 1] + ' - '(Join[ConstantArray[0, Length[v]], {#}]& /@ Join[v, {0}])]', + "TranslationTransform[v_]": "TransformationFunction[IdentityMatrix[Length[v] + 1] + " + "(Join[ConstantArray[0, Length[v]], {#}]& /@ Join[v, {0}])]", } @@ -538,10 +543,8 @@ 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]', + "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]", } @@ -556,10 +559,8 @@ class ScalingTransform(Builtin): """ rules = { - 'ScalingTransform[v_]': - 'TransformationFunction[DiagonalMatrix[Join[v, {1}]]]', - 'ScalingTransform[v_, p_]': - 'TranslationTransform[p] . ScalingTransform[v] . TranslationTransform[-p]', + "ScalingTransform[v_]": "TransformationFunction[DiagonalMatrix[Join[v, {1}]]]", + "ScalingTransform[v_, p_]": "TranslationTransform[p] . ScalingTransform[v] . TranslationTransform[-p]", } @@ -576,10 +577,7 @@ class ShearingTransform(Builtin): """ rules = { - 'ShearingTransform[phi_, {1, 0}, {0, 1}]': - 'TransformationFunction[{{1, Tan[phi], 0}, {0, 1, 0}, {0, 0, 1}}]', - 'ShearingTransform[phi_, {0, 1}, {1, 0}]': - 'TransformationFunction[{{1, 0, 0}, {Tan[phi], 1, 0}, {0, 0, 1}}]', - 'ShearingTransform[phi_, u_, v_, p_]': - 'TranslationTransform[p] . ShearingTransform[phi, u, v] . TranslationTransform[-p]', + "ShearingTransform[phi_, {1, 0}, {0, 1}]": "TransformationFunction[{{1, Tan[phi], 0}, {0, 1, 0}, {0, 0, 1}}]", + "ShearingTransform[phi_, {0, 1}, {1, 0}]": "TransformationFunction[{{1, 0, 0}, {Tan[phi], 1, 0}, {0, 0, 1}}]", + "ShearingTransform[phi_, u_, v_, p_]": "TranslationTransform[p] . ShearingTransform[phi, u, v] . TranslationTransform[-p]", } diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index a71f941210..fde5951703 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -14,7 +14,13 @@ from mathics import settings from mathics.layout.client import NoWebEngine -from mathics.core.expression import ensure_context, KeyComparable, SymbolAborted, SymbolList, SymbolNull +from mathics.core.expression import ( + ensure_context, + KeyComparable, + SymbolAborted, + SymbolList, + SymbolNull, +) FORMATS = [ "StandardForm", @@ -504,7 +510,7 @@ def message(self, symbol, tag, *args, **kwargs) -> None: pattern = Expression("MessageName", Symbol(symbol), String(tag)) - if kwargs.get('once', False): + if kwargs.get("once", False): if pattern in self.once_messages: return self.once_messages.add(pattern) diff --git a/mathics/layout/client.py b/mathics/layout/client.py index 776bf763ca..9fe2b7f346 100644 --- a/mathics/layout/client.py +++ b/mathics/layout/client.py @@ -34,7 +34,7 @@ def __init__(self, sock): def _recvall(self, n): # Helper function to recv n bytes or return None if EOF is hit - data = b'' + data = b"" sock = self.sock while len(data) < n: packet = sock.recv(n - len(data)) @@ -44,9 +44,9 @@ def _recvall(self, n): return data def put(self, msg): - msg = json.dumps(msg).encode('utf8') + msg = json.dumps(msg).encode("utf8") # Prefix each message with a 4-byte length (network byte order) - msg = struct.pack('>I', len(msg)) + msg + msg = struct.pack(">I", len(msg)) + msg self.sock.sendall(msg) def get(self): @@ -54,9 +54,9 @@ def get(self): raw_msglen = self._recvall(4) if not raw_msglen: return None - msglen = struct.unpack('>I', raw_msglen)[0] + msglen = struct.unpack(">I", raw_msglen)[0] # Read the message data - return json.loads(self._recvall(msglen).decode('utf8')) + return json.loads(self._recvall(msglen).decode("utf8")) class RemoteMethod: @@ -65,14 +65,14 @@ def __init__(self, socket, name): self.name = name def __call__(self, *args): - self.pipe.put({'call': self.name, 'args': args}) + self.pipe.put({"call": self.name, "args": args}) reply = self.pipe.get() - error = reply.get('error') + error = reply.get("error") if error: raise WebEngineError(str(error)) else: - return reply.get('data') + return reply.get("data") class Client: @@ -91,6 +91,7 @@ def close(self): # that "provides functionality for rendering regions of dynamic web content". This # is not about web servers but layout (http://doc.qt.io/qt-5/qtwebengine-index.html). + class NoWebEngine: def assume_is_available(self): raise WebEngineUnavailable @@ -107,34 +108,42 @@ def _normalize_svg(svg): import base64 import re - ET.register_namespace('', 'http://www.w3.org/2000/svg') + ET.register_namespace("", "http://www.w3.org/2000/svg") root = ET.fromstring(svg) - prefix = 'data:image/svg+xml;base64,' + prefix = "data:image/svg+xml;base64," def rewrite(up): changes = [] for i, node in enumerate(up): - if node.tag == '{http://www.w3.org/2000/svg}image': - src = node.attrib.get('src', '') + if node.tag == "{http://www.w3.org/2000/svg}image": + src = node.attrib.get("src", "") if src.startswith(prefix): attrib = node.attrib - if 'width' in attrib and 'height' in attrib: - target_width = float(attrib['width']) - target_height = float(attrib['height']) - target_transform = attrib.get('transform', '') + if "width" in attrib and "height" in attrib: + target_width = float(attrib["width"]) + target_height = float(attrib["height"]) + target_transform = attrib.get("transform", "") - image_svg = _normalize_svg(base64.b64decode(src[len(prefix):])) + image_svg = _normalize_svg(base64.b64decode(src[len(prefix) :])) root = ET.fromstring(image_svg) - view_box = re.split('\s+', root.attrib.get('viewBox', '')) + view_box = re.split("\s+", root.attrib.get("viewBox", "")) if len(view_box) == 4: x, y, w, h = (float(t) for t in view_box) - root.tag = '{http://www.w3.org/2000/svg}g' - root.attrib = {'transform': '%s scale(%f, %f) translate(%f, %f)' % ( - target_transform, target_width / w, target_height / h, -x, -y)} + root.tag = "{http://www.w3.org/2000/svg}g" + root.attrib = { + "transform": "%s scale(%f, %f) translate(%f, %f)" + % ( + target_transform, + target_width / w, + target_height / h, + -x, + -y, + ) + } changes.append((i, node, root)) else: @@ -146,7 +155,7 @@ def rewrite(up): rewrite(root) - return ET.tostring(root, 'utf8').decode('utf8') + return ET.tostring(root, "utf8").decode("utf8") class WebEngine: @@ -160,51 +169,55 @@ def _create_client(self): popen_env = os.environ.copy() server_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'server.js') + os.path.dirname(os.path.realpath(__file__)), "server.js" + ) if False: # fixes problems on Windows network drives import tempfile - fd, copied_path = tempfile.mkstemp(suffix='js') - with open(server_path, 'rb') as f: + + fd, copied_path = tempfile.mkstemp(suffix="js") + with open(server_path, "rb") as f: os.write(fd, f.read()) os.fsync(fd) server_path = copied_path def abort(message): - error_text = '\n'.join([ - '', - 'Node.js failed to start Mathics server.', - 'You might need to run: npm install -g mathjax-node svg2png', - '', - '']) + error_text = "\n".join( + [ + "", + "Node.js failed to start Mathics server.", + "You might need to run: npm install -g mathjax-node svg2png", + "", + "", + ] + ) raise WebEngineUnavailable(error_text + message) process = Popen( - ['node', server_path], - stdout=subprocess.PIPE, - env=popen_env) + ["node", server_path], stdout=subprocess.PIPE, env=popen_env + ) - hello = 'HELLO:' # agreed upon "all ok" hello message. + hello = "HELLO:" # agreed upon "all ok" hello message. - status = process.stdout.readline().decode('utf8').strip() + status = process.stdout.readline().decode("utf8").strip() if not status.startswith(hello): - error = '' + error = "" while True: - line = process.stdout.readline().decode('utf8') + line = process.stdout.readline().decode("utf8") if not line: break - error += ' ' + line + error += " " + line process.terminate() abort(error) - port = int(status[len(hello):]) + port = int(status[len(hello) :]) except OSError as e: abort(str(e)) try: - self.client = Client('127.0.0.1', port) + self.client = Client("127.0.0.1", port) self.process = process except Exception as e: self.client = None @@ -233,11 +246,10 @@ def mathml_to_svg(self, mathml): def rasterize(self, svg, size): buffer = self._ensure_client().rasterize(_normalize_svg(svg), size) - return bytearray(buffer['data']) + return bytearray(buffer["data"]) def terminate(self): if self.process: self.process.terminate() self.process = None self.client = None - From 23309b459580e3d8014efe988c5eb9a0c7450bf2 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 15 May 2021 12:36:19 -0300 Subject: [PATCH 47/48] implementig boxerror --- mathics/builtin/graphics.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 6576a0a5c5..d15ab70508 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -23,6 +23,7 @@ from mathics.builtin.options import options_to_rules from mathics.layout.client import WebEngineUnavailable from mathics.core.expression import ( + BoxError, Expression, Integer, Rational, @@ -3506,11 +3507,7 @@ def boxes_to_text(self, leaves=None, **options): try: self._prepare_elements(leaves, options) # to test for Box errors except Exception: - if self.evaluation: - self.evaluation.message( - "General", "notboxes", Expression("GraphicsBox", self._leaves) - ) - return + raise BoxError(Expression("GraphicsBox", self._leaves), "text") return "-Graphics-" def _get_image_size(self, options, graphics_options, max_width): @@ -3740,11 +3737,7 @@ def boxes_to_tex(self, leaves=None, **options): leaves, options, max_width=450 ) except: - if self.evaluation: - self.evaluation.message( - "General", "notboxes", Expression("GraphicsBox", self._leaves) - ) - return + raise BoxError(Expression("GraphicsBox", self._leaves), "tex") xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() asy_completely_visible = "\n".join( @@ -3805,7 +3798,7 @@ def to_svg(self, leaves=None, **options): leaves, options, neg_y=True ) except Exception: - return + raise BoxError(Expression("GraphicsBox", self._leaves), "svg") xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() elements.view_width = w @@ -3850,11 +3843,7 @@ def boxes_to_mathml(self, leaves=None, **options): leaves, options, neg_y=True ) except: - if self.evaluation: - self.evaluation.message( - "General", "notboxes", Expression("GraphicsBox", self._leaves) - ) - return + raise BoxError(Expression("GraphicsBox", self._leaves), "MathML") xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() data = (elements, xmin, xmax, ymin, ymax, w, h, width, height) From 3ea6856aaafc5ca69a137814614c277614f5320d Mon Sep 17 00:00:00 2001 From: mmatera Date: Sat, 15 May 2021 14:10:29 -0300 Subject: [PATCH 48/48] clean --- mathics/builtin/graphics.py | 4 ++-- mathics/builtin/inout.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index d15ab70508..6fb6645d96 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -516,7 +516,7 @@ class Graphics(Builtin): Invalid graphics directives yield invalid box structures: >> Graphics[Circle[{a, b}]] - : GraphicsBox[{CircleBox[{a, b}], $OptionSyntax -> Ignore, AspectRatio -> Automatic, Axes -> False, AxesStyle -> {}, Background -> Automatic, ImageSize -> Automatic, LabelStyle -> {}, PlotRange -> Automatic, PlotRangePadding -> Automatic, TicksStyle -> {}, Transformation -> Automatic}] is not a valid box structure. + : 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 @@ -1680,7 +1680,7 @@ def to_asy(self, transform): asy = "" for line in self.lines: for x, y in transform(*line): - asy += "dot(%f, %f);" % ((float(x), float(y)), pen) + asy += "dot(%s, %s);" % ((float(x), float(y)), pen) return asy diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 2f7f9b819b..758148bc23 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -2101,7 +2101,7 @@ def apply_mathml(self, expr, evaluation) -> Expression: "notboxes", Expression("FullForm", boxes).evaluate(evaluation), ) - xml = None + xml = "" # if xml is None: # return Expression("RowBox", Expression(SymbolList, String("")))