From 153c1994e04f499708ada413d15aa40397b9c1a2 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 30 Aug 2016 16:10:32 +0200 Subject: [PATCH 01/22] 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 6f75ba18e5..1774bf7aaf 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -10,6 +10,7 @@ from __future__ import division from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp +import re import json import base64 from six.moves import map @@ -179,48 +180,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))) @@ -230,19 +231,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); @@ -746,7 +747,7 @@ class ColorDistance(Builtin): = 0.557976 #> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)] = 0.542917 - + """ options = { @@ -757,17 +758,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)): @@ -2214,13 +2215,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 @@ -2239,29 +2242,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() @@ -2827,19 +2902,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): @@ -2959,6 +3038,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), @@ -2973,7 +3053,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 6ddd9c20d5..a2bc37e22e 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1773,8 +1773,16 @@ def apply_mathml(self, expr, evaluation): 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 e4e48d9731..f7ba578a94 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -180,6 +180,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): return settings.MAX_STORED_SIZE @@ -192,6 +201,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 e124a07af0..70c462691a 100644 --- a/mathics/server.py +++ b/mathics/server.py @@ -15,6 +15,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(): @@ -86,6 +89,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) @@ -103,10 +109,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 295e84dda4..f276f15cf6 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -13,6 +13,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 f003ca4392..339c9e6f11 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -107,9 +107,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 ad1ae25335..e9f3ae1b21 100644 --- a/setup.py +++ b/setup.py @@ -162,7 +162,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', 'mathics.web.templatetags', + 'mathics.layout', ], install_requires=INSTALL_REQUIRES, @@ -182,6 +183,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 1331428f4f7edafc4943db9977a257d71d11332c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 30 Aug 2016 17:27:00 +0200 Subject: [PATCH 02/22] 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 1774bf7aaf..a65b2f93ac 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 @@ -2222,7 +2270,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): @@ -2296,17 +2349,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 @@ -2495,6 +2540,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): @@ -3018,6 +3064,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) @@ -3038,7 +3086,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), @@ -3465,6 +3512,7 @@ class Large(Builtin): 'Thick': Thick, 'Thin': Thin, 'PointSize': PointSize, + 'FontSize': FontSize, 'Arrowheads': Arrowheads, }) From a6aecb47d8f6f98802a2048922e3af6fbaf02b2e Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 08:08:59 +0200 Subject: [PATCH 03/22] added --svg-math parameter to enable layout engine; not used by default --- mathics/server.py | 12 +++++++++--- mathics/web/views.py | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/mathics/server.py b/mathics/server.py index 70c462691a..803e904a27 100644 --- a/mathics/server.py +++ b/mathics/server.py @@ -66,6 +66,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() @@ -90,7 +93,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 ( @@ -109,12 +113,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 339c9e6f11..c2170ddef9 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -26,6 +26,7 @@ from mathics.web.forms import LoginForm, SaveForm from mathics.doc import documentation from mathics.doc.doc import DocPart, DocChapter, DocSection +from mathics.server import layout_engine import six from six.moves import range from string import Template @@ -48,7 +49,8 @@ def __init__(self, result={}): class WebOutput(Output): - pass + def svgify(self): + return layout_engine is not None def require_ajax_login(func): From 00174f1424866c30f7485b30e70f69a795f0db0f Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 16:27:23 +0200 Subject: [PATCH 04/22] 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 4ab4193965705c7f216fcfad8716565dc729c2b1 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 18:46:51 +0200 Subject: [PATCH 05/22] 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 e2d410b393cabd72620cc90f14835d85e0f844b8 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 31 Aug 2016 18:53:25 +0200 Subject: [PATCH 06/22] 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 71bec1a7ecbd1aa144d46b426404c7f13a2ee6fe Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 1 Sep 2016 13:51:06 +0200 Subject: [PATCH 07/22] 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 a532a243b4d7432d9a8a01764841b346cc74a0a7 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 1 Sep 2016 15:16:14 +0200 Subject: [PATCH 08/22] 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 36c1f1e3cae6056d638168e7a39b713047abdc7d Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 1 Sep 2016 15:21:40 +0200 Subject: [PATCH 09/22] 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 f276f15cf6..eeb6829e4a 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -15,7 +15,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 eda5f7df0536b9890f65d647854de021d0220afa Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 3 Sep 2016 07:16:16 +0200 Subject: [PATCH 10/22] 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 a65b2f93ac..02dc499357 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2313,7 +2313,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 @@ -2329,6 +2329,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. @@ -2348,16 +2350,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' % ( @@ -2948,23 +2949,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 a2bc37e22e..82ca0c8575 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1774,13 +1774,7 @@ def apply_mathml(self, expr, evaluation): # 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 4b9849e4d5692db82f55091d5c7139ef4321d0c8 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 4 Sep 2016 16:31:13 +0200 Subject: [PATCH 11/22] 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 58e8d997a6247ad05e1aced1adb64159f73da6f0 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 4 Sep 2016 19:00:55 +0200 Subject: [PATCH 12/22] 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 02dc499357..14a7432509 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -22,6 +22,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) @@ -2296,14 +2297,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() @@ -2323,6 +2329,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) @@ -2542,6 +2550,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 f7ba578a94..6e150149b0 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -180,14 +180,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): return settings.MAX_STORED_SIZE @@ -201,21 +195,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 803e904a27..e9afd8c4bb 100644 --- a/mathics/server.py +++ b/mathics/server.py @@ -15,9 +15,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(): @@ -66,9 +66,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() @@ -92,9 +89,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 ( @@ -113,14 +109,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 c2170ddef9..904b94701d 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -49,8 +49,7 @@ def __init__(self, result={}): class WebOutput(Output): - def svgify(self): - return layout_engine is not None + pass def require_ajax_login(func): From 2ebc49faeaa87cb8fbaf283fbb2c564ff844871d Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sun, 4 Sep 2016 22:55:44 +0200 Subject: [PATCH 13/22] 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 | 5 ++--- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 14a7432509..1e420292eb 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2305,10 +2305,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 6e150149b0..343e3c1e57 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -195,8 +195,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) @@ -225,6 +228,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.' @@ -407,7 +411,7 @@ def get_quiet_messages(self): return [] return value.leaves - def message(self, symbol, tag, *args): + def message(self, symbol, tag, *args, once=False): from mathics.core.expression import (String, Symbol, Expression, from_python) @@ -418,6 +422,11 @@ def message(self, symbol, tag, *args): 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 904b94701d..454991aab3 100644 --- a/mathics/web/views.py +++ b/mathics/web/views.py @@ -26,7 +26,6 @@ from mathics.web.forms import LoginForm, SaveForm from mathics.doc import documentation from mathics.doc.doc import DocPart, DocChapter, DocSection -from mathics.server import layout_engine import six from six.moves import range from string import Template @@ -108,11 +107,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 4b1a218bc8ba6e41738136d2f52be2f544d4f958 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 5 Sep 2016 10:58:19 +0200 Subject: [PATCH 14/22] 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 2f9d1e8ac0..3debfc2bf5 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -11,6 +11,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 six import base64 @@ -2424,3 +2425,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 343e3c1e57..db56c71701 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -15,6 +15,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', @@ -180,7 +181,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): @@ -204,8 +205,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 ae0b3221fe44e4173c2c87e3e621afe0538b929f Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Mon, 5 Sep 2016 12:31:31 +0200 Subject: [PATCH 15/22] 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 3debfc2bf5..a383cf46d1 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -11,7 +11,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 six import base64 @@ -2434,10 +2434,6 @@ class Rasterize(Builtin): 'RasterSize': '300', } - messages = { - 'err': 'Rasterize[] failed: `1`', - } - def apply(self, expr, evaluation, options): 'Rasterize[expr_, OptionsPattern[%(name)s]]' @@ -2453,21 +2449,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 4718a3f64849b9c81c6bdecdf9b02a0327577b80 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Sat, 10 Sep 2016 09:14:35 +0200 Subject: [PATCH 16/22] 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 db56c71701..07f97bbf9a 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -412,7 +412,7 @@ def get_quiet_messages(self): return [] return value.leaves - def message(self, symbol, tag, *args, once=False): + def message(self, symbol, tag, *args, **kwargs): from mathics.core.expression import (String, Symbol, Expression, from_python) @@ -423,7 +423,7 @@ def message(self, symbol, tag, *args, once=False): 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 fbb224b29fb8ea8df264f79168f6a53c74069fe0 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 14 Sep 2016 18:41:28 +0200 Subject: [PATCH 17/22] 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 82ca0c8575..ff018f0876 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -1632,6 +1632,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 fdf29e385dfa5e90eb4875aec440ca29178ecac1 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 14 Sep 2016 19:55:02 +0200 Subject: [PATCH 18/22] 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 eeb6829e4a..295e84dda4 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -13,10 +13,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 66bf7f66e8fb85481a7581fdc5d6b92a7782a4a9 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 15 Sep 2016 15:25:27 +0200 Subject: [PATCH 19/22] 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 a383cf46d1..77a3f9f59a 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -2454,7 +2454,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 e784380a966475d75f71fa73feb32786cd166157 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 15 Sep 2016 16:22:10 +0200 Subject: [PATCH 20/22] 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 c2dda3b7d5e0060fb60a9082cf228b5bebb7be4c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 15 Sep 2016 21:05:08 +0200 Subject: [PATCH 21/22] 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 5b0d3480b5..c3f7e7773b 100644 --- a/mathics/main.py +++ b/mathics/main.py @@ -181,6 +181,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 2f61cc1cd08c6793ab5f51f2db6d981c313858fa Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 18 Oct 2016 15:35:00 +0200 Subject: [PATCH 22/22] 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 1e420292eb..a3adb6e70b 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -2308,6 +2308,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)