Commit 2d830491 authored by Guillaume Seguin's avatar Guillaume Seguin

Merge branch 'experimental' of git://github.com/kliment/Printrun into experimental

parents c8138998 9632556c
...@@ -113,6 +113,7 @@ To use pronterface, you need: ...@@ -113,6 +113,7 @@ To use pronterface, you need:
* python (ideally 2.6.x or 2.7.x), * python (ideally 2.6.x or 2.7.x),
* pyserial (or python-serial on ubuntu/debian) * pyserial (or python-serial on ubuntu/debian)
* pyglet * pyglet
* numpy (for 3D view)
* pyreadline (not needed on Linux) and * pyreadline (not needed on Linux) and
* argparse (installed by default with python >= 2.7) * argparse (installed by default with python >= 2.7)
* wxPython * wxPython
......
...@@ -16,12 +16,12 @@ ...@@ -16,12 +16,12 @@
# along with Printrun. If not, see <http://www.gnu.org/licenses/>. # along with Printrun. If not, see <http://www.gnu.org/licenses/>.
from serial import Serial, SerialException from serial import Serial, SerialException
from select import error as SelectError
from threading import Thread from threading import Thread
from select import error as SelectError, select
import time, getopt, sys import time, getopt, sys
import platform, os import platform, os, traceback
import socket # Network import socket
import re # Regex import re
from collections import deque from collections import deque
from printrun.GCodeAnalyzer import GCodeAnalyzer from printrun.GCodeAnalyzer import GCodeAnalyzer
from printrun import gcoder from printrun import gcoder
...@@ -40,9 +40,6 @@ def enable_hup(port): ...@@ -40,9 +40,6 @@ def enable_hup(port):
def disable_hup(port): def disable_hup(port):
control_ttyhup(port, True) control_ttyhup(port, True)
def is_socket(printer):
return (type(printer) == socket._socketobject)
class printcore(): class printcore():
def __init__(self, port = None, baud = None): def __init__(self, port = None, baud = None):
"""Initializes a printcore instance. Pass the port and baud rate to connect immediately """Initializes a printcore instance. Pass the port and baud rate to connect immediately
...@@ -108,14 +105,40 @@ class printcore(): ...@@ -108,14 +105,40 @@ class printcore():
self.baud = baud self.baud = baud
if self.port is not None and self.baud is not None: if self.port is not None and self.baud is not None:
# Connect to socket if "port" is an IP, device if not # Connect to socket if "port" is an IP, device if not
p = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") host_regexp = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
if p.match(port): is_serial = True
self.printer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if ":" in port:
bits = port.split(":")
if len(bits) == 2:
hostname = bits[0]
try:
port = int(bits[1])
if host_regexp.match(hostname) and 1 <= port <= 65535:
is_serial = False
except:
pass
if not is_serial:
self.printer_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.timeout = 0.25 self.timeout = 0.25
self.printer.connect((port, baud)) try:
self.printer_tcp.connect((hostname, port))
self.printer = self.printer_tcp.makefile()
except socket.error:
print _("Could not connect to %s:%s:") % (hostname, port)
self.printer = None
self.printer_tcp = None
traceback.print_exc()
return
else: else:
disable_hup(self.port) disable_hup(self.port)
self.printer_tcp = None
try:
self.printer = Serial(port = self.port, baudrate = self.baud, timeout = 0.25) self.printer = Serial(port = self.port, baudrate = self.baud, timeout = 0.25)
except SerialException:
print _("Could not connect to %s at baudrate %s:") % (self.port, self.baud)
self.printer = None
traceback.print_exc()
return
self.stop_read_thread = False self.stop_read_thread = False
self.read_thread = Thread(target = self._listen) self.read_thread = Thread(target = self._listen)
self.read_thread.start() self.read_thread.start()
...@@ -123,26 +146,19 @@ class printcore(): ...@@ -123,26 +146,19 @@ class printcore():
def reset(self): def reset(self):
"""Reset the printer """Reset the printer
""" """
if self.printer and not is_socket(self.printer): if self.printer and not self.printer_tcp:
self.printer.setDTR(1) self.printer.setDTR(1)
time.sleep(0.2) time.sleep(0.2)
self.printer.setDTR(0) self.printer.setDTR(0)
def _readline(self): def _readline(self):
try: try:
# Read line if socket try:
if is_socket(self.printer):
line = ''
ready = select([self.printer], [], [], self.timeout)
if ready[0]:
while not "\n" in line:
chunk = self.printer.recv(1)
if chunk == '':
raise RuntimeError("socket connection broken")
line = line + chunk
# Read if tty
else:
line = self.printer.readline() line = self.printer.readline()
if self.printer_tcp and not line:
raise OSError("Read EOF from socket")
except socket.timeout:
return ""
if len(line) > 1: if len(line) > 1:
self.log.append(line) self.log.append(line)
...@@ -151,21 +167,22 @@ class printcore(): ...@@ -151,21 +167,22 @@ class printcore():
except: pass except: pass
if self.loud: print "RECV: ", line.rstrip() if self.loud: print "RECV: ", line.rstrip()
return line return line
except SelectError, e: except SelectError as e:
if 'Bad file descriptor' in e.args[1]: if 'Bad file descriptor' in e.args[1]:
print "Can't read from printer (disconnected?)." print "Can't read from printer (disconnected?) (SelectError {0}): {1}".format(e.errno, e.strerror)
return None return None
else: else:
print "SelectError ({0}): {1}".format(e.errno, e.strerror)
raise raise
except SerialException, e: except SerialException as e:
print "Can't read from printer (disconnected?)." print "Can't read from printer (disconnected?) (SerialException {0}): {1}".format(e.errno, e.strerror)
return None return None
except OSError, e: except OSError as e:
print "Can't read from printer (disconnected?)." print "Can't read from printer (disconnected?) (OS Error {0}): {1}".format(e.errno, e.strerror)
return None return None
def _listen_can_continue(self): def _listen_can_continue(self):
if is_socket(self.printer): if self.printer_tcp:
return not self.stop_read_thread and self.printer return not self.stop_read_thread and self.printer
return not self.stop_read_thread and self.printer and self.printer.isOpen() return not self.stop_read_thread and self.printer and self.printer.isOpen()
...@@ -348,7 +365,7 @@ class printcore(): ...@@ -348,7 +365,7 @@ class printcore():
def send_now(self, command, wait = 0): def send_now(self, command, wait = 0):
"""Sends a command to the printer ahead of the command queue, without a checksum """Sends a command to the printer ahead of the command queue, without a checksum
""" """
if self.online or force: if self.online:
if self.printing: if self.printing:
self.priqueue.append(command) self.priqueue.append(command)
else: else:
...@@ -457,21 +474,12 @@ class printcore(): ...@@ -457,21 +474,12 @@ class printcore():
try: self.sendcb(command) try: self.sendcb(command)
except: pass except: pass
try: try:
# If the printer is connected via Ethernet, use send self.printer.write(str(command + "\n"))
if is_socket(self.printer): self.printer.flush()
msg = str(command+"\n")
totalsent = 0
while totalsent < len(msg):
sent = self.printer.send(msg[totalsent:])
if sent == 0:
raise RuntimeError("socket connection broken")
totalsent = totalsent + sent
else:
self.printer.write(str(command+"\n"))
except SerialException, e: except SerialException, e:
print "Can't write to printer (disconnected?)." print "Can't write to printer (disconnected?) ({0}): {1}".format(e.errno, e.strerror)
except RuntimeError, e: except RuntimeError, e:
print "Socket connection broken, disconnected." print "Socket connection broken, disconnected. ({0}): {1}".format(e.errno, e.strerror)
if __name__ == '__main__': if __name__ == '__main__':
baud = 115200 baud = 115200
......
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
CairoSVG - A simple SVG converter for Cairo.
"""
import os
import sys
import optparse
from . import surface
VERSION = '0.4.4'
SURFACES = {
'SVG': surface.SVGSurface, # Tell us if you actually use this one!
'PNG': surface.PNGSurface,
'PDF': surface.PDFSurface,
'PS': surface.PSSurface}
# Generate the svg2* functions from SURFACES
for _output_format, _surface_type in SURFACES.items():
_function = (
# Two lambdas needed for the closure
lambda surface_type: lambda *args, **kwargs: # pylint: disable=W0108
surface_type.convert(*args, **kwargs))(_surface_type)
_name = 'svg2%s' % _output_format.lower()
_function.__name__ = _name
_function.__doc__ = surface.Surface.convert.__doc__.replace(
'the format for this class', _output_format)
setattr(sys.modules[__name__], _name, _function)
def main():
"""Entry-point of the executable."""
# Get command-line options
option_parser = optparse.OptionParser(
usage = "usage: %prog filename [options]", version = VERSION)
option_parser.add_option(
"-f", "--format", help = "output format")
option_parser.add_option(
"-d", "--dpi", help = "svg resolution", default = 96)
option_parser.add_option(
"-o", "--output",
default = "", help = "output filename")
options, args = option_parser.parse_args()
# Print help if no argument is given
if not args:
option_parser.print_help()
sys.exit()
kwargs = {'dpi': float(options.dpi)}
if not options.output or options.output == '-':
# Python 2/3 hack
bytes_stdout = getattr(sys.stdout, "buffer", sys.stdout)
kwargs['write_to'] = bytes_stdout
else:
kwargs['write_to'] = options.output
url = args[0]
if url == "-":
# Python 2/3 hack
bytes_stdin = getattr(sys.stdin, "buffer", sys.stdin)
kwargs['file_obj'] = bytes_stdin
else:
kwargs['url'] = url
output_format = (
options.format or
os.path.splitext(options.output)[1].lstrip(".") or
"pdf")
SURFACES[output_format.upper()].convert(**kwargs)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Optionally handle CSS stylesheets.
"""
from .parser import HAS_LXML
# Detect optional depedencies
# pylint: disable=W0611
try:
import tinycss
import cssselect
CSS_CAPABLE = HAS_LXML
except ImportError:
CSS_CAPABLE = False
# pylint: enable=W0611
# Python 2/3 compat
iteritems = getattr(dict, "iteritems", dict.items) # pylint: disable=C0103
def find_stylesheets(tree):
"""Find the stylesheets included in ``tree``."""
# TODO: support contentStyleType on <svg>
default_type = "text/css"
for element in tree.iter():
# http://www.w3.org/TR/SVG/styling.html#StyleElement
if (element.tag == "style" and
element.get("type", default_type) == "text/css"):
# TODO: pass href for relative URLs
# TODO: support media types
# TODO: what if <style> has children elements?
yield tinycss.make_parser().parse_stylesheet(element.text)
# TODO: support <?xml-stylesheet ... ?>
def find_style_rules(tree):
"""Find the style rules in ``tree``."""
for stylesheet in find_stylesheets(tree):
# TODO: warn for each stylesheet.errors
for rule in stylesheet.rules:
# TODO: support @import and @media
if not rule.at_keyword:
yield rule
def get_declarations(rule):
"""Get the declarations in ``rule``."""
for declaration in rule.declarations:
if declaration.name.startswith("-"):
# Ignore properties prefixed by "-"
continue
# TODO: filter out invalid values
yield (
declaration.name,
declaration.value.as_css(),
bool(declaration.priority))
def match_selector(rule, tree):
"""Yield the ``(element, specificity)`` in ``tree`` matching ``rule``."""
selector_list = cssselect.parse(rule.selector.as_css())
translator = cssselect.GenericTranslator()
for selector in selector_list:
if not selector.pseudo_element:
specificity = selector.specificity()
for element in tree.xpath(translator.selector_to_xpath(selector)):
yield element, specificity
def apply_stylesheets(tree):
"""Apply the stylesheet in ``tree`` to ``tree``."""
if not CSS_CAPABLE:
# TODO: warn?
return
style_by_element = {}
for rule in find_style_rules(tree):
declarations = list(get_declarations(rule))
for element, specificity in match_selector(rule, tree):
style = style_by_element.setdefault(element, {})
for name, value, important in declarations:
weight = important, specificity
if name in style:
_old_value, old_weight = style[name]
if old_weight > weight:
continue
style[name] = value, weight
for element, style in iteritems(style_by_element):
values = ["%s: %s" % (name, value)
for name, (value, weight) in iteritems(style)]
values.append(element.get("style", ""))
element.set("style", ";".join(values))
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG Parser.
"""
# Fallbacks for Python 2/3 and lxml/ElementTree
# pylint: disable=E0611,F0401,W0611
try:
import lxml.etree as ElementTree
from lxml.etree import XMLSyntaxError as ParseError
HAS_LXML = True
except ImportError:
from xml.etree import ElementTree
from xml.parsers import expat
# ElementTree's API changed between 2.6 and 2.7
# pylint: disable=C0103
ParseError = getattr(ElementTree, 'ParseError', expat.ExpatError)
# pylint: enable=C0103
HAS_LXML = False
try:
from urllib import urlopen
import urlparse
except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
# pylint: enable=E0611,F0401,W0611
import gzip
import os.path
from .css import apply_stylesheets
# Python 2/3 compat
# pylint: disable=C0103,W0622
try:
basestring
except NameError:
basestring = str
# pylint: enable=C0103,W0622
def remove_svg_namespace(tree):
"""Remove the SVG namespace from ``tree`` tags.
``lxml.cssselect`` does not support empty/default namespaces, so remove any
SVG namespace.
"""
prefix = "{http://www.w3.org/2000/svg}"
prefix_len = len(prefix)
iterator = (
tree.iter() if hasattr(tree, 'iter')
else tree.getiterator())
for element in iterator:
tag = element.tag
if hasattr(tag, "startswith") and tag.startswith(prefix):
element.tag = tag[prefix_len:]
class Node(dict):
"""SVG node with dict-like properties and children."""
def __init__(self, node, parent = None):
"""Create the Node from ElementTree ``node``, with ``parent`` Node."""
super(Node, self).__init__()
self.children = ()
self.root = False
self.tag = node.tag
self.text = node.text
# Inherits from parent properties
# TODO: drop other attributes that should not be inherited
if parent is not None:
items = parent.copy()
not_inherited = (
"transform", "opacity", "style", "viewBox", "stop-color",
"stop-opacity")
if self.tag in ("tspan", "pattern"):
not_inherited += ("x", "y")
for attribute in not_inherited:
if attribute in items:
del items[attribute]
self.update(items)
self.url = parent.url
self.xml_tree = parent.xml_tree
self.parent = parent
self.update(dict(node.attrib.items()))
# Handle the CSS
style = self.pop("style", "")
for declaration in style.split(";"):
if ":" in declaration:
name, value = declaration.split(":", 1)
self[name.strip()] = value.strip()
# Replace currentColor by a real color value
color_attributes = (
"fill", "stroke", "stop-color", "flood-color",
"lighting-color")
for attribute in color_attributes:
if self.get(attribute) == "currentColor":
self[attribute] = self.get("color", "black")
# Replace inherit by the parent value
for attribute, value in dict(self).items():
if value == "inherit":
if parent is not None and attribute in parent:
self[attribute] = parent.get(attribute)
else:
del self[attribute]
# Manage text by creating children
if self.tag == "text" or self.tag == "textPath":
self.children = self.text_children(node)
if not self.children:
self.children = tuple(
Node(child, self) for child in node
if isinstance(child.tag, basestring))
def text_children(self, node):
"""Create children and return them."""
children = []
for child in node:
children.append(Node(child, parent = self))
if child.tail:
anonymous = ElementTree.Element('tspan')
anonymous.text = child.tail
children.append(Node(anonymous, parent = self))
return list(children)
class Tree(Node):
"""SVG tree."""
def __init__(self, **kwargs):
"""Create the Tree from SVG ``text``."""
# Make the parameters keyword-only:
bytestring = kwargs.pop('bytestring', None)
file_obj = kwargs.pop('file_obj', None)
url = kwargs.pop('url', None)
parent = kwargs.pop('parent', None)
if bytestring is not None:
tree = ElementTree.fromstring(bytestring)
self.url = url
elif file_obj is not None:
tree = ElementTree.parse(file_obj).getroot()
if url:
self.url = url
else:
self.url = getattr(file_obj, 'name', None)
elif url is not None:
if "#" in url:
url, element_id = url.split("#", 1)
else:
element_id = None
if parent and parent.url:
if url:
url = urlparse.urljoin(parent.url, url)
elif element_id:
url = parent.url
self.url = url
if url:
if urlparse.urlparse(url).scheme:
input_ = urlopen(url)
else:
input_ = url # filename
if os.path.splitext(url)[1].lower() == "svgz":
input_ = gzip.open(url)
tree = ElementTree.parse(input_).getroot()
else:
tree = parent.xml_tree
if element_id:
iterator = (
tree.iter() if hasattr(tree, 'iter')
else tree.getiterator())
for element in iterator:
if element.get("id") == element_id:
tree = element
break
else:
raise TypeError(
'No tag with id="%s" found.' % element_id)
else:
raise TypeError(
'No input. Use one of bytestring, file_obj or url.')
remove_svg_namespace(tree)
apply_stylesheets(tree)
self.xml_tree = tree
super(Tree, self).__init__(tree, parent)
self.root = True
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Cairo surface creators.
"""
import cairo
import io
from ..parser import Tree
from .colors import color
from .defs import gradient_or_pattern, parse_def
from .helpers import (
node_format, transform, normalize, filter_fill_or_stroke,
apply_matrix_transform, PointError)
from .path import PATH_TAGS
from .tags import TAGS
from .units import size
from . import units
class Surface(object):
"""Abstract base class for CairoSVG surfaces.
The ``width`` and ``height`` attributes are in device units (pixels for
PNG, else points).
The ``context_width`` and ``context_height`` attributes are in user units
(i.e. in pixels), they represent the size of the active viewport.
"""
# Subclasses must either define this or override _create_surface()
surface_class = None
@classmethod
def convert(cls, bytestring = None, **kwargs):
"""Convert a SVG document to the format for this class.
Specify the input by passing one of these:
:param bytestring: The SVG source as a byte-string.
:param file_obj: A file-like object.
:param url: A filename.
And the output with:
:param write_to: The filename of file-like object where to write the
output. If None or not provided, return a byte string.
Only ``source`` can be passed as a positional argument, other
parameters are keyword-only.
"""
dpi = kwargs.pop('dpi', 96)
write_to = kwargs.pop('write_to', None)
kwargs['bytestring'] = bytestring
tree = Tree(**kwargs)
if write_to is None:
output = io.BytesIO()
else:
output = write_to
cls(tree, output, dpi).finish()
if write_to is None:
return output.getvalue()
def __init__(self, tree, output, dpi):
"""Create the surface from a filename or a file-like object.
The rendered content is written to ``output`` which can be a filename,
a file-like object, ``None`` (render in memory but do not write
anything) or the built-in ``bytes`` as a marker.
Call the ``.finish()`` method to make sure that the output is
actually written.
"""
self.cairo = None
self.context_width, self.context_height = None, None
self.cursor_position = 0, 0
self.total_width = 0
self.markers = {}
self.gradients = {}
self.patterns = {}
self.paths = {}
self.page_sizes = []
self._old_parent_node = self.parent_node = None
self.output = output
self.dpi = dpi
self.font_size = size(self, "12pt")
width, height, viewbox = node_format(self, tree)
# Actual surface dimensions: may be rounded on raster surfaces types
self.cairo, self.width, self.height = self._create_surface(
width * self.device_units_per_user_units,
height * self.device_units_per_user_units)
self.page_sizes.append((self.width, self.height))
self.context = cairo.Context(self.cairo)
# We must scale the context as the surface size is using physical units
self.context.scale(
self.device_units_per_user_units, self.device_units_per_user_units)
# Initial, non-rounded dimensions
self.set_context_size(width, height, viewbox)
self.context.move_to(0, 0)
self.draw_root(tree)
@property
def points_per_pixel(self):
"""Surface resolution."""
return 1 / (self.dpi * units.UNITS["pt"])
@property
def device_units_per_user_units(self):
"""Ratio between Cairo device units and user units.
Device units are points for everything but PNG, and pixels for
PNG. User units are pixels.
"""
return self.points_per_pixel
def _create_surface(self, width, height):
"""Create and return ``(cairo_surface, width, height)``."""
# self.surface_class should not be None when called here
# pylint: disable=E1102
cairo_surface = self.surface_class(self.output, width, height)
# pylint: enable=E1102
return cairo_surface, width, height
def set_context_size(self, width, height, viewbox):
"""Set the Cairo context size, set the SVG viewport size."""
if viewbox:
x, y, x_size, y_size = viewbox
self.context_width, self.context_height = x_size, y_size
x_ratio, y_ratio = width / x_size, height / y_size
matrix = cairo.Matrix()
if x_ratio > y_ratio:
matrix.translate((width - x_size * y_ratio) / 2, 0)
matrix.scale(y_ratio, y_ratio)
matrix.translate(-x, -y / y_ratio * x_ratio)
elif x_ratio < y_ratio:
matrix.translate(0, (height - y_size * x_ratio) / 2)
matrix.scale(x_ratio, x_ratio)
matrix.translate(-x / x_ratio * y_ratio, -y)
else:
matrix.scale(x_ratio, y_ratio)
matrix.translate(-x, -y)
apply_matrix_transform(self, matrix)
else:
self.context_width, self.context_height = width, height
def finish(self):
"""Read the surface content."""
self.cairo.finish()
def draw_root(self, node):
"""Draw the root ``node``."""
self.draw(node)
def draw(self, node, stroke_and_fill = True):
"""Draw ``node`` and its children."""
old_font_size = self.font_size
self.font_size = size(self, node.get("font-size", "12pt"))
# Do not draw defs
if node.tag == "defs":
for child in node.children:
parse_def(self, child)
return
# Do not draw elements with width or height of 0
if (("width" in node and size(self, node["width"]) == 0) or
("height" in node and size(self, node["height"]) == 0)):
return
node.tangents = [None]
node.pending_markers = []
self._old_parent_node = self.parent_node
self.parent_node = node
opacity = float(node.get("opacity", 1))
if opacity < 1:
self.context.push_group()
self.context.save()
self.context.move_to(
size(self, node.get("x"), "x"),
size(self, node.get("y"), "y"))
# Transform the context according to the ``transform`` attribute
transform(self, node.get("transform"))
if node.tag in PATH_TAGS:
# Set 1 as default stroke-width
if not node.get("stroke-width"):
node["stroke-width"] = "1"
# Set node's drawing informations if the ``node.tag`` method exists
line_cap = node.get("stroke-linecap")
if line_cap == "square":
self.context.set_line_cap(cairo.LINE_CAP_SQUARE)
if line_cap == "round":
self.context.set_line_cap(cairo.LINE_CAP_ROUND)
join_cap = node.get("stroke-linejoin")
if join_cap == "round":
self.context.set_line_join(cairo.LINE_JOIN_ROUND)
if join_cap == "bevel":
self.context.set_line_join(cairo.LINE_JOIN_BEVEL)
dash_array = normalize(node.get("stroke-dasharray", "")).split()
if dash_array:
dashes = [size(self, dash) for dash in dash_array]
if sum(dashes):
offset = size(self, node.get("stroke-dashoffset"))
self.context.set_dash(dashes, offset)
miter_limit = float(node.get("stroke-miterlimit", 4))
self.context.set_miter_limit(miter_limit)
if node.tag in TAGS:
try:
TAGS[node.tag](self, node)
except PointError:
# Error in point parsing, do nothing
pass
# Get stroke and fill opacity
stroke_opacity = float(node.get("stroke-opacity", 1))
fill_opacity = float(node.get("fill-opacity", 1))
# Manage dispaly and visibility
display = node.get("display", "inline") != "none"
visible = display and (node.get("visibility", "visible") != "hidden")
if stroke_and_fill and visible:
# Fill
if "url(#" in (node.get("fill") or ""):
name = filter_fill_or_stroke(node.get("fill"))
gradient_or_pattern(self, node, name)
else:
if node.get("fill-rule") == "evenodd":
self.context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
self.context.set_source_rgba(
*color(node.get("fill", "black"), fill_opacity))
self.context.fill_preserve()
# Stroke
self.context.set_line_width(size(self, node.get("stroke-width")))
if "url(#" in (node.get("stroke") or ""):
name = filter_fill_or_stroke(node.get("stroke"))
gradient_or_pattern(self, node, name)
else:
self.context.set_source_rgba(
*color(node.get("stroke"), stroke_opacity))
self.context.stroke()
elif not visible:
self.context.new_path()
# Draw children
if display and node.tag not in (
"linearGradient", "radialGradient", "marker", "pattern"):
for child in node.children:
self.draw(child, stroke_and_fill)
if not node.root:
# Restoring context is useless if we are in the root tag, it may
# raise an exception if we have multiple svg tags
self.context.restore()
if opacity < 1:
self.context.pop_group_to_source()
self.context.paint_with_alpha(opacity)
self.parent_node = self._old_parent_node
self.font_size = old_font_size
class MultipageSurface(Surface):
"""Abstract base class for surfaces that can handle multiple pages."""
def draw_root(self, node):
self.width = None
self.height = None
svg_children = [child for child in node.children if child.tag == 'svg']
if svg_children:
# Multi-page
for page in svg_children:
width, height, viewbox = node_format(self, page)
self.context.save()
self.set_context_size(width, height, viewbox)
width *= self.device_units_per_user_units
height *= self.device_units_per_user_units
self.page_sizes.append((width, height))
self.cairo.set_size(width, height)
self.draw(page)
self.context.restore()
self.cairo.show_page()
else:
self.draw(node)
class PDFSurface(MultipageSurface):
"""A surface that writes in PDF format."""
surface_class = cairo.PDFSurface
class PSSurface(MultipageSurface):
"""A surface that writes in PostScript format."""
surface_class = cairo.PSSurface
class PNGSurface(Surface):
"""A surface that writes in PNG format."""
device_units_per_user_units = 1
def _create_surface(self, width, height):
"""Create and return ``(cairo_surface, width, height)``."""
width = int(width)
height = int(height)
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
return cairo_surface, width, height
def finish(self):
"""Read the PNG surface content."""
if self.output is not None:
self.cairo.write_to_png(self.output)
return super(PNGSurface, self).finish()
class SVGSurface(Surface):
"""A surface that writes in SVG format.
It may seem pointless to render SVG to SVG, but this can be used
with ``output=None`` to get a vector-based single page cairo surface.
"""
surface_class = cairo.SVGSurface
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG colors.
"""
COLORS = {
"aliceblue": "rgb(240, 248, 255)",
"antiquewhite": "rgb(250, 235, 215)",
"aqua": "rgb(0, 255, 255)",
"aquamarine": "rgb(127, 255, 212)",
"azure": "rgb(240, 255, 255)",
"beige": "rgb(245, 245, 220)",
"bisque": "rgb(255, 228, 196)",
"black": "rgb(0, 0, 0)",
"blanchedalmond": "rgb(255, 235, 205)",
"blue": "rgb(0, 0, 255)",
"blueviolet": "rgb(138, 43, 226)",
"brown": "rgb(165, 42, 42)",
"burlywood": "rgb(222, 184, 135)",
"cadetblue": "rgb(95, 158, 160)",
"chartreuse": "rgb(127, 255, 0)",
"chocolate": "rgb(210, 105, 30)",
"coral": "rgb(255, 127, 80)",
"cornflowerblue": "rgb(100, 149, 237)",
"cornsilk": "rgb(255, 248, 220)",
"crimson": "rgb(220, 20, 60)",
"cyan": "rgb(0, 255, 255)",
"darkblue": "rgb(0, 0, 139)",
"darkcyan": "rgb(0, 139, 139)",
"darkgoldenrod": "rgb(184, 134, 11)",
"darkgray": "rgb(169, 169, 169)",
"darkgreen": "rgb(0, 100, 0)",
"darkgrey": "rgb(169, 169, 169)",
"darkkhaki": "rgb(189, 183, 107)",
"darkmagenta": "rgb(139, 0, 139)",
"darkolivegreen": "rgb(85, 107, 47)",
"darkorange": "rgb(255, 140, 0)",
"darkorchid": "rgb(153, 50, 204)",
"darkred": "rgb(139, 0, 0)",
"darksalmon": "rgb(233, 150, 122)",
"darkseagreen": "rgb(143, 188, 143)",
"darkslateblue": "rgb(72, 61, 139)",
"darkslategray": "rgb(47, 79, 79)",
"darkslategrey": "rgb(47, 79, 79)",
"darkturquoise": "rgb(0, 206, 209)",
"darkviolet": "rgb(148, 0, 211)",
"deeppink": "rgb(255, 20, 147)",
"deepskyblue": "rgb(0, 191, 255)",
"dimgray": "rgb(105, 105, 105)",
"dimgrey": "rgb(105, 105, 105)",
"dodgerblue": "rgb(30, 144, 255)",
"firebrick": "rgb(178, 34, 34)",
"floralwhite": "rgb(255, 250, 240)",
"forestgreen": "rgb(34, 139, 34)",
"fuchsia": "rgb(255, 0, 255)",
"gainsboro": "rgb(220, 220, 220)",
"ghostwhite": "rgb(248, 248, 255)",
"gold": "rgb(255, 215, 0)",
"goldenrod": "rgb(218, 165, 32)",
"gray": "rgb(128, 128, 128)",
"grey": "rgb(128, 128, 128)",
"green": "rgb(0, 128, 0)",
"greenyellow": "rgb(173, 255, 47)",
"honeydew": "rgb(240, 255, 240)",
"hotpink": "rgb(255, 105, 180)",
"indianred": "rgb(205, 92, 92)",
"indigo": "rgb(75, 0, 130)",
"ivory": "rgb(255, 255, 240)",
"khaki": "rgb(240, 230, 140)",
"lavender": "rgb(230, 230, 250)",
"lavenderblush": "rgb(255, 240, 245)",
"lawngreen": "rgb(124, 252, 0)",
"lemonchiffon": "rgb(255, 250, 205)",
"lightblue": "rgb(173, 216, 230)",
"lightcoral": "rgb(240, 128, 128)",
"lightcyan": "rgb(224, 255, 255)",
"lightgoldenrodyellow": "rgb(250, 250, 210)",
"lightgray": "rgb(211, 211, 211)",
"lightgreen": "rgb(144, 238, 144)",
"lightgrey": "rgb(211, 211, 211)",
"lightpink": "rgb(255, 182, 193)",
"lightsalmon": "rgb(255, 160, 122)",
"lightseagreen": "rgb(32, 178, 170)",
"lightskyblue": "rgb(135, 206, 250)",
"lightslategray": "rgb(119, 136, 153)",
"lightslategrey": "rgb(119, 136, 153)",
"lightsteelblue": "rgb(176, 196, 222)",
"lightyellow": "rgb(255, 255, 224)",
"lime": "rgb(0, 255, 0)",
"limegreen": "rgb(50, 205, 50)",
"linen": "rgb(250, 240, 230)",
"magenta": "rgb(255, 0, 255)",
"maroon": "rgb(128, 0, 0)",
"mediumaquamarine": "rgb(102, 205, 170)",
"mediumblue": "rgb(0, 0, 205)",
"mediumorchid": "rgb(186, 85, 211)",
"mediumpurple": "rgb(147, 112, 219)",
"mediumseagreen": "rgb(60, 179, 113)",
"mediumslateblue": "rgb(123, 104, 238)",
"mediumspringgreen": "rgb(0, 250, 154)",
"mediumturquoise": "rgb(72, 209, 204)",
"mediumvioletred": "rgb(199, 21, 133)",
"midnightblue": "rgb(25, 25, 112)",
"mintcream": "rgb(245, 255, 250)",
"mistyrose": "rgb(255, 228, 225)",
"moccasin": "rgb(255, 228, 181)",
"navajowhite": "rgb(255, 222, 173)",
"navy": "rgb(0, 0, 128)",
"oldlace": "rgb(253, 245, 230)",
"olive": "rgb(128, 128, 0)",
"olivedrab": "rgb(107, 142, 35)",
"orange": "rgb(255, 165, 0)",
"orangered": "rgb(255, 69, 0)",
"orchid": "rgb(218, 112, 214)",
"palegoldenrod": "rgb(238, 232, 170)",
"palegreen": "rgb(152, 251, 152)",
"paleturquoise": "rgb(175, 238, 238)",
"palevioletred": "rgb(219, 112, 147)",
"papayawhip": "rgb(255, 239, 213)",
"peachpuff": "rgb(255, 218, 185)",
"peru": "rgb(205, 133, 63)",
"pink": "rgb(255, 192, 203)",
"plum": "rgb(221, 160, 221)",
"powderblue": "rgb(176, 224, 230)",
"purple": "rgb(128, 0, 128)",
"red": "rgb(255, 0, 0)",
"rosybrown": "rgb(188, 143, 143)",
"royalblue": "rgb(65, 105, 225)",
"saddlebrown": "rgb(139, 69, 19)",
"salmon": "rgb(250, 128, 114)",
"sandybrown": "rgb(244, 164, 96)",
"seagreen": "rgb(46, 139, 87)",
"seashell": "rgb(255, 245, 238)",
"sienna": "rgb(160, 82, 45)",
"silver": "rgb(192, 192, 192)",
"skyblue": "rgb(135, 206, 235)",
"slateblue": "rgb(106, 90, 205)",
"slategray": "rgb(112, 128, 144)",
"slategrey": "rgb(112, 128, 144)",
"snow": "rgb(255, 250, 250)",
"springgreen": "rgb(0, 255, 127)",
"steelblue": "rgb(70, 130, 180)",
"tan": "rgb(210, 180, 140)",
"teal": "rgb(0, 128, 128)",
"thistle": "rgb(216, 191, 216)",
"tomato": "rgb(255, 99, 71)",
"turquoise": "rgb(64, 224, 208)",
"violet": "rgb(238, 130, 238)",
"wheat": "rgb(245, 222, 179)",
"white": "rgb(255, 255, 255)",
"whitesmoke": "rgb(245, 245, 245)",
"yellow": "rgb(255, 255, 0)",
"yellowgreen": "rgb(154, 205, 50)",
"activeborder": "#0000ff",
"activecaption": "#0000ff",
"appworkspace": "#ffffff",
"background": "#ffffff",
"buttonface": "#000000",
"buttonhighlight": "#cccccc",
"buttonshadow": "#333333",
"buttontext": "#000000",
"captiontext": "#000000",
"graytext": "#333333",
"highlight": "#0000ff",
"highlighttext": "#cccccc",
"inactiveborder": "#333333",
"inactivecaption": "#cccccc",
"inactivecaptiontext": "#333333",
"infobackground": "#cccccc",
"infotext": "#000000",
"menu": "#cccccc",
"menutext": "#333333",
"scrollbar": "#cccccc",
"threeddarkshadow": "#333333",
"threedface": "#cccccc",
"threedhighlight": "#ffffff",
"threedlightshadow": "#333333",
"threedshadow": "#333333",
"window": "#cccccc",
"windowframe": "#cccccc",
"windowtext": "#000000"}
def color(string = None, opacity = 1):
"""Replace ``string`` representing a color by a RGBA tuple."""
if not string or string in ("none", "transparent"):
return (0, 0, 0, 0)
string = string.strip().lower()
if string in COLORS:
string = COLORS[string]
if string.startswith("rgba"):
r, g, b, a = tuple(
float(i.strip(" %")) * 2.55 if "%" in i else float(i)
for i in string.strip(" rgba()").split(","))
return r / 255, g / 255, b / 255, a * opacity
elif string.startswith("rgb"):
r, g, b = tuple(
float(i.strip(" %")) / 100 if "%" in i else float(i) / 255
for i in string.strip(" rgb()").split(","))
return r, g, b, opacity
if len(string) in (4, 5):
string = "#" + "".join(2 * char for char in string[1:])
if len(string) == 9:
opacity *= int(string[7:9], 16) / 255
try:
plain_color = tuple(
int(value, 16) / 255. for value in (
string[1:3], string[3:5], string[5:7]))
except ValueError:
# Unknown color, return black
return (0, 0, 0, 1)
else:
return plain_color + (opacity,)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Externally defined elements managers.
This module handles gradients and patterns.
"""
import cairo
from math import radians
from copy import deepcopy
from .colors import color
from .helpers import node_format, preserve_ratio, urls, transform
from .units import size
from ..parser import Tree
def parse_def(surface, node):
"""Parse the SVG definitions."""
for def_type in ("marker", "gradient", "pattern", "path"):
if def_type in node.tag.lower():
def_list = getattr(surface, def_type + "s")
name = node["id"]
href = node.get("{http://www.w3.org/1999/xlink}href")
if href and href[0] == "#" and href[1:] in def_list:
new_node = deepcopy(def_list[href[1:]])
new_node.update(node)
node = new_node
def_list[name] = node
def gradient_or_pattern(surface, node, name):
"""Gradient or pattern color."""
if name in surface.gradients:
return draw_gradient(surface, node, name)
elif name in surface.patterns:
return draw_pattern(surface, name)
def marker(surface, node):
"""Store a marker definition."""
parse_def(surface, node)
def linear_gradient(surface, node):
"""Store a linear gradient definition."""
parse_def(surface, node)
def radial_gradient(surface, node):
"""Store a radial gradient definition."""
parse_def(surface, node)
def pattern(surface, node):
"""Store a pattern definition."""
parse_def(surface, node)
def draw_gradient(surface, node, name):
"""Gradients colors."""
gradient_node = surface.gradients[name]
transform(surface, gradient_node.get("gradientTransform"))
if gradient_node.get("gradientUnits") == "userSpaceOnUse":
width_ref, height_ref = "x", "y"
diagonal_ref = "xy"
else:
x = float(size(surface, node.get("x"), "x"))
y = float(size(surface, node.get("y"), "y"))
width = float(size(surface, node.get("width"), "x"))
height = float(size(surface, node.get("height"), "y"))
width_ref = height_ref = diagonal_ref = 1
if gradient_node.tag == "linearGradient":
x1 = float(size(surface, gradient_node.get("x1", "0%"), width_ref))
x2 = float(size(surface, gradient_node.get("x2", "100%"), width_ref))
y1 = float(size(surface, gradient_node.get("y1", "0%"), height_ref))
y2 = float(size(surface, gradient_node.get("y2", "0%"), height_ref))
gradient_pattern = cairo.LinearGradient(x1, y1, x2, y2)
elif gradient_node.tag == "radialGradient":
r = float(size(surface, gradient_node.get("r", "50%"), diagonal_ref))
cx = float(size(surface, gradient_node.get("cx", "50%"), width_ref))
cy = float(size(surface, gradient_node.get("cy", "50%"), height_ref))
fx = float(size(surface, gradient_node.get("fx", str(cx)), width_ref))
fy = float(size(surface, gradient_node.get("fy", str(cy)), height_ref))
gradient_pattern = cairo.RadialGradient(fx, fy, 0, cx, cy, r)
if gradient_node.get("gradientUnits") != "userSpaceOnUse":
gradient_pattern.set_matrix(cairo.Matrix(
1 / width, 0, 0, 1 / height, -x / width, -y / height))
gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % node.get("spreadMethod", "pad").upper()))
offset = 0
for child in gradient_node.children:
offset = max(offset, size(surface, child.get("offset"), 1))
stop_color = color(
child.get("stop-color", "black"),
float(child.get("stop-opacity", 1)))
gradient_pattern.add_color_stop_rgba(offset, *stop_color)
gradient_pattern.set_extend(getattr(
cairo, "EXTEND_%s" % gradient_node.get("spreadMethod", "pad").upper()))
surface.context.set_source(gradient_pattern)
def draw_pattern(surface, name):
"""Draw a pattern image."""
pattern_node = surface.patterns[name]
pattern_node.tag = "g"
transform(surface, "translate(%s %s)" % (
pattern_node.get("x"), pattern_node.get("y")))
transform(surface, pattern_node.get("patternTransform"))
from . import SVGSurface # circular import
pattern_surface = SVGSurface(pattern_node, None, surface.dpi)
pattern_pattern = cairo.SurfacePattern(pattern_surface.cairo)
pattern_pattern.set_extend(cairo.EXTEND_REPEAT)
surface.context.set_source(pattern_pattern)
def draw_marker(surface, node, position = "mid"):
"""Draw a marker."""
# TODO: manage markers for other tags than path
if position == "start":
node.markers = {
"start": list(urls(node.get("marker-start", ""))),
"mid": list(urls(node.get("marker-mid", ""))),
"end": list(urls(node.get("marker-end", "")))}
all_markers = list(urls(node.get("marker", "")))
for markers_list in node.markers.values():
markers_list.extend(all_markers)
pending_marker = (
surface.context.get_current_point(), node.markers[position])
if position == "start":
node.pending_markers.append(pending_marker)
return
elif position == "end":
node.pending_markers.append(pending_marker)
while node.pending_markers:
next_point, markers = node.pending_markers.pop(0)
angle1 = node.tangents.pop(0)
angle2 = node.tangents.pop(0)
if angle1 is None:
angle1 = angle2
for active_marker in markers:
if not active_marker.startswith("#"):
continue
active_marker = active_marker[1:]
if active_marker in surface.markers:
marker_node = surface.markers[active_marker]
angle = marker_node.get("orient", "0")
if angle == "auto":
angle = float(angle1 + angle2) / 2
else:
angle = radians(float(angle))
temp_path = surface.context.copy_path()
current_x, current_y = next_point
if node.get("markerUnits") == "userSpaceOnUse":
base_scale = 1
else:
base_scale = size(
surface, surface.parent_node.get("stroke-width"))
# Returns 4 values
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, marker_node)
viewbox = node_format(surface, marker_node)[-1]
viewbox_width = viewbox[2] - viewbox[0]
viewbox_height = viewbox[3] - viewbox[1]
surface.context.new_path()
for child in marker_node.children:
surface.context.save()
surface.context.translate(current_x, current_y)
surface.context.rotate(angle)
surface.context.scale(
base_scale / viewbox_width * float(scale_x),
base_scale / viewbox_height * float(scale_y))
surface.context.translate(translate_x, translate_y)
surface.draw(child)
surface.context.restore()
surface.context.append_path(temp_path)
if position == "mid":
node.pending_markers.append(pending_marker)
def use(surface, node):
"""Draw the content of another SVG file."""
surface.context.save()
surface.context.translate(
size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y"))
if "x" in node:
del node["x"]
if "y" in node:
del node["y"]
if "viewBox" in node:
del node["viewBox"]
href = node.get("{http://www.w3.org/1999/xlink}href")
url = list(urls(href))[0]
tree = Tree(url = url, parent = node)
surface.set_context_size(*node_format(surface, tree))
surface.draw(tree)
surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags
surface.context.restore()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Surface helpers.
"""
import cairo
from math import cos, sin, tan, atan2, radians
from .units import size
# Python 2/3 management
# pylint: disable=C0103
try:
Error = cairo.Error
except AttributeError:
Error = SystemError
# pylint: enable=C0103
class PointError(Exception):
"""Exception raised when parsing a point fails."""
def distance(x1, y1, x2, y2):
"""Get the distance between two points."""
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
def filter_fill_or_stroke(value):
"""Remove unnecessary characters from fill or stroke value."""
if not value:
return
content = list(urls(value))[0]
if "url" in value:
if not content.startswith("#"):
return
content = content[1:]
return content
def node_format(surface, node):
"""Return ``(width, height, viewbox)`` of ``node``."""
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
viewbox = node.get("viewBox")
if viewbox:
viewbox = tuple(float(position) for position in viewbox.split())
width = width or viewbox[2]
height = height or viewbox[3]
return width, height, viewbox
def normalize(string = None):
"""Normalize a string corresponding to an array of various values."""
string = string.replace("-", " -")
string = string.replace(",", " ")
while " " in string:
string = string.replace(" ", " ")
string = string.replace("e -", "e-")
values = string.split(" ")
string = ""
for value in values:
if value.count(".") > 1:
numbers = value.split(".")
string += "%s.%s " % (numbers.pop(0), numbers.pop(0))
string += ".%s " % " .".join(numbers)
else:
string += value + " "
return string.strip()
def point(surface, string = None):
"""Return ``(x, y, trailing_text)`` from ``string``."""
if not string:
return (0, 0, "")
try:
x, y, string = (string.strip() + " ").split(" ", 2)
except ValueError:
raise PointError("The point cannot be found in string %s" % string)
return size(surface, x, "x"), size(surface, y, "y"), string
def point_angle(cx, cy, px, py):
"""Return angle between x axis and point knowing given center."""
return atan2(py - cy, px - cx)
def preserve_ratio(surface, node):
"""Manage the ratio preservation."""
if node.tag == "marker":
scale_x = size(surface, node.get("markerWidth", "3"), "x")
scale_y = size(surface, node.get("markerHeight", "3"), "y")
translate_x = -size(surface, node.get("refX"))
translate_y = -size(surface, node.get("refY"))
elif node.tag in ("svg", "image"):
width, height, _ = node_format(surface, node)
scale_x = width / node.image_width
scale_y = height / node.image_height
align = node.get("preserveAspectRatio", "xMidYMid").split(" ")[0]
if align == "none":
return scale_x, scale_y, 0, 0
else:
mos_properties = node.get("preserveAspectRatio", "").split()
meet_or_slice = (
mos_properties[1] if len(mos_properties) > 1 else None)
if meet_or_slice == "slice":
scale_value = max(scale_x, scale_y)
else:
scale_value = min(scale_x, scale_y)
scale_x = scale_y = scale_value
x_position = align[1:4].lower()
y_position = align[5:].lower()
if x_position == "min":
translate_x = 0
if y_position == "min":
translate_y = 0
if x_position == "mid":
translate_x = (width / scale_x - node.image_width) / 2.
if y_position == "mid":
translate_y = (height / scale_y - node.image_height) / 2.
if x_position == "max":
translate_x = width / scale_x - node.image_width
if y_position == "max":
translate_y = height / scale_y - node.image_height
return scale_x, scale_y, translate_x, translate_y
def quadratic_points(x1, y1, x2, y2, x3, y3):
"""Return the quadratic points to create quadratic curves."""
xq1 = x2 * 2 / 3 + x1 / 3
yq1 = y2 * 2 / 3 + y1 / 3
xq2 = x2 * 2 / 3 + x3 / 3
yq2 = y2 * 2 / 3 + y3 / 3
return xq1, yq1, xq2, yq2, x3, y3
def rotate(x, y, angle):
"""Rotate a point of an angle around the origin point."""
return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle)
def transform(surface, string):
"""Update ``surface`` matrix according to transformation ``string``."""
if not string:
return
transformations = string.split(")")
matrix = cairo.Matrix()
for transformation in transformations:
for ttype in (
"scale", "translate", "matrix", "rotate", "skewX",
"skewY"):
if ttype in transformation:
transformation = transformation.replace(ttype, "")
transformation = transformation.replace("(", "")
transformation = normalize(transformation).strip() + " "
values = []
while transformation:
value, transformation = \
transformation.split(" ", 1)
# TODO: manage the x/y sizes here
values.append(size(surface, value))
if ttype == "matrix":
matrix = cairo.Matrix(*values).multiply(matrix)
elif ttype == "rotate":
angle = radians(float(values.pop(0)))
x, y = values or (0, 0)
matrix.translate(x, y)
matrix.rotate(angle)
matrix.translate(-x, -y)
elif ttype == "skewX":
tangent = tan(radians(float(values[0])))
matrix = \
cairo.Matrix(1, 0, tangent, 1, 0, 0).multiply(matrix)
elif ttype == "skewY":
tangent = tan(radians(float(values[0])))
matrix = \
cairo.Matrix(1, tangent, 0, 1, 0, 0).multiply(matrix)
elif ttype == "translate":
if len(values) == 1:
values += (0,)
matrix.translate(*values)
elif ttype == "scale":
if len(values) == 1:
values = 2 * values
matrix.scale(*values)
apply_matrix_transform(surface, matrix)
def apply_matrix_transform(surface, matrix):
try:
matrix.invert()
except Error:
# Matrix not invertible, clip the surface to an empty path
active_path = surface.context.copy_path()
surface.context.new_path()
surface.context.clip()
surface.context.append_path(active_path)
else:
matrix.invert()
surface.context.transform(matrix)
def urls(string):
"""Parse a comma-separated list of url() strings."""
for link in string.split(","):
link = link.strip()
if link.startswith("url"):
link = link[3:]
yield link.strip("() ")
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Images manager.
"""
import base64
import cairo
from io import BytesIO
try:
from urllib import urlopen, unquote
import urlparse
unquote_to_bytes = lambda data: unquote(
data.encode('ascii') if isinstance(data, unicode) else data)
except ImportError:
from urllib.request import urlopen
from urllib import parse as urlparse # Python 3
from urllib.parse import unquote_to_bytes
from .helpers import node_format, size, preserve_ratio
from ..parser import Tree
def open_data_url(url):
"""Decode URLs with the 'data' scheme. urllib can handle them
in Python 2, but that is broken in Python 3.
Inspired from Python 2.7.2’s urllib.py.
"""
# syntax of data URLs:
# dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
# mediatype := [ type "/" subtype ] *( ";" parameter )
# data := *urlchar
# parameter := attribute "=" value
try:
header, data = url.split(",", 1)
except ValueError:
raise IOError("bad data URL")
header = header[5:] # len("data:") == 5
if header:
semi = header.rfind(";")
if semi >= 0 and "=" not in header[semi:]:
encoding = header[semi + 1:]
else:
encoding = ""
else:
encoding = ""
data = unquote_to_bytes(data)
if encoding == "base64":
missing_padding = 4 - len(data) % 4
if missing_padding:
data += b"=" * missing_padding
return base64.decodestring(data)
return data
def image(surface, node):
"""Draw an image ``node``."""
url = node.get("{http://www.w3.org/1999/xlink}href")
if not url:
return
if url.startswith("data:"):
image_bytes = open_data_url(url)
else:
base_url = node.get("{http://www.w3.org/XML/1998/namespace}base")
if base_url:
url = urlparse.urljoin(base_url, url)
if node.url:
url = urlparse.urljoin(node.url, url)
if urlparse.urlparse(url).scheme:
input_ = urlopen(url)
else:
input_ = open(url, 'rb') # filename
image_bytes = input_.read()
if len(image_bytes) < 5:
return
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
surface.context.rectangle(x, y, width, height)
surface.context.clip()
if image_bytes[:4] == b"\x89PNG":
png_bytes = image_bytes
elif image_bytes[:5] == b"\x3csvg ":
surface.context.save()
surface.context.translate(x, y)
if "x" in node:
del node["x"]
if "y" in node:
del node["y"]
if "viewBox" in node:
del node["viewBox"]
tree = Tree(bytestring = image_bytes)
tree_width, tree_height, viewbox = node_format(surface, tree)
if not tree_width or not tree_height:
tree_width = tree["width"] = width
tree_height = tree["height"] = height
node.image_width = tree_width or width
node.image_height = tree_height or height
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node)
surface.set_context_size(*node_format(surface, tree))
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
surface.draw(tree)
surface.context.restore()
# Restore twice, because draw does not restore at the end of svg tags
surface.context.restore()
return
else:
try:
from pystacia import read_blob
png_bytes = read_blob(image_bytes).get_blob('png')
except:
# No way to handle the image
return
image_surface = cairo.ImageSurface.create_from_png(BytesIO(png_bytes))
node.image_width = image_surface.get_width()
node.image_height = image_surface.get_height()
scale_x, scale_y, translate_x, translate_y = preserve_ratio(surface, node)
surface.context.rectangle(x, y, width, height)
pattern_pattern = cairo.SurfacePattern(image_surface)
surface.context.save()
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
surface.context.set_source(pattern_pattern)
surface.context.fill()
surface.context.restore()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Paths manager.
"""
from math import pi, radians
from .defs import draw_marker
from .helpers import normalize, point, point_angle, quadratic_points, rotate
from .units import size
PATH_LETTERS = "achlmqstvzACHLMQSTVZ"
PATH_TAGS = (
"circle", "ellipse", "line", "path", "polygon", "polyline", "rect")
def path(surface, node):
"""Draw a path ``node``."""
string = node.get("d", "")
if not string.strip():
# Don't draw empty paths at all
return
draw_marker(surface, node, "start")
for letter in PATH_LETTERS:
string = string.replace(letter, " %s " % letter)
last_letter = None
string = normalize(string)
while string:
string = string.strip()
if string.split(" ", 1)[0] in PATH_LETTERS:
letter, string = (string + " ").split(" ", 1)
elif letter == "M":
letter = "L"
elif letter == "m":
letter = "l"
if letter in "aA":
# Elliptic curve
x1, y1 = surface.context.get_current_point()
rx, ry, string = point(surface, string)
rotation, string = string.split(" ", 1)
rotation = radians(float(rotation))
# The large and sweep values are not always separated from the
# following values, here is the crazy parser
large, string = string[0], string[1:].strip()
while not large[-1].isdigit():
large, string = large + string[0], string[1:].strip()
sweep, string = string[0], string[1:].strip()
while not sweep[-1].isdigit():
sweep, string = sweep + string[0], string[1:].strip()
large, sweep = bool(int(large)), bool(int(sweep))
x3, y3, string = point(surface, string)
if letter == "A":
# Absolute x3 and y3, convert to relative
x3 -= x1
y3 -= y1
# rx=0 or ry=0 means straight line
if not rx or not ry:
string = "l %f %f %s" % (x3, y3, string)
continue
radii_ratio = ry / rx
# Cancel the rotation of the second point
xe, ye = rotate(x3, y3, -rotation)
ye /= radii_ratio
# Find the angle between the second point and the x axis
angle = point_angle(0, 0, xe, ye)
# Put the second point onto the x axis
xe = (xe ** 2 + ye ** 2) ** .5
ye = 0
# Update the x radius if it is too small
rx = max(rx, xe / 2)
# Find one circle centre
xc = xe / 2
yc = (rx ** 2 - xc ** 2) ** .5
# Choose between the two circles according to flags
if not (large ^ sweep):
yc = -yc
# Define the arc sweep
arc = \
surface.context.arc if sweep else surface.context.arc_negative
# Put the second point and the center back to their positions
xe, ye = rotate(xe, 0, angle)
xc, yc = rotate(xc, yc, angle)
# Find the drawing angles
angle1 = point_angle(xc, yc, 0, 0)
angle2 = point_angle(xc, yc, xe, ye)
# Store the tangent angles
node.tangents.extend((-angle1, -angle2))
# Draw the arc
surface.context.save()
surface.context.translate(x1, y1)
surface.context.rotate(rotation)
surface.context.scale(1, radii_ratio)
arc(xc, yc, rx, angle1, angle2)
surface.context.restore()
elif letter == "c":
# Relative curve
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "C":
# Curve
x1, y1, string = point(surface, string)
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "h":
# Relative horizontal line
x, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = 0 if size(surface, x, "x") > 0 else pi
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(size(surface, x, "x"), 0)
elif letter == "H":
# Horizontal line
x, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = 0 if size(surface, x, "x") > old_x else pi
node.tangents.extend((-angle, angle))
surface.context.line_to(size(surface, x, "x"), old_y)
elif letter == "l":
# Relative straight line
x, y, string = point(surface, string)
angle = point_angle(0, 0, x, y)
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(x, y)
elif letter == "L":
# Straight line
x, y, string = point(surface, string)
old_x, old_y = surface.context.get_current_point()
angle = point_angle(old_x, old_y, x, y)
node.tangents.extend((-angle, angle))
surface.context.line_to(x, y)
elif letter == "m":
# Current point relative move
x, y, string = point(surface, string)
surface.context.rel_move_to(x, y)
elif letter == "M":
# Current point move
x, y, string = point(surface, string)
surface.context.move_to(x, y)
elif letter == "q":
# Relative quadratic curve
x1, y1 = 0, 0
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
node.tangents.extend((0, 0))
elif letter == "Q":
# Quadratic curve
x1, y1 = surface.context.get_current_point()
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
node.tangents.extend((0, 0))
elif letter == "s":
# Relative smooth curve
# TODO: manage last_letter in "CS"
x1 = x3 - x2 if last_letter in "cs" else 0
y1 = y3 - y2 if last_letter in "cs" else 0
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.rel_curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "S":
# Smooth curve
# TODO: manage last_letter in "cs"
x, y = surface.context.get_current_point()
x1 = 2 * x3 - x2 if last_letter in "CS" else x
y1 = 2 * y3 - y2 if last_letter in "CS" else y
x2, y2, string = point(surface, string)
x3, y3, string = point(surface, string)
node.tangents.extend((
point_angle(x2, y2, x1, y1), point_angle(x2, y2, x3, y3)))
surface.context.curve_to(x1, y1, x2, y2, x3, y3)
elif letter == "t":
# Relative quadratic curve end
if last_letter not in "QqTt":
x2, y2, x3, y3 = 0, 0, 0, 0
elif last_letter in "QT":
x2 -= x1
y2 -= y1
x3 -= x1
y3 -= y1
x2 = x3 - x2
y2 = y3 - y2
x1, y1 = 0, 0
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
node.tangents.extend((0, 0))
surface.context.rel_curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
elif letter == "T":
# Quadratic curve end
abs_x, abs_y = surface.context.get_current_point()
if last_letter not in "QqTt":
x2, y2, x3, y3 = abs_x, abs_y, abs_x, abs_y
elif last_letter in "qt":
x2 += x1
y2 += y1
x2 = 2 * abs_x - x2
y2 = 2 * abs_y - y2
x1, y1 = abs_x, abs_y
x3, y3, string = point(surface, string)
xq1, yq1, xq2, yq2, xq3, yq3 = quadratic_points(
x1, y1, x2, y2, x3, y3)
node.tangents.extend((0, 0))
surface.context.curve_to(xq1, yq1, xq2, yq2, xq3, yq3)
elif letter == "v":
# Relative vertical line
y, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2
node.tangents.extend((-angle, angle))
surface.context.rel_line_to(0, size(surface, y, "y"))
elif letter == "V":
# Vertical line
y, string = (string + " ").split(" ", 1)
old_x, old_y = surface.context.get_current_point()
angle = pi / 2 if size(surface, y, "y") > 0 else -pi / 2
node.tangents.extend((-angle, angle))
surface.context.line_to(old_x, size(surface, y, "y"))
elif letter in "zZ":
# End of path
node.tangents.extend((0, 0))
surface.context.close_path()
string = string.strip()
if letter in "hHvV":
if string.split(" ", 1)[0] not in PATH_LETTERS:
surface.context.move_to(*surface.context.get_current_point())
if string and letter not in "mMzZ":
draw_marker(surface, node, "mid")
last_letter = letter
node.tangents.append(node.tangents[-1])
draw_marker(surface, node, "end")
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Shapes drawers.
"""
from math import pi
from .helpers import normalize, point, size
def circle(surface, node):
"""Draw a circle ``node`` on ``surface``."""
r = size(surface, node.get("r"))
if not r:
return
surface.context.new_sub_path()
surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"),
size(surface, node.get("y"), "y") + size(surface, node.get("cy"), "y"),
r, 0, 2 * pi)
def ellipse(surface, node):
"""Draw an ellipse ``node`` on ``surface``."""
rx = size(surface, node.get("rx"), "x")
ry = size(surface, node.get("ry"), "y")
if not rx or not ry:
return
ratio = ry / rx
surface.context.new_sub_path()
surface.context.save()
surface.context.scale(1, ratio)
surface.context.arc(
size(surface, node.get("x"), "x") + size(surface, node.get("cx"), "x"),
(size(surface, node.get("y"), "y") +
size(surface, node.get("cy"), "y")) / ratio,
size(surface, node.get("rx"), "x"), 0, 2 * pi)
surface.context.restore()
def line(surface, node):
"""Draw a line ``node``."""
x1, y1, x2, y2 = tuple(
size(surface, node.get(position), position[0])
for position in ("x1", "y1", "x2", "y2"))
surface.context.move_to(x1, y1)
surface.context.line_to(x2, y2)
def polygon(surface, node):
"""Draw a polygon ``node`` on ``surface``."""
polyline(surface, node)
surface.context.close_path()
def polyline(surface, node):
"""Draw a polyline ``node``."""
points = normalize(node.get("points"))
if points:
x, y, points = point(surface, points)
surface.context.move_to(x, y)
while points:
x, y, points = point(surface, points)
surface.context.line_to(x, y)
def rect(surface, node):
"""Draw a rect ``node`` on ``surface``."""
# TODO: handle ry
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
width = size(surface, node.get("width"), "x")
height = size(surface, node.get("height"), "y")
if size(surface, node.get("rx"), "x") == 0:
surface.context.rectangle(x, y, width, height)
else:
r = size(surface, node.get("rx"), "x")
a, b, c, d = x, width + x, y, height + y
if r > width - r:
r = width / 2
surface.context.move_to(x, y + height / 2)
surface.context.arc(a + r, c + r, r, 2 * pi / 2, 3 * pi / 2)
surface.context.arc(b - r, c + r, r, 3 * pi / 2, 0 * pi / 2)
surface.context.arc(b - r, d - r, r, 0 * pi / 2, 1 * pi / 2)
surface.context.arc(a + r, d - r, r, 1 * pi / 2, 2 * pi / 2)
surface.context.close_path()
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Root tag drawer.
"""
from .helpers import preserve_ratio, node_format
def svg(surface, node):
"""Draw a svg ``node``."""
if node.get("preserveAspectRatio", "none") != "none":
width, height, viewbox = node_format(surface, node)
node.image_width, node.image_height = viewbox[2:]
scale_x, scale_y, translate_x, translate_y = \
preserve_ratio(surface, node)
surface.context.rectangle(0, 0, width, height)
surface.context.clip()
surface.context.translate(*surface.context.get_current_point())
surface.context.scale(scale_x, scale_y)
surface.context.translate(translate_x, translate_y)
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
SVG tags functions.
"""
from .defs import linear_gradient, marker, pattern, radial_gradient, use
from .image import image
from .path import path
from .shapes import circle, ellipse, line, polygon, polyline, rect
from .svg import svg
from .text import text, text_path, tspan
TAGS = {
"a": tspan,
"circle": circle,
"ellipse": ellipse,
"image": image,
"line": line,
"linearGradient": linear_gradient,
"marker": marker,
"path": path,
"pattern": pattern,
"polyline": polyline,
"polygon": polygon,
"radialGradient": radial_gradient,
"rect": rect,
"svg": svg,
"text": text,
"textPath": text_path,
"tref": use,
"tspan": tspan,
"use": use}
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Text drawers.
"""
import cairo
from math import cos, sin
# Python 2/3 management
# pylint: disable=E0611
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
# pylint: enable=E0611
from .colors import color
from .helpers import distance, normalize, point_angle
from .units import size
def path_length(path):
"""Get the length of ``path``."""
total_length = 0
for item in path:
if item[0] == cairo.PATH_MOVE_TO:
old_point = item[1]
elif item[0] == cairo.PATH_LINE_TO:
new_point = item[1]
length = distance(
old_point[0], old_point[1], new_point[0], new_point[1])
total_length += length
old_point = new_point
return total_length
def point_following_path(path, width):
"""Get the point at ``width`` distance on ``path``."""
total_length = 0
for item in path:
if item[0] == cairo.PATH_MOVE_TO:
old_point = item[1]
elif item[0] == cairo.PATH_LINE_TO:
new_point = item[1]
length = distance(
old_point[0], old_point[1], new_point[0], new_point[1])
total_length += length
if total_length < width:
old_point = new_point
else:
length -= total_length - width
angle = point_angle(
old_point[0], old_point[1], new_point[0], new_point[1])
x = cos(angle) * length + old_point[0]
y = sin(angle) * length + old_point[1]
return x, y
def text(surface, node):
"""Draw a text ``node``."""
# Set black as default text color
if not node.get("fill"):
node["fill"] = "#000000"
# TODO: find a better way to manage white spaces in text nodes
node.text = (node.text or "").lstrip()
node.text = node.text.rstrip() + " "
# TODO: manage font variant
font_size = size(surface, node.get("font-size", "12pt"))
font_family = (node.get("font-family") or "sans-serif").split(",")[0]
font_style = getattr(
cairo, ("font_slant_%s" % node.get("font-style")).upper(),
cairo.FONT_SLANT_NORMAL)
font_weight = getattr(
cairo, ("font_weight_%s" % node.get("font-weight")).upper(),
cairo.FONT_WEIGHT_NORMAL)
surface.context.select_font_face(font_family, font_style, font_weight)
surface.context.set_font_size(font_size)
text_extents = surface.context.text_extents(node.text)
x_bearing = text_extents[0]
width = text_extents[2]
x, y = size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
text_anchor = node.get("text-anchor")
if text_anchor == "middle":
x -= width / 2. + x_bearing
elif text_anchor == "end":
x -= width + x_bearing
surface.context.move_to(x, y)
surface.context.text_path(node.text)
# Remember the absolute cursor position
surface.cursor_position = surface.context.get_current_point()
def text_path(surface, node):
"""Draw text on a path."""
surface.context.save()
if "url(#" not in (node.get("fill") or ""):
surface.context.set_source_rgba(*color(node.get("fill")))
id_path = node.get("{http://www.w3.org/1999/xlink}href", "")
if not id_path.startswith("#"):
return
id_path = id_path[1:]
if id_path in surface.paths:
path = surface.paths.get(id_path)
else:
return
surface.draw(path, False)
cairo_path = surface.context.copy_path_flat()
surface.context.new_path()
start_offset = size(
surface, node.get("startOffset", 0), path_length(cairo_path))
surface.total_width += start_offset
x, y = point_following_path(cairo_path, surface.total_width)
string = (node.text or "").strip(" \n")
letter_spacing = size(surface, node.get("letter-spacing"))
for letter in string:
surface.total_width += (
surface.context.text_extents(letter)[4] + letter_spacing)
point_on_path = point_following_path(cairo_path, surface.total_width)
if point_on_path:
x2, y2 = point_on_path
else:
continue
angle = point_angle(x, y, x2, y2)
surface.context.save()
surface.context.translate(x, y)
surface.context.rotate(angle)
surface.context.translate(0, size(surface, node.get("y"), "y"))
surface.context.move_to(0, 0)
surface.context.show_text(letter)
surface.context.restore()
x, y = x2, y2
surface.context.restore()
# Remember the relative cursor position
surface.cursor_position = \
size(surface, node.get("x"), "x"), size(surface, node.get("y"), "y")
def tspan(surface, node):
"""Draw a tspan ``node``."""
x, y = [[i] for i in surface.cursor_position]
if "x" in node:
x = [size(surface, i, "x")
for i in normalize(node["x"]).strip().split(" ")]
if "y" in node:
y = [size(surface, i, "y")
for i in normalize(node["y"]).strip().split(" ")]
string = (node.text or "").strip()
if not string:
return
fill = node.get("fill")
positions = list(zip_longest(x, y))
letters_positions = list(zip(positions, string))
letters_positions = letters_positions[:-1] + [
(letters_positions[-1][0], string[len(letters_positions) - 1:])]
for (x, y), letters in letters_positions:
if x == None:
x = surface.cursor_position[0]
if y == None:
y = surface.cursor_position[1]
node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y"))
node["fill"] = fill
node.text = letters
if node.parent.tag == "text":
text(surface, node)
else:
node["x"] = str(x + size(surface, node.get("dx"), "x"))
node["y"] = str(y + size(surface, node.get("dy"), "y"))
text_path(surface, node)
if node.parent.children[-1] == node:
surface.total_width = 0
# -*- coding: utf-8 -*-
# This file is part of CairoSVG
# Copyright © 2010-2012 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CairoSVG. If not, see <http://www.gnu.org/licenses/>.
"""
Units functions.
"""
UNITS = {
"mm": 1 / 25.4,
"cm": 1 / 2.54,
"in": 1,
"pt": 1 / 72.,
"pc": 1 / 6.,
"px": None}
def size(surface, string, reference = "xy"):
"""Replace a ``string`` with units by a float value.
If ``reference`` is a float, it is used as reference for percentages. If it
is ``'x'``, we use the viewport width as reference. If it is ``'y'``, we
use the viewport height as reference. If it is ``'xy'``, we use
``(viewport_width ** 2 + viewport_height ** 2) ** .5 / 2 ** .5`` as
reference.
"""
if not string:
return 0
try:
return float(string)
except ValueError:
# Not a float, try something else
pass
if "%" in string:
if reference == "x":
reference = surface.context_width or 0
elif reference == "y":
reference = surface.context_height or 0
elif reference == "xy":
reference = (
(surface.context_width ** 2 + surface.context_height ** 2)
** .5 / 2 ** .5)
return float(string.strip(" %")) * reference / 100
elif "em" in string:
return surface.font_size * float(string.strip(" em"))
elif "ex" in string:
# Assume that 1em == 2ex
return surface.font_size * float(string.strip(" ex")) / 2
for unit, coefficient in UNITS.items():
if unit in string:
number = float(string.strip(" " + unit))
return number * (surface.dpi * coefficient if coefficient else 1)
# Try to return the number at the beginning of the string
return_string = ""
while string and (string[0].isdigit() or string[0] in "+-."):
return_string += string[0]
string = string[1:]
# Unknown size or multiple sizes
return float(return_string) if return_string else 0
...@@ -31,7 +31,7 @@ class Line(object): ...@@ -31,7 +31,7 @@ class Line(object):
'raw','split_raw', 'raw','split_raw',
'command','is_move', 'command','is_move',
'relative','relative_e', 'relative','relative_e',
'current_x', 'current_y', 'current_z', 'extruding', 'current_x', 'current_y', 'current_z', 'extruding', 'current_tool',
'gcview_end_vertex') 'gcview_end_vertex')
def __init__(self, l): def __init__(self, l):
...@@ -131,6 +131,7 @@ class GCode(object): ...@@ -131,6 +131,7 @@ class GCode(object):
imperial = False imperial = False
relative = False relative = False
relative_e = False relative_e = False
current_tool = 0
filament_length = None filament_length = None
xmin = None xmin = None
...@@ -155,6 +156,9 @@ class GCode(object): ...@@ -155,6 +156,9 @@ class GCode(object):
def __len__(self): def __len__(self):
return len(self.line_idxs) return len(self.line_idxs)
def __iter__(self):
return self.lines.__iter__()
def append(self, command): def append(self, command):
command = command.strip() command = command.strip()
if not command: if not command:
...@@ -175,10 +179,12 @@ class GCode(object): ...@@ -175,10 +179,12 @@ class GCode(object):
imperial = self.imperial imperial = self.imperial
relative = self.relative relative = self.relative
relative_e = self.relative_e relative_e = self.relative_e
current_tool = self.current_tool
for line in lines: for line in lines:
if line.is_move: if line.is_move:
line.relative = relative line.relative = relative
line.relative_e = relative_e line.relative_e = relative_e
line.current_tool = current_tool
elif line.command == "G20": elif line.command == "G20":
imperial = True imperial = True
elif line.command == "G21": elif line.command == "G21":
...@@ -193,11 +199,14 @@ class GCode(object): ...@@ -193,11 +199,14 @@ class GCode(object):
relative_e = False relative_e = False
elif line.command == "M83": elif line.command == "M83":
relative_e = True relative_e = True
elif line.command[0] == "T":
current_tool = int(line.command[1:])
if line.command[0] == "G": if line.command[0] == "G":
line.parse_coordinates(imperial) line.parse_coordinates(imperial)
self.imperial = imperial self.imperial = imperial
self.relative = relative self.relative = relative
self.relative_e = relative_e self.relative_e = relative_e
self.current_tool = current_tool
def _preprocess_extrusion(self, lines = None, cur_e = 0): def _preprocess_extrusion(self, lines = None, cur_e = 0):
if not lines: if not lines:
......
...@@ -47,6 +47,7 @@ class wxGLPanel(wx.Panel): ...@@ -47,6 +47,7 @@ class wxGLPanel(wx.Panel):
self.sizer = wx.BoxSizer(wx.HORIZONTAL) self.sizer = wx.BoxSizer(wx.HORIZONTAL)
self.canvas = glcanvas.GLCanvas(self, attribList = attribList) self.canvas = glcanvas.GLCanvas(self, attribList = attribList)
self.context = glcanvas.GLContext(self.canvas)
self.sizer.Add(self.canvas, 1, wx.EXPAND) self.sizer.Add(self.canvas, 1, wx.EXPAND)
self.SetSizer(self.sizer) self.SetSizer(self.sizer)
self.sizer.Fit(self) self.sizer.Fit(self)
...@@ -56,33 +57,18 @@ class wxGLPanel(wx.Panel): ...@@ -56,33 +57,18 @@ class wxGLPanel(wx.Panel):
self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent) self.canvas.Bind(wx.EVT_SIZE, self.processSizeEvent)
self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent) self.canvas.Bind(wx.EVT_PAINT, self.processPaintEvent)
#==========================================================================
# Canvas Proxy Methods
#==========================================================================
def GetGLExtents(self):
'''Get the extents of the OpenGL canvas.'''
return self.canvas.GetClientSize()
def SwapBuffers(self):
'''Swap the OpenGL buffers.'''
self.canvas.SwapBuffers()
#==========================================================================
# wxPython Window Handlers
#==========================================================================
def processEraseBackgroundEvent(self, event): def processEraseBackgroundEvent(self, event):
'''Process the erase background event.''' '''Process the erase background event.'''
pass # Do nothing, to avoid flashing on MSWin pass # Do nothing, to avoid flashing on MSWin
def processSizeEvent(self, event): def processSizeEvent(self, event):
'''Process the resize event.''' '''Process the resize event.'''
if self.canvas.GetContext(): if (wx.VERSION > (2,9) and self.canvas.IsShownOnScreen()) or self.canvas.GetContext():
# Make sure the frame is shown before calling SetCurrent. # Make sure the frame is shown before calling SetCurrent.
self.Show() size = self.GetClientSize()
self.canvas.SetCurrent()
size = self.GetGLExtents()
self.winsize = (size.width, size.height) self.winsize = (size.width, size.height)
self.width, self.height = size.width, size.height self.width, self.height = size.width, size.height
self.canvas.SetCurrent(self.context)
self.OnReshape(size.width, size.height) self.OnReshape(size.width, size.height)
self.canvas.Refresh(False) self.canvas.Refresh(False)
event.Skip() event.Skip()
...@@ -90,7 +76,7 @@ class wxGLPanel(wx.Panel): ...@@ -90,7 +76,7 @@ class wxGLPanel(wx.Panel):
def processPaintEvent(self, event): def processPaintEvent(self, event):
'''Process the drawing event.''' '''Process the drawing event.'''
self.canvas.SetCurrent() self.canvas.SetCurrent(self.context)
if not self.GLinitialized: if not self.GLinitialized:
self.OnInitGL() self.OnInitGL()
...@@ -140,7 +126,6 @@ class wxGLPanel(wx.Panel): ...@@ -140,7 +126,6 @@ class wxGLPanel(wx.Panel):
gluPerspective(60., width / float(height), .1, 1000.) gluPerspective(60., width / float(height), .1, 1000.)
glMatrixMode(GL_MODELVIEW) glMatrixMode(GL_MODELVIEW)
glLoadIdentity() glLoadIdentity()
#pyglet stuff
self.vpmat = (GLint * 4)(0, 0, *list(self.GetClientSize())) self.vpmat = (GLint * 4)(0, 0, *list(self.GetClientSize()))
glGetDoublev(GL_PROJECTION_MATRIX, self.pmat) glGetDoublev(GL_PROJECTION_MATRIX, self.pmat)
...@@ -154,7 +139,7 @@ class wxGLPanel(wx.Panel): ...@@ -154,7 +139,7 @@ class wxGLPanel(wx.Panel):
self.pygletcontext.set_current() self.pygletcontext.set_current()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
self.draw_objects() self.draw_objects()
self.SwapBuffers() self.canvas.SwapBuffers()
#========================================================================== #==========================================================================
# To be implemented by a sub class # To be implemented by a sub class
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
import time import time
import numpy import numpy
import math import math
import sys import logging
from pyglet.gl import * from pyglet.gl import *
from pyglet import gl from pyglet import gl
...@@ -126,6 +126,41 @@ class Platform(object): ...@@ -126,6 +126,41 @@ class Platform(object):
def display(self, mode_2d=False): def display(self, mode_2d=False):
glCallList(self.display_list) glCallList(self.display_list)
class PrintHead(object):
def __init__(self):
self.color = (43. / 255, 0., 175. / 255, 1.0)
self.scale = 5
self.height = 5
self.initialized = False
self.loaded = True
def init(self):
self.display_list = compile_display_list(self.draw)
self.initialized = True
def draw(self):
glPushMatrix()
glBegin(GL_LINES)
glColor4f(*self.color)
for di in [-1, 1]:
for dj in [-1, 1]:
glVertex3f(0, 0, 0)
glVertex3f(self.scale * di, self.scale * dj, self.height)
glEnd()
glPopMatrix()
def display(self, mode_2d=False):
glEnable(GL_LINE_SMOOTH)
orig_linewidth = (GLfloat)()
glGetFloatv(GL_LINE_WIDTH, orig_linewidth)
glLineWidth(3.0)
glCallList(self.display_list)
glLineWidth(orig_linewidth)
glDisable(GL_LINE_SMOOTH)
class Model(object): class Model(object):
""" """
Parent class for models that provides common functionality. Parent class for models that provides common functionality.
...@@ -202,6 +237,9 @@ class GcodeModel(Model): ...@@ -202,6 +237,9 @@ class GcodeModel(Model):
Model for displaying Gcode data. Model for displaying Gcode data.
""" """
color_travel = (0.6, 0.6, 0.6, 0.6)
color_tool0 = (1.0, 0.0, 0.0, 0.6)
color_tool1 = (0.0, 0.0, 1.0, 0.6)
color_printed = (0.2, 0.75, 0, 0.6) color_printed = (0.2, 0.75, 0, 0.6)
use_vbos = True use_vbos = True
...@@ -247,8 +285,8 @@ class GcodeModel(Model): ...@@ -247,8 +285,8 @@ class GcodeModel(Model):
t_end = time.time() t_end = time.time()
print >> sys.stderr, _('Initialized 3D visualization in %.2f seconds') % (t_end - t_start) logging.log(logging.INFO, _('Initialized 3D visualization in %.2f seconds') % (t_end - t_start))
print >> sys.stderr, _('Vertex count: %d') % len(self.vertices) logging.log(logging.INFO, _('Vertex count: %d') % len(self.vertices))
def copy(self): def copy(self):
copy = GcodeModel() copy = GcodeModel()
...@@ -262,28 +300,13 @@ class GcodeModel(Model): ...@@ -262,28 +300,13 @@ class GcodeModel(Model):
""" """
Return the color to use for particular type of movement. Return the color to use for particular type of movement.
""" """
# default movement color is gray
color = [0.6, 0.6, 0.6, 0.6]
"""
extruder_on = (move.flags & Movement.FLAG_EXTRUDER_ON or
move.delta_e > 0)
outer_perimeter = (move.flags & Movement.FLAG_PERIMETER and
move.flags & Movement.FLAG_PERIMETER_OUTER)
if extruder_on and outer_perimeter:
color = [0.0, 0.875, 0.875, 0.6] # cyan
elif extruder_on and move.flags & Movement.FLAG_PERIMETER:
color = [0.0, 1.0, 0.0, 0.6] # green
elif extruder_on and move.flags & Movement.FLAG_LOOP:
color = [1.0, 0.875, 0.0, 0.6] # yellow
elif extruder_on:
color = [1.0, 0.0, 0.0, 0.6] # red
"""
if move.extruding: if move.extruding:
color = [1.0, 0.0, 0.0, 0.6] # red if move.current_tool == 0:
return self.color_tool0
else:
return self.color_tool1
return color return self.color_travel
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
# DRAWING # DRAWING
......
...@@ -67,8 +67,10 @@ class RemainingTimeEstimator(object): ...@@ -67,8 +67,10 @@ class RemainingTimeEstimator(object):
self.previous_layers_estimate = 0 self.previous_layers_estimate = 0
self.current_layer_estimate = 0 self.current_layer_estimate = 0
self.current_layer_lines = 0 self.current_layer_lines = 0
self.remaining_layers_estimate = 0
self.gcode = gcode self.gcode = gcode
self.remaining_layers_estimate = sum(layer.duration for layer in gcode.all_layers)
if len(gcode) > 0:
self.update_layer(0, 0)
def update_layer(self, layer, printtime): def update_layer(self, layer, printtime):
self.previous_layers_estimate += self.current_layer_estimate self.previous_layers_estimate += self.current_layer_estimate
...@@ -77,9 +79,19 @@ class RemainingTimeEstimator(object): ...@@ -77,9 +79,19 @@ class RemainingTimeEstimator(object):
self.current_layer_estimate = self.gcode.all_layers[layer].duration self.current_layer_estimate = self.gcode.all_layers[layer].duration
self.current_layer_lines = len(self.gcode.all_layers[layer].lines) self.current_layer_lines = len(self.gcode.all_layers[layer].lines)
self.remaining_layers_estimate -= self.current_layer_estimate self.remaining_layers_estimate -= self.current_layer_estimate
self.last_idx = -1
self.last_estimate = None
def __call__(self, idx): def __call__(self, idx, printtime):
if not self.current_layer_lines:
return (0, 0)
if idx == self.last_idx:
return self.last_estimate
layer, line = self.gcode.idxs(idx) layer, line = self.gcode.idxs(idx)
layer_progress = (1 - ((line+1) / self.current_layer_lines)) layer_progress = (1 - (float(line+1) / self.current_layer_lines))
remaining = layer_progress * self.current_layer_estimate + self.remaining_layers_estimate remaining = layer_progress * self.current_layer_estimate + self.remaining_layers_estimate
return drift * remaining estimate = self.drift * remaining
total = estimate + printtime
self.last_idx = idx
self.last_estimate = (estimate, total)
return self.last_estimate
...@@ -15,21 +15,30 @@ ...@@ -15,21 +15,30 @@
import xml.etree.ElementTree import xml.etree.ElementTree
import wx import wx
import wx.lib.agw.floatspin as floatspin
import os import os
import time
import zipfile import zipfile
import tempfile import tempfile
import shutil import shutil
import svg.document as wxpsvgdocument from printrun.cairosvg.surface import PNGSurface
import cStringIO
import imghdr import imghdr
import copy
class dispframe(wx.Frame): import re
def __init__(self, parent, title, res = (800, 600), printer = None): from collections import OrderedDict
wx.Frame.__init__(self, parent = parent, title = title) import itertools
self.p = printer
class DisplayFrame(wx.Frame):
def __init__(self, parent, title, res=(1024, 768), printer=None, scale=1.0, offset=(0,0)):
wx.Frame.__init__(self, parent=parent, title=title, size=res)
self.printer = printer
self.control_frame = parent
self.pic = wx.StaticBitmap(self) self.pic = wx.StaticBitmap(self)
self.bitmap = wx.EmptyBitmap(*res) self.bitmap = wx.EmptyBitmap(*res)
self.bbitmap = wx.EmptyBitmap(*res) self.bbitmap = wx.EmptyBitmap(*res)
self.slicer = 'Skeinforge' self.slicer = 'bitmap'
self.dpi = 96
dc = wx.MemoryDC() dc = wx.MemoryDC()
dc.SelectObject(self.bbitmap) dc.SelectObject(self.bbitmap)
dc.SetBackground(wx.Brush("black")) dc.SetBackground(wx.Brush("black"))
...@@ -38,145 +47,447 @@ class dispframe(wx.Frame): ...@@ -38,145 +47,447 @@ class dispframe(wx.Frame):
self.SetBackgroundColour("black") self.SetBackgroundColour("black")
self.pic.Hide() self.pic.Hide()
self.pen = wx.Pen("white")
self.brush = wx.Brush("white")
self.SetDoubleBuffered(True) self.SetDoubleBuffered(True)
self.SetPosition((self.control_frame.GetSize().x, 0))
self.Show() self.Show()
def drawlayer(self, image): self.scale = scale
self.index = 0
self.size = res
self.offset = offset
self.running = False
self.layer_red = False
def clear_layer(self):
try: try:
dc = wx.MemoryDC() dc = wx.MemoryDC()
dc.SelectObject(self.bitmap) dc.SelectObject(self.bitmap)
dc.SetBackground(wx.Brush("black")) dc.SetBackground(wx.Brush("black"))
dc.Clear() dc.Clear()
dc.SetPen(self.pen) self.pic.SetBitmap(self.bitmap)
dc.SetBrush(self.brush) self.pic.Show()
self.Refresh()
if self.slicer == 'Skeinforge': except:
for i in image: raise
#print i pass
points = [wx.Point(*map(lambda x:int(round(float(x) * self.scale)), j.strip().split())) for j in i.strip().split("M")[1].split("L")]
dc.DrawPolygon(points, self.size[0] / 2, self.size[1] / 2) def resize(self, res=(1024, 768)):
elif self.slicer == 'Slic3r': self.bitmap = wx.EmptyBitmap(*res)
gc = wx.GraphicsContext_Create(dc) self.bbitmap = wx.EmptyBitmap(*res)
gc.Translate(*self.offset) dc = wx.MemoryDC()
gc.Scale(self.scale, self.scale) dc.SelectObject(self.bbitmap)
wxpsvgdocument.SVGDocument(image).render(gc) dc.SetBackground(wx.Brush("black"))
dc.Clear()
dc.SelectObject(wx.NullBitmap)
def draw_layer(self, image):
try:
dc = wx.MemoryDC()
dc.SelectObject(self.bitmap)
dc.SetBackground(wx.Brush("black"))
dc.Clear()
if self.slicer == 'Slic3r' or self.slicer == 'Skeinforge':
if int(self.scale) != 1:
layercopy = copy.deepcopy(image)
height = float(layercopy.get('height').replace('m',''))
width = float(layercopy.get('width').replace('m',''))
layercopy.set('height', str(height*self.scale) + 'mm')
layercopy.set('width', str(width*self.scale) + 'mm')
layercopy.set('viewBox', '0 0 ' + str(height*self.scale) + ' ' + str(width*self.scale))
g = layercopy.find("{http://www.w3.org/2000/svg}g")
g.set('transform', 'scale('+str(self.scale)+')')
stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(layercopy)))
else:
stream = cStringIO.StringIO(PNGSurface.convert(dpi=self.dpi, bytestring=xml.etree.ElementTree.tostring(image)))
image = wx.ImageFromStream(stream)
if self.layer_red:
image = image.AdjustChannels(1,0,0,1)
dc.DrawBitmap(wx.BitmapFromImage(image), self.offset[0], self.offset[1], True)
elif self.slicer == 'bitmap': elif self.slicer == 'bitmap':
dc.DrawBitmap(image, self.offset[0], -self.offset[1], True) if isinstance(image, str):
image = wx.Image(image)
if self.layer_red:
image = image.AdjustChannels(1,0,0,1)
dc.DrawBitmap(wx.BitmapFromImage(image.Scale(image.Width * self.scale, image.Height * self.scale)), self.offset[0], -self.offset[1], True)
else: else:
raise Exception(self.slicer + " is an unknown method.") raise Exception(self.slicer + " is an unknown method.")
self.pic.SetBitmap(self.bitmap) self.pic.SetBitmap(self.bitmap)
self.pic.Show() self.pic.Show()
self.Refresh() self.Refresh()
except: except:
raise raise
pass pass
def showimgdelay(self, image): def show_img_delay(self, image):
self.drawlayer(image) print "Showing "+ str(time.clock())
self.pic.Show() self.control_frame.set_current_layer(self.index)
self.Refresh() self.draw_layer(image)
wx.FutureCall(1000 * self.interval, self.hide_pic_and_rise)
self.Refresh() def rise(self):
if self.p != None and self.p.online: if (self.direction == "Top Down"):
self.p.send_now("G91") print "Lowering "+ str(time.clock())
self.p.send_now("G1 Z%f F300" % (self.thickness,)) else:
self.p.send_now("G90") print "Rising "+ str(time.clock())
def nextimg(self, event): if self.printer != None and self.printer.online:
if self.index < len(self.layers): self.printer.send_now("G91")
i = self.index
if (self.prelift_gcode):
for line in self.prelift_gcode.split('\n'):
if line:
self.printer.send_now(line)
if (self.direction == "Top Down"):
self.printer.send_now("G1 Z-%f F%g" % (self.overshoot,self.z_axis_rate,))
self.printer.send_now("G1 Z%f F%g" % (self.overshoot-self.thickness,self.z_axis_rate,))
else: # self.direction == "Bottom Up"
self.printer.send_now("G1 Z%f F%g" % (self.overshoot,self.z_axis_rate,))
self.printer.send_now("G1 Z-%f F%g" % (self.overshoot-self.thickness,self.z_axis_rate,))
if (self.postlift_gcode):
for line in self.postlift_gcode.split('\n'):
if line:
self.printer.send_now(line)
self.printer.send_now("G90")
else:
time.sleep(self.pause)
wx.FutureCall(1000 * self.pause, self.next_img)
print i def hide_pic(self):
wx.CallAfter(self.showimgdelay, self.layers[i]) print "Hiding "+ str(time.clock())
wx.FutureCall(1000 * self.interval, self.pic.Hide) self.pic.Hide()
def hide_pic_and_rise(self):
wx.CallAfter(self.hide_pic)
wx.FutureCall(500, self.rise)
def next_img(self):
if not self.running:
return
if self.index < len(self.layers):
print self.index
wx.CallAfter(self.show_img_delay, self.layers[self.index])
self.index += 1 self.index += 1
else: else:
print "end" print "end"
wx.CallAfter(self.pic.Hide) wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
wx.CallAfter(self.ShowFullScreen, 0)
wx.CallAfter(self.timer.Stop)
def present(self, layers, interval = 0.5, pause = 0.2, thickness = 0.4, scale = 20, size = (800, 600), offset = (0, 0)): def present(self,
layers,
interval=0.5,
pause=0.2,
overshoot=0.0,
z_axis_rate=200,
prelift_gcode="",
postlift_gcode="",
direction="Top Down",
thickness=0.4,
scale=1,
size=(1024, 768),
offset=(0, 0),
layer_red=False):
wx.CallAfter(self.pic.Hide) wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh) wx.CallAfter(self.Refresh)
self.layers = layers self.layers = layers
self.scale = scale self.scale = scale
self.thickness = thickness self.thickness = thickness
self.index = 0 self.size = size
self.size = (size[0] + offset[0], size[1] + offset[1])
self.interval = interval self.interval = interval
self.pause = pause
self.overshoot = overshoot
self.z_axis_rate = z_axis_rate
self.prelift_gcode = prelift_gcode
self.postlift_gcode = postlift_gcode
self.direction = direction
self.layer_red = layer_red
self.offset = offset self.offset = offset
self.timer = wx.Timer(self, 1) self.index = 0
self.timer.Bind(wx.EVT_TIMER, self.nextimg) self.running = True
self.Bind(wx.EVT_TIMER, self.nextimg)
self.timer.Start(1000 * interval + 1000 * pause)
class setframe(wx.Frame): self.next_img()
def __init__(self, parent, printer = None): class SettingsFrame(wx.Frame):
wx.Frame.__init__(self, parent, title = "Projector setup")
self.f = dispframe(None, "", printer = printer) def _set_setting(self, name, value):
self.panel = wx.Panel(self) if self.pronterface:
self.panel.SetBackgroundColour("orange") self.pronterface.set(name,value)
self.bload = wx.Button(self.panel, -1, "Load", pos = (0, 0))
self.bload.Bind(wx.EVT_BUTTON, self.loadfile) def _get_setting(self,name, val):
if self.pronterface:
try:
return getattr(self.pronterface.settings, name)
except AttributeError, x:
return val
else:
return val
def __init__(self, parent, printer=None):
wx.Frame.__init__(self, parent, title="ProjectLayer Control",style=(wx.DEFAULT_FRAME_STYLE | wx.WS_EX_CONTEXTHELP))
self.SetExtraStyle(wx.FRAME_EX_CONTEXTHELP)
self.pronterface = parent
self.display_frame = DisplayFrame(self, title="ProjectLayer Display", printer=printer)
wx.StaticText(self.panel, -1, "Layer:", pos = (0, 30)) self.panel = wx.Panel(self)
wx.StaticText(self.panel, -1, "mm", pos = (130, 30))
self.thickness = wx.TextCtrl(self.panel, -1, "0.5", pos = (50, 30))
wx.StaticText(self.panel, -1, "Exposure:", pos = (0, 60)) vbox = wx.BoxSizer(wx.VERTICAL)
wx.StaticText(self.panel, -1, "s", pos = (130, 60)) buttonbox = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Controls"), wx.HORIZONTAL)
self.interval = wx.TextCtrl(self.panel, -1, "0.5", pos = (50, 60))
load_button = wx.Button(self.panel, -1, "Load")
load_button.Bind(wx.EVT_BUTTON, self.load_file)
load_button.SetHelpText("Choose an SVG file created from Slic3r or Skeinforge, or a zip file of bitmap images (with extension: .3dlp.zip).")
buttonbox.Add(load_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
present_button = wx.Button(self.panel, -1, "Present")
present_button.Bind(wx.EVT_BUTTON, self.start_present)
present_button.SetHelpText("Starts the presentation of the slices.")
buttonbox.Add(present_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
self.pause_button = wx.Button(self.panel, -1, "Pause")
self.pause_button.Bind(wx.EVT_BUTTON, self.pause_present)
self.pause_button.SetHelpText("Pauses the presentation. Can be resumed afterwards by clicking this button, or restarted by clicking present again.")
buttonbox.Add(self.pause_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
stop_button = wx.Button(self.panel, -1, "Stop")
stop_button.Bind(wx.EVT_BUTTON, self.stop_present)
stop_button.SetHelpText("Stops presenting the slices.")
buttonbox.Add(stop_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
self.help_button = wx.ContextHelpButton(self.panel)
buttonbox.Add(self.help_button, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=5)
fieldboxsizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Settings"), wx.VERTICAL)
fieldsizer = wx.GridBagSizer(10,10)
# Left Column
fieldsizer.Add(wx.StaticText(self.panel, -1, "Layer (mm):"), pos=(0, 0), flag=wx.ALIGN_CENTER_VERTICAL)
self.thickness = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_layer", "0.1")), size=(80, -1))
self.thickness.Bind(wx.EVT_TEXT, self.update_thickness)
self.thickness.SetHelpText("The thickness of each slice. Should match the value used to slice the model. SVG files update this value automatically, 3dlp.zip files have to be manually entered.")
fieldsizer.Add(self.thickness, pos=(0, 1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Exposure (s):"), pos=(1, 0), flag=wx.ALIGN_CENTER_VERTICAL)
self.interval = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_interval", "0.5")), size=(80,-1))
self.interval.Bind(wx.EVT_TEXT, self.update_interval)
self.interval.SetHelpText("How long each slice should be displayed.")
fieldsizer.Add(self.interval, pos=(1, 1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Blank (s):"), pos=(2,0), flag=wx.ALIGN_CENTER_VERTICAL)
self.pause = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_pause", "0.5")), size=(80,-1))
self.pause.Bind(wx.EVT_TEXT, self.update_pause)
self.pause.SetHelpText("The pause length between slices. This should take into account any movement of the Z axis, plus time to prepare the resin surface (sliding, tilting, sweeping, etc).")
fieldsizer.Add(self.pause, pos=(2, 1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Scale:"), pos=(3,0), flag=wx.ALIGN_CENTER_VERTICAL)
self.scale = floatspin.FloatSpin(self.panel, -1, value=self._get_setting('project_scale', 1.0), increment=0.1, digits=3, size=(80,-1))
self.scale.Bind(floatspin.EVT_FLOATSPIN, self.update_scale)
self.scale.SetHelpText("The additional scaling of each slice.")
fieldsizer.Add(self.scale, pos=(3, 1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Direction:"), pos=(4,0), flag=wx.ALIGN_CENTER_VERTICAL)
self.direction = wx.ComboBox(self.panel, -1, choices=["Top Down","Bottom Up"], value=self._get_setting('project_direction', "Top Down"), size=(80,-1))
self.direction.Bind(wx.EVT_COMBOBOX, self.update_direction)
self.direction.SetHelpText("The direction the Z axis should move. Top Down is where the projector is above the model, Bottom up is where the projector is below the model.")
fieldsizer.Add(self.direction, pos=(4, 1), flag=wx.ALIGN_CENTER_VERTICAL)
fieldsizer.Add(wx.StaticText(self.panel, -1, "Overshoot (mm):"), pos=(5,0), flag=wx.ALIGN_CENTER_VERTICAL)
self.overshoot= floatspin.FloatSpin(self.panel, -1, value=self._get_setting('project_overshoot', 3.0), increment=0.1, digits=1, min_val=0, size=(80,-1))
self.overshoot.Bind(floatspin.EVT_FLOATSPIN, self.update_overshoot)
self.overshoot.SetHelpText("How far the axis should move beyond the next slice position for each slice. For Top Down printers this would dunk the model under the resi and then return. For Bottom Up printers this would raise the base away from the vat and then return.")
fieldsizer.Add(self.overshoot, pos=(5, 1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Pre-lift Gcode:"), pos=(6, 0), flag=wx.ALIGN_CENTER_VERTICAL)
self.prelift_gcode = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_prelift_gcode", "").replace("\\n",'\n')), size=(-1, 35), style=wx.TE_MULTILINE)
self.prelift_gcode.SetHelpText("Additional gcode to run before raising the Z axis. Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")
self.prelift_gcode.Bind(wx.EVT_TEXT, self.update_prelift_gcode)
fieldsizer.Add(self.prelift_gcode, pos=(6, 1), span=(2,1))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Post-lift Gcode:"), pos=(6, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.postlift_gcode = wx.TextCtrl(self.panel, -1, str(self._get_setting("project_postlift_gcode", "").replace("\\n",'\n')), size=(-1, 35), style=wx.TE_MULTILINE)
self.postlift_gcode.SetHelpText("Additional gcode to run after raising the Z axis. Be sure to take into account any additional time needed in the pause value, and be careful what gcode is added!")
self.postlift_gcode.Bind(wx.EVT_TEXT, self.update_postlift_gcode)
fieldsizer.Add(self.postlift_gcode, pos=(6, 3), span=(2,1))
# Right Column
fieldsizer.Add(wx.StaticText(self.panel, -1, "X (px):"), pos=(0, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.X = wx.SpinCtrl(self.panel, -1, str(int(self._get_setting("project_x", 1024))), max=999999, size=(80,-1))
self.X.Bind(wx.EVT_SPINCTRL, self.update_resolution)
self.X.SetHelpText("The projector resolution in the X axis.")
fieldsizer.Add(self.X, pos=(0, 3))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Y (px):"), pos=(1, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.Y = wx.SpinCtrl(self.panel, -1, str(int(self._get_setting("project_y", 768))), max=999999, size=(80,-1))
self.Y.Bind(wx.EVT_SPINCTRL, self.update_resolution)
self.Y.SetHelpText("The projector resolution in the Y axis.")
fieldsizer.Add(self.Y, pos=(1, 3))
fieldsizer.Add(wx.StaticText(self.panel, -1, "OffsetX (mm):"), pos=(2, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.offset_X = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_offset_x", 0.0), increment=1, digits=1, size=(80,-1))
self.offset_X.Bind(floatspin.EVT_FLOATSPIN, self.update_offset)
self.offset_X.SetHelpText("How far the slice should be offset from the edge in the X axis.")
fieldsizer.Add(self.offset_X, pos=(2, 3))
fieldsizer.Add(wx.StaticText(self.panel, -1, "OffsetY (mm):"), pos=(3, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.offset_Y = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_offset_y", 0.0), increment=1, digits=1, size=(80,-1))
self.offset_Y.Bind(floatspin.EVT_FLOATSPIN, self.update_offset)
self.offset_Y.SetHelpText("How far the slice should be offset from the edge in the Y axis.")
fieldsizer.Add(self.offset_Y, pos=(3, 3))
fieldsizer.Add(wx.StaticText(self.panel, -1, "ProjectedX (mm):"), pos=(4, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.projected_X_mm = floatspin.FloatSpin(self.panel, -1, value=self._get_setting("project_projected_x", 415.0), increment=1, digits=1, size=(80,-1))
self.projected_X_mm.Bind(floatspin.EVT_FLOATSPIN, self.update_projected_Xmm)
self.projected_X_mm.SetHelpText("The actual width of the entire projected image. Use the Calibrate grid to show the full size of the projected image, and measure the width at the same level where the slice will be projected onto the resin.")
fieldsizer.Add(self.projected_X_mm, pos=(4, 3))
fieldsizer.Add(wx.StaticText(self.panel, -1, "Z Axis Speed (mm/min):"), pos=(5, 2), flag=wx.ALIGN_CENTER_VERTICAL)
self.z_axis_rate = wx.SpinCtrl(self.panel, -1, str(self._get_setting("project_z_axis_rate", 200)), max=9999, size=(80,-1))
self.z_axis_rate.Bind(wx.EVT_SPINCTRL, self.update_z_axis_rate)
self.z_axis_rate.SetHelpText("Speed of the Z axis in mm/minute. Take into account that slower rates may require a longer pause value.")
fieldsizer.Add(self.z_axis_rate, pos=(5, 3))
fieldboxsizer.Add(fieldsizer)
# Display
displayboxsizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Display"), wx.VERTICAL)
displaysizer = wx.GridBagSizer(10,10)
displaysizer.Add(wx.StaticText(self.panel, -1, "Fullscreen:"), pos=(0,0), flag=wx.ALIGN_CENTER_VERTICAL)
self.fullscreen = wx.CheckBox(self.panel, -1)
self.fullscreen.Bind(wx.EVT_CHECKBOX, self.update_fullscreen)
self.fullscreen.SetHelpText("Toggles the project screen to full size.")
displaysizer.Add(self.fullscreen, pos=(0, 1), flag=wx.ALIGN_CENTER_VERTICAL)
displaysizer.Add(wx.StaticText(self.panel, -1, "Calibrate:"), pos=(0,2), flag=wx.ALIGN_CENTER_VERTICAL)
self.calibrate = wx.CheckBox(self.panel, -1)
self.calibrate.Bind(wx.EVT_CHECKBOX, self.show_calibrate)
self.calibrate.SetHelpText("Toggles the calibration grid. Each grid should be 10mmx10mm in size. Use the grid to ensure the projected size is correct. See also the help for the ProjectedX field.")
displaysizer.Add(self.calibrate, pos=(0,3), flag=wx.ALIGN_CENTER_VERTICAL)
displaysizer.Add(wx.StaticText(self.panel, -1, "1st Layer:"), pos=(0,4), flag=wx.ALIGN_CENTER_VERTICAL)
first_layer_boxer = wx.BoxSizer(wx.HORIZONTAL)
self.first_layer = wx.CheckBox(self.panel, -1)
self.first_layer.Bind(wx.EVT_CHECKBOX, self.show_first_layer)
self.first_layer.SetHelpText("Displays the first layer of the model. Use this to project the first layer for longer so it holds to the base. Note: this value does not affect the first layer when the \"Present\" run is started, it should be used manually.")
first_layer_boxer.Add(self.first_layer, flag=wx.ALIGN_CENTER_VERTICAL)
first_layer_boxer.Add(wx.StaticText(self.panel, -1, " (s):"), flag=wx.ALIGN_CENTER_VERTICAL)
self.show_first_layer_timer = floatspin.FloatSpin(self.panel, -1, value=-1, increment=1, digits=1, size=(55,-1))
self.show_first_layer_timer.SetHelpText("How long to display the first layer for. -1 = unlimited.")
first_layer_boxer.Add(self.show_first_layer_timer, flag=wx.ALIGN_CENTER_VERTICAL)
displaysizer.Add(first_layer_boxer, pos=(0,6), flag=wx.ALIGN_CENTER_VERTICAL)
displaysizer.Add(wx.StaticText(self.panel, -1, "Red:"), pos=(0,7), flag=wx.ALIGN_CENTER_VERTICAL)
self.layer_red = wx.CheckBox(self.panel, -1)
self.layer_red.Bind(wx.EVT_CHECKBOX, self.show_layer_red)
self.layer_red.SetHelpText("Toggles whether the image should be red. Useful for positioning whilst resin is in the printer as it should not cause a reaction.")
displaysizer.Add(self.layer_red, pos=(0,8), flag=wx.ALIGN_CENTER_VERTICAL)
displayboxsizer.Add(displaysizer)
# Info
infosizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, label="Info"), wx.VERTICAL)
infofieldsizer = wx.GridBagSizer(10,10)
filelabel = wx.StaticText(self.panel, -1, "File:")
filelabel.SetHelpText("The name of the model currently loaded.")
infofieldsizer.Add(filelabel, pos=(0,0))
self.filename = wx.StaticText(self.panel, -1, "")
wx.StaticText(self.panel, -1, "Blank:", pos = (0, 90)) infofieldsizer.Add(self.filename, pos=(0,1))
wx.StaticText(self.panel, -1, "s", pos = (130, 90))
self.delay = wx.TextCtrl(self.panel, -1, "0.5", pos = (50, 90))
wx.StaticText(self.panel, -1, "Scale:", pos = (0, 120)) totallayerslabel = wx.StaticText(self.panel, -1, "Total Layers:")
wx.StaticText(self.panel, -1, "x", pos = (130, 120)) totallayerslabel.SetHelpText("The total number of layers found in the model.")
self.scale = wx.TextCtrl(self.panel, -1, "5", pos = (50, 120)) infofieldsizer.Add(totallayerslabel, pos=(1,0))
self.total_layers = wx.StaticText(self.panel, -1)
wx.StaticText(self.panel, -1, "X:", pos = (160, 30)) infofieldsizer.Add(self.total_layers, pos=(1,1))
self.X = wx.TextCtrl(self.panel, -1, "1024", pos = (210, 30))
wx.StaticText(self.panel, -1, "Y:", pos = (160, 60)) currentlayerlabel = wx.StaticText(self.panel, -1, "Current Layer:")
self.Y = wx.TextCtrl(self.panel, -1, "768", pos = (210, 60)) currentlayerlabel.SetHelpText("The current layer being displayed.")
infofieldsizer.Add(currentlayerlabel, pos=(2,0))
self.current_layer = wx.StaticText(self.panel, -1, "0")
infofieldsizer.Add(self.current_layer, pos=(2,1))
wx.StaticText(self.panel, -1, "OffsetX:", pos = (160, 90)) estimatedtimelabel = wx.StaticText(self.panel, -1, "Estimated Time:")
self.offsetX = wx.TextCtrl(self.panel, -1, "50", pos = (210, 90)) estimatedtimelabel.SetHelpText("An estimate of the remaining time until print completion.")
infofieldsizer.Add(estimatedtimelabel, pos=(3,0))
self.estimated_time = wx.StaticText(self.panel, -1, "")
infofieldsizer.Add(self.estimated_time, pos=(3,1))
wx.StaticText(self.panel, -1, "OffsetY:", pos = (160, 120)) infosizer.Add(infofieldsizer)
self.offsetY = wx.TextCtrl(self.panel, -1, "50", pos = (210, 120))
self.bload = wx.Button(self.panel, -1, "Present", pos = (0, 150)) #
self.bload.Bind(wx.EVT_BUTTON, self.startdisplay)
wx.StaticText(self.panel, -1, "Fullscreen:", pos = (160, 150)) vbox.Add(buttonbox, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP|wx.BOTTOM, border=10)
self.fullscreen = wx.CheckBox(self.panel, -1, pos = (220, 150)) vbox.Add(fieldboxsizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10);
self.fullscreen.SetValue(True) vbox.Add(displayboxsizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10);
vbox.Add(infosizer, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=10)
self.panel.SetSizer(vbox)
self.panel.Fit()
self.Fit()
self.SetPosition((0, 0))
self.Show() self.Show()
def __del__(self): def __del__(self):
if hasattr(self, 'image_dir') and self.image_dir != '': if hasattr(self, 'image_dir') and self.image_dir != '':
shutil.rmtree(self.image_dir) shutil.rmtree(self.image_dir)
if self.display_frame:
self.display_frame.Destroy()
def set_total_layers(self, total):
self.total_layers.SetLabel(str(total))
self.set_estimated_time()
def set_current_layer(self, index):
self.current_layer.SetLabel(str(index))
self.set_estimated_time()
def display_filename(self,name):
self.filename.SetLabel(name)
def set_estimated_time(self):
if not hasattr(self, 'layers'):
return
current_layer = int(self.current_layer.GetLabel())
remaining_layers = len(self.layers[0]) - current_layer
# 0.5 for delay between hide and rise
estimated_time = remaining_layers * (float(self.interval.GetValue()) + float(self.pause.GetValue()) + 0.5)
self.estimated_time.SetLabel(time.strftime("%H:%M:%S",time.gmtime(estimated_time)))
def parsesvg(self, name): def parse_svg(self, name):
et = xml.etree.ElementTree.ElementTree(file = name) et = xml.etree.ElementTree.ElementTree(file=name)
#xml.etree.ElementTree.dump(et) #xml.etree.ElementTree.dump(et)
slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge' slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge'
zlast = 0 zlast = 0
zdiff = 0 zdiff = 0
ol = [] ol = []
if (slicer == 'Slic3r'): if (slicer == 'Slic3r'):
height = et.getroot().get('height') height = et.getroot().get('height').replace('m','')
width = et.getroot().get('width') width = et.getroot().get('width').replace('m','')
for i in et.findall("{http://www.w3.org/2000/svg}g"): for i in et.findall("{http://www.w3.org/2000/svg}g"):
z = float(i.get('{http://slic3r.org/namespaces/slic3r}z')) z = float(i.get('{http://slic3r.org/namespaces/slic3r}z'))
...@@ -186,35 +497,77 @@ class setframe(wx.Frame): ...@@ -186,35 +497,77 @@ class setframe(wx.Frame):
svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg') svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
svgSnippet.set('height', height + 'mm') svgSnippet.set('height', height + 'mm')
svgSnippet.set('width', width + 'mm') svgSnippet.set('width', width + 'mm')
svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width) svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
svgSnippet.set('style','background-color:black')
svgSnippet.append(i) svgSnippet.append(i)
ol += [svgSnippet] ol += [svgSnippet]
else : else :
for i in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"):
z = float(i.get('id').split("z:")[-1]) slice_layers = et.findall("{http://www.w3.org/2000/svg}metadata")[0].findall("{http://www.reprap.org/slice}layers")[0]
minX = slice_layers.get('minX')
maxX = slice_layers.get('maxX')
minY = slice_layers.get('minY')
maxY = slice_layers.get('maxY')
height = str(abs(float(minY)) + abs(float(maxY)))
width = str(abs(float(minX)) + abs(float(maxX)))
for g in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"):
g.set('transform','')
text_element = g.findall("{http://www.w3.org/2000/svg}text")[0]
g.remove(text_element)
path_elements = g.findall("{http://www.w3.org/2000/svg}path")
for p in path_elements:
p.set('transform', 'translate('+maxX+','+maxY+')')
p.set('fill', 'white')
z = float(g.get('id').split("z:")[-1])
zdiff = z - zlast zdiff = z - zlast
zlast = z zlast = z
path = i.find('{http://www.w3.org/2000/svg}path')
ol += [(path.get("d").split("z"))[:-1]] svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
svgSnippet.set('height', height + 'mm')
svgSnippet.set('width', width + 'mm')
svgSnippet.set('viewBox', '0 0 ' + height + ' ' + width)
svgSnippet.set('style','background-color:black;fill:white;')
svgSnippet.append(g)
ol += [svgSnippet]
return ol, zdiff, slicer return ol, zdiff, slicer
def parse3DLPzip(self, name): def parse_3DLP_zip(self, name):
if not zipfile.is_zipfile(name): if not zipfile.is_zipfile(name):
raise Exception(name + " is not a zip file!") raise Exception(name + " is not a zip file!")
acceptedImageTypes = ['gif','tiff','jpg','jpeg','bmp','png'] accepted_image_types = ['gif','tiff','jpg','jpeg','bmp','png']
zipFile = zipfile.ZipFile(name, 'r') zipFile = zipfile.ZipFile(name, 'r')
self.image_dir = tempfile.mkdtemp() self.image_dir = tempfile.mkdtemp()
zipFile.extractall(self.image_dir) zipFile.extractall(self.image_dir)
ol = [] ol = []
for f in os.listdir(self.image_dir):
# Note: the following funky code extracts any numbers from the filenames, matches
# them with the original then sorts them. It allows for filenames of the
# format: abc_1.png, which would be followed by abc_10.png alphabetically.
os.chdir(self.image_dir)
vals = filter(os.path.isfile, os.listdir('.'))
keys = map(lambda p:int(re.search('\d+', p).group()), vals)
imagefilesDict = dict(itertools.izip(keys, vals))
imagefilesOrderedDict = OrderedDict(sorted(imagefilesDict.items(), key=lambda t: t[0]))
for f in imagefilesOrderedDict.values():
path = os.path.join(self.image_dir, f) path = os.path.join(self.image_dir, f)
if os.path.isfile(path) and imghdr.what(path) in acceptedImageTypes: if os.path.isfile(path) and imghdr.what(path) in accepted_image_types:
ol.append(wx.Bitmap(path)) ol.append(path)
return ol, -1, "bitmap" return ol, -1, "bitmap"
def loadfile(self, event): def load_file(self, event):
dlg = wx.FileDialog(self, ("Open file to print"), style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) dlg = wx.FileDialog(self, ("Open file to print"), style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
dlg.SetWildcard(("Slic3r or Skeinforge svg files (;*.svg;*.SVG;);3DLP Zip (;*.3dlp.zip;)")) dlg.SetWildcard(("Slic3r or Skeinforge svg files (;*.svg;*.SVG;);3DLP Zip (;*.3dlp.zip;)"))
if(dlg.ShowModal() == wx.ID_OK): if(dlg.ShowModal() == wx.ID_OK):
name = dlg.GetPath() name = dlg.GetPath()
...@@ -222,32 +575,241 @@ class setframe(wx.Frame): ...@@ -222,32 +575,241 @@ class setframe(wx.Frame):
self.status.SetStatusText(("File not found!")) self.status.SetStatusText(("File not found!"))
return return
if name.endswith(".3dlp.zip"): if name.endswith(".3dlp.zip"):
layers = self.parse3DLPzip(name) layers = self.parse_3DLP_zip(name)
layerHeight = float(self.thickness.GetValue()) layerHeight = float(self.thickness.GetValue())
else: else:
layers = self.parsesvg(name) layers = self.parse_svg(name)
layerHeight = layers[1] layerHeight = layers[1]
self.thickness.SetValue(str(layers[1])) self.thickness.SetValue(str(layers[1]))
print "Layer thickness detected:", layerHeight, "mm" print "Layer thickness detected:", layerHeight, "mm"
print len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm" print len(layers[0]), "layers found, total height", layerHeight * len(layers[0]), "mm"
self.layers = layers self.layers = layers
self.f.slicer = layers[2] self.set_total_layers(len(layers[0]))
self.set_current_layer(0)
self.current_filename = os.path.basename(name)
self.display_filename(self.current_filename)
self.slicer = layers[2]
self.display_frame.slicer = self.slicer
dlg.Destroy() dlg.Destroy()
def startdisplay(self, event): def show_calibrate(self, event):
self.f.Raise() if self.calibrate.IsChecked():
self.present_calibrate(event)
else:
if hasattr(self, 'layers'):
self.display_frame.slicer = self.layers[2]
self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.clear_layer()
def show_first_layer(self, event):
if self.first_layer.IsChecked():
self.present_first_layer(event)
else:
if hasattr(self, 'layers'):
self.display_frame.slicer = self.layers[2]
self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.clear_layer()
def show_layer_red(self, event):
self.display_frame.layer_red = self.layer_red.IsChecked()
def present_calibrate(self, event):
if self.calibrate.IsChecked():
self.display_frame.Raise()
self.display_frame.offset = (float(self.offset_X.GetValue()), -float(self.offset_Y.GetValue()))
self.display_frame.scale = 1.0
resolution_x_pixels = int(self.X.GetValue())
resolution_y_pixels = int(self.Y.GetValue())
gridBitmap = wx.EmptyBitmap(resolution_x_pixels, resolution_y_pixels)
dc = wx.MemoryDC()
dc.SelectObject(gridBitmap)
dc.SetBackground(wx.Brush("black"))
dc.Clear()
dc.SetPen(wx.Pen("red", 7))
dc.DrawLine(0, 0, resolution_x_pixels, 0);
dc.DrawLine(0, 0, 0, resolution_y_pixels);
dc.DrawLine(resolution_x_pixels, 0, resolution_x_pixels, resolution_y_pixels);
dc.DrawLine(0, resolution_y_pixels, resolution_x_pixels, resolution_y_pixels);
dc.SetPen(wx.Pen("red", 2))
aspectRatio = float(resolution_x_pixels) / float(resolution_y_pixels)
projectedXmm = float(self.projected_X_mm.GetValue())
projectedYmm = round(projectedXmm / aspectRatio)
pixelsXPerMM = resolution_x_pixels / projectedXmm
pixelsYPerMM = resolution_y_pixels / projectedYmm
gridCountX = int(projectedXmm / 10)
gridCountY = int(projectedYmm / 10)
for y in xrange(0, gridCountY + 1):
for x in xrange(0, gridCountX + 1):
dc.DrawLine(0, y * (pixelsYPerMM * 10), resolution_x_pixels, y * (pixelsYPerMM * 10));
dc.DrawLine(x * (pixelsXPerMM * 10), 0, x * (pixelsXPerMM * 10), resolution_y_pixels);
self.first_layer.SetValue(False)
self.display_frame.slicer = 'bitmap'
self.display_frame.draw_layer(gridBitmap.ConvertToImage())
def present_first_layer(self, event):
if (self.first_layer.GetValue()):
if not hasattr(self, "layers"):
print "No model loaded!"
self.first_layer.SetValue(False)
return
self.display_frame.offset = (float(self.offset_X.GetValue()), float(self.offset_Y.GetValue()))
self.display_frame.scale = float(self.scale.GetValue())
self.display_frame.slicer = self.layers[2]
self.display_frame.dpi = self.get_dpi()
self.display_frame.draw_layer(copy.deepcopy(self.layers[0][0]))
self.calibrate.SetValue(False)
if self.show_first_layer_timer != -1.0 :
def unpresent_first_layer():
self.display_frame.clear_layer()
self.first_layer.SetValue(False)
wx.CallLater(self.show_first_layer_timer.GetValue() * 1000, unpresent_first_layer)
def update_offset(self, event):
offset_x = float(self.offset_X.GetValue())
offset_y = float(self.offset_Y.GetValue())
self.display_frame.offset = (offset_x, offset_y)
self._set_setting('project_offset_x',offset_x)
self._set_setting('project_offset_y',offset_y)
self.refresh_display(event)
def refresh_display(self, event):
self.present_calibrate(event)
self.present_first_layer(event)
def update_thickness(self, event):
self._set_setting('project_layer',self.thickness.GetValue())
self.refresh_display(event)
def update_projected_Xmm(self, event):
self._set_setting('project_projected_x',self.projected_X_mm.GetValue())
self.refresh_display(event)
def update_scale(self, event):
scale = float(self.scale.GetValue())
self.display_frame.scale = scale
self._set_setting('project_scale',scale)
self.refresh_display(event)
def update_interval(self, event):
interval = float(self.interval.GetValue())
self.display_frame.interval = interval
self._set_setting('project_interval',interval)
self.set_estimated_time()
self.refresh_display(event)
def update_pause(self, event):
pause = float(self.pause.GetValue())
self.display_frame.pause = pause
self._set_setting('project_pause',pause)
self.set_estimated_time()
self.refresh_display(event)
def update_overshoot(self, event):
overshoot = float(self.overshoot.GetValue())
self.display_frame.pause = overshoot
self._set_setting('project_overshoot',overshoot)
def update_prelift_gcode(self, event):
prelift_gcode = self.prelift_gcode.GetValue().replace('\n', "\\n")
self.display_frame.prelift_gcode = prelift_gcode
self._set_setting('project_prelift_gcode',prelift_gcode)
def update_postlift_gcode(self, event):
postlift_gcode = self.postlift_gcode.GetValue().replace('\n', "\\n")
self.display_frame.postlift_gcode = postlift_gcode
self._set_setting('project_postlift_gcode',postlift_gcode)
def update_z_axis_rate(self, event):
z_axis_rate = int(self.z_axis_rate.GetValue())
self.display_frame.z_axis_rate = z_axis_rate
self._set_setting('project_z_axis_rate',z_axis_rate)
def update_direction(self, event):
direction = self.direction.GetValue()
self.display_frame.direction = direction
self._set_setting('project_direction',direction)
def update_fullscreen(self, event):
if (self.fullscreen.GetValue()):
self.display_frame.ShowFullScreen(1)
else:
self.display_frame.ShowFullScreen(0)
self.refresh_display(event)
def update_resolution(self, event):
x = float(self.X.GetValue())
y = float(self.Y.GetValue())
self.display_frame.resize((x,y))
self._set_setting('project_x',x)
self._set_setting('project_y',y)
self.refresh_display(event)
def get_dpi(self):
resolution_x_pixels = int(self.X.GetValue())
projected_x_mm = float(self.projected_X_mm.GetValue())
projected_x_inches = projected_x_mm / 25.4
return resolution_x_pixels / projected_x_inches
def start_present(self, event):
if not hasattr(self, "layers"):
print "No model loaded!"
return
self.pause_button.SetLabel("Pause")
self.set_current_layer(0)
self.display_frame.Raise()
if (self.fullscreen.GetValue()): if (self.fullscreen.GetValue()):
self.f.ShowFullScreen(1) self.display_frame.ShowFullScreen(1)
l = self.layers[0][:] self.display_frame.slicer = self.layers[2]
self.f.present(l, self.display_frame.dpi = self.get_dpi()
thickness = float(self.thickness.GetValue()), self.display_frame.present(self.layers[0][:],
interval = float(self.interval.GetValue()), thickness=float(self.thickness.GetValue()),
scale = float(self.scale.GetValue()), interval=float(self.interval.GetValue()),
pause = float(self.delay.GetValue()), scale=float(self.scale.GetValue()),
size = (float(self.X.GetValue()), float(self.Y.GetValue())), pause=float(self.pause.GetValue()),
offset = (float(self.offsetX.GetValue()), float(self.offsetY.GetValue()))) overshoot=float(self.overshoot.GetValue()),
z_axis_rate=int(self.z_axis_rate.GetValue()),
prelift_gcode=self.prelift_gcode.GetValue(),
postlift_gcode=self.postlift_gcode.GetValue(),
direction=self.direction.GetValue(),
size=(float(self.X.GetValue()), float(self.Y.GetValue())),
offset=(float(self.offset_X.GetValue()), float(self.offset_Y.GetValue())),
layer_red=self.layer_red.IsChecked())
def stop_present(self, event):
print "Stop"
self.pause_button.SetLabel("Pause")
self.set_current_layer(0)
self.display_frame.running = False
def pause_present(self, event):
if self.pause_button.GetLabel() == 'Pause':
print "Pause"
self.pause_button.SetLabel("Continue")
self.display_frame.running = False
else:
print "Continue"
self.pause_button.SetLabel("Pause")
self.display_frame.running = True
self.display_frame.next_img()
if __name__ == "__main__": if __name__ == "__main__":
provider = wx.SimpleHelpProvider()
wx.HelpProvider_Set(provider)
#a = wx.App(redirect=True,filename="mylogfile.txt")
a = wx.App() a = wx.App()
setframe(None).Show() SettingsFrame(None).Show()
a.MainLoop() a.MainLoop()
...@@ -115,6 +115,7 @@ class GLPanel(wx.Panel): ...@@ -115,6 +115,7 @@ class GLPanel(wx.Panel):
self.pmat = (GLdouble * 16)() self.pmat = (GLdouble * 16)()
self.mvmat = (GLdouble * 16)() self.mvmat = (GLdouble * 16)()
self.pygletcontext = Context(current_context) self.pygletcontext = Context(current_context)
self.pygletcontext.canvas = self
self.pygletcontext.set_current() self.pygletcontext.set_current()
self.dist = 1000 self.dist = 1000
self.vpmat = None self.vpmat = None
......
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
"""
import wx
def AddEllipticalArc(self, x, y, w, h, startAngle, endAngle, clockwise = False):
""" Draws an arc of an ellipse within bounding rect (x, y, w, h)
from startArc to endArc (in radians, relative to the horizontal line of the eclipse)"""
if True:
import warnings
warnings.warn("elliptical arcs are not supported")
w = w/2.0
h = h/2.0
self.AddArc(x+w, y+h, ((w+h)/2), startAngle, endAngle, clockwise)
return
else:
#implement in terms of AddArc by applying a transformation matrix
#Sigh this can't work, still need to patch wx to allow
#either a) AddPath that's not a closed path or
#b) allow pushing and popping of states on a path, not just on a context
#a) is possible in GDI+, need to investigate other renderers.
#b) is possible in Quartz and Cairo, but not in GDI+. It could
#possibly be simulated by combining the current transform with option a.
mtx = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix()
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
mtx.Translate(x+(w/2.0), y+(h/2.0))
mtx.Scale(w/2.0, y/2.0)
path.AddArc(0, 0, 1, startAngle, endAngle, clockwise)
path.Transform(mtx)
self.AddPath(path)
self.MoveToPoint(path.GetCurrentPoint())
self.CloseSubpath()
if not hasattr(wx.GraphicsPath, "AddEllipticalArc"):
wx.GraphicsPath.AddEllipticalArc = AddEllipticalArc
del AddEllipticalArc
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
Parsers for specific attributes
"""
import urlparse
from pyparsing import (Literal,
Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums,
CaselessLiteral, SkipTo
)
from css.colour import colourValue
import string
##Paint values
none = CaselessLiteral("none").setParseAction(lambda t: ["NONE", ()])
currentColor = CaselessLiteral("currentColor").setParseAction(lambda t: ["CURRENTCOLOR", ()])
def parsePossibleURL(t):
possibleURL, fallback = t[0]
return [urlparse.urlsplit(possibleURL), fallback]
#Normal color declaration
colorDeclaration = none | currentColor | colourValue
urlEnd = (
Literal(")").suppress() +
Optional(Group(colorDeclaration), default = ()) +
StringEnd()
)
url = (
CaselessLiteral("URL")
+
Literal("(").suppress()+
Group(SkipTo(urlEnd, include = True).setParseAction(parsePossibleURL))
)
#paint value will parse into a (type, details) tuple.
#For none and currentColor, the details tuple will be the empty tuple
#for CSS color declarations, it will be (type, (R, G, B))
#for URLs, it will be ("URL", ((url tuple), fallback))
#The url tuple will be as returned by urlparse.urlsplit, and can be
#an empty tuple if the parser has an error
#The fallback will be another (type, details) tuple as a parsed
#colorDeclaration, but may be the empty tuple if it is not present
paintValue = url | colorDeclaration
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from .transform import transformList
from .inline import inlineStyle
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
""" CSS at-rules"""
from pyparsing import Literal, Combine
from .identifier import identifier
atkeyword = Combine(Literal("@") + identifier)
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
CSS blocks
"""
from pyparsing import nestedExpr
block = nestedExpr(opener="{", closer="}")
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
Parsing for CSS colour values.
Supported formats:
hex literal short: #fff
hex literal long: #fafafa
rgb bytes: rgb(255,100,0)
rgb percent: rgb(100%,100%,0%)
named color: black
"""
import wx
import string
import urlparse
from pyparsing import nums, Literal, Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums
from ..pathdata import number, sign
number = number.copy()
integerConstant = Word(nums+"+-").setParseAction(lambda t:int(t[0]))
#rgb format parser
comma = Literal(",").suppress()
def clampColourByte(val):
val = int(val)
return min(max(0,val), 255)
def clampColourPerc(val):
val = float(val)
return min(max(0,val), 100)
def parseColorPerc(token):
val = token[0]
val = clampColourPerc(val)
#normalize to bytes
return int(255 * (val / 100.0))
colorByte = Optional(sign) + integerConstant.setParseAction(lambda t: clampColourByte(t[0]))
colorPerc = number.setParseAction(parseColorPerc) + Literal("%").suppress()
rgb = (
Literal("rgb(").setParseAction(lambda t: "RGB") +
(
#integer constants, ie 255,255,255
Group(colorByte + comma + colorByte + comma + colorByte) ^
#percentage values, ie 100%, 50%
Group(colorPerc + comma + colorPerc + comma + colorPerc)
)
+
Literal(")").suppress() + StringEnd()
)
def parseShortHex(t):
return tuple(int(x*2, 16) for x in t[0])
doubleHex = Word(hexnums, exact=2).setParseAction(lambda t: int(t[0], 16))
hexLiteral = (Literal("#").setParseAction(lambda t: "RGB") +
(
Group(doubleHex + doubleHex + doubleHex) |
Word(hexnums, exact=3).setParseAction(parseShortHex)
) + StringEnd()
)
def parseNamedColour(t):
try:
return ["RGB", NamedColours[t[0].lower()]]
except KeyError:
return ["RGB", (0,0,0)]
namedColour = Word(alphas).setParseAction(parseNamedColour)
colourValue = rgb | hexLiteral | namedColour
##constants
NamedColours = {
#~ #html named colours
#~ "black":(0,0,0),
#~ "silver": (0xc0, 0xc0, 0xc0, 255),
#~ "gray": (0x80, 0x80, 0x80),
#~ "white":(255,255,255),
#~ "maroon":(0x80, 0, 0),
#~ "red":(0xff, 0, 0),
#~ "purple":(0x80, 0, 0x80),
#~ "fuchsia":(0xff, 0, 0xff),
#~ "green": (0, 0x80, 0),
#~ "lime": (0, 0xff, 0),
#~ "olive": (0x80, 0x80, 00),
#~ "yellow":(0xff, 0xff, 00),
#~ "navy": (0, 0, 0x80),
#~ "blue": (0, 0, 0xff),
#~ "teal": (0, 0x80, 0x80),
#~ "aqua": (0, 0xff, 0xff),
#expanded named colors from SVG spc
'aliceblue' : (240, 248, 255) ,
'antiquewhite' : (250, 235, 215) ,
'aqua' : ( 0, 255, 255) ,
'aquamarine' : (127, 255, 212) ,
'azure' : (240, 255, 255) ,
'beige' : (245, 245, 220) ,
'bisque' : (255, 228, 196) ,
'black' : ( 0, 0, 0) ,
'blanchedalmond' : (255, 235, 205) ,
'blue' : ( 0, 0, 255) ,
'blueviolet' : (138, 43, 226) ,
'brown' : (165, 42, 42) ,
'burlywood' : (222, 184, 135) ,
'cadetblue' : ( 95, 158, 160) ,
'chartreuse' : (127, 255, 0) ,
'chocolate' : (210, 105, 30) ,
'coral' : (255, 127, 80) ,
'cornflowerblue' : (100, 149, 237) ,
'cornsilk' : (255, 248, 220) ,
'crimson' : (220, 20, 60) ,
'cyan' : ( 0, 255, 255) ,
'darkblue' : ( 0, 0, 139) ,
'darkcyan' : ( 0, 139, 139) ,
'darkgoldenrod' : (184, 134, 11) ,
'darkgray' : (169, 169, 169) ,
'darkgreen' : ( 0, 100, 0) ,
'darkgrey' : (169, 169, 169) ,
'darkkhaki' : (189, 183, 107) ,
'darkmagenta' : (139, 0, 139) ,
'darkolivegreen' : ( 85, 107, 47) ,
'darkorange' : (255, 140, 0) ,
'darkorchid' : (153, 50, 204) ,
'darkred' : (139, 0, 0) ,
'darksalmon' : (233, 150, 122) ,
'darkseagreen' : (143, 188, 143) ,
'darkslateblue' : ( 72, 61, 139) ,
'darkslategray' : ( 47, 79, 79) ,
'darkslategrey' : ( 47, 79, 79) ,
'darkturquoise' : ( 0, 206, 209) ,
'darkviolet' : (148, 0, 211) ,
'deeppink' : (255, 20, 147) ,
'deepskyblue' : ( 0, 191, 255) ,
'dimgray' : (105, 105, 105) ,
'dimgrey' : (105, 105, 105) ,
'dodgerblue' : ( 30, 144, 255) ,
'firebrick' : (178, 34, 34) ,
'floralwhite' : (255, 250, 240) ,
'forestgreen' : ( 34, 139, 34) ,
'fuchsia' : (255, 0, 255) ,
'gainsboro' : (220, 220, 220) ,
'ghostwhite' : (248, 248, 255) ,
'gold' : (255, 215, 0) ,
'goldenrod' : (218, 165, 32) ,
'gray' : (128, 128, 128) ,
'grey' : (128, 128, 128) ,
'green' : ( 0, 128, 0) ,
'greenyellow' : (173, 255, 47) ,
'honeydew' : (240, 255, 240) ,
'hotpink' : (255, 105, 180) ,
'indianred' : (205, 92, 92) ,
'indigo' : ( 75, 0, 130) ,
'ivory' : (255, 255, 240) ,
'khaki' : (240, 230, 140) ,
'lavender' : (230, 230, 250) ,
'lavenderblush' : (255, 240, 245) ,
'lawngreen' : (124, 252, 0) ,
'lemonchiffon' : (255, 250, 205) ,
'lightblue' : (173, 216, 230) ,
'lightcoral' : (240, 128, 128) ,
'lightcyan' : (224, 255, 255) ,
'lightgoldenrodyellow' : (250, 250, 210) ,
'lightgray' : (211, 211, 211) ,
'lightgreen' : (144, 238, 144) ,
'lightgrey' : (211, 211, 211) ,
'lightpink' : (255, 182, 193) ,
'lightsalmon' : (255, 160, 122) ,
'lightseagreen' : ( 32, 178, 170) ,
'lightskyblue' : (135, 206, 250) ,
'lightslategray' : (119, 136, 153) ,
'lightslategrey' : (119, 136, 153) ,
'lightsteelblue' : (176, 196, 222) ,
'lightyellow' : (255, 255, 224) ,
'lime' : ( 0, 255, 0) ,
'limegreen' : ( 50, 205, 50) ,
'linen' : (250, 240, 230) ,
'magenta' : (255, 0, 255) ,
'maroon' : (128, 0, 0) ,
'mediumaquamarine' : (102, 205, 170) ,
'mediumblue' : ( 0, 0, 205) ,
'mediumorchid' : (186, 85, 211) ,
'mediumpurple' : (147, 112, 219) ,
'mediumseagreen' : ( 60, 179, 113) ,
'mediumslateblue' : (123, 104, 238) ,
'mediumspringgreen' : ( 0, 250, 154) ,
'mediumturquoise' : ( 72, 209, 204) ,
'mediumvioletred' : (199, 21, 133) ,
'midnightblue' : ( 25, 25, 112) ,
'mintcream' : (245, 255, 250) ,
'mistyrose' : (255, 228, 225) ,
'moccasin' : (255, 228, 181) ,
'navajowhite' : (255, 222, 173) ,
'navy' : ( 0, 0, 128) ,
'oldlace' : (253, 245, 230) ,
'olive' : (128, 128, 0) ,
'olivedrab' : (107, 142, 35) ,
'orange' : (255, 165, 0) ,
'orangered' : (255, 69, 0) ,
'orchid' : (218, 112, 214) ,
'palegoldenrod' : (238, 232, 170) ,
'palegreen' : (152, 251, 152) ,
'paleturquoise' : (175, 238, 238) ,
'palevioletred' : (219, 112, 147) ,
'papayawhip' : (255, 239, 213) ,
'peachpuff' : (255, 218, 185) ,
'peru' : (205, 133, 63) ,
'pink' : (255, 192, 203) ,
'plum' : (221, 160, 221) ,
'powderblue' : (176, 224, 230) ,
'purple' : (128, 0, 128) ,
'red' : (255, 0, 0) ,
'rosybrown' : (188, 143, 143) ,
'royalblue' : ( 65, 105, 225) ,
'saddlebrown' : (139, 69, 19) ,
'salmon' : (250, 128, 114) ,
'sandybrown' : (244, 164, 96) ,
'seagreen' : ( 46, 139, 87) ,
'seashell' : (255, 245, 238) ,
'sienna' : (160, 82, 45) ,
'silver' : (192, 192, 192) ,
'skyblue' : (135, 206, 235) ,
'slateblue' : (106, 90, 205) ,
'slategray' : (112, 128, 144) ,
'slategrey' : (112, 128, 144) ,
'snow' : (255, 250, 250) ,
'springgreen' : ( 0, 255, 127) ,
'steelblue' : ( 70, 130, 180) ,
'tan' : (210, 180, 140) ,
'teal' : ( 0, 128, 128) ,
'thistle' : (216, 191, 216) ,
'tomato' : (255, 99, 71) ,
'turquoise' : ( 64, 224, 208) ,
'violet' : (238, 130, 238) ,
'wheat' : (245, 222, 179) ,
'white' : (255, 255, 255) ,
'whitesmoke' : (245, 245, 245) ,
'yellow' : (255, 255, 0) ,
'yellowgreen' : (154, 205, 50) ,
}
def fillCSS2SystemColours():
#The system colours require a wxApp to be present to retrieve,
#so if you wnat support for them you'll need
#to call this function after your wxApp instance starts
systemColors = {
"ActiveBorder": wx.SYS_COLOUR_ACTIVEBORDER,
"ActiveCaption": wx.SYS_COLOUR_ACTIVECAPTION,
"AppWorkspace": wx.SYS_COLOUR_APPWORKSPACE,
"Background": wx.SYS_COLOUR_BACKGROUND,
"ButtonFace": wx.SYS_COLOUR_BTNFACE,
"ButtonHighlight": wx.SYS_COLOUR_BTNHIGHLIGHT,
"ButtonShadow": wx.SYS_COLOUR_BTNSHADOW,
"ButtonText": wx.SYS_COLOUR_BTNTEXT,
"CaptionText": wx.SYS_COLOUR_CAPTIONTEXT,
"GrayText": wx.SYS_COLOUR_GRAYTEXT,
"Highlight": wx.SYS_COLOUR_HIGHLIGHT,
"HighlightText": wx.SYS_COLOUR_HIGHLIGHTTEXT,
"InactiveBorder": wx.SYS_COLOUR_INACTIVEBORDER,
"InactiveCaption": wx.SYS_COLOUR_INACTIVECAPTION,
"InfoBackground": wx.SYS_COLOUR_INFOBK,
"InfoText": wx.SYS_COLOUR_INFOTEXT,
"Menu": wx.SYS_COLOUR_MENU,
"MenuText": wx.SYS_COLOUR_MENUTEXT,
"Scrollbar": wx.SYS_COLOUR_SCROLLBAR,
"ThreeDDarkShadow": wx.SYS_COLOUR_3DDKSHADOW,
"ThreeDFace": wx.SYS_COLOUR_3DFACE,
"ThreeDHighlight": wx.SYS_COLOUR_3DHIGHLIGHT,
"ThreeDLightShadow": wx.SYS_COLOUR_3DLIGHT,
"ThreeDShadow": wx.SYS_COLOUR_3DSHADOW,
"Window": wx.SYS_COLOUR_WINDOW,
"WindowFrame": wx.SYS_COLOUR_WINDOWFRAME,
"WindowText": wx.SYS_COLOUR_WINDOWTEXT
}
NamedColours.update(
#strip the alpha from the system colors. Is this really what we want to do?
(k.lower(), wx.SystemSettings.GetColour(v)[:3]) for (k,v) in systemColors.iteritems()
)
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
""" Parse CSS identifiers. More complicated than it sounds"""
from pyparsing import Word, Literal, Regex, Combine, Optional, White, oneOf, ZeroOrMore
import string
import re
class White(White):
""" Customize whitespace to match the CSS spec values"""
def __init__(self, ws=" \t\r\n\f", min=1, max=0, exact=0):
super(White, self).__init__(ws, min, max, exact)
escaped = (
Literal("\\").suppress() +
#chr(20)-chr(126) + chr(128)-unichr(sys.maxunicode)
Regex(u"[\u0020-\u007e\u0080-\uffff]", re.IGNORECASE)
)
def convertToUnicode(t):
return unichr(int(t[0], 16))
hex_unicode = (
Literal("\\").suppress() +
Regex("[0-9a-f]{1,6}", re.IGNORECASE) +
Optional(White(exact=1)).suppress()
).setParseAction(convertToUnicode)
escape = hex_unicode | escaped
#any unicode literal outside the 0-127 ascii range
nonascii = Regex(u"[^\u0000-\u007f]")
#single character for starting an identifier.
nmstart = Regex(u"[A-Z]", re.IGNORECASE) | nonascii | escape
nmchar = Regex(u"[0-9A-Z-]", re.IGNORECASE) | nonascii | escape
identifier = Combine(nmstart + ZeroOrMore(nmchar))
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
""" Parser for inline CSS in style attributes """
def inlineStyle(styleString):
if not styleString:
return {}
styles = styleString.split(";")
rv = dict(style.split(":") for style in styles if len(style) != 0)
return rv
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
Parsing for CSS and CSS-style values, such as transform and filter attributes.
"""
from pyparsing import (Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, delimitedList)
#some shared definitions from pathdata
from ..pathdata import number, maybeComma
paren = Literal("(").suppress()
cparen = Literal(")").suppress()
def Parenthised(exp):
return Group(paren + exp + cparen)
skewY = Literal("skewY") + Parenthised(number)
skewX = Literal("skewX") + Parenthised(number)
rotate = Literal("rotate") + Parenthised(
number + Optional(maybeComma + number + maybeComma + number)
)
scale = Literal("scale") + Parenthised(
number + Optional(maybeComma + number)
)
translate = Literal("translate") + Parenthised(
number + Optional(maybeComma + number)
)
matrix = Literal("matrix") + Parenthised(
#there's got to be a better way to write this
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number
)
transform = (skewY | skewX | rotate | scale | translate | matrix)
transformList = delimitedList(Group(transform), delim=maybeComma)
if __name__ == '__main__':
from tests.test_css import *
unittest.main()
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
Parser for various kinds of CSS values as per CSS2 spec section 4.3
"""
from pyparsing import Word, Combine, Optional, Literal, oneOf, CaselessLiteral, StringEnd
def asInt(s,l,t):
return int(t[0])
def asFloat(s,l,t):
return float(t[0])
def asFloatOrInt(s,l,t):
""" Return an int if possible, otherwise a float"""
v = t[0]
try:
return int(v)
except ValueError:
return float(v)
integer = Word("0123456789").setParseAction(asInt)
number = Combine(
Optional(Word("0123456789")) + Literal(".") + Word("01234567890")
| integer
)
number.setName('number')
sign = oneOf("+ -")
signedNumber = Combine(Optional(sign) + number).setParseAction(asFloat)
lengthValue = Combine(Optional(sign) + number).setParseAction(asFloatOrInt)
lengthValue.setName('lengthValue')
#TODO: The physical units like in, mm
lengthUnit = oneOf(['em', 'ex', 'px', 'pt', '%'], caseless=True)
#the spec says that the unit is only optional for a 0 length, but
#there are just too many places where a default is permitted.
#TODO: Maybe should use a ctor like optional to let clients declare it?
length = lengthValue + Optional(lengthUnit, default=None) + StringEnd()
length.leaveWhitespace()
#set the parse action aftward so it doesn't "infect" the parsers that build on it
number.setParseAction(asFloat)
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
SVGDocument
"""
import wx
from cStringIO import StringIO
import warnings
import math
from functools import wraps
import pathdata
import css
from svg.css.colour import colourValue
from svg.css import values
from attributes import paintValue
document = """<?xml version = "1.0" standalone = "no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width = "4cm" height = "4cm" viewBox = "0 0 400 400"
xmlns = "http://www.w3.org/2000/svg" version = "1.1">
<title>Example triangle01- simple example of a 'path'</title>
<desc>A path that draws a triangle</desc>
<rect x = "1" y = "1" width = "398" height = "398"
fill = "none" stroke = "blue" />
<path d = "M 100 100 L 300 100 L 200 300 z"
fill = "red" stroke = "blue" stroke-width = "3" />
</svg>"""
makePath = lambda: wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
def attrAsFloat(node, attr, defaultValue = "0"):
val = node.get(attr, defaultValue)
#TODO: process stuff like "inherit" by walking back up the nodes
#fast path optimization - if it's a valid float, don't
#try to parse it.
try:
return float(val)
except ValueError:
return valueToPixels(val)
def valueToPixels(val, defaultUnits = "px"):
#TODO manage default units
from pyparsing import ParseException
try:
val, unit = values.length.parseString(val)
except ParseException:
print "***", val
raise
#todo: unit conversion to something other than pixels
return val
def pathHandler(func):
"""decorator for methods which return a path operation
Creates the path they will fill,
and generates the path operations for the node
"""
@wraps(func)
def inner(self, node):
#brush = self.getBrushFromState()
#pen = self.getPenFromState()
#if not (brush or pen):
# return None, []
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
func(self, node, path)
ops = self.generatePathOps(path)
return path, ops
return inner
class SVGDocument(object):
lastControl = None
brushCache = {}
penCache = {}
def __init__(self, element):
"""
Create an SVG document from an ElementTree node.
"""
self.handlers = {
'{http://www.w3.org/2000/svg}svg':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}a':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}g':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}rect':self.addRectToDocument,
'{http://www.w3.org/2000/svg}circle': self.addCircleToDocument,
'{http://www.w3.org/2000/svg}ellipse': self.addEllipseToDocument,
'{http://www.w3.org/2000/svg}line': self.addLineToDocument,
'{http://www.w3.org/2000/svg}polyline': self.addPolyLineToDocument,
'{http://www.w3.org/2000/svg}polygon': self.addPolygonToDocument,
'{http://www.w3.org/2000/svg}path':self.addPathDataToDocument,
'{http://www.w3.org/2000/svg}text':self.addTextToDocument
}
assert element.tag == '{http://www.w3.org/2000/svg}svg', 'Not an SVG fragment'
self.tree = element
self.paths = {}
self.stateStack = [{}]
path, ops = self.processElement(element)
self.ops = ops
@property
def state(self):
""" Retrieve the current state, without popping"""
return self.stateStack[-1]
def processElement(self, element):
""" Process one element of the XML tree.
Returns the path representing the node,
and an operation list for drawing the node.
Parent nodes should return a path (for hittesting), but
no draw operations
"""
#copy the current state
current = dict(self.state)
current.update(element.items())
current.update(css.inlineStyle(element.get("style", "")))
self.stateStack.append(current)
handler = self.handlers.get(element.tag, lambda *any: (None, None))
path, ops = handler(element)
self.paths[element] = path
self.stateStack.pop()
return path, ops
def createTransformOpsFromNode(self, node):
""" Returns an oplist for transformations.
This applies to a node, not the current state because
the transform stack is saved in the wxGraphicsContext.
This oplist does *not* include the push/pop state commands
"""
ops = []
transform = node.get('transform')
#todo: replace this with a mapping list
if transform:
for transform, args in css.transformList.parseString(transform):
if transform == 'scale':
if len(args) == 1:
x = y = args[0]
else:
x, y = args
ops.append(
(wx.GraphicsContext.Scale, (x, y))
)
if transform == 'translate':
if len(args) == 1:
x = args[0]
y = 0
else:
x, y = args
ops.append(
(wx.GraphicsContext.Translate, (x, y))
)
if transform == 'rotate':
if len(args) == 3:
angle, cx, cy = args
angle = math.radians(angle)
ops.extend([
(wx.GraphicsContext.Translate, (cx, cy)),
(wx.GraphicsContext.Rotate, (angle,)),
(wx.GraphicsContext.Translate, (-cx, -cy)),
])
else:
angle = args[0]
angle = math.radians(angle)
ops.append(
(wx.GraphicsContext.Rotate, (angle,))
)
if transform == 'matrix':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
*args
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewX':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1, 0, math.tan(math.radians(args[0])), 1, 0, 0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewY':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1, math.tan(math.radians(args[0])), 0, 1, 0, 0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
return ops
def addGroupToDocument(self, node):
""" For parent elements: push on a state,
then process all child elements
"""
ops = [
(wx.GraphicsContext.PushState, ())
]
path = makePath()
ops.extend(self.createTransformOpsFromNode(node))
for child in node.getchildren():
cpath, cops = self.processElement(child)
if cpath:
path.AddPath(cpath)
if cops:
ops.extend(cops)
ops.append(
(wx.GraphicsContext.PopState, ())
)
return path, ops
def getFontFromState(self):
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
family = self.state.get("font-family")
if False:#family:
#print "setting font", family
font.SetFaceName(family)
size = self.state.get("font-size")
#I'm not sure if this is right or not
if size:
val, unit = values.length.parseString(size)
if '__WXMSW__' in wx.PlatformInfo:
i = int(val)
font.SetPixelSize((i, i))
else:
font.SetPointSize(int(val))
return font
def addTextToDocument(self, node):
x, y = [attrAsFloat(node, attr) for attr in ('x', 'y')]
def DoDrawText(context, text, x, y, brush = wx.NullGraphicsBrush):
#SVG spec appears to originate text from the bottom
#rather than the top as with our API. This function
#will measure and then re-orient the text as needed.
w, h = context.GetTextExtent(text)
y -= h
context.DrawText(text, x, y, brush)
font = self.getFontFromState()
brush = self.getBrushFromState()
if not (brush and brush.IsOk()):
brush = wx.BLACK_BRUSH
#normalize whitespace
#TODO: SVG probably has rules for this
text = ' '.join(node.text.split() if node.text else "")
if text is None:
return None, []
ops = [
(wx.GraphicsContext.SetFont, (font, brush.Colour)),
(DoDrawText, (text, x, y))
]
return None, ops
@pathHandler
def addRectToDocument(self, node, path):
x, y, w, h = (attrAsFloat(node, attr) for attr in ['x', 'y', 'width', 'height'])
rx = node.get('rx')
ry = node.get('ry')
if not (w and h):
path.MoveToPoint(x, y) #keep the current point correct
return
if rx or ry:
if rx and ry:
rx, ry = float(rx), float(ry)
elif rx:
rx = ry = float(rx)
elif ry:
rx = ry = float(ry)
#value clamping as per spec section 9.2
rx = min(rx, w/2)
ry = min(ry, h/2)
#origin
path.MoveToPoint(x+rx, y)
path.AddLineToPoint(x+w-rx, y)
#top right
cx = rx * 2
cy = ry * 2
path.AddEllipticalArc(
x+w-cx, y,
cx, cy,
math.radians(270), math.radians(0),
True
)
path.AddLineToPoint(x+w, y+h-ry)
#bottom right
path.AddEllipticalArc(
x+w-cx, y+h-cy,
cx, cy,
math.radians(0), math.radians(90),
True
)
path.AddLineToPoint(x+rx, y+h)
#bottom left
path.AddEllipticalArc(
x, y+h-cy,
cx, cy,
math.radians(90),
math.radians(180),
True
)
path.AddLineToPoint(x, y+ry)
#bottom right
path.AddEllipticalArc(
x, y,
cx, cy,
math.radians(180),
math.radians(270),
True
)
path.CloseSubpath()
else:
path.AddRectangle(
x, y, w, h
)
@pathHandler
def addCircleToDocument(self, node, path):
cx, cy, r = [attrAsFloat(node, attr) for attr in ('cx', 'cy', 'r')]
path.AddCircle(cx, cy, r)
@pathHandler
def addEllipseToDocument(self, node, path):
cx, cy, rx, ry = [float(node.get(attr, 0)) for attr in ('cx', 'cy', 'rx', 'ry')]
#cx, cy are centerpoint.
#rx, ry are radius.
#wxGC coords are the rect of the ellipse bounding box.
if rx <= 0 or ry <= 0:
return
x = cx - rx
y = cy - ry
path.AddEllipse(x, y, rx*2, ry*2)
@pathHandler
def addLineToDocument(self, node, path):
x1, y1, x2, y2 = [attrAsFloat(node, attr) for attr in ('x1', 'y1', 'x2', 'y2')]
path.MoveToPoint(x1, y1)
path.AddLineToPoint(x2, y2)
@pathHandler
def addPolyLineToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points")
self.addPathDataToPath(data, path)
@pathHandler
def addPolygonToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points") + " Z"
self.addPathDataToPath(data, path)
@pathHandler
def addPathDataToDocument(self, node, path):
self.addPathDataToPath(node.get('d', ''), path)
def addPathDataToPath(self, data, path):
self.lastControl = None
self.lastControlQ = None
self.firstPoints = []
def normalizeStrokes(parseResults):
""" The data comes from the parser in the
form of (command, [list of arguments]).
We translate that to [(command, args[0]), (command, args[1])]
via a generator.
M is special cased because its subsequent arguments
become linetos.
"""
for command, arguments in parseResults:
if not arguments:
yield (command, ())
else:
arguments = iter(arguments)
if command == 'm':
yield (command, arguments.next())
command = "l"
elif command == "M":
yield (command, arguments.next())
command = "L"
for arg in arguments:
yield (command, arg)
#print "data length", len(data)
import time
t = time.time()
parsed = pathdata.svg.parseString(data)
#print "parsed in", time.time()-t
for stroke in normalizeStrokes(parsed):
self.addStrokeToPath(path, stroke)
def generatePathOps(self, path):
""" Look at the current state and generate the
draw operations (fill, stroke, neither) for the path"""
ops = []
brush = self.getBrushFromState(path)
fillRule = self.state.get('fill-rule', 'nonzero')
frMap = {'nonzero':wx.WINDING_RULE, 'evenodd': wx.ODDEVEN_RULE}
fr = frMap.get(fillRule, wx.ODDEVEN_RULE)
if brush:
ops.append(
(wx.GraphicsContext.SetBrush, (brush,))
)
ops.append(
(wx.GraphicsContext.FillPath, (path, fr))
)
pen = self.getPenFromState()
if pen:
ops.append(
(wx.GraphicsContext.SetPen, (pen,))
)
ops.append(
(wx.GraphicsContext.StrokePath, (path,))
)
return ops
def getPenFromState(self):
pencolour = self.state.get('stroke', 'none')
if pencolour == 'currentColor':
pencolour = self.state.get('color', 'none')
if pencolour == 'transparent':
return wx.TRANSPARENT_PEN
if pencolour == 'none':
return wx.NullPen
type, value = colourValue.parseString(pencolour)
if type == 'URL':
warnings.warn("Color servers for stroking not implemented")
return wx.NullPen
else:
if value[:3] == (-1, -1, -1):
return wx.NullPen
pen = wx.Pen(wx.Colour(*value))
width = self.state.get('stroke-width')
if width:
width, units = values.length.parseString(width)
pen.SetWidth(width)
capmap = {
'butt':wx.CAP_BUTT,
'round':wx.CAP_ROUND,
'square':wx.CAP_PROJECTING
}
joinmap = {
'miter':wx.JOIN_MITER,
'round':wx.JOIN_ROUND,
'bevel':wx.JOIN_BEVEL
}
pen.SetCap(capmap.get(self.state.get('stroke-linecap', None), wx.CAP_BUTT))
pen.SetJoin(joinmap.get(self.state.get('stroke-linejoin', None), wx.JOIN_MITER))
return wx.GraphicsRenderer_GetDefaultRenderer().CreatePen(pen)
def getBrushFromState(self, path = None):
brushcolour = self.state.get('fill', 'black').strip()
type, details = paintValue.parseString(brushcolour)
if type == "URL":
url, fallback = details
element = self.resolveURL(url)
if not element:
if fallback:
type, details = fallback
else:
r, g, b, = 0, 0, 0
else:
if element.tag == '{http://www.w3.org/2000/svg}linearGradient':
box = path.GetBox()
x, y, w, h = box.Get()
return wx.GraphicsRenderer.GetDefaultRenderer().CreateLinearGradientBrush(
x, y, x+w, y+h, wx.Colour(0, 0, 255, 128), wx.RED
)
elif element.tag == '{http://www.w3.org/2000/svg}radialGradient':
box = path.GetBox()
x, y, w, h = box.Get()
#print w
mx = wx.GraphicsRenderer.GetDefaultRenderer().CreateMatrix(x, y, w, h)
cx, cy = mx.TransformPoint(0.5, 0.5)
fx, fy = cx, cy
return wx.GraphicsRenderer.GetDefaultRenderer().CreateRadialGradientBrush(
cx, cy,
fx, fy,
(max(w, h))/2,
wx.Colour(0, 0, 255, 128), wx.RED
)
else:
#invlid gradient specified
return wx.NullBrush
r, g, b = 0, 0, 0
if type == 'CURRENTCOLOR':
type, details = paintValue.parseString(self.state.get('color', 'none'))
if type == 'RGB':
r, g, b = details
elif type == "NONE":
return wx.NullBrush
opacity = self.state.get('fill-opacity', self.state.get('opacity', '1'))
opacity = float(opacity)
opacity = min(max(opacity, 0.0), 1.0)
a = 255 * opacity
#using try/except block instead of
#just setdefault because the wxBrush and wxColour would
#be created every time anyway in order to pass them,
#defeating the purpose of the cache
try:
return SVGDocument.brushCache[(r, g, b, a)]
except KeyError:
return SVGDocument.brushCache.setdefault((r, g, b, a), wx.Brush(wx.Colour(r, g, b, a)))
def resolveURL(self, urlData):
"""
Resolve a URL and return an elementTree Element from it.
Return None if unresolvable
"""
scheme, netloc, path, query, fragment = urlData
if scheme == netloc == path == '':
#horrible. There's got to be a better way?
if self.tree.get("id") == fragment:
return self.tree
else:
for child in self.tree.getiterator():
#print child.get("id")
if child.get("id") == fragment:
return child
return None
else:
return self.resolveRemoteURL(urlData)
def resolveRemoteURL(self, url):
return None
def addStrokeToPath(self, path, stroke):
""" Given a stroke from a path command
(in the form (command, arguments)) create the path
commands that represent it.
TODO: break out into (yet another) class/module,
especially so we can get O(1) dispatch on type?
"""
type, arg = stroke
relative = False
if type == type.lower():
relative = True
ox, oy = path.GetCurrentPoint().Get()
else:
ox = oy = 0
def normalizePoint(arg):
x, y = arg
return x+ox, y+oy
def reflectPoint(point, relativeTo):
x, y = point
a, b = relativeTo
return ((a*2)-x), ((b*2)-y)
type = type.upper()
if type == 'M':
pt = normalizePoint(arg)
self.firstPoints.append(pt)
path.MoveToPoint(pt)
elif type == 'L':
path.AddLineToPoint(normalizePoint(arg))
elif type == 'C':
#control1, control2, endpoint = arg
control1, control2, endpoint = map(
normalizePoint, arg
)
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
#~ cp = path.GetCurrentPoint()
#~ path.AddCircle(c1x, c1y, 5)
#~ path.AddCircle(c2x, c2y, 3)
#~ path.AddCircle(x, y, 7)
#~ path.MoveToPoint(cp)
#~ print "C", control1, control2, endpoint
elif type == 'S':
#control2, endpoint = arg
control2, endpoint = map(
normalizePoint, arg
)
if self.lastControl:
control1 = reflectPoint(self.lastControl, path.GetCurrentPoint())
else:
control1 = path.GetCurrentPoint()
#~ print "S", self.lastControl, ":", control1, control2, endpoint
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
elif type == "Q":
(cx, cy), (x, y) = map(normalizePoint, arg)
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "T":
x, y, = normalizePoint(arg)
if self.lastControlQ:
cx, cy = reflectPoint(self.lastControlQ, path.GetCurrentPoint())
else:
cx, cy = path.GetCurrentPoint()
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "V":
_, y = normalizePoint((0, arg))
x, _ = path.GetCurrentPoint()
path.AddLineToPoint(x, y)
elif type == "H":
x, _ = normalizePoint((arg, 0))
_, y = path.GetCurrentPoint()
path.AddLineToPoint(x, y)
elif type == "A":
#wxGC currently only supports circular arcs,
#not eliptical ones
(
(rx, ry), #radii of ellipse
angle, #angle of rotation on the ellipse in degrees
(fa, fs), #arc and stroke angle flags
(x, y) #endpoint on the arc
) = arg
x, y = normalizePoint((x, y))
cx, cy = path.GetCurrentPoint()
if (cx, cy) == (x, y):
return #noop
if (rx == 0 or ry == 0):
#no radius is effectively a line
path.AddLineToPoint(x, y)
return
#find the center point for the ellipse
#translation via the angle
angle = angle % 360
angle = math.radians(angle)
#translated endpoint
xPrime = math.cos(angle) * ((cx-x)/2)
xPrime += math.sin(angle) * ((cx-x)/2)
yPrime = -(math.sin(angle)) * ((cy-y)/2)
yPrime += (math.cos(angle)) * ((cy-y)/2)
temp = ((rx**2) * (ry**2)) - ((rx**2) * (yPrime**2)) - ((ry**2) * (xPrime**2))
temp /= ((rx**2) * (yPrime**2)) + ((ry**2)*(xPrime**2))
temp = abs(temp)
try:
temp = math.sqrt(temp)
except ValueError:
import pdb
pdb.set_trace()
cxPrime = temp * ((rx * yPrime) / ry)
cyPrime = temp * -((ry * xPrime) / rx)
if fa == fs:
cxPrime, cyPrime = -cxPrime, -cyPrime
#reflect backwards now for the origin
cnx = math.cos(angle) * cxPrime
cnx += math.sin(angle) * cxPrime
cny = -(math.sin(angle)) * cyPrime
cny += (math.cos(angle)) * cyPrime
cnx += ((cx+x)/2.0)
cny += ((cy+y)/2.0)
#calculate the angle between the two endpoints
lastArc = wx.Point2D(x-cnx, y-cny).GetVectorAngle()
firstArc = wx.Point2D(cx-cnx, cy-cny).GetVectorAngle()
lastArc = math.radians(lastArc)
firstArc = math.radians(firstArc)
#aargh buggines.
#AddArc draws a straight line between
#the endpoints of the arc.
#putting it in a subpath makes the strokes come out
#correctly, but it still only fills the slice
#taking out the MoveToPoint fills correctly.
path.AddEllipse(cnx-rx, cny-ry, rx*2, ry*2)
path.MoveToPoint(x, y)
#~ npath = makePath()
#~ npath.AddEllipticalArc(cnx-rx, cny-ry, rx*2, ry*2, firstArc, lastArc, False)
#~ npath.MoveToPoint(x, y)
#~ path.AddPath(npath)
elif type == 'Z':
#~ Bugginess:
#~ CloseSubpath() doesn't change the
#~ current point, as SVG spec requires.
#~ However, manually moving to the endpoint afterward opens a new subpath
#~ and (apparently) messes with stroked but not filled paths.
#~ This is possibly a bug in GDI+?
#~ Manually closing the path via AddLineTo gives incorrect line join
#~ results
#~ Manually closing the path *and* calling CloseSubpath() appears
#~ to give correct results on win32
pt = self.firstPoints.pop()
path.AddLineToPoint(pt)
path.CloseSubpath()
def render(self, context):
if not hasattr(self, "ops"):
return
for op, args in self.ops:
op(context, *args)
if __name__ == '__main__':
from tests.test_document import *
unittest.main()
# This file is part of the Printrun suite.
#
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Printrun. If not, see <http://www.gnu.org/licenses/>.
"""
SVG path data parser
Usage:
steps = svg.parseString(pathdata)
for command, arguments in steps:
pass
"""
from pyparsing import (ParserElement, Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, ParseException, OneOrMore)
#ParserElement.enablePackrat()
def Command(char):
""" Case insensitive but case preserving"""
return CaselessPreservingLiteral(char)
def Arguments(token):
return Group(token)
class CaselessPreservingLiteral(CaselessLiteral):
""" Like CaselessLiteral, but returns the match as found
instead of as defined.
"""
def __init__( self, matchString ):
super(CaselessPreservingLiteral, self).__init__( matchString.upper() )
self.name = "'%s'" % matchString
self.errmsg = "Expected " + self.name
self.myException.msg = self.errmsg
def parseImpl( self, instring, loc, doActions = True ):
test = instring[ loc:loc+self.matchLen ]
if test.upper() == self.match:
return loc+self.matchLen, test
#~ raise ParseException( instring, loc, self.errmsg )
exc = self.myException
exc.loc = loc
exc.pstr = instring
raise exc
def Sequence(token):
""" A sequence of the token"""
return OneOrMore(token+maybeComma)
digit_sequence = Word(nums)
sign = oneOf("+ -")
def convertToFloat(s, loc, toks):
try:
return float(toks[0])
except:
raise ParseException(loc, "invalid float format %s"%toks[0])
exponent = CaselessLiteral("e")+Optional(sign)+Word(nums)
#note that almost all these fields are optional,
#and this can match almost anything. We rely on Pythons built-in
#float() function to clear out invalid values - loosely matching like this
#speeds up parsing quite a lot
floatingPointConstant = Combine(
Optional(sign) +
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
floatingPointConstant.setParseAction(convertToFloat)
number = floatingPointConstant
#same as FP constant but don't allow a - sign
nonnegativeNumber = Combine(
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
nonnegativeNumber.setParseAction(convertToFloat)
coordinate = number
#comma or whitespace can seperate values all over the place in SVG
maybeComma = Optional(Literal(',')).suppress()
coordinateSequence = Sequence(coordinate)
coordinatePair = (coordinate + maybeComma + coordinate).setParseAction(lambda t: tuple(t))
coordinatePairSequence = Sequence(coordinatePair)
coordinatePairPair = coordinatePair + maybeComma + coordinatePair
coordinatePairPairSequence = Sequence(Group(coordinatePairPair))
coordinatePairTriple = coordinatePair + maybeComma + coordinatePair + maybeComma + coordinatePair
coordinatePairTripleSequence = Sequence(Group(coordinatePairTriple))
#commands
lineTo = Group(Command("L") + Arguments(coordinatePairSequence))
moveTo = Group(Command("M") + Arguments(coordinatePairSequence))
closePath = Group(Command("Z")).setParseAction(lambda t: ('Z', (None,)))
flag = oneOf("1 0").setParseAction(lambda t: bool(int((t[0]))))
arcRadius = (
nonnegativeNumber + maybeComma + #rx
nonnegativeNumber #ry
).setParseAction(lambda t: tuple(t))
arcFlags = (flag + maybeComma + flag).setParseAction(lambda t: tuple(t))
ellipticalArcArgument = Group(
arcRadius + maybeComma + #rx, ry
number + maybeComma +#rotation
arcFlags + #large-arc-flag, sweep-flag
coordinatePair #(x, y)
)
ellipticalArc = Group(Command("A") + Arguments(Sequence(ellipticalArcArgument)))
smoothQuadraticBezierCurveto = Group(Command("T") + Arguments(coordinatePairSequence))
quadraticBezierCurveto = Group(Command("Q") + Arguments(coordinatePairPairSequence))
smoothCurve = Group(Command("S") + Arguments(coordinatePairPairSequence))
curve = Group(Command("C") + Arguments(coordinatePairTripleSequence))
horizontalLine = Group(Command("H") + Arguments(coordinateSequence))
verticalLine = Group(Command("V") + Arguments(coordinateSequence))
drawToCommand = (
lineTo | moveTo | closePath | ellipticalArc | smoothQuadraticBezierCurveto |
quadraticBezierCurveto | smoothCurve | curve | horizontalLine | verticalLine
)
#~ number.debug = True
moveToDrawToCommands = moveTo + ZeroOrMore(drawToCommand)
svg = ZeroOrMore(moveToDrawToCommands)
svg.keepTabs = True
def profile():
import cProfile
p = cProfile.Profile()
p.enable()
ptest()
ptest()
ptest()
p.disable()
p.print_stats()
bpath = """M204.33 139.83 C196.33 133.33 206.68 132.82 206.58 132.58 C192.33 97.08 169.35
81.41 167.58 80.58 C162.12 78.02 159.48 78.26 160.45 76.97 C161.41 75.68 167.72 79.72 168.58
80.33 C193.83 98.33 207.58 132.33 207.58 132.33 C207.58 132.33 209.33 133.33 209.58 132.58
C219.58 103.08 239.58 87.58 246.33 81.33 C253.08 75.08 256.63 74.47 247.33 81.58 C218.58 103.58
210.34 132.23 210.83 132.33 C222.33 134.83 211.33 140.33 211.83 139.83 C214.85 136.81 214.83 145.83 214.83
145.83 C214.83 145.83 231.83 110.83 298.33 66.33 C302.43 63.59 445.83 -14.67 395.83 80.83 C393.24 85.79 375.83
105.83 375.83 105.83 C375.83 105.83 377.33 114.33 371.33 121.33 C370.3 122.53 367.83 134.33 361.83 140.83 C360.14 142.67
361.81 139.25 361.83 140.83 C362.33 170.83 337.76 170.17 339.33 170.33 C348.83 171.33 350.19 183.66 350.33 183.83 C355.83
190.33 353.83 191.83 355.83 194.83 C366.63 211.02 355.24 210.05 356.83 212.83 C360.83 219.83 355.99 222.72 357.33 224.83
C360.83 230.33 354.75 233.84 354.83 235.33 C355.33 243.83 349.67 240.73 349.83 244.33 C350.33 255.33 346.33 250.83 343.83 254.83
C336.33 266.83 333.46 262.38 332.83 263.83 C329.83 270.83 325.81 269.15 324.33 270.83 C320.83 274.83 317.33 274.83 315.83 276.33
C308.83 283.33 304.86 278.39 303.83 278.83 C287.83 285.83 280.33 280.17 277.83 280.33 C270.33 280.83 271.48 279.67 269.33 277.83
C237.83 250.83 219.33 211.83 215.83 206.83 C214.4 204.79 211.35 193.12 212.33 195.83 C214.33 201.33 213.33 250.33 207.83 250.33
C202.33 250.33 201.83 204.33 205.33 195.83 C206.43 193.16 204.4 203.72 201.79 206.83 C196.33 213.33 179.5 250.83 147.59 277.83
C145.42 279.67 146.58 280.83 138.98 280.33 C136.46 280.17 128.85 285.83 112.65 278.83 C111.61 278.39 107.58 283.33 100.49 276.33
C98.97 274.83 95.43 274.83 91.88 270.83 C90.39 269.15 86.31 270.83 83.27 263.83 C82.64 262.38 79.73 266.83 72.13 254.83 C69.6 250.83
65.54 255.33 66.05 244.33 C66.22 240.73 60.48 243.83 60.99 235.33 C61.08 233.84 54.91 230.33 58.45 224.83 C59.81 222.72 54.91 219.83
58.96 212.83 C60.57 210.05 49.04 211.02 59.97 194.83 C62 191.83 59.97 190.33 65.54 183.83 C65.69 183.66 67.06 171.33 76.69 170.33
C78.28 170.17 53.39 170.83 53.9 140.83 C53.92 139.25 55.61 142.67 53.9 140.83 C47.82 134.33 45.32 122.53 44.27 121.33 C38.19 114.33
39.71 105.83 39.71 105.83 C39.71 105.83 22.08 85.79 19.46 80.83 C-31.19 -14.67 114.07 63.59 118.22 66.33 C185.58 110.83 202 145.83
202 145.83 C202 145.83 202.36 143.28 203 141.83 C203.64 140.39 204.56 140.02 204.33 139.83 z"""
def ptest():
svg.parseString(bpath)
if __name__ == '__main__':
#~ from tests.test_pathdata import *
#~ unittest.main()
profile()
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
import cmd, sys import cmd, sys
import glob, os, time, datetime import glob, os, time, datetime
import sys, subprocess import sys, subprocess, traceback
import math, codecs import math, codecs
import shlex import shlex
from math import sqrt from math import sqrt
...@@ -205,6 +205,21 @@ class Settings(object): ...@@ -205,6 +205,21 @@ class Settings(object):
self._add(StringSetting("sliceoptscommand", "python skeinforge/skeinforge_application/skeinforge.py", _("Slicer options command"), _("Slice settings command\n default:\n python skeinforge/skeinforge_application/skeinforge.py"))) self._add(StringSetting("sliceoptscommand", "python skeinforge/skeinforge_application/skeinforge.py", _("Slicer options command"), _("Slice settings command\n default:\n python skeinforge/skeinforge_application/skeinforge.py")))
self._add(StringSetting("final_command", "", _("Final command"), _("Executable to run when the print is finished"))) self._add(StringSetting("final_command", "", _("Final command"), _("Executable to run when the print is finished")))
self._add(HiddenSetting("project_offset_x", 0.0))
self._add(HiddenSetting("project_offset_y", 0.0))
self._add(HiddenSetting("project_interval", 2.0))
self._add(HiddenSetting("project_pause", 2.5))
self._add(HiddenSetting("project_scale", 1.0))
self._add(HiddenSetting("project_x", 1024.0))
self._add(HiddenSetting("project_y", 768.0))
self._add(HiddenSetting("project_projected_x", 150.0))
self._add(HiddenSetting("project_direction", "Top Down"))
self._add(HiddenSetting("project_overshoot", 3.0))
self._add(HiddenSetting("project_z_axis_rate", 200))
self._add(HiddenSetting("project_layer", 0.1))
self._add(HiddenSetting("project_prelift_gcode", ""))
self._add(HiddenSetting("project_postlift_gcode", ""))
_settings = [] _settings = []
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name.startswith("_"): if name.startswith("_"):
...@@ -952,7 +967,7 @@ class pronsole(cmd.Cmd): ...@@ -952,7 +967,7 @@ class pronsole(cmd.Cmd):
self.tempreadings = l self.tempreadings = l
self.status.update_tempreading(l) self.status.update_tempreading(l)
tstring = l.rstrip() tstring = l.rstrip()
if(tstring!="ok" and not tstring.startswith("ok T") and not tstring.startswith("T:") and not self.listing and not self.monitoring): if tstring != "ok" and not self.listing and not self.monitoring:
if tstring[:5] == "echo:": if tstring[:5] == "echo:":
tstring = tstring[5:].lstrip() tstring = tstring[5:].lstrip()
if self.silent == False: print "\r" + tstring.ljust(15) if self.silent == False: print "\r" + tstring.ljust(15)
...@@ -1210,7 +1225,6 @@ class pronsole(cmd.Cmd): ...@@ -1210,7 +1225,6 @@ class pronsole(cmd.Cmd):
print "Setting bed temp to 0" print "Setting bed temp to 0"
self.p.send_now("M140 S0.0") self.p.send_now("M140 S0.0")
self.log("Disconnecting from printer...") self.log("Disconnecting from printer...")
print self.p.printing
if self.p.printing: if self.p.printing:
print "Are you sure you want to exit while printing?" print "Are you sure you want to exit while printing?"
print "(this will terminate the print)." print "(this will terminate the print)."
...@@ -1434,6 +1448,9 @@ if __name__ == "__main__": ...@@ -1434,6 +1448,9 @@ if __name__ == "__main__":
interp.parse_cmdline(sys.argv[1:]) interp.parse_cmdline(sys.argv[1:])
try: try:
interp.cmdloop() interp.cmdloop()
except SystemExit:
interp.p.disconnect()
except: except:
print _("Caught an exception, exiting:")
traceback.print_exc()
interp.p.disconnect() interp.p.disconnect()
#raise
...@@ -49,11 +49,11 @@ import pronsole ...@@ -49,11 +49,11 @@ import pronsole
from pronsole import dosify, wxSetting, HiddenSetting, StringSetting, SpinSetting, FloatSpinSetting, BooleanSetting from pronsole import dosify, wxSetting, HiddenSetting, StringSetting, SpinSetting, FloatSpinSetting, BooleanSetting
from printrun import gcoder from printrun import gcoder
def parse_temperature_report(report, key): tempreport_exp = re.compile("([TB]\d*):([-+]?\d*\.?\d*)(?: \/)?([-+]?\d*\.?\d*)")
if key in report:
return float(filter(lambda x: x.startswith(key), report.split())[0].split(":")[1].split("/")[0]) def parse_temperature_report(report):
else: matches = tempreport_exp.findall(report)
return -1.0 return dict((m[0], (m[1], m[2])) for m in matches)
def format_time(timestamp): def format_time(timestamp):
return datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") return datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S")
...@@ -95,6 +95,8 @@ def parse_build_dimensions(bdim): ...@@ -95,6 +95,8 @@ def parse_build_dimensions(bdim):
bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)] bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)]
if len(bdl_float) < len(defaults): if len(bdl_float) < len(defaults):
bdl_float += [defaults[i] for i in range(len(bdl_float), len(defaults))] bdl_float += [defaults[i] for i in range(len(bdl_float), len(defaults))]
for i in range(3): # Check for nonpositive dimensions for build volume
if bdl_float[i] <= 0: bdl_float[i] = 1
return bdl_float return bdl_float
class BuildDimensionsSetting(wxSetting): class BuildDimensionsSetting(wxSetting):
...@@ -532,10 +534,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -532,10 +534,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def project(self,event): def project(self,event):
from printrun import projectlayer from printrun import projectlayer
if self.p.online: projectlayer.SettingsFrame(self, self.p).Show()
projectlayer.setframe(self,self.p).Show()
else:
print _("Printer is not online.")
def popmenu(self): def popmenu(self):
self.menustrip = wx.MenuBar() self.menustrip = wx.MenuBar()
...@@ -578,7 +577,7 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -578,7 +577,7 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def do_editgcode(self, e = None): def do_editgcode(self, e = None):
if self.filename is not None: if self.filename is not None:
MacroEditor(self.filename, "\n".join([line.raw for line in self.fgcode]), self.doneediting, 1) MacroEditor(self.filename, [line.raw for line in self.fgcode], self.doneediting, 1)
def new_macro(self, e = None): def new_macro(self, e = None):
dialog = wx.Dialog(self, -1, _("Enter macro name"), size = (260, 85)) dialog = wx.Dialog(self, -1, _("Enter macro name"), size = (260, 85))
...@@ -859,10 +858,10 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -859,10 +858,10 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def cbutton_remove(self, e, button): def cbutton_remove(self, e, button):
n = button.custombutton n = button.custombutton
self.custombuttons[n]=None
self.cbutton_save(n, None) self.cbutton_save(n, None)
#while len(self.custombuttons) and self.custombuttons[-1] is None: del self.custombuttons[n]
# del self.custombuttons[-1] for i in range(n, len(self.custombuttons)):
self.cbutton_save(i, self.custombuttons[i])
wx.CallAfter(self.cbuttons_reload) wx.CallAfter(self.cbuttons_reload)
def cbutton_order(self, e, button, dir): def cbutton_order(self, e, button, dir):
...@@ -1136,10 +1135,18 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1136,10 +1135,18 @@ class PronterWindow(MainWindow, pronsole.pronsole):
def update_tempdisplay(self): def update_tempdisplay(self):
try: try:
hotend_temp = parse_temperature_report(self.tempreport, "T:") # FIXME : we don't use setpoints here, we should probably exploit them
temps = parse_temperature_report(self.tempreport)
if "T0" in temps:
hotend_temp = float(temps["T0"][0])
else:
hotend_temp = float(temps["T"][0]) if "T" in temps else -1.0
wx.CallAfter(self.graph.SetExtruder0Temperature, hotend_temp) wx.CallAfter(self.graph.SetExtruder0Temperature, hotend_temp)
if self.display_gauges: wx.CallAfter(self.hottgauge.SetValue, hotend_temp) if self.display_gauges: wx.CallAfter(self.hottgauge.SetValue, hotend_temp)
bed_temp = parse_temperature_report(self.tempreport, "B:") if "T1" in temps:
hotend_temp = float(temps["T1"][0])
wx.CallAfter(self.graph.SetExtruder1Temperature, hotend_temp)
bed_temp = float(temps["B"][0]) if "B" in temps else -1.0
wx.CallAfter(self.graph.SetBedTemperature, bed_temp) wx.CallAfter(self.graph.SetBedTemperature, bed_temp)
if self.display_gauges: wx.CallAfter(self.bedtgauge.SetValue, bed_temp) if self.display_gauges: wx.CallAfter(self.bedtgauge.SetValue, bed_temp)
except: except:
...@@ -1181,14 +1188,13 @@ class PronterWindow(MainWindow, pronsole.pronsole): ...@@ -1181,14 +1188,13 @@ class PronterWindow(MainWindow, pronsole.pronsole):
string += _(" Line# %d of %d lines |" ) % (self.p.queueindex, len(self.p.mainqueue)) string += _(" Line# %d of %d lines |" ) % (self.p.queueindex, len(self.p.mainqueue))
if self.p.queueindex > 0: if self.p.queueindex > 0:
secondselapsed = int(time.time() - self.starttime + self.extra_print_time) secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
secondsremain = self.compute_eta(self.p.queueindex) secondsremain, secondsestimate = self.compute_eta(self.p.queueindex, secondselapsed)
secondsestimate = secondselapsed + secondsremain
string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain), string += _(" Est: %s of %s remaining | ") % (format_duration(secondsremain),
format_duration(secondsestimate)) format_duration(secondsestimate))
string += _(" Z: %.3f mm") % self.curlayer string += _(" Z: %.3f mm") % self.curlayer
wx.CallAfter(self.status.SetStatusText, string) wx.CallAfter(self.status.SetStatusText, string)
wx.CallAfter(self.gviz.Refresh) wx.CallAfter(self.gviz.Refresh)
if(self.monitor and self.p.online): if self.monitor and self.p.online:
if self.sdprinting: if self.sdprinting:
self.p.send_now("M27") self.p.send_now("M27")
if not hasattr(self, "auto_monitor_pattern"): if not hasattr(self, "auto_monitor_pattern"):
......
...@@ -145,7 +145,7 @@ setup ( ...@@ -145,7 +145,7 @@ setup (
license = "GPLv3", license = "GPLv3",
data_files = data_files, data_files = data_files,
packages = ["printrun", "printrun.svg"], packages = ["printrun", "printrun.svg"],
scripts = ["pronsole.py", "pronterface.py", "plater.py", "printcore.py"], scripts = ["pronsole.py", "pronterface.py", "plater.py", "printcore.py", "pronserve.py"],
cmdclass = {"uninstall" : uninstall, cmdclass = {"uninstall" : uninstall,
"install" : install, "install" : install,
"install_data" : install_data} "install_data" : install_data}
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" onload='init()' width="150mm" height="150mm" version="1.1">
<script type='text/ecmascript'>
<![CDATA[
function getParameterByName(name)
{
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(window.location.search);
if(results == null)
return "";
else
return decodeURIComponent(results[1].replace(/\+/g, " "));
}
function init() {
var width = getParameterByName('width') || 150;
var height = getParameterByName('height') || 150;
var gridheight = getParameterByName('gridheight') || 10;
var gridwidth = getParameterByName('gridwidth') || 10;
var lineheight = getParameterByName('lineheight') || 0.5;
var linewidth = getParameterByName('linewidth') || 0.5;
var lineblockheight = (gridheight/2)-lineheight;
var lineblockwidth = (gridwidth/2)-linewidth;
console.log(lineblockheight,lineblockwidth)
document.getElementsByTagName('svg')[0].setAttribute('width',width+'mm');
document.getElementsByTagName('svg')[0].setAttribute('height',height+'mm');
document.getElementsByTagName('pattern')[0].setAttribute('height',gridheight+'mm');
document.getElementsByTagName('pattern')[0].setAttribute('width',gridwidth+'mm');
document.getElementsByClassName('background')[0].setAttribute('height',gridheight+'mm');
document.getElementsByClassName('background')[0].setAttribute('width',gridwidth+'mm');
var blocks = document.getElementsByClassName('block');
for (var i in blocks){
if (blocks[i] instanceof SVGRectElement){
blocks[i].setAttribute('height', lineblockheight+'mm');
blocks[i].setAttribute('width', lineblockwidth+'mm');
}
}
document.getElementsByClassName('topright')[0].setAttribute('x',lineblockwidth+'mm');
document.getElementsByClassName('bottomleft')[0].setAttribute('y',lineblockheight+'mm');
document.getElementsByClassName('bottomright')[0].setAttribute('x',lineblockwidth+'mm');
document.getElementsByClassName('bottomright')[0].setAttribute('y',lineblockheight+'mm');
}
]]>
</script>
<defs>
<pattern id="grd" patternUnits="userSpaceOnUse" width="10mm" height="10mm">
<rect class="background" width="10mm" height="10mm" fill="red"/>
<rect class="block topleft" width="4.9mm" height="4.9mm"/>
<rect class="block topright" width="4.9mm" height="4.9mm" x="4.9mm"/>
<rect class="block bottomleft" width="4.9mm" height="4.9mm" y="4.9mm"/>
<rect class="block bottomright" width="4.9mm" height="4.9mm" x="4.9mm" y="4.9mm"/>
</pattern>
</defs>
<rect height="100%" width="100%" fill="url(#grd)"/>
</svg>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment