1#!/usr/local/bin/python3.8 2# coding=utf-8 3# 4# Copyright (C) 2007 Terry Brown, terry_n_brown@yahoo.com 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19# 20from __future__ import absolute_import, unicode_literals 21 22import inkex 23from inkex import Use, Rectangle 24from inkex.base import SvgOutputMixin 25 26class Nup(inkex.OutputExtension, SvgOutputMixin): 27 """N-up Layout generator""" 28 def add_arguments(self, pars): 29 pars.add_argument('--unit', default='px') 30 pars.add_argument('--rows', type=int, default=2) 31 pars.add_argument('--cols', type=int, default=2) 32 pars.add_argument('--paddingTop', type=float) 33 pars.add_argument('--paddingBottom', type=float) 34 pars.add_argument('--paddingLeft', type=float) 35 pars.add_argument('--paddingRight', type=float) 36 pars.add_argument('--marginTop', type=float) 37 pars.add_argument('--marginBottom', type=float) 38 pars.add_argument('--marginLeft', type=float) 39 pars.add_argument('--marginRight', type=float) 40 pars.add_argument('--pgMarginTop', type=float) 41 pars.add_argument('--pgMarginBottom', type=float) 42 pars.add_argument('--pgMarginLeft', type=float) 43 pars.add_argument('--pgMarginRight', type=float) 44 pars.add_argument('--pgSizeX', type=float) 45 pars.add_argument('--pgSizeY', type=float) 46 pars.add_argument('--sizeX', type=float) 47 pars.add_argument('--sizeY', type=float) 48 pars.add_argument('--calculateSize', type=inkex.Boolean, default=True) 49 pars.add_argument('--showHolder', type=inkex.Boolean, default=True) 50 pars.add_argument('--showCrosses', type=inkex.Boolean, default=True) 51 pars.add_argument('--showInner', type=inkex.Boolean, default=True) 52 pars.add_argument('--showOuter', type=inkex.Boolean, default=False) 53 pars.add_argument('--showInnerBox', type=inkex.Boolean, default=False) 54 pars.add_argument('--showOuterBox', type=inkex.Boolean, default=False) 55 pars.add_argument('--tab') 56 57 def save(self, stream): 58 show_list = [] 59 for i in ['showHolder', 'showCrosses', 'showInner', 'showOuter', 60 'showInnerBox', 'showOuterBox', ]: 61 if getattr(self.options, i): 62 show_list.append(i.lower().replace('show', '')) 63 opt = self.options 64 ret = self.generate_nup( 65 unit=opt.unit, 66 pgSize=(opt.pgSizeX, opt.pgSizeY), 67 pgMargin=(opt.pgMarginTop, opt.pgMarginRight, opt.pgMarginBottom, opt.pgMarginLeft), 68 num=(opt.rows, opt.cols), 69 calculateSize=opt.calculateSize, 70 size=(opt.sizeX, opt.sizeY), 71 margin=(opt.marginTop, opt.marginRight, opt.marginBottom, opt.marginLeft), 72 padding=(opt.paddingTop, opt.paddingRight, opt.paddingBottom, opt.paddingLeft), 73 show=show_list, 74 ) 75 if ret: 76 stream.write(ret) 77 78 def expandTuple(self, unit, x, length=4): 79 try: 80 iter(x) 81 except: 82 return None 83 84 if len(x) != length: 85 x *= 2 86 if len(x) != length: 87 raise Exception("expandTuple: requires 2 or 4 item tuple") 88 try: 89 return tuple(map(lambda ev: (self.svg.unittouu(str(eval(str(ev))) + unit) / self.svg.unittouu('1px')), x)) 90 except: 91 return None 92 93 def generate_nup(self, 94 unit="px", 95 pgSize=("8.5*96", "11*96"), 96 pgMargin=(0, 0), 97 pgPadding=(0, 0), 98 num=(2, 2), 99 calculateSize=True, 100 size=None, 101 margin=(0, 0), 102 padding=(20, 20), 103 show=['default'], 104 ): 105 """Generate the SVG. Inputs are run through 'eval(str(x))' so you can use 106 '8.5*72' instead of 612. Margin / padding dimension tuples can be 107 (top & bottom, left & right) or (top, right, bottom, left). 108 109 Keyword arguments: 110 pgSize -- page size, width x height 111 pgMargin -- extra space around each page 112 pgPadding -- added to pgMargin 113 n -- rows x cols 114 size -- override calculated size, width x height 115 margin -- white space around each piece 116 padding -- inner padding for each piece 117 show -- list of keywords indicating what to show 118 - 'crosses' - cutting guides 119 - 'inner' - inner boundary 120 - 'outer' - outer boundary 121 """ 122 123 if 'default' in show: 124 show = set(show).union(['inner', 'innerbox', 'holder', 'crosses']) 125 126 pgMargin = self.expandTuple(unit, pgMargin) 127 pgPadding = self.expandTuple(unit, pgPadding) 128 margin = self.expandTuple(unit, margin) 129 padding = self.expandTuple(unit, padding) 130 131 pgSize = self.expandTuple(unit, pgSize, length=2) 132 # num = tuple(map(lambda ev: eval(str(ev)), num)) 133 134 if not pgMargin or not pgPadding: 135 return inkex.errormsg("No padding or margin available.") 136 137 page_edge = list(map(sum, zip(pgMargin, pgPadding))) 138 139 top, right, bottom, left = 0, 1, 2, 3 140 width, height = 0, 1 141 rows, cols = 0, 1 142 size = self.expandTuple(unit, size, length=2) 143 if size is None or calculateSize or len(size) < 2 or size[0] == 0 or size[1] == 0: 144 size = ((pgSize[width] 145 - page_edge[left] - page_edge[right] 146 - num[cols] * (margin[left] + margin[right])) / num[cols], 147 (pgSize[height] 148 - page_edge[top] - page_edge[bottom] 149 - num[rows] * (margin[top] + margin[bottom])) / num[rows] 150 ) 151 else: 152 size = self.expandTuple(unit, size, length=2) 153 154 # sep is separation between same points on pieces 155 sep = (size[width] + margin[right] + margin[left], 156 size[height] + margin[top] + margin[bottom]) 157 158 style = 'stroke:#000000;stroke-opacity:1;fill:none;fill-opacity:1;' 159 160 padbox = Rectangle( 161 x=str(page_edge[left] + margin[left] + padding[left]), 162 y=str(page_edge[top] + margin[top] + padding[top]), 163 width=str(size[width] - padding[left] - padding[right]), 164 height=str(size[height] - padding[top] - padding[bottom]), 165 style=style, 166 ) 167 margbox = Rectangle( 168 x=str(page_edge[left] + margin[left]), 169 y=str(page_edge[top] + margin[top]), 170 width=str(size[width]), 171 height=str(size[height]), 172 style=style, 173 ) 174 175 doc = self.get_template(width=pgSize[width], height=pgSize[height]) 176 svg = doc.getroot() 177 178 def make_clones(under, to): 179 for row in range(0, num[rows]): 180 for col in range(0, num[cols]): 181 if row == 0 and col == 0: 182 continue 183 use = under.add(Use()) 184 use.set('xlink:href', '#' + to) 185 use.transform.add_translate(col * sep[width], row * sep[height]) 186 187 # guidelayer ##################################################### 188 if {'inner', 'outer'}.intersection(show): 189 layer = svg.add(inkex.Layer.new('Guide Layer')) 190 if 'inner' in show: 191 ibox = layer.add(padbox.copy()) 192 ibox.style['stroke'] = '#8080ff' 193 ibox.set('id', 'innerguide') 194 make_clones(layer, 'innerguide') 195 if 'outer' in show: 196 obox = layer.add(margbox.copy()) 197 obox.style['stroke'] = '#8080ff' 198 obox.set('id', 'outerguide') 199 make_clones(layer, 'outerguide') 200 201 # crosslayer ##################################################### 202 if {'crosses'}.intersection(show): 203 layer = svg.add(inkex.Layer.new('Cut Layer')) 204 205 if 'crosses' in show: 206 crosslen = 12 207 group = layer.add(inkex.Group(id='cross')) 208 x, y = 0, 0 209 path = 'M%f %f' % (x + page_edge[left] + margin[left], 210 y + page_edge[top] + margin[top] - crosslen) 211 path += ' L%f %f' % (x + page_edge[left] + margin[left], 212 y + page_edge[top] + margin[top] + crosslen) 213 path += ' M%f %f' % (x + page_edge[left] + margin[left] - crosslen, 214 y + page_edge[top] + margin[top]) 215 path += ' L%f %f' % (x + page_edge[left] + margin[left] + crosslen, 216 y + page_edge[top] + margin[top]) 217 group.add(inkex.PathElement(style=style + 'stroke-width:0.05', 218 d=path, id='crossmarker')) 219 for row in 0, 1: 220 for col in 0, 1: 221 if row or col: 222 cln = group.add(Use()) 223 cln.set('xlink:href', '#crossmarker') 224 cln.transform.add_translate(col * size[width], row * size[height]) 225 make_clones(layer, 'cross') 226 227 # clonelayer ##################################################### 228 layer = svg.add(inkex.Layer.new('Clone Layer')) 229 make_clones(layer, 'main') 230 231 # mainlayer ###################################################### 232 layer = svg.add(inkex.Layer.new('Main Layer')) 233 group = layer.add(inkex.Group(id='main')) 234 235 if 'innerbox' in show: 236 group.add(padbox) 237 if 'outerbox' in show: 238 group.add(margbox) 239 if 'holder' in show: 240 x, y = (page_edge[left] + margin[left] + padding[left], 241 page_edge[top] + margin[top] + padding[top]) 242 w, h = (size[width] - padding[left] - padding[right], 243 size[height] - padding[top] - padding[bottom]) 244 path = 'M{:f} {:f}'.format(x + w / 2., y) 245 path += ' L{:f} {:f}'.format(x + w, y + h / 2.) 246 path += ' L{:f} {:f}'.format(x + w / 2., y + h) 247 path += ' L{:f} {:f}'.format(x, y + h / 2.) 248 path += ' Z' 249 group.add(inkex.PathElement(style=style, d=path)) 250 251 return svg.tostring() 252 253 254if __name__ == '__main__': 255 Nup().run() 256