added back some more extensions

This commit is contained in:
Mario Voigt 2022-10-10 03:43:34 +02:00
parent bc2301079d
commit 3277fba958
42 changed files with 6624 additions and 0 deletions

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Box Maker - Elliptical Box</name>
<id>fablabchemnitz.de.box_maker_elliptical_box</id>
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
<option value="pc">pc</option>
</param>
<param name="thickness" type="float" min="1.0" max="100.0" gui-text="Material thickness">3.0</param>
<param name="height" type="float" min="0.0" max="10000.0" gui-text="Height">30.0</param>
<param name="width" type="float" min="0.0" max="10000.0" gui-text="Width">50.0</param>
<param name="depth" type="float" min="0.0" max="10000.0" gui-text="Depth">40.0</param>
<param name="cut_dist" type="float" min="0.1" max="100.0" gui-text="Cut distance">1.5</param>
<param name="auto_cut_dist" type="bool" gui-text="Automatically vary cut distance according to curvature">false</param>
<param name="cut_nr" type="int" min="1" max="100" gui-text="Number of cuts">4</param>
<param name="lid_angle" type="float" min="0.0" max="360.0" gui-text="Lid angle">90</param>
<param name="invert_lid_notches" type="bool" gui-text="Invert lid notch pattern (this will create a lid without sideways support)">false</param>
<param name="central_rib_lid" type="bool" gui-text="Create central rib in the lid(requires an even number of cuts)">false</param>
<param name="central_rib_body" type="bool" gui-text="Create central rib in the body(requires an even number of cuts)">false</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">box_maker_elliptical_box.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,278 @@
#!/usr/bin/env python3
from inkscape_helper.Coordinate import Coordinate
import inkscape_helper.Effect as eff
import inkscape_helper.SVG as svg
from inkscape_helper.Ellipse import Ellipse
from inkscape_helper.Line import Line
from inkscape_helper.EllipticArc import EllipticArc
from math import *
import inkex
#Note: keep in mind that SVG coordinates start in the top-left corner i.e. with an inverted y-axis
# first define some SVG primitives
greenStyle = svg.green_style
def _makeCurvedSurface(topLeft, w, h, cutSpacing, hCutCount, thickness, parent, invertNotches = False, centralRib = False):
width = Coordinate(w, 0)
height = Coordinate(0, h)
wCutCount = int(floor(w / cutSpacing))
if wCutCount % 2 == 0:
wCutCount += 1 # make sure we have an odd number of cuts
xCutDist = w / wCutCount
xSpacing = Coordinate(xCutDist, 0)
ySpacing = Coordinate(0, cutSpacing)
cut = height / hCutCount - ySpacing
plateThickness = Coordinate(0, thickness)
notchEdges = []
topHCuts = []
bottomHCuts = []
p = svg.Path()
for cutIndex in range(wCutCount):
if (cutIndex % 2 == 1) != invertNotches: # make a notch here
inset = plateThickness
else:
inset = Coordinate(0, 0)
# A-column of cuts
aColStart = topLeft + xSpacing * cutIndex
notchEdges.append((aColStart - topLeft).x)
if cutIndex > 0: # no cuts at x == 0
p.move_to(aColStart, True)
p.line_to(cut / 2)
for j in range(hCutCount - 1):
pos = aColStart + cut / 2 + ySpacing + (cut + ySpacing) * j
p.move_to(pos, True)
p.line_to(cut)
p.move_to(aColStart + height - cut / 2, True)
p.line_to(cut / 2)
# B-column of cuts, offset by half the cut length; these cuts run in the opposite direction
bColStart = topLeft + xSpacing * cutIndex + xSpacing / 2
for j in reversed(range(hCutCount)):
end = bColStart + ySpacing / 2 + (cut + ySpacing) * j
start = end + cut
if centralRib and hCutCount % 2 == 0 and cutIndex % 2 == 1:
holeTopLeft = start + (ySpacing - plateThickness - xSpacing) / 2
if j == hCutCount // 2 - 1:
start -= plateThickness / 2
p.move_to(holeTopLeft + plateThickness + xSpacing, True)
p.line_to(-xSpacing)
p.move_to(holeTopLeft, True)
p.line_to(xSpacing)
elif j == hCutCount // 2:
end += plateThickness / 2
if j == 0: # first row
end += inset
elif j == hCutCount - 1: # last row
start -= inset
p.move_to(start, True)
p.line_to(end, True)
#horizontal cuts (should be done last)
topHCuts.append((aColStart + inset, aColStart + inset + xSpacing))
bottomHCuts.append((aColStart + height - inset, aColStart + height - inset + xSpacing))
# draw the outline
for c in reversed(bottomHCuts):
p.move_to(c[1], True)
p.line_to(c[0], True)
p.move_to(topLeft + height, True)
p.line_to(-height)
for c in topHCuts:
p.move_to(c[0], True)
p.line_to(c[1], True)
p.move_to(topLeft + width, True)
p.line_to(height)
group = svg.group(parent)
p.path(group)
notchEdges.append(w)
return notchEdges
def _makeNotchedEllipse(center, ellipse, start_theta, thickness, notches, parent, invertNotches):
start_theta += pi # rotate 180 degrees to put the lid on the topside
ell_radius = Coordinate(ellipse.x_radius, ellipse.y_radius)
ell_radius_t = ell_radius + Coordinate(thickness, thickness)
theta = ellipse.theta_from_dist(start_theta, notches[0])
ell_point = center + ellipse.coordinate_at_theta(theta)
prev_offset = ellipse.tangent(theta) * thickness
p = svg.Path()
p.move_to(ell_point, absolute=True)
for n in range(len(notches) - 1):
theta = ellipse.theta_from_dist(start_theta, notches[n + 1])
ell_point = center + ellipse.coordinate_at_theta(theta)
notch_offset = ellipse.tangent(theta) * thickness
notch_point = ell_point + notch_offset
if (n % 2 == 0) != invertNotches:
p.arc_to(ell_radius, ell_point, absolute=True)
prev_offset = notch_offset
else:
p.line_to(prev_offset)
p.arc_to(ell_radius_t, notch_point, absolute=True)
p.line_to(-notch_offset)
p.path(parent)
class EllipticalBox(eff.Effect):
"""
Creates a new layer with the drawings for a parametrically generaded box.
"""
def __init__(self):
options = [
['unit', str, 'mm', 'Unit, one of: cm, mm, in, ft, ...'],
['thickness', float, '3.0', 'Material thickness'],
['width', float, '100', 'Box width'],
['height', float, '100', 'Box height'],
['depth', float, '100', 'Box depth'],
['cut_dist', float, '1.5', 'Distance between cuts on the wrap around. Note that this value will change slightly to evenly fill up the available space.'],
['auto_cut_dist', inkex.Boolean, 'false', 'Automatically set the cut distance based on the curvature.'], # in progress
['cut_nr', int, '3', 'Number of cuts across the depth of the box.'],
['lid_angle', float, '120', 'Angle that forms the lid (in degrees, measured from centerpoint of the ellipse)'],
['body_ribcount', int, '0', 'Number of ribs in the body'],
['lid_ribcount', int, '0', 'Number of ribs in the lid'],
['invert_lid_notches', inkex.Boolean, 'false', 'Invert the notch pattern on the lid (keeps the lid from sliding sideways)'],
['central_rib_lid', inkex.Boolean, 'false', 'Create a central rib in the lid'],
['central_rib_body', inkex.Boolean, 'false', 'Create a central rib in the body']
]
eff.Effect.__init__(self, options)
def effect(self):
"""
Draws as basic elliptical box, based on provided parameters
"""
# input sanity check
error = False
if min(self.options.height, self.options.width, self.options.depth) == 0:
eff.errormsg('Error: Dimensions must be non zero')
error = True
if self.options.cut_nr < 1:
eff.errormsg('Error: Number of cuts should be at least 1')
error = True
if (self.options.central_rib_lid or self.options.central_rib_body) and self.options.cut_nr % 2 == 1:
eff.errormsg('Error: Central rib is only valid with an even number of cuts')
error = True
if self.options.unit not in self.knownUnits:
eff.errormsg('Error: unknown unit. '+ self.options.unit)
error = True
if error:
exit()
# convert units
unit = self.options.unit
H = self.svg.unittouu(str(self.options.height) + unit)
W = self.svg.unittouu(str(self.options.width) + unit)
D = self.svg.unittouu(str(self.options.depth) + unit)
thickness = self.svg.unittouu(str(self.options.thickness) + unit)
cutSpacing = self.svg.unittouu(str(self.options.cut_dist) + unit)
cutNr = self.options.cut_nr
doc_root = self.document.getroot()
layer = svg.layer(doc_root, 'Elliptical Box')
ell = Ellipse(W/2, H/2)
#body and lid
lidAngleRad = self.options.lid_angle * 2 * pi / 360
lid_start_theta = ell.theta_at_angle(pi / 2 - lidAngleRad / 2)
lid_end_theta = ell.theta_at_angle(pi / 2 + lidAngleRad / 2)
lidLength = ell.dist_from_theta(lid_start_theta, lid_end_theta)
bodyLength = ell.dist_from_theta(lid_end_theta, lid_start_theta)
# do not put elements right at the edge of the page
xMargin = 3
yMargin = 3
bottom_grp = svg.group(layer)
top_grp = svg.group(layer)
bodyNotches = _makeCurvedSurface(Coordinate(xMargin, yMargin), bodyLength, D, cutSpacing, cutNr,
thickness, bottom_grp, False, self.options.central_rib_body)
lidNotches = _makeCurvedSurface(Coordinate(xMargin, D + 2 * yMargin), lidLength, D, cutSpacing, cutNr,
thickness, top_grp, not self.options.invert_lid_notches,
self.options.central_rib_lid)
# create elliptical sides
sidesGrp = svg.group(layer)
elCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + H / 2 + thickness + 3 * yMargin)
# indicate the division between body and lid
p = svg.Path()
if self.options.invert_lid_notches:
p.move_to(elCenter + ell.coordinate_at_theta(lid_start_theta + pi), True)
p.line_to(elCenter, True)
p.line_to(elCenter + ell.coordinate_at_theta(lid_end_theta + pi), True)
else:
angleA = ell.theta_from_dist(lid_start_theta, lidNotches[1])
angleB = ell.theta_from_dist(lid_start_theta, lidNotches[-2])
p.move_to(elCenter + ell.coordinate_at_theta(angleA + pi), True)
p.line_to(elCenter, True)
p.line_to(elCenter + ell.coordinate_at_theta(angleB + pi), True)
_makeNotchedEllipse(elCenter, ell, lid_end_theta, thickness, bodyNotches, sidesGrp, False)
_makeNotchedEllipse(elCenter, ell, lid_start_theta, thickness, lidNotches, sidesGrp,
not self.options.invert_lid_notches)
p.path(sidesGrp, greenStyle)
# ribs
if self.options.central_rib_lid or self.options.central_rib_body:
innerRibCenter = Coordinate(xMargin + thickness + W / 2, 2 * D + 1.5 * (H + 2 *thickness) + 4 * yMargin)
innerRibGrp = svg.group(layer)
outerRibCenter = Coordinate(2 * xMargin + 1.5 * (W + 2 * thickness),
2 * D + 1.5 * (H + 2 * thickness) + 4 * yMargin)
outerRibGrp = svg.group(layer)
if self.options.central_rib_lid:
_makeNotchedEllipse(innerRibCenter, ell, lid_start_theta, thickness, lidNotches, innerRibGrp, False)
_makeNotchedEllipse(outerRibCenter, ell, lid_start_theta, thickness, lidNotches, outerRibGrp, True)
if self.options.central_rib_body:
spacer = Coordinate(0, 10)
_makeNotchedEllipse(innerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, innerRibGrp, False)
_makeNotchedEllipse(outerRibCenter + spacer, ell, lid_end_theta, thickness, bodyNotches, outerRibGrp, True)
if self.options.central_rib_lid or self.options.central_rib_body:
svg.text(sidesGrp, elCenter, 'side (duplicate this)')
svg.text(innerRibGrp, innerRibCenter, 'inside rib')
svg.text(outerRibGrp, outerRibCenter, 'outside rib')
if __name__ == '__main__':
EllipticalBox().run()

View File

@ -0,0 +1,92 @@
from __future__ import division
from PathSegment import *
from math import hypot
class BezierCurve(PathSegment):
nr_points = 10
def __init__(self, P): # number of points is limited to 3 or 4
if len(P) == 3: # quadratic
self.B = lambda t : (1 - t)**2 * P[0] + 2 * (1 - t) * t * P[1] + t**2 * P[2]
self.Bd = lambda t : 2 * (1 - t) * (P[1] - P[0]) + 2 * t * (P[2] - P[1])
self.Bdd = lambda t : 2 * (P[2] - 2 * P[1] + P[0])
elif len(P) == 4: #cubic
self.B = lambda t : (1 - t)**3 * P[0] + 3 * (1 - t)**2 * t * P[1] + 3 * (1 - t) * t**2 * P[2] + t**3 * P[3]
self.Bd = lambda t : 3 * (1 - t)**2 * (P[1] - P[0]) + 6 * (1 - t) * t * (P[2] - P[1]) + 3 * t**2 * (P[3] - P[2])
self.Bdd = lambda t : 6 * (1 - t) * (P[2] - 2 * P[1] + P[0]) + 6 * t * (P[3] - 2 * P[2] + P[1])
self.tangent = lambda t : self.Bd(t)
# self.curvature = lambda t : (Bd(t).x * Bdd(t).y - Bd(t).y * Bdd(t).x) / hypot(Bd(t).x, Bd(t).y)**3
self.distances = [0] # cumulative distances for each 't'
prev_pt = self.B(0)
for i in range(self.nr_points):
t = (i + 1) / self.nr_points
pt = self.B(t)
self.distances.append(self.distances[-1] + hypot(prev_pt.x - pt.x, prev_pt.y - pt.y))
prev_pt = pt
self._length = self.distances[-1]
def curvature(self, t):
n = self.Bd(t).x * self.Bdd(t).y - self.Bd(t).y * self.Bdd(t).x
d = hypot(self.Bd(t).x, self.Bd(t).y)**3
if d == 0:
return n * float('inf')
else:
return n / d
@classmethod
def quadratic(cls, start, c, end):
bezier = cls()
@classmethod
def cubic(cls, start, c1, c2, end):
bezier = cls()
def __make_eq__(self):
pass
@property
def length(self):
return self._length
def subdivide(self, part_length, start_offset=0):
nr_parts = int((self.length - start_offset) // part_length)
k_o = start_offset / self.length
k2t = lambda k : k_o + k * part_length / self.length
points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
return(points, self.length - points[-1].c_dist)
def pathpoint_at_t(self, t):
"""pathpoint on the curve from t=0 to point at t."""
step = 1 / self.nr_points
pt_idx = int(t / step)
length = self.distances[pt_idx]
ip_fact = (t - pt_idx * step) / step
if ip_fact > 0 and t < 1: # not a perfect match, need to interpolate
length += ip_fact * (self.distances[pt_idx + 1] - self.distances[pt_idx])
return PathPoint(t, self.B(t), self.tangent(t), self.curvature(t), length)
def t_at_length(self, length):
"""interpolated t where the curve is at the given length"""
if length == self.length:
return 1
i_small = 0
i_big = self.nr_points + 1
while i_big - i_small > 1: # binary search
i_half = i_small + (i_big - i_small) // 2
if self.distances[i_half] <= length:
i_small = i_half
else:
i_big = i_half
small_dist = self.distances[i_small]
return i_small / self.nr_points + (length - small_dist) * (self.distances[i_big] - small_dist) # interpolated length

View File

@ -0,0 +1,104 @@
from math import *
def inner_product(a, b):
return a.x * b.x + a.y * b.y
class Coordinate(object):
"""
Basic (x, y) coordinate class (or should it be called vector?) which allows some simple operations.
"""
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
#polar coordinates
@property
def t(self):
angle = atan2(self.y, self.x)
if angle < 0:
angle += pi * 2
return angle
@t.setter
def t(self, value):
length = self.r
self.x = cos(value) * length
self.y = sin(value) * length
@property
def r(self):
return hypot(self.x, self.y)
@r.setter
def r(self, value):
angle = self.t
self.x = cos(angle) * value
self.y = sin(angle) * value
def __repr__(self):
return self.__str__()
def __str__(self):
return "(%f, %f)" % (self.x, self.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __add__(self, other):
return Coordinate(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Coordinate(self.x - other.x, self.y - other.y)
def __mul__(self, factor):
return Coordinate(self.x * factor, self.y * factor)
def __neg__(self):
return Coordinate(-self.x, -self.y)
def __rmul__(self, other):
return self * other
def __div__(self, quotient):
return Coordinate(self.x / quotient, self.y / quotient)
def __truediv__(self, quotient):
return self.__div__(quotient)
def dot(self, other):
"""dot product"""
return self.x * other.x + self.y * other.y
def cross_norm(self, other):
""""the norm of the cross product"""
self.x * other.y - self.y * other.x
def close_enough_to(self, other, limit=1E-9):
return (self - other).r < limit
class IntersectionError(ValueError):
"""Raised when two lines do not intersect."""
def on_segment(pt, start, end):
"""Check if pt is between start and end. The three points are presumed to be collinear."""
pt -= start
end -= start
ex, ey = end.x, end.y
px, py = pt.x, pt.y
px *= cmp(ex, 0)
py *= cmp(ey, 0)
return px >= 0 and px <= abs(ex) and py >= 0 and py <= abs(ey)
def intersection (s1, e1, s2, e2, on_segments = True):
D = (s1.x - e1.x) * (s2.y - e2.y) - (s1.y - e1.y) * (s2.x - e2.x)
if D == 0:
raise IntersectionError("Lines from {s1} to {e1} and {s2} to {e2} are parallel")
N1 = s1.x * e1.y - s1.y * e1.x
N2 = s2.x * e2.y - s2.y * e2.x
I = ((s2 - e2) * N1 - (s1 - e1) * N2) / D
if on_segments and not (on_segment(I, s1, e1) and on_segment(I, s2, e2)):
raise IntersectionError("Intersection {0} is not on line segments [{1} -> {2}] [{3} -> {4}]".format(I, s1, e1, s2, e2))
return I

View File

@ -0,0 +1,36 @@
import inkex
errormsg = inkex.errormsg
debug = inkex.debug
class Effect(inkex.Effect):
"""
Provides some extra features to inkex.Effect:
- Allows you to pass a list of options in stead of setting them one by one
- acces to unittouu() that is compatible between Inkscape versions 0.48 and 0.91
"""
def __init__(self, options=None):
inkex.Effect.__init__(self)
self.knownUnits = ['in', 'pt', 'px', 'mm', 'cm', 'm', 'km', 'pc', 'yd', 'ft']
if options != None:
for opt in options:
if len(opt) == 2:
self.arg_parser.add_argument('--' + opt[0], type = opt[1])
else:
self.arg_parser.add_argument('--' + opt[0], type = opt[1],default = opt[2], help = opt[3])
try:
inkex.Effect.unittouu # unitouu has moved since Inkscape 0.91
except AttributeError:
try:
def unittouu(self, unit):
return inkex.unittouu(unit)
except AttributeError:
pass
def effect(self):
"""
"""
pass

View File

@ -0,0 +1,157 @@
from __future__ import division
from math import *
from inkscape_helper.Coordinate import Coordinate
class Ellipse(object):
"""Used as a base class for EllipticArc."""
nr_points = 1024 #used for piecewise linear circumference calculation (ellipse circumference is tricky to calculate)
# approximate circumfere: c = pi * (3 * (a + b) - sqrt(10 * a * b + 3 * (a ** 2 + b ** 2)))
def __init__(self, x_radius, y_radius):
self.y_radius = y_radius
self.x_radius = x_radius
self.distances = [0]
theta = 0
self.angle_step = 2 * pi / self.nr_points
for i in range(self.nr_points):
prev_dist = self.distances[-1]
prev_coord = self.coordinate_at_theta(theta)
theta += self.angle_step
x, y = x_radius * cos(theta), y_radius * sin(theta)
self.distances.append(prev_dist + hypot(prev_coord.x - x, prev_coord.y - y))
@property
def circumference(self):
return self.distances[-1]
def curvature(self, theta):
c = self.coordinate_at_theta(theta)
return (self.x_radius*self.y_radius)/((cos(theta)**2*self.y_radius**2 + sin(theta)**2*self.x_radius**2)**(3/2))
def tangent(self, theta):
angle = self.theta_at_angle(theta)
return Coordinate(cos(angle), sin(angle))
def coordinate_at_theta(self, theta):
"""Coordinate of the point at theta."""
return Coordinate(self.x_radius * cos(theta), self.y_radius * sin(theta))
def dist_from_theta(self, theta_start, theta_end):
"""Distance accross the surface from point at angle theta_end to point at angle theta_end. Measured in positive (CCW) sense."""
#print 'thetas ', theta_start, theta_end # TODO: figure out why are there so many with same start and end?
# make sure thetas are between 0 and 2 * pi
theta_start %= 2 * pi
theta_end %= 2 * pi
i1 = int(theta_start / self.angle_step)
p1 = theta_start % self.angle_step
l1 = self.distances[i1 + 1] - self.distances[i1]
i2 = int(theta_end / self.angle_step)
p2 = theta_end % self.angle_step
l2 = self.distances[i2 + 1] - self.distances[i2]
if theta_start <= theta_end:
len = self.distances[i2] - self.distances[i1] + l2 * p2 - l1 * p1
else:
len = self.circumference + self.distances[i2] - self.distances[i1]
return len
def theta_from_dist(self, theta_start, dist):
"""Returns the angle that you get when starting at theta_start and moving a distance (dist) in CCW direction"""
si = int(theta_start / self.angle_step) % self.nr_points
p = theta_start % self.angle_step
piece_length = self.distances[si + 1] - self.distances[si]
start_dist = self.distances[si] + p * piece_length
end_dist = dist + start_dist
if end_dist > self.circumference: # wrap around zero angle
end_dist -= self.circumference
min_idx = 0
max_idx = self.nr_points
while max_idx - min_idx > 1: # binary search
half_idx = min_idx + (max_idx - min_idx) // 2
if self.distances[half_idx] < end_dist:
min_idx = half_idx
else:
max_idx = half_idx
step_dist = self.distances[max_idx] - self.distances[min_idx]
return (min_idx + (end_dist - self.distances[min_idx]) / step_dist) * self.angle_step
def theta_at_angle(self, angle):
cf = 0
if angle > pi / 2:
cf = pi
if angle > 3 * pi / 2:
cf = 2 * pi
return atan(self.x_radius/self.y_radius * tan(angle)) + cf
def skewTransform(self, l, a2, b2):
x0 = a2**2
x1 = b2**2
x2 = l**2
x3 = x0*x2
x4 = x0 - x1 + x3
x5 = 2*a2*b2
x6 = x0 + x1 + x3
x7 = sqrt((-x5 + x6)*(x5 + x6))
x9 = 1/(x4 - x7)
x10 = x6 - x7
x11 = l*x10
x12 = b2**4
x13 = 4*x12
x14 = x10**2
x15 = 4*x1
x16 = sqrt(-x10*x15 + x13 + x14*x2 + x14)
x17 = 2*atan(x9*(x11 - x16))
x18 = sqrt(2)
x19 = sqrt(x10)
x20 = b2*x18*x19/2
x21 = x0/2
x22 = x1/2
x23 = x2*x21
x24 = x21 - x22 + x23
x25 = x7/2
x27 = 1/(x24 - x25)
x28 = x21 + x22 + x23
x29 = x28 - x25
x30 = l*x29
x31 = x14/4
x32 = 2*x1
x33 = sqrt(x12 + x2*x31 - x29*x32 + x31)
x34 = 2*atan(x27*(x30 - x33))
x35 = x20*sqrt(1/(-x1*cos(x34)**2 + x29))*sin(x34)
x36 = x18/2
x37 = -x19*x36
x39 = 2*atan(x9*(x11 + x16))
x40 = 2*atan(x27*(x30 + x33))
x41 = x20*sqrt(1/(-x1*cos(x40)**2 + x29))*sin(x40)
x42 = 1/(x4 + x7)
x43 = x6 + x7
x44 = l*x43
x45 = x43**2
x46 = sqrt(x13 - x15*x43 + x2*x45 + x45)
x47 = 2*atan(x42*(x44 - x46))
x48 = sqrt(x43)
x49 = b2*x18*x48/2
x50 = 1/(x24 + x25)
x51 = x25 + x28
x52 = l*x51
x53 = x45/4
x54 = sqrt(x12 + x2*x53 - x32*x51 + x53)
x55 = 2*atan(x50*(x52 - x54))
x56 = x49*sqrt(1/(-x1*cos(x55)**2 + x51))*sin(x55)
x57 = -x36*x48
x59 = 2*atan(x42*(x44 + x46))
x60 = 2*atan(x50*(x52 + x54))
x61 = x49*sqrt(1/(-x1*cos(x60)**2 + x51))*sin(x60)
#solutions (alpha, a1, b1)
(x17, -x35, x19*x36)
(x17, x35, x37)
(x39, -x41, x19*x36)
(x39, x41, x37)
(x47, -x56, x36*x48)
(x47, x56, x57)
(x59, -x61, x36*x48)
(x59, x61, x57)

View File

@ -0,0 +1,125 @@
from inkscape_helper.PathSegment import *
from inkscape_helper.Coordinate import Coordinate
from inkscape_helper.Ellipse import Ellipse
from math import sqrt, pi
import copy
class EllipticArc(PathSegment):
ell_dict = {}
def __init__(self, start, end, rx, ry, axis_rot, pos_dir=True, large_arc=False):
self.rx = rx
self.ry = ry
# calculate ellipse center
# the center is on two ellipses one with its center at the start point, the other at the end point
# for simplicity take the one ellipse at the origin and the other with offset (tx, ty),
# find the center and translate back to the original offset at the end
axis_rot *= pi / 180 # convert to radians
# start and end are mutable objects, copy to avoid modifying them
r_start = copy.copy(start)
r_end = copy.copy(end)
r_start.t -= axis_rot
r_end.t -= axis_rot
end_o = r_end - r_start # offset end vector
tx = end_o.x
ty = end_o.y
# some helper variables for the intersection points
# used sympy to come up with the equations
ff = (rx**2*ty**2 + ry**2*tx**2)
cx = rx**2*ry*tx*ty**2 + ry**3*tx**3
cy = rx*ty*ff
sx = rx*ty*sqrt(4*rx**4*ry**2*ty**2 - rx**4*ty**4 + 4*rx**2*ry**4*tx**2 - 2*rx**2*ry**2*tx**2*ty**2 - ry**4*tx**4)
sy = ry*tx*sqrt(-ff*(-4*rx**2*ry**2 + rx**2*ty**2 + ry**2*tx**2))
# intersection points
c1 = Coordinate((cx - sx) / (2*ry*ff), (cy + sy) / (2*rx*ff))
c2 = Coordinate((cx + sx) / (2*ry*ff), (cy - sy) / (2*rx*ff))
if end_o.cross_norm(c1 - r_start) < 0: # c1 is to the left of end_o
left = c1
right = c2
else:
left = c2
right = c1
if pos_dir != large_arc: #center should be on the left of end_o
center_o = left
else: #center should be on the right of end_o
center_o = right
#re-use ellipses with same rx, ry to save some memory
if (rx, ry) in self.ell_dict:
self.ellipse = self.ell_dict[(rx, ry)]
else:
self.ellipse = Ellipse(rx, ry)
self.ell_dict[(rx, ry)] = self.ellipse
self.start = start
self.end = end
self.axis_rot = axis_rot
self.pos_dir = pos_dir
self.large_arc = large_arc
self.start_theta = self.ellipse.theta_at_angle((-center_o).t)
self.end_theta = self.ellipse.theta_at_angle((end_o - center_o).t)
# translate center back to original offset
center_o.t += axis_rot
self.center = center_o + start
@property
def length(self):
return self.ellipse.dist_from_theta(self.start_theta, self.end_theta)
def t_to_theta(self, t):
"""convert t (always between 0 and 1) to angle theta"""
start = self.start_theta
end = self.end_theta
if self.pos_dir and end < start:
end += 2 * pi
if not self.pos_dir and start < end:
end -= 2 * pi
arc_size = end - start
return (start + (end - start) * t) % (2 * pi)
def theta_to_t(self, theta):
full_arc_size = (self.end_theta - self.start_theta + 2 * pi) % (2 * pi)
theta_arc_size = (theta - self.start_theta + 2 * pi) % (2 * pi)
return theta_arc_size / full_arc_size
def curvature(self, t):
theta = self.t_to_theta(t)
return self.ellipse.curvature(theta)
def tangent(self, t):
theta = self.t_to_theta(t)
return self.ellipse.tangent(theta)
def t_at_length(self, length):
"""interpolated t where the curve is at the given length"""
theta = self.ellipse.theta_from_dist(length, self.start_theta)
return self.theta_to_t(theta)
def length_at_t(self, t):
return self.ellipse.dist_from_theta(self.start_theta, self.t_to_theta(t))
def pathpoint_at_t(self, t):
"""pathpoint on the curve from t=0 to point at t."""
centered = self.ellipse.coordinate_at_theta(self.t_to_theta(t))
centered.t += self.axis_rot
return PathPoint(t, centered + self.center, self.tangent(t), self.curvature(t), self.length_at_t(t))
# identical to Bezier code
def subdivide(self, part_length, start_offset=0):
nr_parts = int((self.length - start_offset) // part_length)
k_o = start_offset / self.length
k2t = lambda k : k_o + k * part_length / self.length
points = [self.pathpoint_at_t(k2t(k)) for k in range(nr_parts + 1)]
return(points, self.length - points[-1].c_dist)

View File

@ -0,0 +1,25 @@
from inkscape_helper.PathSegment import *
class Line(PathSegment):
def __init__(self, start, end):
self.start = start
self.end = end
self.pp = lambda t : PathPoint(t, self.start + t * (self.end - self.start), self.end - self.start, 0, t * self.length)
@property
def length(self):
return (self.end - self.start).r
def subdivide(self, part_length, start_offset=0): # note: start_offset should be smaller than part_length
nr_parts = int((self.length - start_offset) // part_length)
k_o = start_offset / self.length
k2t = lambda k : k_o + k * part_length / self.length
points = [self.pp(k2t(k)) for k in range(nr_parts + 1)]
return(points, self.length - points[-1].c_dist)
def pathpoint_at_t(self, t):
return self.pp(t)
def t_at_length(self, length):
return length / self.length

View File

@ -0,0 +1,49 @@
import copy
class Matrix(object):
"""
Matrix class with some basic matrix operations
"""
def __init__(self, array):
columns = len(array[0])
for r in array[1:]: #make sure each row has same number of columns
assert len(r) == columns
self.array = copy.copy(array)
self.rows = len(array)
self.columns = columns
def __repr__(self):
return self.__str__()
def __str__(self):
a = ['[' + ', '.join([str(i) for i in r]) + ']' for r in self.array]
return '[\n' + ',\n'.join(a) + '\n]'
def minor(self, row, col):
return Matrix([[self[r][c] for c in range(self.columns) if c != col] for r in range(self.rows) if r != row])
def det(self):
if self.rows != self.columns:
raise TypeError, 'Can only calculate determinant for a square matrix'
if self.rows == 1:
return self[0][0]
if self.rows == 2:
return self[0][0] * self[1][1] - self[0][1] * self[1][0]
det = 0
for i in range(self.columns):
det += (-1)**i * self.array[0][i] * self.minor(0, i).det()
return det
def __getitem__(self, index):
return self.array[index]
def __add__(self, other):
if self.rows != other.rows or self.columns != other.columns:
raise TypeError, 'Both matrices should have equal dimensions. Is ({} x {}) and ({} x {}).'.format(self.rows, self.columns, other.rows, other.columns)
return Matrix([[self[r][c] + other[r][c] for c in range(self.columns)] for r in range(self.rows)])
def __mul__(self, other):
if self.columns != other.rows:
raise TypeError, 'Left matrix should have same number of columns as right matrix has rows. Is ({} x {}) and ({} x {}).'.format(self.rows, self.columns, other.rows, other.columns)
return Matrix([[sum([self[r][i] * other[i][c] for i in range(self.columns)]) for c in range(other.columns)] for r in range(self.rows)])

View File

@ -0,0 +1,29 @@
from collections import namedtuple
PathPoint = namedtuple('PathPoint', 't coord tangent curvature c_dist')
class PathSegment(object):
def __init__(self):
raise NotImplementedError
@property
def lenth(self):
raise NotImplementedError
def subdivide(self, part_length):
raise NotImplementedError
def pathpoint_at_t(self, t):
raise NotImplementedError
def t_at_length(self, length):
raise NotImplementedError
# also need:
# find a way do do curvature dependent spacing
# - based on deviation from a standard radius?
# - or ratio between thickness and curvature?
#def point_at_distance(d):
# pass

View File

@ -0,0 +1,93 @@
import inkex
import simplestyle
from lxml import etree
def _format_1st(command, is_absolute):
"""Small helper function for the Path class"""
return command.upper() if is_absolute else command.lower()
default_style = str(inkex.Style(
{'stroke': '#000000',
'stroke-width': '0.1',
'fill': 'none'
}))
red_style = str(inkex.Style(
{'stroke': '#FF0000',
'stroke-width': '0.1',
'fill': 'none'
}))
green_style = str(inkex.Style(
{'stroke': '#00FF00',
'stroke-width': '0.1',
'fill': 'none'
}))
blue_style = str(inkex.Style(
{'stroke': '#0000FF',
'stroke-width': '0.1',
'fill': 'none'
}))
def layer(parent, layer_name):
layer = etree.SubElement(parent, 'g')
layer.set(inkex.addNS('label', 'inkscape'), layer_name)
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
return layer
def group(parent):
return etree.SubElement(parent, 'g')
def text(parent, coordinate, txt, style=default_style):
text = etree.Element(inkex.addNS('text', 'svg'))
text.text = txt
text.set('x', str(coordinate.x))
text.set('y', str(coordinate.y))
style = {'text-align': 'center', 'text-anchor': 'middle'}
text.set('style', str(inkex.Style(style)))
parent.append(text)
class Path(object):
"""
Generates SVG paths
"""
def __init__(self):
self.nodes = []
def move_to(self, coord, absolute=False):
self.nodes.append("{0} {1} {2}".format(_format_1st('m', absolute), coord.x, coord.y))
def line_to(self, coord, absolute=False):
self.nodes.append("{0} {1} {2}".format(_format_1st('l', absolute), coord.x, coord.y))
def h_line_to(self, dist, absolute=False):
self.nodes.append("{0} {1}".format(_format_1st('h', absolute), dist))
def v_line_to(self, dist, absolute=False):
self.nodes.append("{0} {1}".format(_format_1st('v', absolute), dist))
def arc_to(self, radius, coord, rotation=0, pos_sweep=True, large_arc=False, absolute=False):
self.nodes.append("{0} {1} {2} {3} {4} {5} {6} {7}"
.format(_format_1st('a', absolute), radius.x, radius.y, rotation,
1 if large_arc else 0, 1 if pos_sweep else 0, coord.x, coord.y))
def close(self):
self.nodes.append('z')
def path(self, parent, style=default_style):
attribs = {'style': style,
'd': ' '.join(self.nodes)}
etree.SubElement(parent, inkex.addNS('path', 'svg'), attribs)
def curve(self, parent, segments, style, closed=True):
pathStr = ' '.join(segments)
if closed:
pathStr += ' z'
attributes = {
'style': style,
'd': pathStr}
etree.SubElement(parent, inkex.addNS('path', 'svg'), attributes)
def remove_last(self):
self.nodes.pop()

View File

@ -0,0 +1,21 @@
[
{
"name": "Box Maker - Elliptical Box",
"id": "fablabchemnitz.de.box_maker_elliptical_box",
"path": "box_maker_elliptical_box",
"dependent_extensions": null,
"original_name": " Elliptical Box Maker",
"original_id": "be.fablab-leuven.inkscape.elliptical_box",
"license": "MIT License",
"license_url": "https://github.com/BvdP/elliptical-box-maker/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_elliptical_box",
"fork_url": "https://github.com/BvdP/elliptical-box-maker",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Elliptical+Box",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/BvdP",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Box Maker - Lasercut Box</name>
<id>fablabchemnitz.de.box_maker_lasercut_box</id>
<param name="tab" type="notebook">
<page name="Dimensions" gui-text="Dimensions">
<label xml:space="preserve">Dimensions can measure the external or internal size of the box.
The corner cubes can be omitted.
The document units should be set to mm instead of pixels (assuming you mean to really cut it out with a laser).
</label>
<param name="int_ext" gui-text="Dimensions are" type="optiongroup" appearance="combo">
<option value="true">External</option>
<option value="false">Internal</option>
</param>
<param name="units" gui-text="Units" type="optiongroup" appearance="combo">
<option value="in">in</option>
<option value="cm">cm</option>
<option value="mm">mm</option>
<option value="px">px</option>
<option value="pt">pt</option>
</param>
<param name="width" type="float" min="0.1" max="1000.0" gui-text="Width">50.0</param>
<param name="height" type="float" min="0.1" max="1000.0" gui-text="Height">30.0</param>
<param name="depth" type="float" min="0.1" max="1000.0" gui-text="Depth">15</param>
<param name="thickness" type="float" min="0.0" max="20.0" precision="2" gui-text="Material Thickness">3.0</param>
<param name="ntab_W" type="int" min="1" max="299" gui-text="Width Tab Count">11</param>
<param name="ntab_H" type="int" min="1" max="299" gui-text="Height Tab Count">11</param>
<param name="ntab_D" type="int" min="1" max="299" gui-text="Depth Tab Count">6</param>
<param name="corners" type="bool" gui-text="Include corners">true</param>
<param name="halftabs" type="bool" gui-text="Use half-sized tabs near corners">true</param>
</page>
<page name="Usage2" gui-text="Laser Kerf">
<label xml:space="preserve">The kerf is the amount lost to the burning width of the laser.
Typically in the range 0.1 to 0.4 mm. Check with your laser vendor. This will depend on the material being cut and the speed of the laser.
Setting the kerf to 0 will create a box that will use the least material. However the material lost to the laser will make for a loose fit and probably require glue.
If the Kerf is greater than zero then you can have a calculated "Precise fit" or add dimples for a "Press fit".
A "Precise fit" will change the width of the tabs to allow for the material burned away by the laser and make a tight fit.
Dimples will add notches for a "Press fit" (see next tab)
The pattern will be spread out, using more material, because a common laser cut line cannot be used.
</label>
<param name="kerf_size" type="float" min="0.0" max="3.0" precision="2" gui-text="Kerf (amount lost to laser)">0.0</param>
<param name="linewidth" type="bool" gui-text="Display Line width = kerf">false</param>
</page>
<page name="Usage3" gui-text="Dimples">
<label xml:space="preserve">Dimples are used so that a press-fit can be
made in deformable materials like wood.
If Dimple is checked then no "precise fit" kerf adjustment is made to the tabs. Instead a small dimple is added to each notch to enable a press fit.
- the dimple size is equal to the kerf size.
Dimples are useful for flexible materials like wood but are not good for rigid materials like perspex.
Dimple style is half rounds or triangles. Triangles are cheaper to cut, Half rounds fit better.
</label>
<param name="dimples" type="bool" gui-text="Dimples instead of tight fit">false</param>
<param name="dstyle" gui-text="Dimple Style" type="optiongroup" appearance="combo">
<option value="false">Dimples</option>
<option value="true">Triangles</option>
</param>
</page>
<page name="Usage4" gui-text="Misc">
<label xml:space="preserve">Colours:
The color scheme used by Ponoko is used here:
Specifically:
- Blue (0,0,255) is the lasercut line color
- Orange is a non-printing annotation
The lines are all 0.1mm in width - as required by Ponoko.
Annotations can be shown. They describe the kerf amount only and are shown on each piece.
This can be helpful if printing tests fits for different materials.
</label>
<param name="annotation" type="bool" gui-text="Include annotation">true</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">box_maker_lasercut_box.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,473 @@
#!/usr/bin/env python3
'''
Copyright (C)2011 Mark Schafer <neon.mark(a)gmaildotcom>
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
'''
# Build a tabbed box for lasercutting with tight fit, and minimal material use options.
# User defines:
# - internal or external dimensions,
# - number of tabs,
# - amount lost to laser (kerf),
# - include corner cubes or not,
# - dimples, or perfect fit (accounting for kerf).
# If zero kerf - will be perfectly packed for minimal laser cuts and material size.
### Todo
# add option to pack multiple boxes (if zero kerf) - new tab maybe?
# add option for little circles at sharp corners for acrylic
# annotations: - add overlap value as markup - Ponoko annotation color
# choose colours from a dictionary
### Versions
# 0.1 February 2011 - basic lasercut box with dimples etc
# 0.2 changes to unittouu for Inkscape 0.91
# 0.3 Option to avoid half-sized tabs at corners. <juergen@fabmail.org>
__version__ = "0.3"
import inkex
from inkex.paths import Path
from lxml import etree
class BoxMakerLasercutBox(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("-i", "--int_ext", type = inkex.Boolean, default=False, help="Are the Dimensions for External or Internal sizing.")
pars.add_argument("-x", "--width", type=float, default=50.0, help="The Box Width - in the X dimension")
pars.add_argument("-y", "--height", type=float, default=30.0, help="The Box Height - in the Y dimension")
pars.add_argument("-z", "--depth", type=float, default=15.0, help="The Box Depth - in the Z dimension")
pars.add_argument("-t", "--thickness", type=float, default=3.0, help="Material Thickness - critical to know")
pars.add_argument("-u", "--units", default="cm", help="The unit of the box dimensions")
pars.add_argument("-c", "--corners", type = inkex.Boolean, default=True, help="The corner cubes can be removed for a different look")
pars.add_argument("-H", "--halftabs", type = inkex.Boolean, default=True, help="Start/End with half-sized tabs - Avoid with very small tabs")
pars.add_argument("-p", "--ntab_W", type=int, default=11, help="Number of tabs in Width")
pars.add_argument("-q", "--ntab_H", type=int, default=11, help="Number of tabs in Height")
pars.add_argument("-r", "--ntab_D", type=int, default=6, help="Number of tabs in Depth")
pars.add_argument("-k", "--kerf_size", type=float,default=0.0, help="Kerf size - amount lost to laser for this material. 0 = loose fit")
pars.add_argument("-d", "--dimples", type=inkex.Boolean, default=False, help="Add dimples for press fitting wooden materials")
pars.add_argument("-s", "--dstyle", type=inkex.Boolean, default=False, help="Dimples can be triangles(cheaper) or half rounds(better)")
pars.add_argument("-g", "--linewidth", type=inkex.Boolean, default=False, help="Use the kerf value as the drawn line width")
pars.add_argument("-j", "--annotation", type=inkex.Boolean, default=True, help="Show Kerf value as annotation")
#dummy for the doc tab - which is named
pars.add_argument("--tab", default="use", help="The selected UI-tab when OK was pressed")
#internal useful variables
def annotation(self, x, y, text):
""" Draw text at this location
- change to path
- use annotation color """
pass
def thickness_line(self, dimple, vert_horiz, pos_neg):
""" called to draw dimples (also draws simple lines if no dimple)
- pos_neg is 1, -1 for direction
- vert_horiz is v or h """
if dimple and self.kerf > 0.0: # we need a dimple
# size is radius = kerf
# short line, half circle, short line
#[ 'C', [x1,y1, x2,y2, x,y] ] x1 is first handle, x2 is second
lines = []
radius = self.kerf
if self.thick - 2 * radius < 0.2: # correct for large dimples(kerf) on small thicknesses
radius = (self.thick - 0.2) / 2
short = 0.1
else:
short = self.thick/2 - radius
if vert_horiz == 'v': # vertical line
# first short line
lines.append(['v', [pos_neg*short]])
# half circle
if pos_neg == 1: # only the DH_sides need reversed tabs to interlock
if self.dimple_tri:
lines.append(['l', [radius, pos_neg*radius]])
lines.append(['l', [-radius, pos_neg*radius]])
else:
lines.append(['c', [radius, 0, radius, pos_neg*2*radius, 0, pos_neg*2*radius]])
else:
if self.dimple_tri:
lines.append(['l', [-radius, pos_neg*radius]])
lines.append(['l', [radius, pos_neg*radius]])
else:
lines.append(['c', [-radius, 0, -radius, pos_neg*2*radius, 0, pos_neg*2*radius]])
# last short line
lines.append(['v', [pos_neg*short]])
else: # horizontal line
# first short line
lines.append(['h', [pos_neg*short]])
# half circle
if self.dimple_tri:
lines.append(['l', [pos_neg*radius, radius]])
lines.append(['l', [pos_neg*radius, -radius]])
else:
lines.append(['c', [0, radius, pos_neg*2*radius, radius, pos_neg*2*radius, 0]])
# last short line
lines.append(['h', [pos_neg*short]])
return lines
# No dimple - so much easier
else: # return a straight v or h line same as thickness
if vert_horiz == 'v':
return [ ['v', [pos_neg*self.thick]] ]
else:
return [ ['h', [pos_neg*self.thick]] ]
def draw_WH_lid(self, startx, starty, masktop=False):
""" Return an SVG path for the top or bottom of box
- the Width * Height dimension """
line_path = []
line_path.append(['M', [startx, starty]])
# top row of tabs
if masktop and self.kerf ==0.0: # don't draw top for packing with no extra cuts
line_path.append(['m', [self.boxW, 0]])
else:
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 - self.pf/2, 0]])
for i in range(int(self.Wtabs)):
line_path.append(['h', [self.boxW/self.Wtabs/4 - self.pf/2]])
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'v', -1):
line_path.append(l)
line_path.append(['h', [self.boxW/self.Wtabs/2 + self.pf]])
line_path.append(['v', [self.thick]])
line_path.append(['h', [self.boxW/self.Wtabs/4 - self.pf/2]])
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 - self.pf/2, 0]])
# right hand vertical drop
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 - self.pf/2]])
for i in range(int(self.Htabs)):
line_path.append(['v', [self.boxH/self.Htabs/4 - self.pf/2]])
line_path.append(['h', [self.thick]])
line_path.append(['v', [self.boxH/self.Htabs/2 + self.pf]])
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'h', -1):
line_path.append(l)
line_path.append(['v', [self.boxH/self.Htabs/4 - self.pf/2]])
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 - self.pf/2]])
# bottom row (in reverse)
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 + self.pf/2, 0]])
for i in range(int(self.Wtabs)):
line_path.append(['h', [-self.boxW/self.Wtabs/4 + self.pf/2]])
line_path.append(['v', [self.thick]])
line_path.append(['h', [-self.boxW/self.Wtabs/2 - self.pf]])
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'v', -1):
line_path.append(l)
line_path.append(['h', [-self.boxW/self.Wtabs/4 + self.pf/2]])
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 + self.pf/2, 0]])
# up the left hand side
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 + self.pf/2]])
for i in range(int(self.Htabs)):
line_path.append(['v', [-self.boxH/self.Htabs/4 + self.pf/2]])
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'h', -1):
line_path.append(l)
line_path.append(['v', [-self.boxH/self.Htabs/2 - self.pf]])
line_path.append(['h', [self.thick]])
line_path.append(['v', [-self.boxH/self.Htabs/4 + self.pf/2]])
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 + self.pf/2]])
return line_path
def draw_WD_side(self, startx, starty, mask=False, corners=True):
""" Return an SVG path for the long side of box
- the Width * Depth dimension """
# Draw side of the box (placed below the lid)
line_path = []
# top row of tabs
if corners:
line_path.append(['M', [startx - self.thick, starty]])
line_path.append(['v', [-self.thick]])
line_path.append(['h', [self.thick]])
else:
line_path.append(['M', [startx, starty]])
line_path.append(['v', [-self.thick]])
#
if self.kerf > 0.0: # if fit perfectly - don't draw double line
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 + self.pf/2, 0]])
for i in range(int(self.Wtabs)):
line_path.append(['h', [self.boxW/self.Wtabs/4 + self.pf/2]])
line_path.append(['v', [self.thick]])
line_path.append(['h', [self.boxW/self.Wtabs/2 - self.pf]])
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'v', -1):
line_path.append(l)
line_path.append(['h', [self.boxW/self.Wtabs/4 + self.pf/2]])
if not self.ht: line_path.append(['l', [self.boxW/self.Wtabs/4 + self.pf/2, 0]])
if corners: line_path.append(['h', [self.thick]])
else: # move to skipped drawn lines
if corners:
line_path.append(['m', [self.boxW + self.thick, 0]])
else:
line_path.append(['m', [self.boxW, 0]])
#
line_path.append(['v', [self.thick]])
if not corners: line_path.append(['h', [self.thick]])
# RHS
if not self.ht: line_path.append(['l', [0, self.boxD/self.Dtabs/4 + self.pf/2]])
for i in range(int(self.Dtabs)):
line_path.append(['v', [self.boxD/self.Dtabs/4 + self.pf/2]])
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'h', -1):
line_path.append(l)
line_path.append(['v', [self.boxD/self.Dtabs/2 - self.pf]])
line_path.append(['h', [self.thick]])
line_path.append(['v', [self.boxD/self.Dtabs/4 + self.pf/2]])
if not self.ht: line_path.append(['l', [0, self.boxD/self.Dtabs/4 + self.pf/2]])
#
if corners:
line_path.append(['v', [self.thick]])
line_path.append(['h', [-self.thick]])
else:
line_path.append(['h', [-self.thick]])
line_path.append(['v', [self.thick]])
# base
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 - self.pf/2, 0]])
for i in range(int(self.Wtabs)):
line_path.append(['h', [-self.boxW/self.Wtabs/4 - self.pf/2]])
#line_path.append(['v', [0, -thick]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'v', -1):
line_path.append(l)
line_path.append(['h', [-self.boxW/self.Wtabs/2 + self.pf]])
line_path.append(['v', [self.thick]])
line_path.append(['h', [-self.boxW/self.Wtabs/4 - self.pf/2]])
if not self.ht: line_path.append(['l', [-self.boxW/self.Wtabs/4 - self.pf/2, 0]])
#
if corners:
line_path.append(['h', [-self.thick]])
line_path.append(['v', [-self.thick]])
else:
line_path.append(['v', [-self.thick]])
line_path.append(['h', [-self.thick]])
# LHS
if not self.ht: line_path.append(['l', [0, -self.boxD/self.Dtabs/4 - self.pf/2]])
for i in range(int(self.Dtabs)):
line_path.append(['v', [-self.boxD/self.Dtabs/4 - self.pf/2]])
line_path.append(['h', [self.thick]])
line_path.append(['v', [-self.boxD/self.Dtabs/2 + self.pf]])
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'h', -1):
line_path.append(l)
line_path.append(['v', [-self.boxD/self.Dtabs/4 - self.pf/2]])
if not self.ht: line_path.append(['l', [0, -self.boxD/self.Dtabs/4 - self.pf/2]])
#
if not corners: line_path.append(['h', [self.thick]])
return line_path
def draw_HD_side(self, startx, starty, corners, mask=False):
""" Return an SVG path for the short side of box
- the Height * Depth dimension """
line_path = []
# top row of tabs
line_path.append(['M', [startx, starty]])
if not(mask and corners and self.kerf == 0.0):
line_path.append(['h', [self.thick]])
else:
line_path.append(['m', [self.thick, 0]])
if not self.ht: line_path.append(['l', [self.boxD/self.Dtabs/4 - self.pf/2, 0]])
for i in range(int(self.Dtabs)):
line_path.append(['h', [self.boxD/self.Dtabs/4 - self.pf/2]])
line_path.append(['v', [-self.thick]])
line_path.append(['h', [self.boxD/self.Dtabs/2 + self.pf]])
#line_path.append(['v', [0, thick]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'v', 1):
line_path.append(l)
line_path.append(['h', [self.boxD/self.Dtabs/4 - self.pf/2]])
if not self.ht: line_path.append(['l', [self.boxD/self.Dtabs/4 - self.pf/2, 0]])
line_path.append(['h', [self.thick]])
#
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 + self.pf/2]])
for i in range(int(self.Htabs)):
line_path.append(['v', [self.boxH/self.Htabs/4 + self.pf/2]])
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'h', -1):
line_path.append(l)
line_path.append(['v', [self.boxH/self.Htabs/2 - self.pf]])
line_path.append(['h', [self.thick]])
line_path.append(['v', [self.boxH/self.Htabs/4 + self.pf/2]])
if not self.ht: line_path.append(['l', [0, self.boxH/self.Htabs/4 + self.pf/2]])
line_path.append(['h', [-self.thick]])
#
if not self.ht: line_path.append(['l', [-self.boxD/self.Dtabs/4 + self.pf/2, 0]])
for i in range(int(self.Dtabs)):
line_path.append(['h', [-self.boxD/self.Dtabs/4 + self.pf/2]])
#line_path.append(['v', [0, thick]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'v', 1): # this is the weird +1 instead of -1 dimple
line_path.append(l)
line_path.append(['h', [-self.boxD/self.Dtabs/2 - self.pf]])
line_path.append(['v', [-self.thick]])
line_path.append(['h', [-self.boxD/self.Dtabs/4 + self.pf/2]])
if not self.ht: line_path.append(['l', [-self.boxD/self.Dtabs/4 + self.pf/2, 0]])
line_path.append(['h', [-self.thick]])
#
if self.kerf > 0.0: # if fit perfectly - don't draw double line
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 - self.pf/2]])
for i in range(int(self.Htabs)):
line_path.append(['v', [-self.boxH/self.Htabs/4 - self.pf/2]])
line_path.append(['h', [self.thick]])
line_path.append(['v', [-self.boxH/self.Htabs/2 + self.pf]])
#line_path.append(['h', [-thick, 0]]) # replaced with dimpled version
for l in self.thickness_line(self.dimple, 'h', -1):
line_path.append(l)
line_path.append(['v', [-self.boxH/self.Htabs/4 - self.pf/2]])
if not self.ht: line_path.append(['l', [0, -self.boxH/self.Htabs/4 - self.pf/2]])
return line_path
###--------------------------------------------
### The main function called by the inkscape UI
def effect(self):
self.stroke_width = self.svg.unittouu('1px') #default for visiblity
self.line_style = {'stroke': '#0000FF', # Ponoko blue
'fill': 'none',
'stroke-width': self.stroke_width,
'stroke-linecap': 'butt',
'stroke-linejoin': 'miter'}
# document dimensions (for centering)
docW = self.svg.unittouu(self.document.getroot().get('width'))
docH = self.svg.unittouu(self.document.getroot().get('height'))
# extract fields from UI
self.boxW = self.svg.unittouu(str(self.options.width) + self.options.units)
self.boxH = self.svg.unittouu(str(self.options.height) + self.options.units)
self.boxD = self.svg.unittouu(str(self.options.depth) + self.options.units)
self.thick = self.svg.unittouu(str(self.options.thickness) + self.options.units)
self.kerf = self.svg.unittouu(str(self.options.kerf_size) + self.options.units)
if self.kerf < 0.01: self.kerf = 0.0 # snap to 0 for UI error when setting spinner to 0.0
self.Wtabs = self.options.ntab_W
self.Htabs = self.options.ntab_H
self.Dtabs = self.options.ntab_D
self.dimple = self.options.dimples
line_width = self.options.linewidth
corners = self.options.corners
self.dimple_tri = self.options.dstyle
self.annotation = self.options.annotation
self.ht = self.options.halftabs
if not self.ht:
self.Wtabs += 0.5
self.Htabs += 0.5
self.Dtabs += 0.5
# Correct for thickness in dimensions
if self.options.int_ext: # external so add thickness
self.boxW -= self.thick*2
self.boxH -= self.thick*2
self.boxD -= self.thick*2
# adjust for laser kerf (precise measurement)
self.boxW += self.kerf
self.boxH += self.kerf
self.boxD += self.kerf
# Precise fit or dimples (if kerf > 0.0)
if self.dimple == False: # and kerf > 0.0:
self.pf = self.kerf
else:
self.pf = 0.0
# set the stroke width and line style
sw = self.kerf
if self.kerf == 0.0: sw = self.stroke_width
ls = self.line_style
if line_width: # user wants drawn line width to be same as kerf size
ls['stroke-width'] = sw
line_style = str(inkex.Style(ls))
###---------------------------
### create the inkscape object
box_id = self.svg.get_unique_id('box')
self.box = g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':box_id})
#Set local position for drawing (will transform to center of doc at end)
lower_pos = 0
left_pos = 0
# Draw Lid (using SVG path definitions)
line_path = self.draw_WH_lid(left_pos, lower_pos)
# Add to scene
line_atts = { 'style':line_style, 'id':box_id+'-lid', 'd':str(Path(line_path)) }
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
# draw the side of the box directly below
if self.kerf > 0.0:
lower_pos += self.boxH + (3*self.thick)
else: # kerf = 0 so don't draw extra lines and fit perfectly
lower_pos += self.boxH + self.thick # at lower edge of lid
left_pos += 0
# Draw side of the box (placed below the lid)
line_path = self.draw_WD_side(left_pos, lower_pos, corners=corners)
# Add to scene
line_atts = { 'style':line_style, 'id':box_id+'-longside1', 'd':str(Path(line_path)) }
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
# draw the bottom of the box directly below
if self.kerf > 0.0:
lower_pos += self.boxD + (3*self.thick)
else: # kerf = 0 so don't draw extra lines and fit perfectly
lower_pos += self.boxD + self.thick # at lower edge
left_pos += 0
# Draw base of the box
line_path = self.draw_WH_lid(left_pos, lower_pos, True)
# Add to scene
line_atts = { 'style':line_style, 'id':box_id+'-base', 'd':str(Path(line_path)) }
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
# draw the second side of the box directly below
if self.kerf > 0.0:
lower_pos += self.boxH + (3*self.thick)
else: # kerf = 0 so don't draw extra lines and fit perfectly
lower_pos += self.boxH + self.thick # at lower edge of lid
left_pos += 0
# Draw side of the box (placed below the lid)
line_path = self.draw_WD_side(left_pos, lower_pos, corners=corners)
# Add to scene
line_atts = { 'style':line_style, 'id':box_id+'-longside2', 'd':str(Path(line_path)) }
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
# draw next on RHS of lid
if self.kerf > 0.0:
left_pos += self.boxW + (2*self.thick) # adequate space (could be a param for separation when kerf > 0)
else:
left_pos += self.boxW # right at right edge of lid
lower_pos = 0
# Side of the box (placed next to the lid)
line_path = self.draw_HD_side(left_pos, lower_pos, corners)
# Add to scene
line_atts = { 'style':line_style, 'id':box_id+'-endface2', 'd':str(Path(line_path)) }
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
# draw next on RHS of base
if self.kerf > 0.0:
lower_pos += self.boxH + self.boxD + 6*self.thick
else:
lower_pos += self.boxH +self.boxD + 2*self.thick
# Side of the box (placed next to the lid)
line_path = self.draw_HD_side(left_pos, lower_pos, corners, True)
# Add to scene
line_atts = { 'style':line_style, 'id':box_id+'-endface1', 'd':str(Path(line_path)) }
etree.SubElement(g, inkex.addNS('path','svg'), line_atts)
###----------------------------------------
# Transform entire drawing to center of doc
lower_pos += self.boxH*2 + self.boxD*2 + 2*self.thick
left_pos += self.boxH + 2*self.thick
g.set( 'transform', 'translate(%f,%f)' % ( (docW-left_pos)/2, (docH-lower_pos)/2))
# The implementation algorithm has added intermediate short lines and doubled up when using h,v with extra zeros
#self.thin(g) # remove short straight lines
if __name__ == '__main__':
BoxMakerLasercutBox().run()

View File

@ -0,0 +1,22 @@
[
{
"name": "Box Maker - Lasercut Box",
"id": "fablabchemnitz.de.box_maker_lasercut_box",
"path": "box_maker_lasercut_box",
"dependent_extensions": null,
"original_name": "Lasercut Box",
"original_id": "org.inkscape.LasercutBox",
"license": "GNU GPL v2",
"license_url": "https://github.com/Neon22/inkscape-LasercutBox/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_lasercut_box",
"fork_url": "https://github.com/Neon22/inkscape-LasercutBox",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Lasercut+Box",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/Neon22",
"github.com/jnweiger",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1 @@
/DebugPath2Flex.txt

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Box Maker - Path To Flex</name>
<id>fablabchemnitz.de.box_maker_path_to_flex</id>
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
<option value="pc">pc</option>
</param>
<param name="thickness" type="float" min="1.0" max="10.0" gui-text="Material thickness">3.0</param>
<param name="zc" type="float" min="15.0" max="1000.0" gui-text="Structure height">50.0</param>
<param name="notch_interval" type="int" min="2" max="10" gui-text="Interval between notches">2</param>
<param name="max_size_flex" type="float" min="200.0" max="100000.0" gui-text="Limit length of flex band">1000.0</param>
<param name="Mode_Debug" type="bool" gui-text="Debugging information output">true</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz Boxes/Papercraft">
<submenu name="Finger-jointed/Tabbed Boxes"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">box_maker_path_to_flex.py</command>
</script>
</inkscape-extension>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
[
{
"name": "Box Maker - Path To Flex",
"id": "fablabchemnitz.de.box_maker_path_to_flex",
"path": "box_maker_path_to_flex",
"dependent_extensions": null,
"original_name": "Paths To Flex",
"original_id": "fr.fablab-lannion.inkscape.Path2Flex",
"license": "GNU GPL v3",
"license_url": "https://github.com/thierry7100/Path2flex/blob/master/LICENSE",
"comment": "ported to Inkscape v1 by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/box_maker_path_to_flex",
"fork_url": "https://github.com/thierry7100/Path2flex",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Box+Maker+-+Path+To+Flex",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/thierry7100",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>JPEG Export</name>
<id>fablabchemnitz.de.jpeg_export</id>
<param name="path" type="path" gui-text="Export path" gui-description="Full path to your file, e.g. 'C:\Users\Username\Documents\myimage.jpg'" filetypes="jpg" mode="file_new">C:\Users\</param>
<param name="bgcol" type="string" gui-text="Background color (leave blank for white)" gui-description="Background color hex code, e.g. '#abc123'"></param>
<param name="quality" type="int" min="0" max="100" gui-text="Quality %" gui-description="JPG compression quality">100</param>
<param name="density" type="int" min="30" max="2400" gui-text="Resolution (ppi)" gui-description="Recommended: 90 (screens) or 300 (print)">90</param>
<param name="page" type="bool" gui-text="Export whole page" gui-description="If checked, the whole page will be exported, else the selection.">true</param>
<param name="fast" type="bool" gui-text="Fast export (suggested)" gui-description="Will use an approximate bounding box. If unchecked, export will take longer.">true</param>
<label appearance="header">Usage</label>
<label xml:space="preserve">Select the objects in the drawing that you wish to export, or make a check at "Export whole page".
Enter a name for your JPG file (with full path) and choose a background color for the exported image (JPG format does not support transparency).
Leave background color field blank for white.
This extension requires that imagemagick is installed, more info and download at http://www.imagemagick.org.</label>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Import/Export/Transfer"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">jpeg_export.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
#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 3 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, see <http://www.gnu.org/licenses/>.
# Author: Giacomo Mirabassi <giacomo@mirabassi.it>
# Version: 0.2
import os
import re
import subprocess
import math
import inkex
import shutil
inkex.localization.localize
class JPEGExport(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--path", default="")
pars.add_argument("--bgcol", default="#ffffff")
pars.add_argument("--quality",type=int, default="90")
pars.add_argument("--density", type=int, default="90")
pars.add_argument("--page", type=inkex.Boolean, default=False)
pars.add_argument("--fast", type=inkex.Boolean, default=True)
def effect(self):
"""get selected item coords and call command line command to export as a png"""
# The user must supply a directory to export:
if not self.options.path:
inkex.errormsg(_('Please indicate a file name and path to export the jpg.'))
exit()
if not os.path.basename(self.options.path):
inkex.errormsg(_('Please indicate a file name.'))
exit()
if not os.path.dirname(self.options.path):
inkex.errormsg(_('Please indicate a directory other than your system\'s base directory.'))
exit()
# Test if the directory exists and filename is valid:
filebase = os.path.dirname(self.options.path)
if not os.path.exists(filebase):
inkex.errormsg(_('The directory "%s" does not exist.') % filebase)
exit()
filename = os.path.splitext(os.path.basename(self.options.path))
filename_base = filename[0]
filename_ending = filename[1]
if self.get_valid_filename(filename_base) != filename_base:
inkex.errormsg(_('The file name "%s" is invalid.') % filename_base)
return
if filename_ending != 'jpg' or filename_ending != 'jpeg':
filename_ending = 'jpg'
outfile = os.path.join(filebase, filename_base + '.' + filename_ending)
shutil.copy(self.options.input_file, self.options.input_file + ".svg") #make a file copy with file ending to suppress import warnings
curfile = self.options.input_file + ".svg"
#inkex.utils.debug("curfile:" + curfile)
# Test if color is valid
_rgbhexstring = re.compile(r'#[a-fA-F0-9]{6}$')
if not _rgbhexstring.match(self.options.bgcol):
inkex.errormsg(_('Please indicate the background color like this: \"#abc123\" or leave the field empty for white.'))
exit()
bgcol = self.options.bgcol
if self.options.page == False:
if len(self.svg.selected) == 0:
inkex.errormsg(_('Please select something.'))
exit()
coords=self.processSelected()
self.exportArea(int(coords[0]),int(coords[1]),int(coords[2]),int(coords[3]),curfile,outfile,bgcol)
elif self.options.page == True:
self.exportPage(curfile,outfile,bgcol)
def processSelected(self):
"""Iterate trough nodes and find the bounding coordinates of the selected area"""
startx=None
starty=None
endx=None
endy=None
nodelist=[]
root=self.document.getroot();
toty=self.svg.unittouu(root.attrib['height'])
scale = self.svg.unittouu('1px')
props=['x', 'y', 'width', 'height']
for id in self.svg.selected:
if self.options.fast == True:
nodelist.append(self.svg.getElementById(id))
else: # uses command line
rawprops=[]
for prop in props:
command=("inkscape", "--query-id", id, "--query-"+prop, self.options.input_file)
proc=subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
proc.wait()
rawprops.append(math.ceil(self.svg.unittouu(proc.stdout.read())))
proc.stdout.close()
proc.stderr.close()
nodeEndX = rawprops[0] + rawprops[2]
nodeStartY = toty - rawprops[1] - rawprops[3]
nodeEndY = toty - rawprops[1]
if rawprops[0] < startx or startx is None:
startx = rawprops[0]
if nodeStartY < starty or starty is None:
starty = nodeStartY
if nodeEndX > endx or endx is None:
endx = nodeEndX
if nodeEndY > endy or endy is None:
endy = nodeEndY
if self.options.fast == True:
bbox = sum([node.bounding_box() for node in nodelist], None)
#inkex.utils.debug(bbox) - see transform.py
'''
width = property(lambda self: self.x.size)
height = property(lambda self: self.y.size)
top = property(lambda self: self.y.minimum)
left = property(lambda self: self.x.minimum)
bottom = property(lambda self: self.y.maximum)
right = property(lambda self: self.x.maximum)
center_x = property(lambda self: self.x.center)
center_y = property(lambda self: self.y.center)
'''
startx = math.ceil(bbox.left)
endx = math.ceil(bbox.right)
h = -bbox.top + bbox.bottom
starty = toty - math.ceil(bbox.top) -h
endy = toty - math.ceil(bbox.top)
coords = [startx / scale, starty / scale, endx / scale, endy / scale]
return coords
def exportArea(self, x0, y0, x1, y1, curfile, outfile, bgcol):
tmp = self.getTmpPath()
command="inkscape --export-area %s:%s:%s:%s -d %s --export-filename \"%sjpinkexp.png\" -b \"%s\" \"%s\"" % (x0, y0, x1, y1, self.options.density, tmp, bgcol, curfile)
p = subprocess.Popen(command, shell=True)
return_code = p.wait()
self.tojpeg(outfile)
#inkex.utils.debug("command:" + command)
#inkex.utils.debug("Errorcode:" + str(return_code))
def exportPage(self, curfile, outfile, bgcol):
tmp = self.getTmpPath()
command = "inkscape --export-area-drawing -d %s --export-filename \"%sjpinkexp.png\" -b \"%s\" \"%s\"" % (self.options.density, tmp, bgcol, curfile)
p = subprocess.Popen(command, shell=True)
return_code = p.wait()
self.tojpeg(outfile)
#inkex.utils.debug("command:" + command)
#inkex.utils.debug("Errorcode:" + str(return_code))
def tojpeg(self, outfile):
tmp = self.getTmpPath()
if os.name == 'nt':
outfile = outfile.replace("\\","\\\\")
# set the ImageMagick command to run based on what's installed
if shutil.which('magick'):
command = "magick \"%sjpinkexp.png\" -sampling-factor 4:4:4 -strip -interlace JPEG -colorspace RGB -quality %s -density %s \"%s\" " % (tmp, self.options.quality, self.options.density, outfile)
# inkex.utils.debug(command)
elif shutil.which('convert'):
command = "convert \"%sjpinkexp.png\" -sampling-factor 4:4:4 -strip -interlace JPEG -colorspace RGB -quality %s -density %s \"%s\" " % (tmp, self.options.quality, self.options.density, outfile)
# inkex.utils.debug(command)
else:
inkex.errormsg(_('ImageMagick does not appear to be installed.'))
exit()
p = subprocess.Popen(command, shell=True)
return_code = p.wait()
#inkex.utils.debug("command:" + command)
#inkex.utils.debug("Errorcode:" + str(return_code))
def getTmpPath(self):
"""Define the temporary folder path depending on the operating system"""
if os.name == 'nt':
return os.getenv('TEMP') + '\\'
else:
return '/tmp/'
def get_valid_filename(self, s):
s = str(s).strip().replace(" ", "_")
return re.sub(r"(?u)[^-\w.]", "", s)
if __name__ == '__main__':
JPEGExport().run()

View File

@ -0,0 +1,21 @@
[
{
"name": "JPEG Export",
"id": "fablabchemnitz.de.jpeg_export",
"path": "jpeg_export",
"dependent_extensions": null,
"original_name": "JPEG Export",
"original_id": "id.giac.export.jpg",
"license": "GNU GPL v3",
"license_url": "https://github.com/giacmir/Inkscape-JPEG-export-extension/blob/master/jpegexport.py",
"comment": "ported to Inkscape v1 by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/jpeg_export",
"fork_url": "https://github.com/giacmir/Inkscape-JPEG-export-extension",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/JPEG+Export",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/giacmir",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>LaserDraw Export (lyz)</name>
<id>fablabchemnitz.de.laserdraw_export.lyz</id>
<!--
<param name="area_select" type="optiongroup" appearance="combo" gui-text="Area: ">
<option value="page_area">Page Area (use for engrave and cut)</option>
<option value="object_area">Object Area (for single operations only)</option>
</param>
-->
<label>Formatting can be used to break a design into separate cutting and engraving files. Red lines indicate cutting, blue lines indicate engraving, black indicates raster engraving.</label>
<param name="cut_select" type="optiongroup" appearance="combo" gui-text="Type: ">
<option value="vector_red">Vector Cuts (export red lines)</option>
<option value="vector_blue">Vector Engrave (export blue lines)</option>
<option value="raster">Raster Engrave (export everything else)</option>
<option value="all">All (export vectors and raster engrave to one file)</option>
<option value="image">Image (export all items as raster)</option>
</param>
<param name="resolution" type="int" min="100" max="1000" gui-text="Raster Image Resolution">1000</param>
<param name="margin" type="float" precision="1" min="0" max="9999" gui-text="Laser Draw Margin Size">2.0</param>
<param name="txt2paths" type="bool" gui-text="Convert Text to Paths">false</param>
<param name="inkscape_version" type="optiongroup" appearance="combo" gui-text="Inkscape Version:">
<option value="100">1.00 or Newer</option>
<option value="92">0.92 to 0.99</option>
<option value="91">0.91 or Older</option>
</param>
<output>
<extension>.lyz</extension>
<mimetype>image/lyz</mimetype>
<filetypename>Laser Draw LYZ (*.lyz)</filetypename>
<filetypetooltip>LaserDraw LYZ Output</filetypetooltip>
<dataloss>true</dataloss>
</output>
<script>
<command location="inx" interpreter="python">lyz_export.py</command>
<helper_extension>org.inkscape.output.svg.inkscape</helper_extension>
</script>
</inkscape-extension>

View File

@ -0,0 +1,286 @@
#!/usr/bin/env python3
'''
Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
Copyright (C) 2005 Aaron Spike, aaron@ekips.org
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.
'''
import math
import cmath
def rootWrapper(a,b,c,d):
if a:
# Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
a,b,c = (b/a, c/a, d/a)
m = 2.0*a**3 - 9.0*a*b + 27.0*c
k = a**2 - 3.0*b
n = m**2 - 4.0*k**3
w1 = -.5 + .5*cmath.sqrt(-3.0)
w2 = -.5 - .5*cmath.sqrt(-3.0)
if n < 0:
m1 = pow(complex((m+cmath.sqrt(n))/2),1./3)
n1 = pow(complex((m-cmath.sqrt(n))/2),1./3)
else:
if m+math.sqrt(n) < 0:
m1 = -pow(-(m+math.sqrt(n))/2,1./3)
else:
m1 = pow((m+math.sqrt(n))/2,1./3)
if m-math.sqrt(n) < 0:
n1 = -pow(-(m-math.sqrt(n))/2,1./3)
else:
n1 = pow((m-math.sqrt(n))/2,1./3)
x1 = -1./3 * (a + m1 + n1)
x2 = -1./3 * (a + w1*m1 + w2*n1)
x3 = -1./3 * (a + w2*m1 + w1*n1)
return (x1,x2,x3)
elif b:
det=c**2.0-4.0*b*d
if det:
return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b)
else:
return -c/(2.0*b),
elif c:
return 1.0*(-d/c),
return ()
def bezierparameterize(xxx_todo_changeme):
#parametric bezier
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme
x0=bx0
y0=by0
cx=3*(bx1-x0)
bx=3*(bx2-bx1)-cx
ax=bx3-x0-cx-bx
cy=3*(by1-y0)
by=3*(by2-by1)-cy
ay=by3-y0-cy-by
return ax,ay,bx,by,cx,cy,x0,y0
#ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
def linebezierintersect(xxx_todo_changeme1, xxx_todo_changeme2):
#parametric line
((lx1,ly1),(lx2,ly2)) = xxx_todo_changeme1
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme2
dd=lx1
cc=lx2-lx1
bb=ly1
aa=ly2-ly1
if aa:
coef1=cc/aa
coef2=1
else:
coef1=1
coef2=aa/cc
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
#cubic intersection coefficients
a=coef1*ay-coef2*ax
b=coef1*by-coef2*bx
c=coef1*cy-coef2*cx
d=coef1*(y0-bb)-coef2*(x0-dd)
roots = rootWrapper(a,b,c,d)
retval = []
for i in roots:
if type(i) is complex and i.imag==0:
i = i.real
if type(i) is not complex and 0<=i<=1:
retval.append(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i))
return retval
def bezierpointatt(xxx_todo_changeme3,t):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme3
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
x=ax*(t**3)+bx*(t**2)+cx*t+x0
y=ay*(t**3)+by*(t**2)+cy*t+y0
return x,y
def bezierslopeatt(xxx_todo_changeme4,t):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme4
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
dx=3*ax*(t**2)+2*bx*t+cx
dy=3*ay*(t**2)+2*by*t+cy
return dx,dy
def beziertatslope(xxx_todo_changeme5, xxx_todo_changeme6):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme5
(dy,dx) = xxx_todo_changeme6
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
#quadratic coefficents of slope formula
if dx:
slope = 1.0*(dy/dx)
a=3*ay-3*ax*slope
b=2*by-2*bx*slope
c=cy-cx*slope
elif dy:
slope = 1.0*(dx/dy)
a=3*ax-3*ay*slope
b=2*bx-2*by*slope
c=cx-cy*slope
else:
return []
roots = rootWrapper(0,a,b,c)
retval = []
for i in roots:
if type(i) is complex and i.imag==0:
i = i.real
if type(i) is not complex and 0<=i<=1:
retval.append(i)
return retval
def tpoint(xxx_todo_changeme7, xxx_todo_changeme8,t):
(x1,y1) = xxx_todo_changeme7
(x2,y2) = xxx_todo_changeme8
return x1+t*(x2-x1),y1+t*(y2-y1)
def beziersplitatt(xxx_todo_changeme9,t):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme9
m1=tpoint((bx0,by0),(bx1,by1),t)
m2=tpoint((bx1,by1),(bx2,by2),t)
m3=tpoint((bx2,by2),(bx3,by3),t)
m4=tpoint(m1,m2,t)
m5=tpoint(m2,m3,t)
m=tpoint(m4,m5,t)
return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3))
'''
Approximating the arc length of a bezier curve
according to <http://www.cit.gu.edu.au/~anthony/info/graphics/bezier.curves>
if:
L1 = |P0 P1| +|P1 P2| +|P2 P3|
L0 = |P0 P3|
then:
L = 1/2*L0 + 1/2*L1
ERR = L1-L0
ERR approaches 0 as the number of subdivisions (m) increases
2^-4m
Reference:
Jens Gravesen <gravesen@mat.dth.dk>
"Adaptive subdivision and the length of Bezier curves"
mat-report no. 1992-10, Mathematical Institute, The Technical
University of Denmark.
'''
def pointdistance(xxx_todo_changeme10, xxx_todo_changeme11):
(x1,y1) = xxx_todo_changeme10
(x2,y2) = xxx_todo_changeme11
return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
def Gravesen_addifclose(b, len, error = 0.001):
box = 0
for i in range(1,4):
box += pointdistance(b[i-1], b[i])
chord = pointdistance(b[0], b[3])
if (box - chord) > error:
first, second = beziersplitatt(b, 0.5)
Gravesen_addifclose(first, len, error)
Gravesen_addifclose(second, len, error)
else:
len[0] += (box / 2.0) + (chord / 2.0)
def bezierlengthGravesen(b, error = 0.001):
len = [0]
Gravesen_addifclose(b, len, error)
return len[0]
# balf = Bezier Arc Length Function
balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0
def balf(t):
retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2
return math.sqrt(retval)
def Simpson(f, a, b, n_limit, tolerance):
n = 2
multiplier = (b - a)/6.0
endsum = f(a) + f(b)
interval = (b - a)/2.0
asum = 0.0
bsum = f(a + interval)
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
est0 = 2.0 * est1
#print multiplier, endsum, interval, asum, bsum, est1, est0
while n < n_limit and abs(est1 - est0) > tolerance:
n *= 2
multiplier /= 2.0
interval /= 2.0
asum += bsum
bsum = 0.0
est0 = est1
for i in range(1, n, 2):
bsum += f(a + (i * interval))
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
#print multiplier, endsum, interval, asum, bsum, est1, est0
return est1
def bezierlengthSimpson(xxx_todo_changeme12, tolerance = 0.001):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme12
global balfax,balfbx,balfcx,balfay,balfby,balfcy
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
return Simpson(balf, 0.0, 1.0, 4096, tolerance)
def beziertatlength(xxx_todo_changeme13, l = 0.5, tolerance = 0.001):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme13
global balfax,balfbx,balfcx,balfay,balfby,balfcy
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
t = 1.0
tdiv = t
curlen = Simpson(balf, 0.0, t, 4096, tolerance)
targetlen = l * curlen
diff = curlen - targetlen
while abs(diff) > tolerance:
tdiv /= 2.0
if diff < 0:
t += tdiv
else:
t -= tdiv
curlen = Simpson(balf, 0.0, t, 4096, tolerance)
diff = curlen - targetlen
return t
#default bezier length method
bezierlength = bezierlengthSimpson
if __name__ == '__main__':
import timing
#print linebezierintersect(((,),(,)),((,),(,),(,),(,)))
#print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0)))
tol = 0.00000001
curves = [((0,0),(1,5),(4,5),(5,5)),
((0,0),(0,0),(5,0),(10,0)),
((0,0),(0,0),(5,1),(10,0)),
((-10,0),(0,0),(10,0),(10,10)),
((15,10),(0,0),(10,0),(-5,10))]
'''
for curve in curves:
timing.start()
g = bezierlengthGravesen(curve,tol)
timing.finish()
gt = timing.micro()
timing.start()
s = bezierlengthSimpson(curve,tol)
timing.finish()
st = timing.micro()
print g, gt
print s, st
'''
for curve in curves:
print(beziertatlength(curve,0.5))

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
from lyz_bezmisc import *
from lyz_ffgeom import *
def maxdist(xxx_todo_changeme):
((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = xxx_todo_changeme
p0 = Point(p0x,p0y)
p1 = Point(p1x,p1y)
p2 = Point(p2x,p2y)
p3 = Point(p3x,p3y)
s1 = Segment(p0,p3)
return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2))
def cspsubdiv(csp,flat):
for sp in csp:
subdiv(sp,flat)
def subdiv(sp,flat,i=1):
while i < len(sp):
p0 = sp[i-1][1]
p1 = sp[i-1][2]
p2 = sp[i][0]
p3 = sp[i][1]
b = (p0,p1,p2,p3)
m = maxdist(b)
if m <= flat:
i += 1
else:
one, two = beziersplitatt(b,0.5)
sp[i-1][2] = one[1]
sp[i][0] = two[2]
p = [one[2],one[3],two[1]]
sp[i:1] = [p]

View File

@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
cubicsuperpath.py
Copyright (C) 2005 Aaron Spike, aaron@ekips.org
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.
"""
import lyz_simplepath as simplepath
from math import *
def matprod(mlist):
prod=mlist[0]
for m in mlist[1:]:
a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0]
a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1]
a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0]
a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1]
prod=[[a00,a01],[a10,a11]]
return prod
def rotmat(teta):
return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]]
def applymat(mat, pt):
x=mat[0][0]*pt[0]+mat[0][1]*pt[1]
y=mat[1][0]*pt[0]+mat[1][1]*pt[1]
pt[0]=x
pt[1]=y
def norm(pt):
return sqrt(pt[0]*pt[0]+pt[1]*pt[1])
def ArcToPath(p1,params):
A=p1[:]
rx,ry,teta,longflag,sweepflag,x2,y2=params[:]
teta = teta*pi/180.0
B=[x2,y2]
if rx==0 or ry==0 or A==B:
return([[A[:],A[:],A[:]],[B[:],B[:],B[:]]])
mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta)))
applymat(mat, A)
applymat(mat, B)
k=[-(B[1]-A[1]),B[0]-A[0]]
d=k[0]*k[0]+k[1]*k[1]
k[0]/=sqrt(d)
k[1]/=sqrt(d)
d=sqrt(max(0,1-d/4))
if longflag==sweepflag:
d*=-1
O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]]
OA=[A[0]-O[0],A[1]-O[1]]
OB=[B[0]-O[0],B[1]-O[1]]
start=acos(OA[0]/norm(OA))
if OA[1]<0:
start*=-1
end=acos(OB[0]/norm(OB))
if OB[1]<0:
end*=-1
if sweepflag and start>end:
end +=2*pi
if (not sweepflag) and start<end:
end -=2*pi
NbSectors=int(abs(start-end)*2/pi)+1
dTeta=(end-start)/NbSectors
#v=dTeta*2/pi*0.552
#v=dTeta*2/pi*4*(sqrt(2)-1)/3
v = 4*tan(dTeta/4)/3
#if not sweepflag:
# v*=-1
p=[]
for i in range(0,NbSectors+1,1):
angle=start+i*dTeta
v1=[O[0]+cos(angle)-(-v)*sin(angle),O[1]+sin(angle)+(-v)*cos(angle)]
pt=[O[0]+cos(angle) ,O[1]+sin(angle) ]
v2=[O[0]+cos(angle)- v *sin(angle),O[1]+sin(angle)+ v *cos(angle)]
p.append([v1,pt,v2])
p[ 0][0]=p[ 0][1][:]
p[-1][2]=p[-1][1][:]
mat=matprod((rotmat(teta),[[rx,0],[0,ry]],rotmat(-teta)))
for pts in p:
applymat(mat, pts[0])
applymat(mat, pts[1])
applymat(mat, pts[2])
return(p)
def CubicSuperPath(simplepath):
csp = []
subpath = -1
subpathstart = []
last = []
lastctrl = []
for s in simplepath:
cmd, params = s
if cmd == 'M':
if last:
csp[subpath].append([lastctrl[:],last[:],last[:]])
subpath += 1
csp.append([])
subpathstart = params[:]
last = params[:]
lastctrl = params[:]
elif cmd == 'L':
csp[subpath].append([lastctrl[:],last[:],last[:]])
last = params[:]
lastctrl = params[:]
elif cmd == 'C':
csp[subpath].append([lastctrl[:],last[:],params[:2]])
last = params[-2:]
lastctrl = params[2:4]
elif cmd == 'Q':
q0=last[:]
q1=params[0:2]
q2=params[2:4]
x0= q0[0]
x1=1./3*q0[0]+2./3*q1[0]
x2= 2./3*q1[0]+1./3*q2[0]
x3= q2[0]
y0= q0[1]
y1=1./3*q0[1]+2./3*q1[1]
y2= 2./3*q1[1]+1./3*q2[1]
y3= q2[1]
csp[subpath].append([lastctrl[:],[x0,y0],[x1,y1]])
last = [x3,y3]
lastctrl = [x2,y2]
elif cmd == 'A':
arcp=ArcToPath(last[:],params[:])
arcp[ 0][0]=lastctrl[:]
last=arcp[-1][1]
lastctrl = arcp[-1][0]
csp[subpath]+=arcp[:-1]
elif cmd == 'Z':
csp[subpath].append([lastctrl[:],last[:],last[:]])
last = subpathstart[:]
lastctrl = subpathstart[:]
#append final superpoint
csp[subpath].append([lastctrl[:],last[:],last[:]])
return csp
def unCubicSuperPath(csp):
a = []
for subpath in csp:
if subpath:
a.append(['M',subpath[0][1][:]])
for i in range(1,len(subpath)):
a.append(['C',subpath[i-1][2][:] + subpath[i][0][:] + subpath[i][1][:]])
return a
def parsePath(d):
return CubicSuperPath(simplepath.parsePath(d))
def formatPath(p):
return simplepath.formatPath(unCubicSuperPath(p))

View File

@ -0,0 +1,571 @@
#!/usr/bin/env python3
'''
This file output script for Inkscape creates Laser Draw (LaserDRW) LYZ files.
File history:
0.1 Initial code (2/5/2017)
0.2 - Added support for rectangle, circle and ellipse (2/7/2017)
- Added option to automatically convert text to paths
0.3 - Fixed x,y translation when view box is used in SVG file for scaling (2/10/2017)
0.4 - Changed limits in resolution to 100 dpi minimum and 1000 dpi maximum (if you get an out of memory error in LaserDRW try reducing the resolution)
0.5 - Removed some messages that were not needed
- Fixed default resolution in inx files
0.6 - Made compatible with Python 3 and Inkscape 1.0
Copyright (C) 2017-2020 Scorch www.scorchworks.com
Derived from dxf_outlines.py by Aaron Spike and Alvin Penner
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
'''
import math
import tempfile, os, sys, shutil
from subprocess import Popen, PIPE
import zipfile
import re
import lyz_inkex as inkex
import lyz_simplestyle as simplestyle
import lyz_simpletransform as simpletransform
import lyz_cubicsuperpath as cubicsuperpath
import lyz_cspsubdiv as cspsubdiv
from lyz_library import LYZ_CLASS
## Subprocess timout stuff ######
from threading import Timer
def run_external(cmd, timeout_sec):
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
kill_proc = lambda p: p.kill()
timer = Timer(timeout_sec, kill_proc, [proc])
try:
timer.start()
stdout,stderr = proc.communicate()
finally:
timer.cancel()
##################################
class LYZExport(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
self.flatness = 0.01
self.lines =[]
self.Cut_Type = {}
self.Xsize=40
self.Ysize=40
self.margin = 2
self.PNG_DATA = None
self.png_area = "--export-area-page"
self.timout = 60 #timeout time for external calls to Inkscape in seconds
self.OptionParser.add_option("--area_select", type="string", default="page_area")
self.OptionParser.add_option("--cut_select", type="string", default="zip")
self.OptionParser.add_option("--resolution", type="int", default=1000)
self.OptionParser.add_option("--margin", type="float", default=2.00)
self.OptionParser.add_option("--inkscape_version", type="int", default=100)
self.OptionParser.add_option("--txt2paths", type="inkbool", default=False)
self.layers = ['0']
self.layer = '0'
self.layernames = []
self.PYTHON_VERSION = sys.version_info[0]
def stream_binary_data(self,filename):
# Change the format for STDOUT to binary to support
# writing the binary output file through STDOUT
if os.name == 'nt': #if sys.platform == "win32":
try:
import msvcrt
#msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
msvcrt.setmode(1, os.O_BINARY)
except:
pass
# Open the temporary file for reading
out = open(filename,'rb')
# Send the contents of the temp file to STDOUT
if self.PYTHON_VERSION < 3:
sys.stdout.write(out.read())
else:
sys.stdout.buffer.write(out.read())
out.close()
def output(self):
#create OS temp folder
self.tmp_dir = tempfile.mkdtemp()
if (self.cut_select=="image" ):
LYZ=LYZ_CLASS()
LYZ.setup_new_header()
LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
LYZ.set_margin(self.margin)
image_file = os.path.join(self.tmp_dir, "image.lyz")
LYZ.write_file(image_file)
if (self.cut_select=="all" ) or (self.cut_select=="zip" ):
LYZ=LYZ_CLASS()
LYZ.setup_new_header()
LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
for line in self.lines:
ID=line[7]
if (self.Cut_Type[ID]=="cut") or (self.Cut_Type[ID]=="engrave"):
LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
LYZ.set_margin(self.margin)
all_file = os.path.join(self.tmp_dir, "all.lyz")
LYZ.write_file(all_file)
if (self.cut_select=="raster" ) or (self.cut_select=="zip" ):
LYZ=LYZ_CLASS()
LYZ.setup_new_header()
LYZ.add_png(self.PNG_DATA,self.Xsize,self.Ysize)
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
LYZ.set_margin(self.margin)
raster_file = os.path.join(self.tmp_dir, "01_raster_engrave.lyz")
LYZ.write_file(raster_file)
if (self.cut_select=="vector_red" ) or (self.cut_select=="zip" ):
LYZ=LYZ_CLASS()
LYZ.setup_new_header()
for line in self.lines:
ID=line[7]
if (self.Cut_Type[ID]=="cut"):
LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
LYZ.set_margin(self.margin)
cut_file = os.path.join(self.tmp_dir, "03_vector_cut.lyz")
LYZ.write_file(cut_file)
if (self.cut_select=="vector_blue" ) or (self.cut_select=="zip" ):
LYZ=LYZ_CLASS()
LYZ.setup_new_header()
for line in self.lines:
ID=line[7]
if (self.Cut_Type[ID]=="engrave"):
LYZ.add_line(line[0],line[1],line[2],line[3],0.025)
LYZ.set_size(self.Xsize+self.margin,self.Ysize+self.margin)
LYZ.set_margin(self.margin)
engrave_file = os.path.join(self.tmp_dir, "02_vector_engrave.lyz")
LYZ.write_file(engrave_file)
if (self.cut_select=="image" ):
self.stream_binary_data(image_file)
if (self.cut_select=="all" ):
self.stream_binary_data(all_file)
if (self.cut_select=="raster" ):
self.stream_binary_data(raster_file)
if (self.cut_select=="vector_red" ):
self.stream_binary_data(cut_file)
if (self.cut_select=="vector_blue"):
self.stream_binary_data(engrave_file)
if (self.cut_select=="zip" ):
# Add image LYZ file? Encode zip file names?
zip_file = os.path.join(self.tmp_dir, "lyz_files.zip")
z = zipfile.ZipFile(zip_file, 'w')
z.write(all_file , os.path.basename(all_file) )
z.write(raster_file , os.path.basename(raster_file) )
z.write(cut_file , os.path.basename(cut_file) )
z.write(engrave_file, os.path.basename(engrave_file))
z.write(sys.argv[-1], "design.svg" )
z.close()
self.stream_binary_data(zip_file)
#Delete the temp folder and file
shutil.rmtree(self.tmp_dir)
def dxf_line(self,csp,pen_width=0.025,color=None,path_id="",layer="none"):
x1 = csp[0][0]
y1 = csp[0][1]
x2 = csp[1][0]
y2 = csp[1][1]
self.lines.append([x1,-y1,x2,-y2,layer,pen_width,color,path_id])
def colmod(self,r,g,b,path_id):
if (r,g,b) ==(255,0,0):
self.Cut_Type[path_id]="cut"
(r,g,b) = (255,255,255)
elif (r,g,b)==(0,0,255):
self.Cut_Type[path_id]="engrave"
(r,g,b) = (255,255,255)
else:
self.Cut_Type[path_id]="raster"
(r,g,b) = (0,0,0)
return '%02x%02x%02x' % (r,g,b)
def process_shape(self, node, mat):
rgb = (0,0,0)
path_id = node.get('id')
style = node.get('style')
self.Cut_Type[path_id]="raster" # Set default type to raster
color_props_fill = ('fill', 'stop-color', 'flood-color', 'lighting-color')
color_props_stroke = ('stroke',)
color_props = color_props_fill + color_props_stroke
#####################################################
## The following is ripped off from Coloreffect.py ##
#####################################################
if style:
declarations = style.split(';')
for i,decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, col) = parts
prop = prop.strip().lower()
#if prop in color_props:
if prop == 'stroke':
col= col.strip()
if simplestyle.isColor(col):
c=simplestyle.parseColor(col)
new_val='#'+self.colmod(c[0],c[1],c[2],path_id)
else:
new_val = col
if new_val != col:
declarations[i] = prop + ':' + new_val
node.set('style', ';'.join(declarations))
#####################################################
if node.tag == inkex.addNS('path','svg'):
d = node.get('d')
if not d:
return
p = cubicsuperpath.parsePath(d)
elif node.tag == inkex.addNS('rect','svg'):
x = float(node.get('x'))
y = float(node.get('y'))
width = float(node.get('width'))
height = float(node.get('height'))
#d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y, x+width,y+height, x,y+height)
#p = cubicsuperpath.parsePath(d)
rx = 0.0
ry = 0.0
if node.get('rx'):
rx=float(node.get('rx'))
if node.get('ry'):
ry=float(node.get('ry'))
if max(rx,ry) > 0.0:
if rx==0.0 or ry==0.0:
rx = max(rx,ry)
ry = rx
#msg = "rx = %f ry = %f " %(rx,ry)
#inkex.errormsg(msg)
L1 = "M %f,%f %f,%f " %(x+rx , y , x+width-rx , y )
C1 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width , y+ry )
L2 = "M %f,%f %f,%f " %(x+width , y+ry , x+width , y+height-ry)
C2 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+width-rx , y+height )
L3 = "M %f,%f %f,%f " %(x+width-rx , y+height , x+rx , y+height )
C3 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x , y+height-ry)
L4 = "M %f,%f %f,%f " %(x , y+height-ry, x , y+ry )
C4 = "A %f,%f 0 0 1 %f,%f" %(rx , ry , x+rx , y )
d = L1 + C1 + L2 + C2 + L3 + C3 + L4 + C4
else:
d = "M %f,%f %f,%f %f,%f %f,%f Z" %(x,y, x+width,y, x+width,y+height, x,y+height)
p = cubicsuperpath.parsePath(d)
elif node.tag == inkex.addNS('circle','svg'):
cx = float(node.get('cx') )
cy = float(node.get('cy'))
r = float(node.get('r'))
d = "M %f,%f A %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f Z" %(cx+r,cy, r,r,cx,cy+r, r,r,cx-r,cy, r,r,cx,cy-r, r,r,cx+r,cy)
p = cubicsuperpath.parsePath(d)
elif node.tag == inkex.addNS('ellipse','svg'):
cx = float(node.get('cx'))
cy = float(node.get('cy'))
rx = float(node.get('rx'))
ry = float(node.get('ry'))
d = "M %f,%f A %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f %f,%f 0 0 1 %f,%f Z" %(cx+rx,cy, rx,ry,cx,cy+ry, rx,ry,cx-rx,cy, rx,ry,cx,cy-ry, rx,ry,cx+rx,cy)
p = cubicsuperpath.parsePath(d)
else:
return
trans = node.get('transform')
if trans:
mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
simpletransform.applyTransformToPath(mat, p)
###################################################
## Break Curves down into small lines
###################################################
f = self.flatness
is_flat = 0
while is_flat < 1:
try:
cspsubdiv.cspsubdiv(p, f)
is_flat = 1
except IndexError:
break
except:
f += 0.1
if f>2 :
break
#something has gone very wrong.
###################################################
for sub in p:
for i in range(len(sub)-1):
s = sub[i]
e = sub[i+1]
self.dxf_line([s[1],e[1]],0.025,rgb,path_id)
def process_clone(self, node):
trans = node.get('transform')
x = node.get('x')
y = node.get('y')
mat = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
if trans:
mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
if x:
mat = simpletransform.composeTransform(mat, [[1.0, 0.0, float(x)], [0.0, 1.0, 0.0]])
if y:
mat = simpletransform.composeTransform(mat, [[1.0, 0.0, 0.0], [0.0, 1.0, float(y)]])
# push transform
if trans or x or y:
self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], mat))
# get referenced node
refid = node.get(inkex.addNS('href','xlink'))
refnode = self.getElementById(refid[1:])
if refnode is not None:
if refnode.tag == inkex.addNS('g','svg'):
self.process_group(refnode)
elif refnode.tag == inkex.addNS('use', 'svg'):
self.process_clone(refnode)
else:
self.process_shape(refnode, self.groupmat[-1])
# pop transform
if trans or x or y:
self.groupmat.pop()
def process_group(self, group):
if group.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
style = group.get('style')
if style:
style = simplestyle.parseStyle(style)
if style.has_key('display'):
if style['display'] == 'none' and self.options.layer_option and self.options.layer_option=='visible':
return
layer = group.get(inkex.addNS('label', 'inkscape'))
layer = layer.replace(' ', '_')
if layer in self.layers:
self.layer = layer
trans = group.get('transform')
if trans:
self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans)))
for node in group:
if node.tag == inkex.addNS('g','svg'):
self.process_group(node)
elif node.tag == inkex.addNS('use', 'svg'):
self.process_clone(node)
else:
self.process_shape(node, self.groupmat[-1])
if trans:
self.groupmat.pop()
def Make_PNG(self):
#create OS temp folder
tmp_dir = tempfile.mkdtemp()
svg_temp_file = os.path.join(tmp_dir, "LYZimage.svg")
png_temp_file = os.path.join(tmp_dir, "LYZpngdata.png")
dpi = "%d" %(self.options.resolution)
if self.inkscape_version >= 100:
cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, \
"--export-background","rgb(255, 255, 255)","--export-background-opacity", \
"255" ,"--export-type=png", "--export-filename=%s" %(png_temp_file), svg_temp_file ]
else:
cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, \
"--export-background","rgb(255, 255, 255)","--export-background-opacity", \
"255" ,"--export-png", png_temp_file, svg_temp_file ]
if (self.cut_select=="raster") or (self.cut_select=="all") or (self.cut_select=="zip"):
self.document.write(svg_temp_file)
#cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
#p = Popen(cmd, stdout=PIPE, stderr=PIPE)
#stdout, stderr = p.communicate()
run_external(cmd, self.timout)
else:
shutil.copyfile(sys.argv[-1], svg_temp_file)
#cmd = [ "inkscape", self.png_area, "--export-dpi", dpi, "--export-background","rgb(255, 255, 255)","--export-background-opacity", "255" ,"--export-png", png_temp_file, svg_temp_file ]
#p = Popen(cmd, stdout=PIPE, stderr=PIPE)
#stdout, stderr = p.communicate()
run_external(cmd, self.timout)
try:
with open(png_temp_file, 'rb') as f:
self.PNG_DATA = f.read()
except:
inkex.errormsg("PNG generation timed out.\nTry saving again.\n\n")
#Delete the temp folder and any files
shutil.rmtree(tmp_dir)
def unit2mm(self, string):
# Returns mm given a string representation of units in another system
# a dictionary of unit to user unit conversion factors
uuconv = {'in': 25.4,
'pt': 25.4/72.0,
'px': 25.4/self.inkscape_dpi,
'mm': 1.0,
'cm': 10.0,
'm' : 1000.0,
'km': 1000.0*1000.0,
'pc': 25.4/6.0,
'yd': 25.4*12*3,
'ft': 25.4*12}
unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
p = param.match(string)
u = unit.search(string)
if p:
retval = float(p.string[p.start():p.end()])
else:
inkex.errormsg("Size was not determined returning zero value")
retval = 0.0
if u:
retunit = u.string[u.start():u.end()]
else:
inkex.errormsg("units not understood assuming px at %d dpi" %(self.inkscape_dpi))
retunit = 'px'
try:
return retval * uuconv[retunit]
except KeyError:
return retval
def effect(self):
msg = ""
#area_select = self.options.area_select # "page_area", "object_area"
area_select = "page_area"
self.cut_select = self.options.cut_select # "vector_red", "vector_blue", "raster", "all", "image", "Zip"
self.margin = self.options.margin # float value
#self.inkscape_dpi = self.options.inkscape_dpi # float value
self.inkscape_version = self.options.inkscape_version # float value
self.txt2paths = self.options.txt2paths # boolean Value
if self.inkscape_version > 91:
self.inkscape_dpi = 96
else:
self.inkscape_dpi = 90
if (self.txt2paths):
#create OS temp folder
tmp_dir = tempfile.mkdtemp()
txt2path_file = os.path.join(tmp_dir, "txt2path.svg")
if self.inkscape_version >= 100:
cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg", "--export-filename=%s" %(txt2path_file), sys.argv[-1] ]
else:
cmd = [ "inkscape", "--export-text-to-path","--export-plain-svg",txt2path_file, sys.argv[-1] ]
run_external(cmd, self.timout)
self.document.parse(txt2path_file)
#Delete the temp folder and any files
shutil.rmtree(tmp_dir)
h_uu = self.unittouu(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
w_uu = self.unittouu(self.document.getroot().xpath('@width' , namespaces=inkex.NSS)[0])
h_mm = self.unit2mm(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0])
w_mm = self.unit2mm(self.document.getroot().xpath('@width', namespaces=inkex.NSS)[0])
try:
view_box_str = self.document.getroot().xpath('@viewBox', namespaces=inkex.NSS)[0]
view_box_list = view_box_str.split(' ')
Wpix = float(view_box_list[2])
Hpix = float(view_box_list[3])
scale = h_mm/Hpix
Dx = float(view_box_list[0]) / ( float(view_box_list[2])/w_mm )
Dy = float(view_box_list[1]) / ( float(view_box_list[3])/h_mm )
except:
#inkex.errormsg("Using Default Inkscape Scale")
scale = 25.4/self.inkscape_dpi
Dx = 0
Dy = 0
for node in self.document.getroot().xpath('//svg:g', namespaces=inkex.NSS):
if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
layer = node.get(inkex.addNS('label', 'inkscape'))
self.layernames.append(layer.lower())
# if self.options.layer_name and self.options.layer_option and self.options.layer_option=='name' and not layer.lower() in self.options.layer_name:
# continue
layer = layer.replace(' ', '_')
if layer and not layer in self.layers:
self.layers.append(layer)
#self.groupmat = [[[scale, 0.0, 0.0], [0.0, -scale, h_mm]]]
self.groupmat = [[[scale, 0.0, 0.0-Dx],
[0.0 , -scale, h_mm+Dy]]]
#doc = self.document.getroot()
self.process_group(self.document.getroot())
#################################################
# msg = msg + self.getDocumentUnit() + "\n"
# msg = msg + "scale = %f\n" %(scale)
msg = msg + "Height(mm)= %f\n" %(h_mm)
msg = msg + "Width (mm)= %f\n" %(w_mm)
# msg = msg + "h_uu = %f\n" %(h_uu)
# msg = msg + "w_uu = %f\n" %(w_uu)
#inkex.errormsg(msg)
if (area_select=="object_area"):
self.png_area = "--export-area-drawing"
xmin= self.lines[0][0]+0.0
xmax= self.lines[0][0]+0.0
ymin= self.lines[0][1]+0.0
ymax= self.lines[0][1]+0.0
for line in self.lines:
x1= line[0]
y1= line[1]
x2= line[2]
y2= line[3]
xmin = min(min(xmin,x1),x2)
ymin = min(min(ymin,y1),y2)
xmax = max(max(xmax,x1),x2)
ymax = max(max(ymax,y1),y2)
else:
self.png_area = "--export-area-page"
xmin= 0.0
xmax= w_mm
ymin= -h_mm
ymax= 0.0
self.Xsize=xmax-xmin
self.Ysize=ymax-ymin
Xcenter=(xmax+xmin)/2.0
Ycenter=(ymax+ymin)/2.0
for ii in range(len(self.lines)):
self.lines[ii][0] = self.lines[ii][0]-Xcenter
self.lines[ii][1] = self.lines[ii][1]-Ycenter
self.lines[ii][2] = self.lines[ii][2]-Xcenter
self.lines[ii][3] = self.lines[ii][3]-Ycenter
if (self.cut_select=="raster") or \
(self.cut_select=="all" ) or \
(self.cut_select=="image" ) or \
(self.cut_select=="zip" ):
self.Make_PNG()
LYZExport().affect()

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>LaserDraw Export (zip)</name>
<id>fablabchemnitz.de.laserdraw_export.zip</id>
<!--
<param name="area_select" type="optiongroup" appearance="combo" gui-text="Area: ">
<option value="page_area">Page Area (use for engrave and cut)</option>
<option value="object_area">Object Area (for single operations only)</option>
</param>
-->
<label>Formatting can be used to break a design into separate cutting and engraving files. Red lines indicate cutting, blue lines indicate engraving, black indicates raster engraving.</label>
<!--
<param name="cut_select" type="optiongroup" appearance="combo" gui-text="Type: ">
<option value="vector_red"> Vector Cuts (export red lines) </option>
<option value="vector_blue"> Vector Engrave (export blue lines) </option>
<option value="raster" > Raster Engrave (export everything else) </option>
<option value="all" > All (export vectors and raster engrave to one file) </option>
<option value="image" > Image (export all items as raster) </option>
</param>
-->
<param name="resolution" type="int" min="100" max="1000" gui-text="Raster Image Resolution">1000</param>
<param name="margin" type="float" precision="1" min="0" max="9999" gui-text="Laser Draw Margin Size">2.0</param>
<param name="txt2paths" type="bool" gui-text="Convert Text to Paths">false</param>
<!--
<param name="texthelp" type="description">
Depending on your Inkscape version the internal resolution need to be 72, 90 or 96 to get the properly scaled output.</param>
<param name="inkscape_version" type="int" min="72" max="96" gui-text="Inkscape Internal Resolution">1</param>
-->
<param name="inkscape_version" type="optiongroup" appearance="combo" gui-text="Inkscape Version:">
<option value="100">1.00 or Newer</option>
<option value="92">0.92 to 0.99</option>
<option value="91">0.91 or Older</option>
</param>
<output>
<extension>.zip</extension>
<mimetype>application/x-zip</mimetype>
<filetypename>Laser Draw LYZ (ZIP)(*.zip)</filetypename>
<filetypetooltip>LaserDraw LYZ Output Zipped</filetypetooltip>
<dataloss>true</dataloss>
</output>
<script>
<command location="inx" interpreter="python">lyz_export.py</command>
<helper_extension>org.inkscape.output.svg.inkscape</helper_extension>
</script>
</inkscape-extension>

View File

@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
ffgeom.py
Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org
This file is part of FretFind 2-D.
FretFind 2-D 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.
FretFind 2-D 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 FretFind 2-D; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
import math
try:
NaN = float('NaN')
except ValueError:
PosInf = 1e300000
NaN = PosInf/PosInf
class Point:
precision = 5
def __init__(self, x, y):
self.__coordinates = {'x' : float(x), 'y' : float(y)}
def __getitem__(self, key):
return self.__coordinates[key]
def __setitem__(self, key, value):
self.__coordinates[key] = float(value)
def __repr__(self):
return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision))
def copy(self):
return Point(self['x'],self['y'])
def translate(self, x, y):
self['x'] += x
self['y'] += y
def move(self, x, y):
self['x'] = float(x)
self['y'] = float(y)
class Segment:
def __init__(self, e0, e1):
self.__endpoints = [e0, e1]
def __getitem__(self, key):
return self.__endpoints[key]
def __setitem__(self, key, value):
self.__endpoints[key] = value
def __repr__(self):
return repr(self.__endpoints)
def copy(self):
return Segment(self[0],self[1])
def translate(self, x, y):
self[0].translate(x,y)
self[1].translate(x,y)
def move(self,e0,e1):
self[0] = e0
self[1] = e1
def delta_x(self):
return self[1]['x'] - self[0]['x']
def delta_y(self):
return self[1]['y'] - self[0]['y']
#alias functions
run = delta_x
rise = delta_y
def slope(self):
if self.delta_x() != 0:
return self.delta_x() / self.delta_y()
return NaN
def intercept(self):
if self.delta_x() != 0:
return self[1]['y'] - (self[0]['x'] * self.slope())
return NaN
def distanceToPoint(self, p):
s2 = Segment(self[0],p)
c1 = dot(s2,self)
if c1 <= 0:
return Segment(p,self[0]).length()
c2 = dot(self,self)
if c2 <= c1:
return Segment(p,self[1]).length()
return self.perpDistanceToPoint(p)
def perpDistanceToPoint(self, p):
len = self.length()
if len == 0: return NaN
return math.fabs(((self[1]['x'] - self[0]['x']) * (self[0]['y'] - p['y'])) - \
((self[0]['x'] - p['x']) * (self[1]['y'] - self[0]['y']))) / len
def angle(self):
return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180
def length(self):
return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2))
def pointAtLength(self, len):
if self.length() == 0: return Point(NaN, NaN)
ratio = len / self.length()
x = self[0]['x'] + (ratio * self.delta_x())
y = self[0]['y'] + (ratio * self.delta_y())
return Point(x, y)
def pointAtRatio(self, ratio):
if self.length() == 0: return Point(NaN, NaN)
x = self[0]['x'] + (ratio * self.delta_x())
y = self[0]['y'] + (ratio * self.delta_y())
return Point(x, y)
def createParallel(self, p):
return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p)
def intersect(self, s):
return intersectSegments(self, s)
def intersectSegments(s1, s2):
x1 = s1[0]['x']
x2 = s1[1]['x']
x3 = s2[0]['x']
x4 = s2[1]['x']
y1 = s1[0]['y']
y2 = s1[1]['y']
y3 = s2[0]['y']
y4 = s2[1]['y']
denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1))
num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3))
num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3))
num = num1
if denom != 0:
x = x1 + ((num / denom) * (x2 - x1))
y = y1 + ((num / denom) * (y2 - y1))
return Point(x, y)
return Point(NaN, NaN)
def dot(s1, s2):
return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y()

View File

@ -0,0 +1,399 @@
#!/usr/bin/env python3
"""
inkex.py
A helper module for creating Inkscape extensions
Copyright (C) 2005,2010 Aaron Spike <aaron@ekips.org> and contributors
Contributors:
Aurelio A. Heckert <aurium(a)gmail.com>
Bulia Byak <buliabyak@users.sf.net>
Nicolas Dufour, nicoduf@yahoo.fr
Peter J. R. Moulder <pjrm@users.sourceforge.net>
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.
"""
import copy
import gettext
import optparse
import os
import random
import re
import sys
from math import *
from lxml import etree
# a dictionary of all of the xmlns prefixes in a standard inkscape doc
NSS = {
u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
u'cc' :u'http://creativecommons.org/ns#',
u'ccOLD' :u'http://web.resource.org/cc/',
u'svg' :u'http://www.w3.org/2000/svg',
u'dc' :u'http://purl.org/dc/elements/1.1/',
u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape',
u'xlink' :u'http://www.w3.org/1999/xlink',
u'xml' :u'http://www.w3.org/XML/1998/namespace'
}
def localize():
domain = 'inkscape'
if sys.platform.startswith('win'):
import locale
current_locale, encoding = locale.getdefaultlocale()
os.environ['LANG'] = current_locale
try:
localdir = os.environ['INKSCAPE_LOCALEDIR']
trans = gettext.translation(domain, localdir, [current_locale], fallback=True)
except KeyError:
trans = gettext.translation(domain, fallback=True)
elif sys.platform.startswith('darwin'):
try:
localdir = os.environ['INKSCAPE_LOCALEDIR']
trans = gettext.translation(domain, localdir, fallback=True)
except KeyError:
try:
localdir = os.environ['PACKAGE_LOCALE_DIR']
trans = gettext.translation(domain, localdir, fallback=True)
except KeyError:
trans = gettext.translation(domain, fallback=True)
else:
try:
localdir = os.environ['PACKAGE_LOCALE_DIR']
trans = gettext.translation(domain, localdir, fallback=True)
except KeyError:
trans = gettext.translation(domain, fallback=True)
#sys.stderr.write(str(localdir) + "\n")
trans.install()
def debug(what):
sys.stderr.write(str(what) + "\n")
return what
def errormsg(msg):
"""Intended for end-user-visible error messages.
(Currently just writes to stderr with an appended newline, but could do
something better in future: e.g. could add markup to distinguish error
messages from status messages or debugging output.)
Note that this should always be combined with translation:
import inkex
...
inkex.errormsg("This extension requires two selected paths.")
"""
#if isinstance(msg, unicode):
# sys.stderr.write(msg.encode("utf-8") + "\n")
#else:
# sys.stderr.write((unicode(msg, "utf-8", errors='replace') + "\n").encode("utf-8"))
print(msg)
def are_near_relative(a, b, eps):
return (a-b <= a*eps) and (a-b >= -a*eps)
def check_inkbool(option, opt, value):
if str(value).capitalize() == 'True':
return True
elif str(value).capitalize() == 'False':
return False
else:
raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value))
def addNS(tag, ns=None):
val = tag
if ns is not None and len(ns) > 0 and ns in NSS and len(tag) > 0 and tag[0] != '{':
val = "{%s}%s" % (NSS[ns], tag)
return val
class InkOption(optparse.Option):
TYPES = optparse.Option.TYPES + ("inkbool",)
TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
TYPE_CHECKER["inkbool"] = check_inkbool
class Effect:
"""A class for creating Inkscape SVG Effects"""
def __init__(self, *args, **kwargs):
self.document = None
self.original_document = None
self.ctx = None
self.selected = {}
self.doc_ids = {}
self.options = None
self.args = None
self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile", option_class=InkOption)
self.OptionParser.add_option("--id", dest="ids", default=[], help="id attribute of object to manipulate")
self.OptionParser.add_option("--selected-nodes", dest="selected_nodes", default=[], help="id:subpath:position of selected nodes, if any")
# TODO write a parser for this
def effect(self):
"""Apply some effects on the document. Extensions subclassing Effect
must override this function and define the transformations
in it."""
pass
def getoptions(self,args=sys.argv[1:]):
"""Collect command line arguments"""
self.options, self.args = self.OptionParser.parse_args(args)
def parse(self, filename=None, encoding=None):
"""Parse document in specified file or on stdin"""
# First try to open the file from the function argument
if filename is not None:
try:
stream = open(filename, 'r')
except IOError:
errormsg("Unable to open specified file: %s" % filename)
sys.exit()
# If it wasn't specified, try to open the file specified as
# an object member
elif self.svg_file is not None:
try:
stream = open(self.svg_file, 'r')
except IOError:
errormsg("Unable to open object member file: %s" % self.svg_file)
sys.exit()
# Finally, if the filename was not specified anywhere, use
# standard input stream
else:
stream = sys.stdin
if encoding == None:
p = etree.XMLParser(huge_tree=True, recover=True)
else:
p = etree.XMLParser(huge_tree=True, recover=True, encoding=encoding)
self.document = etree.parse(stream, parser=p)
self.original_document = copy.deepcopy(self.document)
stream.close()
# defines view_center in terms of document units
def getposinlayer(self):
#defaults
self.current_layer = self.document.getroot()
self.view_center = (0.0, 0.0)
layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS)
if layerattr:
layername = layerattr[0]
layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS)
if layer:
self.current_layer = layer[0]
xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS)
yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS)
if xattr and yattr:
x = self.unittouu(xattr[0] + 'px')
y = self.unittouu(yattr[0] + 'px')
doc_height = self.unittouu(self.getDocumentHeight())
if x and y:
self.view_center = (float(x), doc_height - float(y))
# FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape
def getselected(self):
"""Collect selected nodes"""
for i in self.options.ids:
path = '//*[@id="%s"]' % i
for node in self.document.xpath(path, namespaces=NSS):
self.selected[i] = node
def getElementById(self, id):
path = '//*[@id="%s"]' % id
el_list = self.document.xpath(path, namespaces=NSS)
if el_list:
return el_list[0]
else:
return None
def getParentNode(self, node):
for parent in self.document.getiterator():
if node in parent.getchildren():
return parent
def getdocids(self):
docIdNodes = self.document.xpath('//@id', namespaces=NSS)
for m in docIdNodes:
self.doc_ids[m] = 1
def getNamedView(self):
return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0]
def createGuide(self, posX, posY, angle):
atts = {
'position': str(posX)+','+str(posY),
'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle)))
}
guide = etree.SubElement(
self.getNamedView(),
addNS('guide','sodipodi'), atts)
return guide
def output(self):
"""Serialize document into XML on stdout"""
original = etree.tostring(self.original_document)
result = etree.tostring(self.document)
if original != result:
self.document.write(sys.stdout)
def affect(self, args=sys.argv[1:], output=True):
"""Affect an SVG document with a callback effect"""
self.svg_file = args[-1]
localize()
self.getoptions(args)
self.parse()
self.getposinlayer()
self.getselected()
self.getdocids()
self.effect()
if output:
self.output()
def uniqueId(self, old_id, make_new_id=True):
new_id = old_id
if make_new_id:
while new_id in self.doc_ids:
new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
self.doc_ids[new_id] = 1
return new_id
def xpathSingle(self, path):
try:
retval = self.document.xpath(path, namespaces=NSS)[0]
except:
errormsg("No matching node for expression: %s" % path)
retval = None
return retval
# a dictionary of unit to user unit conversion factors
__uuconv = {'in': 96.0, 'pt': 1.33333333333, 'px': 1.0, 'mm': 3.77952755913, 'cm': 37.7952755913,
'm': 3779.52755913, 'km': 3779527.55913, 'pc': 16.0, 'yd': 3456.0, 'ft': 1152.0}
# Fault tolerance for lazily defined SVG
def getDocumentWidth(self):
width = self.document.getroot().get('width')
if width:
return width
else:
viewbox = self.document.getroot().get('viewBox')
if viewbox:
return viewbox.split()[2]
else:
return '0'
# Fault tolerance for lazily defined SVG
def getDocumentHeight(self):
"""Returns a string corresponding to the height of the document, as
defined in the SVG file. If it is not defined, returns the height
as defined by the viewBox attribute. If viewBox is not defined,
returns the string '0'."""
height = self.document.getroot().get('height')
if height:
return height
else:
viewbox = self.document.getroot().get('viewBox')
if viewbox:
return viewbox.split()[3]
else:
return '0'
def getDocumentUnit(self):
"""Returns the unit used for in the SVG document.
In the case the SVG document lacks an attribute that explicitly
defines what units are used for SVG coordinates, it tries to calculate
the unit from the SVG width and viewBox attributes.
Defaults to 'px' units."""
svgunit = 'px' # default to pixels
svgwidth = self.getDocumentWidth()
viewboxstr = self.document.getroot().get('viewBox')
if viewboxstr:
unitmatch = re.compile('(%s)$' % '|'.join(self.__uuconv.keys()))
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
p = param.match(svgwidth)
u = unitmatch.search(svgwidth)
width = 100 # default
viewboxwidth = 100 # default
svgwidthunit = 'px' # default assume 'px' unit
if p:
width = float(p.string[p.start():p.end()])
else:
errormsg("SVG Width not set correctly! Assuming width = 100")
if u:
svgwidthunit = u.string[u.start():u.end()]
viewboxnumbers = []
for t in viewboxstr.split():
try:
viewboxnumbers.append(float(t))
except ValueError:
pass
if len(viewboxnumbers) == 4: # check for correct number of numbers
viewboxwidth = viewboxnumbers[2]
svgunitfactor = self.__uuconv[svgwidthunit] * width / viewboxwidth
# try to find the svgunitfactor in the list of units known. If we don't find something, ...
eps = 0.01 # allow 1% error in factor
for key in self.__uuconv:
if are_near_relative(self.__uuconv[key], svgunitfactor, eps):
# found match!
svgunit = key
return svgunit
def unittouu(self, string):
"""Returns userunits given a string representation of units in another system"""
unit = re.compile('(%s)$' % '|'.join(self.__uuconv.keys()))
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
p = param.match(string)
u = unit.search(string)
if p:
retval = float(p.string[p.start():p.end()])
else:
retval = 0.0
if u:
try:
return retval * (self.__uuconv[u.string[u.start():u.end()]] / self.__uuconv[self.getDocumentUnit()])
except KeyError:
pass
else: # default assume 'px' unit
return retval / self.__uuconv[self.getDocumentUnit()]
return retval
def uutounit(self, val, unit):
return val / (self.__uuconv[unit] / self.__uuconv[self.getDocumentUnit()])
def addDocumentUnit(self, value):
"""Add document unit when no unit is specified in the string """
try:
float(value)
return value + self.getDocumentUnit()
except ValueError:
return value

View File

@ -0,0 +1,441 @@
#!/usr/bin/env python3
'''
This script reads and writes Laser Draw (LaserDRW) LYZ files.
File history:
0.1 Initial code (2/5/2017)
Copyright (C) 2017 Scorch www.scorchworks.com
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
'''
import struct
import sys
import os
from time import time
from shutil import copyfile
##############################################################################
def show(byte_in):
print("%03d" %ord(byte_in))
def possible_values(loc,len,type,bf):
cur= bf.tell()
vals=""
if type=='d' or type=='Q' or type=='>d' or type=='>Q':
tl=8
elif type=='i' or type=='f' or type=='l' or type=='L' or type=='I':
tl=4
elif type=='>i' or type=='>f' or type=='>l' or type=='>L' or type=='>I':
tl=4
elif type=='h' or type=='H' or type=='>h' or type=='>H':
tl=2
for i in range(tl):
for j in range(i,(len-tl+1),tl):
bf.seek(loc+j)
vals = vals + "\t"+ str( struct.unpack(type,bf.read(tl))[0] )
vals = vals+"\n"
bf.seek(cur)
return vals
##############################################################################
class LYZ_CLASS:
def __init__(self):
# DEFINE HEADER FIELDS
self.header_fields = []
self.header_data = []
self.feature_list = []
self.left_over = ""
self.EOF = ""
########################## Description ,location,length,type,default value
self.header_fields.append(["EXTENSION" , 9999999, 4, 't', ".LYZ" ]) #0
self.header_fields.append(["LENGTH" , 9999999, 4, 'i', 221 ]) #1
self.header_fields.append(["N FEATURES" , 9999999, 4, 'i', 0 ]) #2
self.header_fields.append(["?A(4)" , 9999999, 4, 'i', 0 ]) #3
self.header_fields.append(["CREATOR" , 9999999, 50, 't',"Creater: LaserDraw.exe(Lihuiyu software Co., Ltd.)"]) #4
self.header_fields.append(["?B(14)" , 9999999, 14, 'z',[0,0,0,0,0,0,0,0,0,2,0,0,0,128] ]) #5
self.header_fields.append(["DESC" , 9999999, 37, 't',"Description: LaserDraw Graphics File."]) #6
self.header_fields.append(["?C(1)" , 9999999, 1, 'z', [0] ]) #7
self.header_fields.append(["?D(1)" , 9999999, 1, 'z', [0] ]) #8
self.header_fields.append(["Time(8)" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ]) #9
self.header_fields.append(["TIME(8)" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ]) #10
self.header_fields.append(["?G(8)" , 9999999, 4, 'z', [0,0,0,0] ]) #11
self.header_fields.append(["?H(8)" , 9999999, 4, 'z', [0,0,0,0] ]) #12
self.header_fields.append(["?I(18)" , 9999999, 18, 'z', [176,8,210,125,0,65,206,0,0,19,17,126,0,36,35,23,0,2] ]) #13
self.header_fields.append(["OFFSET" , 9999999, 8, 'd', 0.0 ]) #14 #was 84
self.header_fields.append(["X SIZE" , 9999999, 8, 'd', 42.0 ]) #15
self.header_fields.append(["Y SIZE" , 9999999, 8, 'd', 42.0 ]) #16
self.header_fields.append(["BORDER1" , 9999999, 8, 'd', 1.0 ]) #17
self.header_fields.append(["BORDER2" , 9999999, 8, 'd', 1.0 ]) #18
self.header_fields.append(["BORDER3" , 9999999, 8, 'd', 1.0 ]) #19
self.header_fields.append(["BORDER4" , 9999999, 8, 'd', 1.0 ]) #20
# DEFINE FEATURE FIELDS
self.feature_fields=[]
########################## Description ,location,length,type,default value
self.feature_fields.append(["?a(4)" , 9999999, 4, 'i', 0 ]) #0
self.feature_fields.append(["SHAPE TYPE", 9999999, 1, 'b', 10 ]) #1
#SHAPE TYPE NUMBERS
#0, circle
#1 square
#2 Square Rounded Corners
#3 Square Bevel Corners
#4 triangle
#5 diamond
#8 Star
#10 line
#12 PNG
#22 line text
self.feature_fields.append(["AC Density", 9999999, 4, 'z', [75,0,0,0] ]) #2 [ACdensity,color 0 or 8, ?,?]
self.feature_fields.append(["?b(1)" , 9999999, 1, 'z', [134] ]) #3 #solid fill 134
self.feature_fields.append(["AC cnt" , 9999999, 1, 'z', [2] ]) #4 This needs to be 2 for lines
self.feature_fields.append(["?c(1)" , 9999999, 1, 'z', [0] ]) #5
self.feature_fields.append(["?d(1)" , 9999999, 1, 'z', [6] ]) #6
self.feature_fields.append(["?e(3)" , 9999999, 3, 'z', [0 ,0 ,0 ] ]) #7
self.feature_fields.append(["?f(4)" , 9999999, 4, 'i', 16 ]) #8
self.feature_fields.append(["ZOOM" , 9999999, 8, 'd', 96 ]) #9
self.feature_fields.append(["?g(8)" , 9999999, 8, 'd', 0 ]) #10
self.feature_fields.append(["?h(8)" , 9999999, 8, 'd', 0 ]) #11
self.feature_fields.append(["?i(8)" , 9999999, 8, 'd', 0 ]) #12
self.feature_fields.append(["?j(8)" , 9999999, 8, 'd', 0 ]) #13
self.feature_fields.append(["X cent Loc", 9999999, 8, 'd', 0 ]) #14 To the Right of the center of the laser area
self.feature_fields.append(["Y cent Loc", 9999999, 8, 'd', 0 ]) #15 Down from the center of the laser area
self.feature_fields.append(["Width" , 9999999, 8, 'd', 0 ]) #16
self.feature_fields.append(["Height" , 9999999, 8, 'd', 0 ]) #17
self.feature_fields.append(["Pen Width" , 9999999, 8, 'd', 0.025 ]) #18
self.feature_fields.append(["AC Line" , 9999999, 8, 'd', 0.127 ]) #19
self.feature_fields.append(["Rot(deg)" , 9999999, 8, 'd', 0 ]) #20
self.feature_fields.append(["Corner Rad", 9999999, 8, 'd', 0 ]) #21
self.feature_fields.append(["?k(8)" , 9999999, 8, 'd', 0 ]) #22
self.feature_fields.append(["?l(8)" , 9999999, 8, 'd', 0 ]) #23
self.feature_fields.append(["?m(8)" , 9999999, 8, 'd', 0 ]) #24
self.feature_fields.append(["?n(4)" , 9999999, 4, 'i', 4 ]) #25
self.feature_fields.append(["?o(4)" , 9999999, 4, 'i', 0 ]) #26
self.feature_fields.append(["?p(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #27
self.feature_fields.append(["?q(4)" , 9999999, 4, 'z', [255,255,255,0 ] ]) #28
self.feature_fields.append(["?r(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #29
self.feature_fields.append(["string_len", 9999999, 4, 'i', 0 ]) #30
self.feature_fields.append(["filename" , 9999999, 4, 'x', "\000" ]) #31
self.feature_fields.append(["?u(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #32
self.feature_fields.append(["ACtexture1", 9999999, 4, 'z', [255,255,255,255] ]) #33 [0 ,0 ,0 ,0 ]
self.feature_fields.append(["ACtexture2", 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #34
self.feature_fields.append(["?v(4)" , 9999999, 4, 'z', [0 ,0 ,0 ,0 ] ]) #35
self.feature_fields.append(["?w(4)" , 9999999, 4, 'z', [2 ,0 ,0 ,0 ] ]) #36
self.feature_fields.append(["data length", 9999999, 4, 'i', 2 ]) #37 needs to be 2 for line
self.feature_appendix = []
for i in range(13):
self.feature_appendix.append([])
## Appendix values for Line
self.feature_appendix[10].append(["line X1", 9999999, 4, 'i', -10000 ]) #position as 1000*value
self.feature_appendix[10].append(["line Y1", 9999999, 4, 'i', -10000 ]) #position as 1000*value
self.feature_appendix[10].append(["line X2", 9999999, 4, 'i', 10000 ]) #position as 1000*value
self.feature_appendix[10].append(["line Y2", 9999999, 4, 'i', 10000 ]) #position as 1000*value
self.feature_appendix[10].append(["lineEND", 9999999, 4, 'i', 0 ])
##Appendix values for PNG
self.feature_appendix[12].append(["PNGdata", 9999999, 0, 't', "" ])
self.feature_appendix[12].append(["PNGend" , 9999999, 8, 'z', [0,0,0,0,0,0,0,0] ])
def lyz_read(self,loc,len,type,bf):
#try:
if 1==1:
#bf.seek(loc)
if type=='t':
data = bf.read(len)
elif type == 'z':
data = []
for i in range(len):
data.append(ord(bf.read(1)))
elif type == 'x':
data = ""
for i in range(0,len,4):
data_temp = bf.read(4)
data = data + data_temp[0]
else:
data = struct.unpack(type, bf.read(len))[0]
return data
#except:
# print("Error Reading data (lyz_read)")
# return []
def lyz_write(self,data,type,bf):
#print("type,data: ",type,data)
if type=='t':
#print("data:",data)
#bf.write(data)
try:
bf.write(data)
except:
bf.write(data.encode())
elif type == 'z':
for i in range(len(data)):
#bf.write(chr(data[i]))
bf.write(struct.pack('B',data[i]))
elif type == 'x':
for char in data:
bf.write(char.encode())
bf.write(struct.pack('B',0))
bf.write(struct.pack('B',0))
bf.write(struct.pack('B',0))
else:
bf.write(struct.pack(type,data))
def read_header(self,f):
self.header_data=[]
for line in self.header_fields:
#pos = line[1]
len = line[2]
typ = line[3]
self.header_data.append(self.lyz_read(None,len,typ,f))
def read_feature(self,f):
feature_data=[]
for i in range(len(self.feature_fields)):
length = self.feature_fields[i][2]
typ = self.feature_fields[i][3]
if i==31 and feature_data[1]==12:
string_length = feature_data[-1]*4
feature_data.append(self.lyz_read(None,string_length,typ,f))
else:
feature_data.append(self.lyz_read(None,length,typ,f))
#if i==30 and feature_data[1]==12:
# self.feature_fields[i+1][2] = feature_data[-1]*4
feat_type = feature_data[1]
if feat_type==10 or feat_type==12:
for i in range(len(self.feature_appendix[feat_type])):
if feat_type==12 and i==0:
length = feature_data[-1]
else:
length = self.feature_appendix[feat_type][i][2]
typ = self.feature_appendix[feat_type][i][3]
feature_data.append(self.lyz_read(None,length,typ,f))
return feature_data
def setup_new_header(self):
self.header_data=[]
for line in self.header_fields:
data = line[4]
self.header_data.append(data)
def add_line(self,x1,y1,x2,y2,Pen_Width=.025):
feature_data=[]
for line in self.feature_fields:
data = line[4]
feature_data.append(data)
feature_data.append(int(x1*1000.0))
feature_data.append(int(y1*1000.0))
feature_data.append(int(x2*1000.0))
feature_data.append(int(y2*1000.0))
feature_data.append(0)
feature_data[1]=10 #set type to line
feature_data[4]=[2] #Not sure what this is for lines but it needs to be 2
feature_data[18]=Pen_Width
self.header_data[2]=self.header_data[2]+1
self.feature_list.append(feature_data)
def add_png(self,PNG_DATA,Xsixe,Ysize):
filename="filename"
feature_data=[]
for line in self.feature_fields:
data = line[4]
feature_data.append(data)
feature_data.append(PNG_DATA)
feature_data.append([0,0,0,0,0,0,0,0])
feature_data[1] = 12 # set type to PNG
feature_data[3] = [150]
feature_data[2] = [75, 4, 0, 144]
feature_data[4] = [0] # Number of Anti-Counterfeit lines
feature_data[6] = [12] # if this is not set to [12] the image does not get passed to the engrave window
feature_data[16] = Xsixe # set PNG width
feature_data[17] = Ysize # set PNG height
feature_data[18] = 1.0
feature_data[26] = 16777215
feature_data[30]= len(filename) # set filename length
feature_data[31]= filename # set filename
feature_data[33]=[0 ,0 ,0 ,0 ]
feature_data[34]=[255,255,255,255]
feature_data[36]=[226, 29, 5, 175]
feature_data[37]= len(PNG_DATA) # set PNG data length
self.header_data[2]=self.header_data[2]+1
self.feature_list.append(feature_data)
def set_size(self,Xsize,Ysize):
self.header_data[15]=Xsize
self.header_data[16]=Ysize
def set_margin(self,margin):
self.header_data[17]=margin/2
self.header_data[18]=margin/2
self.header_data[19]=margin/2
self.header_data[20]=margin/2
def find_PNG(self,f):
self.PNGstart = -1
self.PNGend = -1
f.seek(0)
loc=0
flag = True
while flag:
byte=f.read(1)
if byte=="":
flag=False
if byte =="P":
if byte =="N":
if byte =="G":
self.PNGstart = f.tell()-4
if byte =="E":
if byte =="N":
if byte =="D":
self.PNGend = f.tell()+4
flag = False
f.seek(0)
def read_file(self, file_name):
with open(file_name, "rb") as f:
self.find_PNG(f)
PNGlen = self.PNGend-self.PNGstart
self.png_message = "PNGlen: ",PNGlen
self.read_header(f)
for i in range(self.header_data[2]):
data = self.read_feature(f)
self.feature_list.append(data)
self.left_over = f.read( self.header_data[1]-4-f.tell() )
self.EOF = ""
byte = f.read(1)
while byte!="":
self.EOF=self.EOF+byte
byte = f.read(1)
#print(possible_values(200+217,348-200,'d',f))
def write_file(self, file_name):
with open(file_name, "wb") as f:
for i in range(len(self.header_fields)):
typ = self.header_fields[i][3]
data = self.header_data[i]
self.lyz_write(data,typ,f)
for j in range(len(self.feature_list)):
for i in range(len(self.feature_fields)):
typ = self.feature_fields[i][3]
data = self.feature_list[j][i]
#print(j,i," typ,data: ",typ,data)
self.lyz_write(data,typ,f)
feat_type=self.feature_list[j][1]
if feat_type==10 or feat_type==12:
appendix_data=[]
for i in range(len(self.feature_appendix[feat_type])):
typ = self.feature_appendix[feat_type][i][3]
data = self.feature_list[j][i+len(self.feature_fields)] #appendix_data
#print(j,i," typ,data: "typ,data)
self.lyz_write(data,typ,f)
f.write("@EOF".encode())
length=f.tell()
f.seek(4)
f.write(struct.pack('i',length))
def print_header(self):
print("\nHEADER DATA:")
print("--------------------")
for i in range(len(self.header_data)):
print("%11s : " %(self.header_fields[i][0]),self.header_data[i])
def print_features(self):
for i in range(len(self.feature_list)):
print("\nFEATURE #%d:" %(i+1))
print("--------------------")
feature = self.feature_list[i]
for j in range(len(self.feature_fields)):
try:
print("%11s : " %(self.feature_fields[j][0]),feature[j])
except:
print("error")
feat_type = feature[1]
if feat_type==10 or feat_type==12:
print("---LINE COORDS---")
for j in range(len(self.feature_appendix[feat_type])):
jj = j+len(self.feature_fields)
if feat_type==12 and jj==38:
print("%11s : " %(self.feature_appendix[feat_type][j][0]),"....")
else:
print("%11s : " %(self.feature_appendix[feat_type][j][0]),feature[jj])
print("--------------------")
if __name__ == "__main__":
###############################
try:
file_name = sys.argv[1]
print("input: ",file_name)
except:
file_name = ""
###############################
try:
file_out = sys.argv[2]
print("output: ",file_name)
except:
file_out = ""
###############################
if file_name=="test":
LYZ=LYZ_CLASS()
LYZ.setup_new_header()
#image_file = "squigles.png"
#image_file = "drawing_mod.png"
image_file = "temp.png"
with open(image_file, 'rb') as f:
PNG_DATA = f.read()
LYZ.add_png(PNG_DATA,20,20)
LYZ.add_line(5,5,-10,-10,0.025)
#LYZ.print_header()
#LYZ.print_features()
LYZ.write_file("test.lyz")
else:
if file_name!="":
LYZ=LYZ_CLASS()
LYZ.read_file(file_name)
LYZ.print_header()
LYZ.print_features()
print("LEFTOVER :", LYZ.left_over)
print("EOF :",LYZ.EOF)
if file_out!="":
LYZ.write_file(file_out)

View File

@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""
simplepath.py
functions for digesting paths into a simple list structure
Copyright (C) 2005 Aaron Spike, aaron@ekips.org
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.
"""
import re, math
def lexPath(d):
"""
returns and iterator that breaks path data
identifies command and parameter tokens
"""
offset = 0
length = len(d)
delim = re.compile(r'[ \t\r\n,]+')
command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]')
parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
while 1:
m = delim.match(d, offset)
if m:
offset = m.end()
if offset >= length:
break
m = command.match(d, offset)
if m:
yield [d[offset:m.end()], True]
offset = m.end()
continue
m = parameter.match(d, offset)
if m:
yield [d[offset:m.end()], False]
offset = m.end()
continue
#TODO: create new exception
raise Exception('Invalid path data!')
'''
pathdefs = {commandfamily:
[
implicitnext,
#params,
[casts,cast,cast],
[coord type,x,y,0]
]}
'''
pathdefs = {
'M':['L', 2, [float, float], ['x','y']],
'L':['L', 2, [float, float], ['x','y']],
'H':['H', 1, [float], ['x']],
'V':['V', 1, [float], ['y']],
'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']],
'S':['S', 4, [float, float, float, float], ['x','y','x','y']],
'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']],
'T':['T', 2, [float, float], ['x','y']],
'A':['A', 7, [float, float, float, int, int, float, float], ['r','r','a',0,'s','x','y']],
'Z':['L', 0, [], []]
}
def parsePath(d):
"""
Parse SVG path and return an array of segments.
Removes all shorthand notation.
Converts coordinates to absolute.
"""
retval = []
lexer = lexPath(d)
pen = (0.0,0.0)
subPathStart = pen
lastControl = pen
lastCommand = ''
while 1:
try:
token, isCommand = next(lexer)
except StopIteration:
break
params = []
needParam = True
if isCommand:
if not lastCommand and token.upper() != 'M':
raise Exception('Invalid path, must begin with moveto.')
else:
command = token
else:
#command was omited
#use last command's implicit next command
needParam = False
if lastCommand:
if lastCommand.isupper():
command = pathdefs[lastCommand][0]
else:
command = pathdefs[lastCommand.upper()][0].lower()
else:
raise Exception('Invalid path, no initial command.')
numParams = pathdefs[command.upper()][1]
while numParams > 0:
if needParam:
try:
token, isCommand = next(lexer)
if isCommand:
raise Exception('Invalid number of parameters')
except StopIteration:
raise Exception('Unexpected end of path')
cast = pathdefs[command.upper()][2][-numParams]
param = cast(token)
if command.islower():
if pathdefs[command.upper()][3][-numParams]=='x':
param += pen[0]
elif pathdefs[command.upper()][3][-numParams]=='y':
param += pen[1]
params.append(param)
needParam = True
numParams -= 1
#segment is now absolute so
outputCommand = command.upper()
#Flesh out shortcut notation
if outputCommand in ('H','V'):
if outputCommand == 'H':
params.append(pen[1])
if outputCommand == 'V':
params.insert(0,pen[0])
outputCommand = 'L'
if outputCommand in ('S','T'):
params.insert(0,pen[1]+(pen[1]-lastControl[1]))
params.insert(0,pen[0]+(pen[0]-lastControl[0]))
if outputCommand == 'S':
outputCommand = 'C'
if outputCommand == 'T':
outputCommand = 'Q'
#current values become "last" values
if outputCommand == 'M':
subPathStart = tuple(params[0:2])
pen = subPathStart
if outputCommand == 'Z':
pen = subPathStart
else:
pen = tuple(params[-2:])
if outputCommand in ('Q','C'):
lastControl = tuple(params[-4:-2])
else:
lastControl = pen
lastCommand = command
retval.append([outputCommand,params])
return retval
def formatPath(a):
"""Format SVG path data from an array"""
return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a])
def translatePath(p, x, y):
for cmd,params in p:
defs = pathdefs[cmd]
for i in range(defs[1]):
if defs[3][i] == 'x':
params[i] += x
elif defs[3][i] == 'y':
params[i] += y
def scalePath(p, x, y):
for cmd,params in p:
defs = pathdefs[cmd]
for i in range(defs[1]):
if defs[3][i] == 'x':
params[i] *= x
elif defs[3][i] == 'y':
params[i] *= y
elif defs[3][i] == 'r': # radius parameter
params[i] *= x
elif defs[3][i] == 's': # sweep-flag parameter
if x*y < 0:
params[i] = 1 - params[i]
elif defs[3][i] == 'a': # x-axis-rotation angle
if y < 0:
params[i] = - params[i]
def rotatePath(p, a, cx = 0, cy = 0):
if a == 0:
return p
for cmd,params in p:
defs = pathdefs[cmd]
for i in range(defs[1]):
if defs[3][i] == 'x':
x = params[i] - cx
y = params[i + 1] - cy
r = math.sqrt((x**2) + (y**2))
if r != 0:
theta = math.atan2(y, x) + a
params[i] = (r * math.cos(theta)) + cx
params[i + 1] = (r * math.sin(theta)) + cy

View File

@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""
simplestyle.py
Two simple functions for working with inline css
and some color handling on top.
Copyright (C) 2005 Aaron Spike, aaron@ekips.org
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.
"""
svgcolors={
'aliceblue':'#f0f8ff',
'antiquewhite':'#faebd7',
'aqua':'#00ffff',
'aquamarine':'#7fffd4',
'azure':'#f0ffff',
'beige':'#f5f5dc',
'bisque':'#ffe4c4',
'black':'#000000',
'blanchedalmond':'#ffebcd',
'blue':'#0000ff',
'blueviolet':'#8a2be2',
'brown':'#a52a2a',
'burlywood':'#deb887',
'cadetblue':'#5f9ea0',
'chartreuse':'#7fff00',
'chocolate':'#d2691e',
'coral':'#ff7f50',
'cornflowerblue':'#6495ed',
'cornsilk':'#fff8dc',
'crimson':'#dc143c',
'cyan':'#00ffff',
'darkblue':'#00008b',
'darkcyan':'#008b8b',
'darkgoldenrod':'#b8860b',
'darkgray':'#a9a9a9',
'darkgreen':'#006400',
'darkgrey':'#a9a9a9',
'darkkhaki':'#bdb76b',
'darkmagenta':'#8b008b',
'darkolivegreen':'#556b2f',
'darkorange':'#ff8c00',
'darkorchid':'#9932cc',
'darkred':'#8b0000',
'darksalmon':'#e9967a',
'darkseagreen':'#8fbc8f',
'darkslateblue':'#483d8b',
'darkslategray':'#2f4f4f',
'darkslategrey':'#2f4f4f',
'darkturquoise':'#00ced1',
'darkviolet':'#9400d3',
'deeppink':'#ff1493',
'deepskyblue':'#00bfff',
'dimgray':'#696969',
'dimgrey':'#696969',
'dodgerblue':'#1e90ff',
'firebrick':'#b22222',
'floralwhite':'#fffaf0',
'forestgreen':'#228b22',
'fuchsia':'#ff00ff',
'gainsboro':'#dcdcdc',
'ghostwhite':'#f8f8ff',
'gold':'#ffd700',
'goldenrod':'#daa520',
'gray':'#808080',
'grey':'#808080',
'green':'#008000',
'greenyellow':'#adff2f',
'honeydew':'#f0fff0',
'hotpink':'#ff69b4',
'indianred':'#cd5c5c',
'indigo':'#4b0082',
'ivory':'#fffff0',
'khaki':'#f0e68c',
'lavender':'#e6e6fa',
'lavenderblush':'#fff0f5',
'lawngreen':'#7cfc00',
'lemonchiffon':'#fffacd',
'lightblue':'#add8e6',
'lightcoral':'#f08080',
'lightcyan':'#e0ffff',
'lightgoldenrodyellow':'#fafad2',
'lightgray':'#d3d3d3',
'lightgreen':'#90ee90',
'lightgrey':'#d3d3d3',
'lightpink':'#ffb6c1',
'lightsalmon':'#ffa07a',
'lightseagreen':'#20b2aa',
'lightskyblue':'#87cefa',
'lightslategray':'#778899',
'lightslategrey':'#778899',
'lightsteelblue':'#b0c4de',
'lightyellow':'#ffffe0',
'lime':'#00ff00',
'limegreen':'#32cd32',
'linen':'#faf0e6',
'magenta':'#ff00ff',
'maroon':'#800000',
'mediumaquamarine':'#66cdaa',
'mediumblue':'#0000cd',
'mediumorchid':'#ba55d3',
'mediumpurple':'#9370db',
'mediumseagreen':'#3cb371',
'mediumslateblue':'#7b68ee',
'mediumspringgreen':'#00fa9a',
'mediumturquoise':'#48d1cc',
'mediumvioletred':'#c71585',
'midnightblue':'#191970',
'mintcream':'#f5fffa',
'mistyrose':'#ffe4e1',
'moccasin':'#ffe4b5',
'navajowhite':'#ffdead',
'navy':'#000080',
'oldlace':'#fdf5e6',
'olive':'#808000',
'olivedrab':'#6b8e23',
'orange':'#ffa500',
'orangered':'#ff4500',
'orchid':'#da70d6',
'palegoldenrod':'#eee8aa',
'palegreen':'#98fb98',
'paleturquoise':'#afeeee',
'palevioletred':'#db7093',
'papayawhip':'#ffefd5',
'peachpuff':'#ffdab9',
'peru':'#cd853f',
'pink':'#ffc0cb',
'plum':'#dda0dd',
'powderblue':'#b0e0e6',
'purple':'#800080',
'rebeccapurple':'#663399',
'red':'#ff0000',
'rosybrown':'#bc8f8f',
'royalblue':'#4169e1',
'saddlebrown':'#8b4513',
'salmon':'#fa8072',
'sandybrown':'#f4a460',
'seagreen':'#2e8b57',
'seashell':'#fff5ee',
'sienna':'#a0522d',
'silver':'#c0c0c0',
'skyblue':'#87ceeb',
'slateblue':'#6a5acd',
'slategray':'#708090',
'slategrey':'#708090',
'snow':'#fffafa',
'springgreen':'#00ff7f',
'steelblue':'#4682b4',
'tan':'#d2b48c',
'teal':'#008080',
'thistle':'#d8bfd8',
'tomato':'#ff6347',
'turquoise':'#40e0d0',
'violet':'#ee82ee',
'wheat':'#f5deb3',
'white':'#ffffff',
'whitesmoke':'#f5f5f5',
'yellow':'#ffff00',
'yellowgreen':'#9acd32'
}
def parseStyle(s):
"""Create a dictionary from the value of an inline style attribute"""
if s is None:
return {}
else:
return dict([[x.strip() for x in i.split(":")] for i in s.split(";") if len(i.strip())])
def formatStyle(a):
"""Format an inline style attribute from a dictionary"""
return ";".join([att+":"+str(val) for att,val in a.iteritems()])
def isColor(c):
"""Determine if its a color we can use. If not, leave it unchanged."""
if c.startswith('#') and (len(c)==4 or len(c)==7):
return True
if c.lower() in svgcolors.keys():
return True
#might be "none" or some undefined color constant or rgb()
#however, rgb() shouldnt occur at this point
return False
def parseColor(c):
"""Creates a rgb int array"""
tmp = svgcolors.get(c.lower())
if tmp is not None:
c = tmp
elif c.startswith('#') and len(c)==4:
c='#'+c[1:2]+c[1:2]+c[2:3]+c[2:3]+c[3:]+c[3:]
elif c.startswith('rgb('):
# remove the rgb(...) stuff
tmp = c.strip()[4:-1]
numbers = [number.strip() for number in tmp.split(',')]
converted_numbers = []
if len(numbers) == 3:
for num in numbers:
if num.endswith(r'%'):
converted_numbers.append(int(float(num[0:-1])*255/100))
else:
converted_numbers.append(int(num))
return tuple(converted_numbers)
else:
return (0,0,0)
try:
r=int(c[1:3],16)
g=int(c[3:5],16)
b=int(c[5:],16)
except:
# unknown color ...
# Return a default color. Maybe not the best thing to do but probably
# better than raising an exception.
return(0,0,0)
return (r,g,b)
def formatColoria(a):
"""int array to #rrggbb"""
return '#%02x%02x%02x' % (a[0],a[1],a[2])
def formatColorfa(a):
"""float array to #rrggbb"""
return '#%02x%02x%02x' % (int(round(a[0]*255)),int(round(a[1]*255)),int(round(a[2]*255)))
def formatColor3i(r,g,b):
"""3 ints to #rrggbb"""
return '#%02x%02x%02x' % (r,g,b)
def formatColor3f(r,g,b):
"""3 floats to #rrggbb"""
return '#%02x%02x%02x' % (int(round(r*255)),int(round(g*255)),int(round(b*255)))

View File

@ -0,0 +1,259 @@
#!/usr/bin/env python3
'''
Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
Copyright (C) 2010 Alvin Penner, penner@vaxxine.com
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.
barraud@math.univ-lille1.fr
This code defines several functions to make handling of transform
attribute easier.
'''
import inkex, cubicsuperpath, bezmisc, simplestyle
import copy, math, re
def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
if transf=="" or transf==None:
return(mat)
stransf = transf.strip()
result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf)
#-- translate --
if result.group(1)=="translate":
args=result.group(2).replace(',',' ').split()
dx=float(args[0])
if len(args)==1:
dy=0.0
else:
dy=float(args[1])
matrix=[[1,0,dx],[0,1,dy]]
#-- scale --
if result.group(1)=="scale":
args=result.group(2).replace(',',' ').split()
sx=float(args[0])
if len(args)==1:
sy=sx
else:
sy=float(args[1])
matrix=[[sx,0,0],[0,sy,0]]
#-- rotate --
if result.group(1)=="rotate":
args=result.group(2).replace(',',' ').split()
a=float(args[0])*math.pi/180
if len(args)==1:
cx,cy=(0.0,0.0)
else:
cx,cy=list(map(float,args[1:]))
matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]]
matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]])
#-- skewX --
if result.group(1)=="skewX":
a=float(result.group(2))*math.pi/180
matrix=[[1,math.tan(a),0],[0,1,0]]
#-- skewY --
if result.group(1)=="skewY":
a=float(result.group(2))*math.pi/180
matrix=[[1,0,0],[math.tan(a),1,0]]
#-- matrix --
if result.group(1)=="matrix":
a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split()
matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]]
matrix=composeTransform(mat,matrix)
if result.end() < len(stransf):
return(parseTransform(stransf[result.end():], matrix))
else:
return matrix
def formatTransform(mat):
return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2]))
def invertTransform(mat):
det = mat[0][0]*mat[1][1] - mat[0][1]*mat[1][0]
if det !=0: # det is 0 only in case of 0 scaling
# invert the rotation/scaling part
a11 = mat[1][1]/det
a12 = -mat[0][1]/det
a21 = -mat[1][0]/det
a22 = mat[0][0]/det
# invert the translational part
a13 = -(a11*mat[0][2] + a12*mat[1][2])
a23 = -(a21*mat[0][2] + a22*mat[1][2])
return [[a11,a12,a13],[a21,a22,a23]]
else:
return[[0,0,-mat[0][2]],[0,0,-mat[1][2]]]
def composeTransform(M1,M2):
a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0]
a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1]
a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0]
a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1]
v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2]
v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2]
return [[a11,a12,v1],[a21,a22,v2]]
def composeParents(node, mat):
trans = node.get('transform')
if trans:
mat = composeTransform(parseTransform(trans), mat)
if node.getparent().tag == inkex.addNS('g','svg'):
mat = composeParents(node.getparent(), mat)
return mat
def applyTransformToNode(mat,node):
m=parseTransform(node.get("transform"))
newtransf=formatTransform(composeTransform(mat,m))
node.set("transform", newtransf)
def applyTransformToPoint(mat,pt):
x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2]
y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2]
pt[0]=x
pt[1]=y
def applyTransformToPath(mat,path):
for comp in path:
for ctl in comp:
for pt in ctl:
applyTransformToPoint(mat,pt)
def fuseTransform(node):
if node.get('d')==None:
#FIXME: how do you raise errors?
raise AssertionError('can not fuse "transform" of elements that have no "d" attribute')
t = node.get("transform")
if t == None:
return
m = parseTransform(t)
d = node.get('d')
p = cubicsuperpath.parsePath(d)
applyTransformToPath(m,p)
node.set('d', cubicsuperpath.formatPath(p))
del node.attrib["transform"]
####################################################################
##-- Some functions to compute a rough bbox of a given list of objects.
##-- this should be shipped out in an separate file...
def boxunion(b1,b2):
if b1 is None:
return b2
elif b2 is None:
return b1
else:
return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3])))
def roughBBox(path):
xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1]
for pathcomp in path:
for ctl in pathcomp:
for pt in ctl:
xmin = min(xmin,pt[0])
xMax = max(xMax,pt[0])
ymin = min(ymin,pt[1])
yMax = max(yMax,pt[1])
return xmin,xMax,ymin,yMax
def refinedBBox(path):
xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1]
for pathcomp in path:
for i in range(1, len(pathcomp)):
cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0])
xmin = min(xmin, cmin)
xMax = max(xMax, cmax)
cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1])
ymin = min(ymin, cmin)
yMax = max(yMax, cmax)
return xmin,xMax,ymin,yMax
def cubicExtrema(y0, y1, y2, y3):
cmin = min(y0, y3)
cmax = max(y0, y3)
d1 = y1 - y0
d2 = y2 - y1
d3 = y3 - y2
if (d1 - 2*d2 + d3):
if (d2*d2 > d1*d3):
t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3)
if (t > 0) and (t < 1):
y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
cmin = min(cmin, y)
cmax = max(cmax, y)
t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3)
if (t > 0) and (t < 1):
y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
cmin = min(cmin, y)
cmax = max(cmax, y)
elif (d3 - d1):
t = -d1/(d3 - d1)
if (t > 0) and (t < 1):
y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t
cmin = min(cmin, y)
cmax = max(cmax, y)
return cmin, cmax
def computeBBox(aList,mat=[[1,0,0],[0,1,0]]):
bbox=None
for node in aList:
m = parseTransform(node.get('transform'))
m = composeTransform(mat,m)
#TODO: text not supported!
d = None
if node.get("d"):
d = node.get('d')
elif node.get('points'):
d = 'M' + node.get('points')
elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]:
d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \
'h' + node.get('width') + 'v' + node.get('height') + \
'h-' + node.get('width')
elif node.tag in [ inkex.addNS('line','svg'), 'line' ]:
d = 'M' + node.get('x1') + ',' + node.get('y1') + \
' ' + node.get('x2') + ',' + node.get('y2')
elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \
inkex.addNS('ellipse','svg'), 'ellipse' ]:
rx = node.get('r')
if rx is not None:
ry = rx
else:
rx = node.get('rx')
ry = node.get('ry')
cx = float(node.get('cx', '0'))
cy = float(node.get('cy', '0'))
x1 = cx - float(rx)
x2 = cx + float(rx)
d = 'M %f %f ' % (x1, cy) + \
'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \
'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy)
if d is not None:
p = cubicsuperpath.parsePath(d)
applyTransformToPath(m,p)
bbox=boxunion(refinedBBox(p),bbox)
elif node.tag == inkex.addNS('use','svg') or node.tag=='use':
refid=node.get(inkex.addNS('href','xlink'))
path = '//*[@id="%s"]' % refid[1:]
refnode = node.xpath(path)
bbox=boxunion(computeBBox(refnode,m),bbox)
bbox=boxunion(computeBBox(node,m),bbox)
return bbox
def computePointInNode(pt, node, mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
if node.getparent() is not None:
applyTransformToPoint(invertTransform(composeParents(node, mat)), pt)
return pt

View File

@ -0,0 +1,21 @@
[
{
"name": "LaserDraw Export (<various>)",
"id": "fablabchemnitz.de.laserdraw_export.<various>",
"path": "laserdraw_export",
"dependent_extensions": null,
"original_name": "LaserDraw (LaserDRW) <various>",
"original_id": "com.scorchworks.output.<various>",
"license": "GNU GPL v2",
"license_url": "https://www.scorchworks.com/LaserDRW_extension/LaserDRW_extension-0.06.zip",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/laserdraw_export",
"fork_url": "https://www.scorchworks.com/LaserDRW_extension/LaserDRW_extension-0.06.zip",
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55019345",
"inkscape_gallery_url": null,
"main_authors": [
"scorchworks.com",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,22 @@
[
{
"name": "NextGenerator",
"id": "fablabchemnitz.de.nextgenerator",
"path": "nextgenerator",
"dependent_extensions": null,
"original_name": "NextGenerator",
"original_id": "de.vektorrascheln.extension.next_gen",
"license": "GNU GPL v3",
"license_url": "https://gitlab.com/Moini/nextgenerator/-/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/nextgenerator",
"fork_url": "https://gitlab.com/Moini/nextgenerator",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/NextGenerator",
"inkscape_gallery_url": null,
"main_authors": [
"Aurélio A. Heckert",
"gitlab.com/Moini",
"github.com/eridur-de"
]
}
]

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>NextGenerator</name>
<id>fablabchemnitz.de.nextgenerator</id>
<param name="tab" type="notebook">
<page name="config" gui-text="Options">
<label appearance="header">Input options</label>
<param gui-text="CSV file:" gui-description="A file with comma-separated values, one line per exported file. The first line contains the column names. Put quotation marks around your variables if they contain a comma." name="csv_file" type="path" mode="file" filetypes="csv">/path/to/file.csv</param>
<spacer />
<label>Non-text values to replace (see Help tab):</label>
<param name="extra-vars" type="string" gui-text="" />
<param gui-text="Number of sets in the template:" gui-description="How many datasets fit into one template file - e.g. if you have an A4 page with 8 visitor badges for 8 different persons which use the same variable names, choose '8'. See 'Help' tab for more info." name="num_sets" type="int" min="1" max="10000">1</param>
<spacer />
<separator />
<spacer />
<label appearance="header">Output options</label>
<param name="format" type="optiongroup" appearance="combo" gui-text="Export file format:">
<option value="png">PNG</option>
<option value="pdf">PDF</option>
<option value="svg">SVG</option>
<option value="ps">PS</option>
<option value="eps">EPS</option>
</param>
<param gui-text="DPI (for PNG and filters):" gui-description="The resolution for your exported raster images" name="dpi" type="int" min="1" max="10000">300</param>
<param gui-text="File name pattern:" gui-description="The pattern for the names of the generated files. If 'Number of sets' is 1, it should contain at least one unique column variable (in the form of '%VAR_my_variable_name%'), or a unique combination of column variables, so new files won't overwrite those that have already been generated. Do not include the file extension, it will be added automatically. If 'Number of sets' is greater than 1, use any name you like. The exported files will be numbered automatically." name="file_pattern" type="string">%VAR_my_variable_name%</param>
<param gui-text="Save in:" gui-description="The name of the folder where the generated images should be saved" name="output_folder" type="path" mode="folders">/tmp</param>
</page>
<page name="help" gui-text="Help">
<param name="helptabs" type="notebook">
<page name="help1" gui-text="Replacing text">
<label xml:space="preserve">In your SVG file, create any texts that you want to replace by clicking (not clicking and dragging) on the canvas. If you want to limit the width of the text, use the diamond-shaped handle at the end of the first line to indicate a maximum width. The text will auto-flow into the next line if it is longer. Make sure there is enough space for your texts.
As (or into) the text, type '%VAR_my_variable_name%' (without the quotes) as a placeholder, where 'my_variable_name' is the title of the corresponding column in your CSV data file.</label>
</page>
<page name="help2" gui-text="Replacing attributes">
<label xml:space="preserve">If you want to replace attribute values in your SVG file (e.g. a color, or the name of a linked image file), you can assign them to columns in the field labeled "Non-text values to replace" in JSON format like this (no linebreaks allowed):
{"background_color":"#ff0000", "photo":"image.png"}
All objects that use the red color (#ff0000) will then be exported using the colors in the column 'background_color'. The linked image 'image.png' will be replaced by the image files listed in the column 'photo' (make sure to replace the complete image path).</label>
</page>
<page name="help3" gui-text="Multiple sets">
<label xml:space="preserve">If you intend to have multiple sets in a single template for example, multiple conference badges on a larger page that will later be cut make sure to group each set together.
Also make sure to use a variable only exactly once per set. If you do not adhere to this, your variables will get out of sync and your generated files will contain incorrect data.</label>
</page>
</param>
</page>
<page name="about" gui-text="About">
<label appearance="header">NextGenerator</label>
<label indent="1">Version 1.1.1</label>
<spacer />
<label xml:space="preserve">An Inkscape extension to automatically replace values (text, attribute values) in an SVG file and to then export the result to various file formats. This is useful e.g. for generating images for name badges and other similar items.
This extension is a Python rewrite of the Generator bash script extension by Aurélio A. Heckert. It is compatible with Inkscape starting from version 1.0 and requires Python 3.</label>
</page>
</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Import/Export/Transfer" />
</submenu>
</effects-menu>
<menu-tip>Automatically replace values and export the result.</menu-tip>
</effect>
<script>
<command location="inx" interpreter="python">nextgenerator.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
# coding=utf-8
#
# NextGenerator - an Inkscape extension to export images with automatically replaced values
# Copyright (C) 2008 Aurélio A. Heckert (original Generator extension in Bash)
# 2019-2021 Maren Hachmann (Python rewrite, update for Inkscape 1.0)
#
# 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 3 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.
#
"""
An Inkscape extension to automatically replace values (text, attribute values)
in an SVG file and to then export the result to various file formats.
This is useful e.g. for generating images for name badges and other similar items.
"""
from __future__ import unicode_literals
import os
import csv
import json
import time #for debugging purposes
import inkex
import html
from inkex.command import inkscape
__version__ = '1.2'
class NextGenerator(inkex.base.TempDirMixin, inkex.base.InkscapeExtension):
"""Generate image files by replacing variables in the current file"""
def add_arguments(self, pars):
pars.add_argument("-c", "--csv_file", type=str, dest="csv_file", help="path to a CSV file")
pars.add_argument("-e", "--extra-vars", help="additional variables to replace and the corresponding columns, in JSON format")
pars.add_argument("-n", "--num_sets", type=int, default="1", help="number of sets in the template")
pars.add_argument("-f", "--format", help="file format to export to: png, pdf, svg, ps, eps")
pars.add_argument("-d", "--dpi", type=int, default="300", help="dpi value for exported raster images")
pars.add_argument("-o", "--output_folder", help="path to output folder")
pars.add_argument("-p", "--file_pattern", help="pattern for the output file")
pars.add_argument("-t", "--tab", default="", help="not needed at all")
pars.add_argument("-l", "--helptabs", default="", help="not needed at all")
pars.add_argument("-i", "--id", default="", help="not needed at all")
def effect(self):
# load the attributes that should be replaced in addition to textual values
if self.options.extra_vars == None:
self.options.extra_vars = '{}'
extra_vars = json.loads(self.options.extra_vars)
# load the CSV file
# spaces around commas will be stripped
csv.register_dialect('generator', 'excel', skipinitialspace=True)
with open(self.options.csv_file, newline='', encoding='utf-8-sig') as csvfile:
data = csv.DictReader(csvfile, dialect='generator')
if self.options.num_sets == 1:
for row in data:
export_base_name = self.options.file_pattern
self.new_doc = self.document
for i, (key, value) in enumerate(row.items()):
search_string = "%VAR_" + key + "%"
# replace any occurrances of %VAR_my_variable_name% in the SVG file source code
self.new_doc = self.new_doc.replace(search_string, html.escape(value))
# build the file name, still without file extension
export_base_name = export_base_name.replace(search_string, value)
for key, svg_cont in extra_vars.items():
if key in row.keys():
# replace any attributes and other SVG content by the values from the CSV file
self.new_doc = self.new_doc.replace(svg_cont, row[key])
else:
inkex.errormsg("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key)
if self.export(export_base_name) != True:
return
elif self.options.num_sets > 1:
# we need a list to access specific rows and to be able to count it
data = list(data)
# check if user's indication of num_sets is compatible with file
for key in data[0].keys():
num_occurr = self.document.count("%VAR_" + key + "%")
# We ignore keys that don't appear in the document
if num_occurr != 0 and num_occurr != self.options.num_sets:
return inkex.errormsg("There are {0} occurrances of the variable '{1}' in the document, but the number of sets you indicated is {2}. Please make sure that each set contains all variables and that there are just as many sets in your document as you indicate.".format(num_occurr, key, self.options.num_sets))
# abusing negative floor division which rounds to the next lowest number to figure out how many pages we will get
num_exports = -((-len(data))//self.options.num_sets)
# now we hope that the document is properly prepared and the stacking order cycles through datasets - if not, the result will be nonsensical, but we can't know.
for export_file_num in range(num_exports):
# we only number the export files if there are sets
export_base_name = "".join([x if x.isalnum() else "_" for x in self.options.file_pattern]) + '_{}'.format(str(export_file_num))
self.new_doc = self.document
for set_num in range(self.options.num_sets):
# number of the data row in the CSV file
n = export_file_num * self.options.num_sets + set_num
if n < len(data):
dataset = data[n]
else:
# no more values available, stop trying to replace them
break
for i, (key, value) in enumerate(dataset.items()):
search_string = "%VAR_" + key + "%"
# replace the next occurrance of %VAR_my_variable_name% in the SVG file source code
self.new_doc = self.new_doc.replace(search_string, html.escape(value), 1)
for key, svg_cont in extra_vars.items():
if key in dataset.keys():
# replace any attributes and other SVG content by the values from the CSV file
self.new_doc = self.new_doc.replace(svg_cont, dataset[key], 1)
else:
inkex.errormsg(_("The replacements in the generated images may be incomplete. Please check your entry '{key}' in the field for the non-text values.").format(key=key))
self.export(export_base_name)
def export(self, export_base_name):
export_file_name = '{0}.{1}'.format(export_base_name, self.options.format)
if os.path.exists(self.options.output_folder):
export_file_path = os.path.join(self.options.output_folder, export_file_name)
else:
inkex.errormsg("The selected output folder does not exist.")
return False
if self.options.format == 'svg':
# would use this, but it cannot overwrite, nor handle strings for writing...:
# write_svg(self.new_doc, export_file_path)
with open(export_file_path, 'w') as f:
f.write(self.new_doc)
else:
actions = {
'png' : 'export-dpi:{dpi};export-filename:{file_name};export-do;FileClose'.\
format(dpi=self.options.dpi, file_name=export_file_path),
'pdf' : 'export-dpi:{dpi};export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
format(dpi=self.options.dpi, file_name=export_file_path),
'ps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
format(dpi=self.options.dpi, file_name=export_file_path),
'eps' : 'export-dpi:{dpi};export-text-to-path;export-filename:{file_name};export-do;FileClose'.\
format(dpi=self.options.dpi, file_name=export_file_path),
}
# create a temporary svg file from our string
temp_svg_name = '{0}.{1}'.format(export_base_name, 'svg')
temp_svg_path = os.path.join(self.tempdir, temp_svg_name)
#inkex.utils.debug("temp_svg_path=" + temp_svg_path)
with open(temp_svg_path, 'w') as f:
f.write(self.new_doc)
#inkex.utils.debug("self.new_doc=" + self.new_doc)
# let Inkscape do the exporting
# self.debug(actions[self.options.format])
cli_output = inkscape(temp_svg_path, actions=actions[self.options.format])
if len(cli_output) > 0:
self.debug(_("Inkscape returned the following output when trying to run the file export; the file export may still have worked:"))
self.debug(cli_output)
return False
return True
def load(self, stream):
return str(stream.read(), 'utf-8')
def save(self, stream):
# must be implemented, but isn't needed.
pass
if __name__ == '__main__':
NextGenerator().run()

View File

@ -0,0 +1,20 @@
[
{
"name": "Raster Perspective",
"id": "fablabchemnitz.de.raster_perspective",
"path": "raster_perspective",
"dependent_extensions": null,
"original_name": "Perspective",
"original_id": "org.test.filter.imagePerspective",
"license": "GNU GPL v3",
"license_url": "https://github.com/s1291/InkRasterPerspective/blob/master/LICENSE",
"comment": "",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/raster_perspective",
"fork_url": "https://github.com/s1291/InkRasterPerspective",
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Raster+Perspective",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/s1291"
]
}
]

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Raster Perspective</name>
<id>fablabchemnitz.de.raster_perspective</id>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Transformations"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">raster_perspective.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
# Copyright (C) 2022 Samir OUCHENE, samirmath01@gmail.com
import os
import sys
import io
import inkex
from inkex import Image
from PIL import Image as PIL_Image
from PIL.Image import Transform, Resampling
import base64
import numpy
try:
from base64 import decodebytes
except ImportError:
from base64 import decodestring as decodebytes
class RasterPerspective(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
@staticmethod
def mime_to_ext(mime):
"""Return an extension based on the mime type"""
# Most extensions are automatic (i.e. extension is same as minor part of mime type)
part = mime.split("/", 1)[1].split("+")[0]
return (
"."
+ {
# These are the non-matching ones.
"svg+xml": ".svg",
"jpeg": ".jpg",
"icon": ".ico",
}.get(part, part)
)
def extract_image(self, node):
"""Extract the node as if it were an image."""
xlink = node.get("xlink:href")
if not xlink.startswith("data:"):
return # Not embedded image data
try:
data = xlink[5:]
(mimetype, data) = data.split(";", 1)
(base, data) = data.split(",", 1)
except ValueError:
inkex.errormsg("Invalid image format found")
return
if base != "base64":
inkex.errormsg("Can't decode encoding: {}".format(base))
return
file_ext = self.mime_to_ext(mimetype)
return decodebytes(data.encode("utf-8"))
def find_coeffs(self, source_coords, target_coords):
matrix = []
for s, t in zip(source_coords, target_coords):
matrix.append([t[0], t[1], 1, 0, 0, 0, -s[0] * t[0], -s[0] * t[1]])
matrix.append([0, 0, 0, t[0], t[1], 1, -s[1] * t[0], -s[1] * t[1]])
A = numpy.array(matrix, dtype=float)
B = numpy.array(source_coords).reshape(8)
res = numpy.linalg.inv(A.T @ A) @ A.T @ B
return numpy.array(res).reshape(8)
def effect(self):
WARN = "Your selection must contain an image and a path with at least 4 points."
if len(self.options.ids) < 2:
inkex.errormsg(WARN)
exit()
the_image_node, envelope_node = self.svg.selection
if str(envelope_node) == "image" and str(the_image_node) == "path":
envelope_node, the_image_node = self.svg.selection #switch
if str(the_image_node) != "image" and str(envelope_node) != "path":
inkex.utils.debug(WARN)
return
img_width, img_height = the_image_node.width, the_image_node.height
try:
unit_to_vp = self.svg.unit_to_viewport
except AttributeError:
unit_to_vp = self.svg.uutounit
try:
vp_to_unit = self.svg.viewport_to_unit
except AttributeError:
vp_to_unit = self.svg.unittouu
img_width = unit_to_vp(img_width)
img_height = unit_to_vp(img_height)
nodes_pts = list(envelope_node.path.control_points)
node1 = (unit_to_vp(nodes_pts[0][0]), unit_to_vp(nodes_pts[0][1]))
node2 = (unit_to_vp(nodes_pts[1][0]), unit_to_vp(nodes_pts[1][1]))
node3 = (unit_to_vp(nodes_pts[2][0]), unit_to_vp(nodes_pts[2][1]))
node4 = (unit_to_vp(nodes_pts[3][0]), unit_to_vp(nodes_pts[3][1]))
nodes = [node1, node2, node3, node4]
xMax = max([node[0] for node in nodes])
xMin = min([node[0] for node in nodes])
yMax = max([node[1] for node in nodes])
yMin = min([node[1] for node in nodes])
# add some assertions (FIXME)
img_data = self.extract_image(the_image_node)
orig_image = PIL_Image.open(io.BytesIO(img_data))
pil_img_size = orig_image.size
scale = pil_img_size[0] / img_width
coeffs = self.find_coeffs(
[
(0, 0),
(img_width * scale, 0),
(img_width * scale, img_height * scale),
(0, img_height * scale),
],
[
(node1[0] - xMin, node1[1] - yMin),
(node2[0] - xMin, node2[1] - yMin),
(node3[0] - xMin, node3[1] - yMin),
(node4[0] - xMin, node4[1] - yMin),
],
)
W, H = xMax - xMin, yMax - yMin
final_w, final_h = int(W), int(H)
# Check if the image has transparency
hasTransparency = orig_image.mode in ("RGBA", "LA") or (
orig_image.mode == "P" and "transparency" in orig_image.info
)
transp_img = orig_image
# If the original image is not transparent, create a new image with alpha channel
if not hasTransparency:
transp_img = PIL_Image.new("RGBA", orig_image.size)
transp_img.format = "PNG"
transp_img.paste(orig_image)
image = transp_img.transform(
(final_w, final_h), Transform.PERSPECTIVE, coeffs, Resampling.BICUBIC
)
obj = inkex.Image()
obj.set("x", vp_to_unit(xMin))
obj.set("y", vp_to_unit(yMin))
obj.set("width", vp_to_unit(final_w))
obj.set("height", vp_to_unit(final_h))
# embed the transformed image
persp_img_data = io.BytesIO()
image.save(persp_img_data, transp_img.format)
mime = PIL_Image.MIME[transp_img.format]
b64 = base64.b64encode(persp_img_data.getvalue()).decode("utf-8")
uri = f"data:{mime};base64,{b64}"
obj.set("xlink:href", uri)
self.svg.add(obj)
RasterPerspective = RasterPerspective()
RasterPerspective.run()