diff --git a/app.py b/app.py index 342e8203bfb4f9bd2f8a2a189ae967d3b3730955..3a1f049c0109313ce467022701e5e0861060c5be 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,11 @@ -from flask import Flask, request, render_template +from flask import Flask, Response, request, render_template import subprocess import os import re import sys +import tempfile import io +from svg_to_hpgl import svgToHPGL app = Flask(__name__) @@ -158,5 +160,30 @@ def catalogue(): output = output, params = params) +def make_svg (): + return '' + +@app.route('/hpgl/') +def hpgl (): + # generate svg + svg = make_svg() + # store as a temporary file + (svg_file, svg_path) = tempfile.mkstemp() + svg_file.write(svg) + + # transform to hpgl + hpgl = svgToHPGL(svg_path) + + # remove tmp file + os.remove(svg_path) + + r = Response(hpgl, mimetype='application/hpgl') + + r.headers.extend({ + 'Content-Disposition': 'attachment; filename="cobbled-paths.hpgl"' + }) + + return r + if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') \ No newline at end of file diff --git a/hpgl_multipen_encoder.py b/hpgl_multipen_encoder.py new file mode 100644 index 0000000000000000000000000000000000000000..c6373f3b00bb903acc2d68b0b712457df05d30dd --- /dev/null +++ b/hpgl_multipen_encoder.py @@ -0,0 +1,395 @@ +# coding=utf-8 +''' +Copyright (C) 2008 Aaron Spike, aaron@ekips.org +Copyright (C) 2013 Sebastian Wüst, sebi@timewaster.de + +This program 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 2 of the License, or +(at your option) any later version. + +This program 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 this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +# standard libraries +import math +import re +import sys +sys.path.append('/usr/share/inkscape/extensions') +# local libraries +import bezmisc +import cspsubdiv +import cubicsuperpath +import inkex +import simplestyle +import simpletransform + + +class hpglMultipenEncoder: + PI = math.pi + TWO_PI = PI * 2 + + def __init__(self, effect): + ''' options: + "resolutionX":float + "resolutionY":float + "pen":int + "force:int + "speed:int + "orientation":string // "0", "90", "-90", "180" + "mirrorX":bool + "mirrorY":bool + "center":bool + "flat":float + "overcut":float + "toolOffset":float + "precut":bool + "autoAlign":bool + "debug":bool + ''' + self.options = effect.options + self.doc = effect.document.getroot() + self.docWidth = effect.unittouu(self.doc.get('width')) + self.docHeight = effect.unittouu(self.doc.get('height')) + self.hpgl = '' + self.divergenceX = 'False' + self.divergenceY = 'False' + self.sizeX = 'False' + self.sizeY = 'False' + self.dryRun = True + self.lastPoint = [0, 0, 0] + self.lastPen = -1 + self.offsetX = 0 + self.offsetY = 0 + self.penIndex = {} + self.penCount = self.options.penCount + self.scaleX = self.options.resolutionX / effect.unittouu("1.0in") # dots per inch to dots per user unit + self.scaleY = self.options.resolutionY / effect.unittouu("1.0in") # dots per inch to dots per user unit + scaleXY = (self.scaleX + self.scaleY) / 2 + self.overcut = effect.unittouu(str(self.options.overcut) + "mm") * scaleXY # mm to dots (plotter coordinate system) + self.toolOffset = effect.unittouu(str(self.options.toolOffset) + "mm") * scaleXY # mm to dots + self.flat = self.options.flat / (1016 / ((self.options.resolutionX + self.options.resolutionY) / 2)) # scale flatness to resolution + if self.toolOffset > 0.0: + self.toolOffsetFlat = self.flat / self.toolOffset * 4.5 # scale flatness to offset + else: + self.toolOffsetFlat = 0.0 + self.mirrorX = 1.0 + if self.options.mirrorX: + self.mirrorX = -1.0 + self.mirrorY = -1.0 + if self.options.mirrorY: + self.mirrorY = 1.0 + if self.options.debug: + self.debugValues = {} + self.debugValues['docWidth'] = self.docWidth + self.debugValues['docHeight'] = self.docHeight + # process viewBox attribute to correct page scaling + self.viewBoxTransformX = 1 + self.viewBoxTransformY = 1 + if self.options.debug: + self.debugValues['viewBoxWidth'] = "-" + self.debugValues['viewBoxHeight'] = "-" + viewBox = self.doc.get('viewBox') + if viewBox: + viewBox2 = viewBox.split(',') + if len(viewBox2) < 4: + viewBox2 = viewBox.split(' ') + if self.options.debug: + self.debugValues['viewBoxWidth'] = viewBox2[2] + self.debugValues['viewBoxHeight'] = viewBox2[3] + self.viewBoxTransformX = self.docWidth / effect.unittouu(effect.addDocumentUnit(viewBox2[2])) + self.viewBoxTransformY = self.docHeight / effect.unittouu(effect.addDocumentUnit(viewBox2[3])) + + def getHpgl(self): + # dryRun to find edges + groupmat = [[self.mirrorX * self.scaleX * self.viewBoxTransformX, 0.0, 0.0], [0.0, self.mirrorY * self.scaleY * self.viewBoxTransformY, 0.0]] + groupmat = simpletransform.composeTransform(groupmat, simpletransform.parseTransform('rotate(' + self.options.orientation + ')')) + self.vData = [['', 'False', 0, 0], ['', 'False', 0, 0], ['', 'False', 0, 0], ['', 'False', 0, 0]] + self.processGroups(self.doc, groupmat) + if self.divergenceX == 'False' or self.divergenceY == 'False' or self.sizeX == 'False' or self.sizeY == 'False': + raise Exception('NO_PATHS') + # live run + self.dryRun = False + if self.options.debug: + self.debugValues['drawingWidth'] = self.sizeX - self.divergenceX + self.debugValues['drawingHeight'] = self.sizeY - self.divergenceY + self.debugValues['drawingWidthUU'] = self.debugValues['drawingWidth'] / self.scaleX + self.debugValues['drawingHeightUU'] = self.debugValues['drawingHeight'] / self.scaleY + # move drawing according to various modifiers + if self.options.autoAlign: + if self.options.center: + self.offsetX -= (self.sizeX - self.divergenceX) / 2 + self.offsetY -= (self.sizeY - self.divergenceY) / 2 + else: + self.divergenceX = 0.0 + self.divergenceY = 0.0 + if self.options.center: + if self.options.orientation == '0': + self.offsetX -= (self.docWidth * self.scaleX) / 2 + self.offsetY += (self.docHeight * self.scaleY) / 2 + if self.options.orientation == '90': + self.offsetY += (self.docWidth * self.scaleX) / 2 + self.offsetX += (self.docHeight * self.scaleY) / 2 + if self.options.orientation == '180': + self.offsetX += (self.docWidth * self.scaleX) / 2 + self.offsetY -= (self.docHeight * self.scaleY) / 2 + if self.options.orientation == '270': + self.offsetY -= (self.docWidth * self.scaleX) / 2 + self.offsetX -= (self.docHeight * self.scaleY) / 2 + else: + if self.options.orientation == '0': + self.offsetY += self.docHeight * self.scaleY + if self.options.orientation == '90': + self.offsetY += self.docWidth * self.scaleX + self.offsetX += self.docHeight * self.scaleY + if self.options.orientation == '180': + self.offsetX += self.docWidth * self.scaleX + if not self.options.center and self.toolOffset > 0.0: + self.offsetX += self.toolOffset + self.offsetY += self.toolOffset + # initialize transformation matrix and cache + groupmat = [[self.mirrorX * self.scaleX * self.viewBoxTransformX, 0.0, -self.divergenceX + self.offsetX], + [0.0, self.mirrorY * self.scaleY * self.viewBoxTransformY, -self.divergenceY + self.offsetY]] + groupmat = simpletransform.composeTransform(groupmat, simpletransform.parseTransform('rotate(' + self.options.orientation + ')')) + self.vData = [['', 'False', 0, 0], ['', 'False', 0, 0], ['', 'False', 0, 0], ['', 'False', 0, 0]] + # add move to zero point and precut + if self.toolOffset > 0.0 and self.options.precut: + if self.options.center: + # position precut outside of drawing plus one time the tooloffset + if self.offsetX >= 0.0: + precutX = self.offsetX + self.toolOffset + else: + precutX = self.offsetX - self.toolOffset + if self.offsetY >= 0.0: + precutY = self.offsetY + self.toolOffset + else: + precutY = self.offsetY - self.toolOffset + self.processOffset('PU', precutX, precutY, self.options.pen) + self.processOffset('PD', precutX, precutY + self.toolOffset * 8, self.options.pen) + else: + self.processOffset('PU', 0, 0, self.options.pen) + self.processOffset('PD', 0, self.toolOffset * 8, self.options.pen) + # start conversion + self.processGroups(self.doc, groupmat) + # shift an empty node in in order to process last node in cache + if self.toolOffset > 0.0 and not self.dryRun: + self.processOffset('PU', 0, 0, 0) + if self.options.debug: + return self.hpgl, self + else: + return self.hpgl, "" + + def processGroups(self, doc, groupmat): + # flatten layers and groups to avoid recursion + paths = [] + for node in doc: + if (node.tag == inkex.addNS('g', 'svg') and self.isGroupVisible(node)) or node.tag == inkex.addNS('path', 'svg'): + paths.append([node.tag, node, self.mergeTransform(node, groupmat), self.getPenNumber(node)]) + doc = '' + hasGroups = True + while hasGroups: + hasGroups = False + for i, elm in enumerate(paths): + if paths[i][0] == inkex.addNS('g', 'svg') and self.isGroupVisible(paths[i][1]): + hasGroups = True + for path in paths[i][1]: + if (path.tag == inkex.addNS('g', 'svg') and self.isGroupVisible(path)) or path.tag == inkex.addNS('path', 'svg'): + paths.insert(i + 1, [path.tag, path, self.mergeTransform(path, paths[i][2]), paths[i][3]]) + paths[i][0] = '' + for node in paths: + if node[0] == inkex.addNS('path', 'svg'): + self.processPath(node[1], node[2], node[3]) + + def getPenNumber(self, doc): + penNum = str(doc.get('{' + inkex.NSS['inkscape'] + '}label')).lower().strip(' \t\n\r') + if re.search(r'( |\A)pen *\d+( |\Z)', penNum): + penNum = re.sub(r'(.* |\A)pen *(\d+)( .*|\Z)', r'\2', penNum, 1) + return int(penNum) + else: + style = doc.get('style') + style = simplestyle.parseStyle(style) + if 'stroke' in style and style['stroke'] != 'none': + color = style['stroke'] + elif 'fill' in style and style['fill'] != 'none': + color = style['fill'] + else: + return None + + if not self.penIndex.has_key(color): + penNum = (len(self.penIndex) + 1) % self.penCount + self.penIndex[color] = self.penCount if penNum == 0 else penNum + + return self.penIndex[color] + # return self.options.pen + + + def mergeTransform(self, doc, matrix): + # get and merge two matrixes into one + trans = doc.get('transform') + if trans: + return simpletransform.composeTransform(matrix, simpletransform.parseTransform(trans)) + else: + return matrix + + def isGroupVisible(self, group): + style = group.get('style') + if style: + style = simplestyle.parseStyle(style) + if 'display' in style and style['display'] == 'none': + return False + return True + + def processPath(self, node, mat, pen): + # process path + path = node.get('d') + if path: + # parse and transform path + path = cubicsuperpath.parsePath(path) + simpletransform.applyTransformToPath(mat, path) + cspsubdiv.cspsubdiv(path, self.flat) + # path to HPGL commands + oldPosX = 0.0 + oldPosY = 0.0 + for singlePath in path: + cmd = 'PU' + for singlePathPoint in singlePath: + posX, posY = singlePathPoint[1] + # check if point is repeating, if so, ignore + if int(round(posX)) != int(round(oldPosX)) or int(round(posY)) != int(round(oldPosY)): + self.processOffset(cmd, posX, posY, pen) + cmd = 'PD' + oldPosX = posX + oldPosY = posY + # perform overcut + if self.overcut > 0.0 and not self.dryRun: + # check if last and first points are the same, otherwise the path is not closed and no overcut can be performed + if int(round(oldPosX)) == int(round(singlePath[0][1][0])) and int(round(oldPosY)) == int(round(singlePath[0][1][1])): + overcutLength = 0 + for singlePathPoint in singlePath: + posX, posY = singlePathPoint[1] + # check if point is repeating, if so, ignore + if int(round(posX)) != int(round(oldPosX)) or int(round(posY)) != int(round(oldPosY)): + overcutLength += self.getLength(oldPosX, oldPosY, posX, posY) + if overcutLength >= self.overcut: + newLength = self.changeLength(oldPosX, oldPosY, posX, posY, - (overcutLength - self.overcut)) + self.processOffset(cmd, newLength[0], newLength[1], pen) + break + else: + self.processOffset(cmd, posX, posY, pen) + oldPosX = posX + oldPosY = posY + + def getLength(self, x1, y1, x2, y2, absolute=True): + # calc absoulute or relative length between two points + length = math.sqrt((x2 - x1) ** 2.0 + (y2 - y1) ** 2.0) + if absolute: + length = math.fabs(length) + return length + + def changeLength(self, x1, y1, x2, y2, offset): + # change length of line + if offset < 0: + offset = max( - self.getLength(x1, y1, x2, y2), offset) + x = x2 + (x2 - x1) / self.getLength(x1, y1, x2, y2, False) * offset + y = y2 + (y2 - y1) / self.getLength(x1, y1, x2, y2, False) * offset + return [x, y] + + def processOffset(self, cmd, posX, posY, pen): + # calculate offset correction (or dont) + if self.toolOffset == 0.0 or self.dryRun: + self.storePoint(cmd, posX, posY, pen) + else: + # insert data into cache + self.vData.pop(0) + self.vData.insert(3, [cmd, posX, posY, pen]) + # decide if enough data is availabe + if self.vData[2][1] != 'False': + if self.vData[1][1] == 'False': + self.storePoint(self.vData[2][0], self.vData[2][1], self.vData[2][2], self.vData[2][3]) + else: + # perform tool offset correction (It's a *tad* complicated, if you want to understand it draw the data as lines on paper) + if self.vData[2][0] == 'PD': # If the 3rd entry in the cache is a pen down command make the line longer by the tool offset + pointThree = self.changeLength(self.vData[1][1], self.vData[1][2], self.vData[2][1], self.vData[2][2], self.toolOffset) + self.storePoint('PD', pointThree[0], pointThree[1], self.vData[2][3]) + elif self.vData[0][1] != 'False': + # Elif the 1st entry in the cache is filled with data and the 3rd entry is a pen up command shift + # the 3rd entry by the current tool offset position according to the 2nd command + pointThree = self.changeLength(self.vData[0][1], self.vData[0][2], self.vData[1][1], self.vData[1][2], self.toolOffset) + pointThree[0] = self.vData[2][1] - (self.vData[1][1] - pointThree[0]) + pointThree[1] = self.vData[2][2] - (self.vData[1][2] - pointThree[1]) + self.storePoint('PU', pointThree[0], pointThree[1], self.vData[2][3]) + else: + # Else just write the 3rd entry + pointThree = [self.vData[2][1], self.vData[2][2]] + self.storePoint('PU', pointThree[0], pointThree[1], self.vData[2][3]) + if self.vData[3][0] == 'PD': + # If the 4th entry in the cache is a pen down command guide tool to next line with a circle between the prolonged 3rd and 4th entry + if self.getLength(self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2]) >= self.toolOffset: + pointFour = self.changeLength(self.vData[3][1], self.vData[3][2], self.vData[2][1], self.vData[2][2], - self.toolOffset) + else: + pointFour = self.changeLength(self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2], + (self.toolOffset - self.getLength(self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2]))) + # get angle start and angle vector + angleStart = math.atan2(pointThree[1] - self.vData[2][2], pointThree[0] - self.vData[2][1]) + angleVector = math.atan2(pointFour[1] - self.vData[2][2], pointFour[0] - self.vData[2][1]) - angleStart + # switch direction when arc is bigger than 180° + if angleVector > self.PI: + angleVector -= self.TWO_PI + elif angleVector < - self.PI: + angleVector += self.TWO_PI + # draw arc + if angleVector >= 0: + angle = angleStart + self.toolOffsetFlat + while angle < angleStart + angleVector: + self.storePoint('PD', self.vData[2][1] + math.cos(angle) * self.toolOffset, self.vData[2][2] + math.sin(angle) * self.toolOffset, self.vData[2][3]) + angle += self.toolOffsetFlat + else: + angle = angleStart - self.toolOffsetFlat + while angle > angleStart + angleVector: + self.storePoint('PD', self.vData[2][1] + math.cos(angle) * self.toolOffset, self.vData[2][2] + math.sin(angle) * self.toolOffset, self.vData[2][3]) + angle -= self.toolOffsetFlat + self.storePoint('PD', pointFour[0], pointFour[1], self.vData[3][3]) + + def storePoint(self, command, x, y, pen): + x = int(round(x)) + y = int(round(y)) + # skip when no change in movement + if self.lastPoint[0] == command and self.lastPoint[1] == x and self.lastPoint[2] == y: + return + if self.dryRun: + # find edges + if self.divergenceX == 'False' or x < self.divergenceX: + self.divergenceX = x + if self.divergenceY == 'False' or y < self.divergenceY: + self.divergenceY = y + if self.sizeX == 'False' or x > self.sizeX: + self.sizeX = x + if self.sizeY == 'False' or y > self.sizeY: + self.sizeY = y + else: + # store point + if not self.options.center: + # only positive values are allowed (usually) + if x < 0: + x = 0 + if y < 0: + y = 0 + # select correct pen + if self.lastPen != pen: + self.hpgl += ';SP%d' % pen + # do not repeat command + if command == 'PD' and self.lastPoint[0] == 'PD' and self.lastPen == pen: + self.hpgl += ',%d,%d' % (x, y) + else: + self.hpgl += ';%s%d,%d' % (command, x, y) + self.lastPen = pen + self.lastPoint = [command, x, y] + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/hpgl_multipen_output.py b/hpgl_multipen_output.py new file mode 100644 index 0000000000000000000000000000000000000000..7ad5db2c37c1779fe30a535999b8dc92cc63c8b4 --- /dev/null +++ b/hpgl_multipen_output.py @@ -0,0 +1,82 @@ +#! /usr/bin/python2 +# coding=utf-8 +''' +Copyright (C) 2013 Sebastian Wüst, sebi@timewaster.de + +This program 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 2 of the License, or +(at your option) any later version. + +This program 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 this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +''' + +# standard library +import sys +# local libraries +import hpgl_multipen_encoder +sys.path.append('/usr/share/inkscape/extensions') +import inkex + +class HpglMultipenOutput(inkex.Effect): + + def __init__(self): + inkex.Effect.__init__(self) + self.OptionParser.add_option('--tab', action='store', type='string', dest='tab') + self.OptionParser.add_option('--resolutionX', action='store', type='float', dest='resolutionX', default=1016.0, help='Resolution X (dpi)') + self.OptionParser.add_option('--resolutionY', action='store', type='float', dest='resolutionY', default=1016.0, help='Resolution Y (dpi)') + self.OptionParser.add_option('--pen', action='store', type='int', dest='pen', default=1, help='Pen number') + self.OptionParser.add_option('--penCount', action='store', type='int', dest='penCount', default=1, help='Amount of pens to choose from') + self.OptionParser.add_option('--force', action='store', type='int', dest='force', default=24, help='Pen force (g)') + self.OptionParser.add_option('--speed', action='store', type='int', dest='speed', default=20, help='Pen speed (cm/s)') + self.OptionParser.add_option('--orientation', action='store', type='string', dest='orientation', default='90', help='Rotation (Clockwise)') + self.OptionParser.add_option('--mirrorX', action='store', type='inkbool', dest='mirrorX', default='FALSE', help='Mirror X axis') + self.OptionParser.add_option('--mirrorY', action='store', type='inkbool', dest='mirrorY', default='FALSE', help='Mirror Y axis') + self.OptionParser.add_option('--center', action='store', type='inkbool', dest='center', default='FALSE', help='Center zero point') + self.OptionParser.add_option('--overcut', action='store', type='float', dest='overcut', default=1.0, help='Overcut (mm)') + self.OptionParser.add_option('--toolOffset', action='store', type='float', dest='toolOffset', default=0.25, help='Tool (Knife) offset correction (mm)') + self.OptionParser.add_option('--precut', action='store', type='inkbool', dest='precut', default='TRUE', help='Use precut') + self.OptionParser.add_option('--flat', action='store', type='float', dest='flat', default=1.2, help='Curve flatness') + self.OptionParser.add_option('--autoAlign', action='store', type='inkbool', dest='autoAlign', default='TRUE', help='Auto align') + + def effect(self): + self.options.debug = False + # get hpgl data + myHpglEncoder = hpgl_multipen_encoder.hpglMultipenEncoder(self) + try: + self.hpgl, debugObject = myHpglEncoder.getHpgl() + except Exception as inst: + if inst.args[0] == 'NO_PATHS': + # issue error if no paths found + inkex.errormsg(_("No paths where found. Please convert all objects you want to save into paths.")) + self.hpgl = '' + return + else: + type, value, traceback = sys.exc_info() + raise ValueError("", type, value).with_traceback(traceback) + # convert raw HPGL to HPGL + hpglInit = 'IN' + if self.options.force > 0: + hpglInit += ';FS%d' % self.options.force + if self.options.speed > 0: + hpglInit += ';VS%d' % self.options.speed + self.hpgl = hpglInit + self.hpgl + ';SP0;PU0,0;IN; ' + + def output(self): + # print to file + if self.hpgl != '': + print(self.hpgl) + +if __name__ == '__main__': + # start extension + e = HpglOutput() + e.affect() + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2077213c37cc027f3b2ee884c940c2f41d7adea7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask \ No newline at end of file diff --git a/svg_to_hpgl.py b/svg_to_hpgl.py new file mode 100644 index 0000000000000000000000000000000000000000..f025451bc52789f0c072ee3cb8c2d704cc6e2885 --- /dev/null +++ b/svg_to_hpgl.py @@ -0,0 +1,19 @@ +from hpgl_multipen_output import HpglMultipenOutput +from sys import argv + +def svgToHPGL (path, speed=1, penCount=8, force=2): + e = HpglMultipenOutput() + e.affect([ + '--orientation', '0', + '--force', '0', + '--overcut', '0', + '--precut', 'false', + '--flat', '4', + '--toolOffset', '0', + '--autoAlign', 'false', + '--speed', str(speed), + '--penCount', str(penCount), + '--force', str(force), + path], False) + + return e.hpgl \ No newline at end of file