diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index 6f75ba18e5..a3adb6e70b 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 @@ -21,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) @@ -179,48 +181,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 +232,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 +748,7 @@ class ColorDistance(Builtin): = 0.557976 #> ColorDistance[Red, Black, DistanceFunction -> (Abs[#1[[1]] - #2[[1]]] &)] = 0.542917 - + """ options = { @@ -757,17 +759,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 +794,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)): @@ -941,6 +943,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 @@ -2214,13 +2264,20 @@ 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) + 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): raise BoxConstructError @@ -2239,29 +2296,105 @@ 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) + + try: + self._prepare_text_svg() + except WebEngineUnavailable as e: + self.svg = None + + self.content_text = self.content.boxes_to_text( + evaluation=self.graphics.evaluation) + + if self.graphics.evaluation.output.warn_about_web_engine(): + self.graphics.evaluation.message( + 'General', 'nowebeng', str(e), once=True) + except Exception as e: + self.svg = None + + self.graphics.evaluation.message( + 'General', 'nowebeng', str(e), once=True) def extent(self): p = self.pos.pos() - h = 25 - w = len(self.content_text) * \ - 7 # rough approximation by numbers of characters + + if not self.svg: + h = 25 + w = len(self.content_text) * \ + 7 # rough approximation by numbers of characters + else: + _, w, h = self.svg + scale = self._text_svg_scale(h) + w *= scale + h *= scale + opos = self.opos x = p[0] - w / 2.0 - opos[0] * w / 2.0 y = p[1] - h / 2.0 + opos[1] * h / 2.0 return [(x, y), (x + w, y + h)] + def _prepare_text_svg(self): + self.graphics.evaluation.output.assume_web_engine() + + content = self.content.boxes_to_xml( + evaluation=self.graphics.evaluation) + + 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. + + 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, height): + size = self.font_size.get_size() + return size / height + + 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() @@ -2420,6 +2553,8 @@ class _GraphicsElements(object): 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): @@ -2827,14 +2962,8 @@ 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) + svg_xml = '%s' % (' '.join('%f' % t for t in (xmin, ymin, w, h)), svg) return '' % ( int(width), @@ -2939,6 +3068,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) @@ -2973,7 +3104,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), @@ -3385,6 +3516,7 @@ class Large(Builtin): 'Thick': Thick, 'Thin': Thin, 'PointSize': PointSize, + 'FontSize': FontSize, 'Arrowheads': Arrowheads, }) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 2f9d1e8ac0..77a3f9f59a 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 WebEngineError import six import base64 @@ -2424,3 +2425,42 @@ 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', + } + + def apply(self, expr, evaluation, options): + 'Rasterize[expr_, OptionsPattern[%(name)s]]' + + raster_size = self.get_option(options, 'RasterSize', evaluation) + if isinstance(raster_size, Integer): + s = raster_size.get_int_value() + py_raster_size = (s, s) + elif raster_size.has_form('List', 2) and all(isinstance(s, Integer) for s in raster_size.leaves): + py_raster_size = tuple(s.get_int_value for s in raster_size.leaves) + else: + return + + mathml = evaluation.format_output(expr, 'xml') + try: + svg = evaluation.output.mathml_to_svg(mathml) + png = evaluation.output.rasterize(svg, py_raster_size) + + stream = BytesIO() + stream.write(png) + stream.seek(0) + im = PIL.Image.open(stream) + # note that we need to get these pixels as long as stream is still open, + # otherwise PIL will generate an IO error. + pixels = numpy.array(im) + stream.close() + + return Image(pixels, 'RGB') + except WebEngineError as e: + evaluation.message( + 'General', 'nowebeng', 'Rasterize[] did not succeed: ' + str(e), once=True) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 6ddd9c20d5..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`", } @@ -1773,8 +1774,10 @@ 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))) + + result = '%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..07f97bbf9a 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,6 +181,9 @@ def get_data(self): class Output(object): + def __init__(self, web_engine=NoWebEngine()): + self.web_engine = web_engine + def max_stored_size(self, settings): return settings.MAX_STORED_SIZE @@ -192,6 +196,18 @@ def clear(self, wait): def display(self, data, metadata): raise NotImplementedError + 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) + + def rasterize(self, svg, *args, **kwargs): + return self.web_engine.rasterize(svg, *args, **kwargs) + class Evaluation(object): def __init__(self, definitions=None, @@ -213,6 +229,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.' @@ -395,7 +412,7 @@ def get_quiet_messages(self): return [] return value.leaves - def message(self, symbol, tag, *args): + def message(self, symbol, tag, *args, **kwargs): from mathics.core.expression import (String, Symbol, Expression, from_python) @@ -406,6 +423,11 @@ def message(self, symbol, tag, *args): pattern = Expression('MessageName', Symbol(symbol), String(tag)) + if kwargs.get('once', False): + 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/__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..a40512b988 --- /dev/null +++ b/mathics/layout/client.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# 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 + +import subprocess +from subprocess import Popen +import os + +import socket +import json +import struct + + +class WebEngineError(RuntimeError): + pass + + +class WebEngineUnavailable(WebEngineError): + pass + + +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 + 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.pipe = Pipe(socket) + self.name = name + + def __call__(self, *args): + self.pipe.put({'call': self.name, 'args': args}) + reply = self.pipe.get() + + error = reply.get('error') + if error: + raise WebEngineError(str(error)) + else: + return reply.get('data') + + +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() + + +# 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 NoWebEngine: + def assume_is_available(self): + raise WebEngineUnavailable + + def mathml_to_svg(self, mathml): + raise WebEngineUnavailable + + 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 + self.client = None + self.unavailable = None + + def _create_client(self): + try: + popen_env = os.environ.copy() + + 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) + + process = Popen( + ['node', server_path], + stdout=subprocess.PIPE, + env=popen_env) + + hello = 'HELLO:' # agreed upon "all ok" hello message. + + status = process.stdout.readline().decode('utf8').strip() + if not status.startswith(hello): + error = '' + while True: + line = process.stdout.readline().decode('utf8') + if not line: + break + error += ' ' + line + + process.terminate() + abort(error + '\nPlease check Node.js modules and NODE_PATH') + + port = int(status[len(hello):]) + except OSError as e: + abort(str(e)) + + try: + self.client = Client('127.0.0.1', port) + self.process = process + except Exception as e: + self.client = None + 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._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): + return self._ensure_client().mathml_to_svg(mathml) + + def rasterize(self, svg, size): + buffer = self._ensure_client().rasterize(_normalize_svg(svg), size) + return bytearray(buffer['data']) + + def terminate(self): + if self.process: + self.process.terminate() + self.process = None + self.client = None + diff --git a/mathics/layout/server.js b/mathics/layout/server.js new file mode 100644 index 0000000000..bedfdd8f54 --- /dev/null +++ b/mathics/layout/server.js @@ -0,0 +1,114 @@ +// to install: npm install mathjax-node svg2png + +try { + 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) { + try { + method.apply(null, json.args.concat([write])); + } catch(e) { + write({error: e.toString() + '; ' + e.stack}); + } + } + } + + 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]); + + 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() { + var port = server.address().port; + process.stdout.write('HELLO:' + port.toString() + '\n'); + }); + + server.listen(0); // pick a free port + } + + var mathjax = require("mathjax-node/lib/mj-single.js"); + mathjax.config({ + MathJax: { + // traditional MathJax configuration + } + }); + mathjax.start(); + + server({ + mathml_to_svg: function(mathml, reply) { + mathjax.typeset({ + math: mathml, + format: "MathML", + svg: true + }, function (data) { + if (!data.errors) { + 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({data: buffer}); + }) + .catch(function(e) { + reply({error: e.toString()}); + }); + } + }); +} catch (ex) { + process.stdout.write('FAIL.' + '\n' + ex.toString() + '\n'); +} 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): diff --git a/mathics/server.py b/mathics/server.py index e124a07af0..e9afd8c4bb 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 WebEngine + +web_engine = None def check_database(): @@ -86,6 +89,9 @@ def launch_app(args): else: addr = '127.0.0.1' + global web_engine + web_engine = WebEngine() + try: from django.core.servers.basehttp import ( run, get_internal_wsgi_application) @@ -103,10 +109,14 @@ def launch_app(args): 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) 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); } } diff --git a/mathics/web/views.py b/mathics/web/views.py index f003ca4392..454991aab3 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 web_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(web_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={