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