mlib-svg.lmt /size: 138 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['mlib-svg'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to mlib-ctx.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files",
8}
9
10-- todo: svg stripper
11
12-- todo: check clip: what if larger than bbox
13
14-- todo: when opacity is 1 don't flush it
15
16-- Just a few notes:
17--
18-- There is no real need to boost performance here .. we can always make a fast
19-- variant when really needed. I will also do some of the todo's when I run into
20-- proper fonts. I need to optimize this a bit but will do that once I'm satisfied
21-- with the outcome and don't need more hooks and plugs. At some point I will
22-- optimize the MetaPost part because now we probably have more image wrapping
23-- than needed.
24--
25-- As usual with these standards, things like a path can be very compact while the
26-- rest is very verbose which defeats the point. This is a first attempt. There will
27-- be a converter to MP as well as directly to PDF. This module was made for one of
28-- the dangerous curves talks at the 2019 CTX meeting. I will do the font when I
29-- need it (not that hard).
30--
31-- The fact that in the more recent versions of SVG the older text related elements
32-- are depricated and not even supposed to be supported, combined with the fact that
33-- the text element assumes css styling, demonstrates that there is not so much as a
34-- standard. It basically means that whatever technology dominates at some point
35-- (probably combined with some libraries that at that point exist) determine what
36-- is standard. Anyway, it probably also means that these formats are not that
37-- suitable for long term archival purposes. So don't take the next implementation
38-- too serious. So in the end we now have (1) attributes for properties (which is
39-- nice and clean and what attributes are for, (2) a style attribute that needs to
40-- be parsed, (3) classes that map to styles and (4) element related styles, plus a
41-- kind of inheritance (given the limited number of elements sticking to only <g> as
42-- wrapper would have made much sense. Anyway, we need to deal with it. With all
43-- these style things going on, one can wonder where it will end. Basically svg
44-- became just a html element that way and less clean too. The same is true for
45-- tspan, which means that text itself is nested xml.
46--
47-- We can do a direct conversion to PDF but then we also loose the abstraction which
48-- in the future will be used, and for fonts we need to spawn out to TeX anyway, so
49-- the little overhead of calling MetaPost is okay I guess. Also, we want to
50-- overload labels, share fonts with the main document, etc. and are not aiming at a
51-- general purpose SVG converter. For going to PDF one can just use InkScape.
52--
53-- Written with Anne Clark on speakers as distraction.
54--
55-- Todo when I run into an example (but ony when needed and reasonable):
56--
57--   var(color,color)
58--   --color<decimal>
59--   currentColor : when i run into an example
60--   a bit more shading
61--   clip = [ auto | rect(llx,lly,urx,ury) ] (in svg)
62--   xlink url ... whatever
63--   masks
64--   opacity per group (i need to add that to metafun first, inefficient pdf but
65--   maybe filldraw can help here)
66--
67-- Maybe in metafun:
68--
69--   penciled    n     -> withpen pencircle scaled n
70--   applied     (...) -> transformed bymatrix (...)
71--   withopacity n     -> withtransparency (1,n)
72
73-- When testing mbo files:
74--
75--   empty paths
76--   missing control points
77--   funny fontnames like abcdefverdana etc
78--   paths representing glyphs but also with style specs
79--   all kind of attributes
80--   very weird and inefficient shading
81
82-- One can run into pretty crazy images, like lines that are fills being clipped
83-- to some width. That's the danger of hiding yourself behind an interface I guess.
84--
85-- One would expect official examples to sort of follow the structure guideline,
86-- like putting gradient definitions in a "defs" element but forget about it ...
87-- structure seems not to be important (one can even wonder why "defs" is there at
88-- all). In the end all these systems (tex macro packages included) end up as a
89-- mess simply because conceptually wrong input gets accepted as normal. A side
90-- effect is that one starts to get a disliking. Anyway, at sime point I can just
91-- simplify some code because ugliness is part of the game.
92
93local rawget, rawset, type, tonumber, tostring, next, setmetatable = rawget, rawset, type, tonumber, tostring, next, setmetatable
94
95local P, S, R, C, Ct, Cs, Cc, Cp, Cg, Cf, Carg = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Cc, lpeg.Cp, lpeg.Cg, lpeg.Cf, lpeg.Carg
96
97local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
98local sqrt, abs = math.sqrt, math.abs
99local concat, setmetatableindex, sortedhash = table.concat, table.setmetatableindex, table.sortedhash
100local gmatch, gsub, find, match = string.gmatch, string.gsub, string.find, string.match
101local formatters, fullstrip = string.formatters, string.fullstrip
102local utfsplit, utfbyte = utf.split, utf.byte
103
104local xmlconvert, xmlcollected, xmlcount, xmlfirst, xmlroot = xml.convert, xml.collected, xml.count, xml.first, xml.root
105local xmltext, xmltextonly = xml.text, xml.textonly
106local css = xml.css or { } -- testing
107
108local function xmlinheritattributes(c,pa)
109    if not c.special then
110        local at = c.at
111        local dt = c.dt
112        if at and dt then
113            if pa then
114                setmetatableindex(at,pa)
115            end
116            for i=1,#dt do
117                local dti = dt[i]
118                if type(dti) == "table" then
119                    xmlinheritattributes(dti,at)
120                end
121            end
122        end
123    end
124end
125
126xml.inheritattributes = xmlinheritattributes
127
128-- Maybe some day helpers will move to the metapost.svg namespace!
129
130metapost       = metapost or { }
131local metapost = metapost
132local context  = context
133
134local report       = logs.reporter("metapost","svg")
135
136local trace        = false  trackers.register("metapost.svg",        function(v) trace        = v end)
137local trace_text   = false  trackers.register("metapost.svg.text",   function(v) trace_text   = v end)
138local trace_path   = false  trackers.register("metapost.svg.path",   function(v) trace_path   = v end)
139local trace_result = false  trackers.register("metapost.svg.result", function(v) trace_result = v end)
140local trace_colors = false  trackers.register("metapost.svg.colors", function(v) trace_colors = v end)
141local trace_fonts  = false  trackers.register("metapost.svg.fonts",  function(v) trace_fonts  = v end)
142
143-- This is just an experiment. Todo: reset hash etc. Also implement an option handler.
144
145local s_draw_image_start <const> = "draw image ("
146local s_draw_image_stop  <const> = ") ;"
147
148local ignoredopacity = 1
149
150local svghash = false  do
151
152    local svglast = 0
153    local svglist = false
154
155    local function checkhash(t,k)
156        local n = svglast + 1
157        svglast = n
158        svglist[n] = k
159        t[k] = n
160        return n
161    end
162
163    function metapost.startsvghashing()
164        svglast = 0
165        svglist = { }
166        svghash = setmetatableindex(checkhash)
167    end
168
169    function metapost.stopsvghashing()
170        svglast = 0
171        svglist = false
172        svghash = false
173    end
174
175    interfaces.implement {
176        name      = "svghashed",
177        arguments = "integer",
178        actions   = function(n)
179            local t = svglist and svglist[n]
180            if t then
181                context(t)
182            end
183        end
184    }
185
186end
187
188-- We have quite some closures because otherwise we run into the local variable
189-- limitations. It doesn't always look pretty now, sorry. I'll clean up this mess
190-- some day (the usual nth iteration of code).
191--
192-- Most of the conversion is rather trivial code till I ran into a file with arcs. A
193-- bit of searching lead to the a2c javascript function but it has some puzzling
194-- thingies (like sin and cos definitions that look like leftovers and possible
195-- division by zero). Anyway, we can if needed optimize it a bit more. Here does it
196-- come from:
197
198-- http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
199-- https://github.com/adobe-webplatform/Snap.svg/blob/b242f49e6798ac297a3dad0dfb03c0893e394464/src/path.js
200
201local a2c  do
202
203    local pi, sin, cos, tan, asin, abs = math.pi, math.sin, math.cos, math.tan, math.asin, math.abs
204
205    local d120 = (pi * 120) / 180
206    local pi2  = 2 * pi
207
208    a2c = function(x1, y1, rx, ry, angle, large, sweep, x2, y2, f1, f2, cx, cy)
209
210        if (rx == 0 or ry == 0) or (x1 == x2 and y1 == y2) then
211            return { x1, y1, x2, y2, x2, y2 }
212        end
213
214        local recursive = f1
215        local rad       = pi / 180 * angle
216        local res       = nil
217        local cosrad    = cos(-rad) -- local cosrad = cosd(angle)
218        local sinrad    = sin(-rad) -- local sinrad = sind(angle)
219
220        if not recursive then
221
222            x1, y1 = x1 * cosrad - y1 * sinrad, x1 * sinrad + y1 * cosrad
223            x2, y2 = x2 * cosrad - y2 * sinrad, x2 * sinrad + y2 * cosrad
224
225            local x  = (x1 - x2) / 2
226            local y  = (y1 - y2) / 2
227            local xx = x * x
228            local yy = y * y
229            local h  = xx / (rx * rx) + yy / (ry * ry)
230
231            if h > 1 then
232                h  = sqrt(h)
233                rx = h * rx
234                ry = h * ry
235            end
236
237            local rx2   = rx * rx
238            local ry2   = ry * ry
239            local ry2xx = ry2 * xx
240            local rx2yy = rx2 * yy
241            local total = rx2yy + ry2xx -- otherwise overflow
242
243            local k     = total == 0 and 0 or sqrt(abs((rx2 * ry2 - rx2yy - ry2xx) / total))
244
245            if large == sweep then
246                k = -k
247            end
248
249            cx = k *  rx * y / ry + (x1 + x2) / 2
250            cy = k * -ry * x / rx + (y1 + y2) / 2
251
252            f1 = (y1 - cy) / ry -- otherwise crash on a tiny eps
253            f2 = (y2 - cy) / ry -- otherwise crash on a tiny eps
254
255            f1 = asin((f1 < -1.0 and -1.0) or (f1 > 1.0 and 1.0) or f1)
256            f2 = asin((f2 < -1.0 and -1.0) or (f2 > 1.0 and 1.0) or f2)
257
258            if x1 < cx then f1 = pi  - f1 end
259            if x2 < cx then f2 = pi  - f2 end
260
261            if f1 < 0  then f1 = pi2 + f1 end
262            if f2 < 0  then f2 = pi2 + f2 end
263
264            if sweep ~= 0 and f1 > f2 then f1 = f1 - pi2 end
265            if sweep == 0 and f2 > f1 then f2 = f2 - pi2 end
266
267        end
268
269        if abs(f2 - f1) > d120 then
270            local f2old = f2
271            local x2old = x2
272            local y2old = y2
273            f2 = f1 + d120 * ((sweep ~= 0 and f2 > f1) and 1 or -1)
274            x2 = cx + rx * cos(f2)
275            y2 = cy + ry * sin(f2)
276            res = a2c(x2, y2, rx, ry, angle, 0, sweep, x2old, y2old, f2, f2old, cx, cy)
277        end
278
279        local c1 = cos(f1)
280        local s1 = sin(f1)
281        local c2 = cos(f2)
282        local s2 = sin(f2)
283
284        local t  = tan((f2 - f1) / 4)
285        local hx = 4 * rx * t / 3
286        local hy = 4 * ry * t / 3
287
288        local r = { x1 - hx * s1, y1 + hy * c1, x2 + hx * s2, y2 - hy * c2, x2, y2, unpack(res or { }) }
289
290        if not recursive then -- we can also check for sin/cos being 0/1
291            cosrad = cos(rad)
292            sinrad = sin(rad)
293         -- cosrad = cosd(angle)
294         -- sinrad = sind(angle)
295            for i0=1,#r,2 do
296                local i1 = i0 + 1
297                local x  = r[i0]
298                local y  = r[i1]
299                r[i0] = x * cosrad - y * sinrad
300                r[i1] = x * sinrad + y * cosrad
301            end
302        end
303
304        return r
305    end
306
307end
308
309-- We share some patterns.
310
311local p_digit    = lpegpatterns.digit
312local p_hexdigit = lpegpatterns.hexdigit
313local p_space    = lpegpatterns.whitespace
314
315local factors = {
316    ["pt"] =  1.25,
317    ["mm"] =  3.543307,
318    ["cm"] = 35.43307,
319    ["px"] =  1,
320    ["pc"] = 15,
321    ["in"] = 90,
322    ["em"] = 12 * 1.25,
323    ["ex"] =  8 * 1.25,
324    ["%"]  =  0.1,
325    ["bp"] =  1,
326}
327
328metapost.svgfactors = factors
329
330local percentage_r = 1/100
331local percentage_x = percentage_r
332local percentage_y = percentage_r
333
334local asnumber, asnumber_r, asnumber_x, asnumber_y, asnumber_vx, asnumber_vy
335local asnumber_vx_t, asnumber_vy_t
336local p_number, p_separator, p_optseparator, p_numbers, p_fournumbers, p_path
337local p_number_n, p_number_x, p_number_vx, p_number_y, p_number_vy, p_number_r
338
339do
340
341    -- incredible: we can find .123.456 => 0.123 0.456 ...
342
343    local p_command_x  = C(S("Hh"))
344    local p_command_y  = C(S("Vv"))
345    local p_command_xy = C(S("CcLlMmQqSsTt"))
346    local p_command_a  = C(S("Aa"))
347    local p_command    = C(S("Zz"))
348
349    p_optseparator = S("\t\n\r ,")^0
350    p_separator    = S("\t\n\r ,")^1
351    p_number       = (S("+-")^0 * (p_digit^0 * P(".") * p_digit^1 + p_digit^1 * P(".") + p_digit^1))
352                   * (P("e") * S("+-")^0 * p_digit^1)^-1
353
354    local function convert   (n)   n =   tonumber(n)                                                                           return n     end
355    local function convert_p (n,u) n =   tonumber(n) if u == true then return n / 100                                     else return n end end
356    local function convert_r (n,u) n =   tonumber(n) if u == true then return percentage_r * n elseif u then return u * n else return n end end
357    local function convert_x (n,u) n =   tonumber(n) if u == true then return percentage_x * n elseif u then return u * n else return n end end
358    local function convert_y (n,u) n =   tonumber(n) if u == true then return percentage_y * n elseif u then return u * n else return n end end
359    local function convert_vx(n,u) n =   tonumber(n) if u == true then return percentage_x * n elseif u then return u * n else return n end end
360    local function convert_vy(n,u) n = - tonumber(n) if u == true then return percentage_y * n elseif u then return u * n else return n end end
361
362    local p_unit      = (P("p") * S("txc") + P("e") * S("xm") + S("mc") * P("m") + P("in")) / factors
363    local p_percent   = P("%") * Cc(true)
364
365    local c_number_n  = C(p_number)
366    local c_number_u  = C(p_number) * (p_percent + p_unit)^-1
367
368    p_number_n  = c_number_n / convert
369    p_number_u  = c_number_u / convert
370    p_number_x  = c_number_u / convert_x
371    p_number_vx = c_number_u / convert_vx
372    p_number_y  = c_number_u / convert_y
373    p_number_vy = c_number_u / convert_vy
374    p_number_r  = c_number_u / convert_r
375    p_number_p  = c_number_u / convert_p
376
377    asnumber    = function(s) return s and lpegmatch(p_number,   s) or 0 end
378    asnumber_r  = function(s) return s and lpegmatch(p_number_r, s) or 0 end
379    asnumber_p  = function(s) return s and lpegmatch(p_number_p, s) or 0 end
380    asnumber_x  = function(s) return s and lpegmatch(p_number_x, s) or 0 end
381    asnumber_y  = function(s) return s and lpegmatch(p_number_y, s) or 0 end
382    asnumber_vx = function(s) return s and lpegmatch(p_number_vx,s) or 0 end
383    asnumber_vy = function(s) return s and lpegmatch(p_number_vy,s) or 0 end
384
385    local p_number_vx_t = Ct { (p_number_vx + p_separator)^1 }
386    local p_number_vy_t = Ct { (p_number_vy + p_separator)^1 }
387
388    local zerotable = { 0 }
389
390    asnumber_vx_t = function(s) return s and lpegmatch(p_number_vx_t,s) or zerotable end
391    asnumber_vy_t = function(s) return s and lpegmatch(p_number_vy_t,s) or zerotable end
392
393--     local p_numbersep   = p_number_n + p_separator
394    local p_numbersep   = p_number_u + p_separator
395          p_numbers     = p_optseparator * P("(") * p_numbersep^0 * p_optseparator * P(")")
396          p_fournumbers = p_numbersep^4
397          p_path        = Ct ( (
398          p_command_xy * (p_optseparator * p_number_vx *
399                          p_optseparator * p_number_vy )^1
400        + p_command_x  * (p_optseparator * p_number_vx )^1
401        + p_command_y  * (p_optseparator * p_number_vy )^1
402        + p_command_a  * (p_optseparator * p_number_vx *
403                          p_optseparator * p_number_vy *
404                          p_optseparator * p_number_r  *
405                          p_optseparator * p_number_n  * -- flags
406                          p_optseparator * p_number_n  * -- flags
407                          p_optseparator * p_number_vx *
408                          p_optseparator * p_number_vy )^1
409        + p_command
410        + p_separator
411    )^1 )
412
413
414end
415
416-- We can actually use the svg color definitions from the tex end but maybe a user
417-- doesn't want those replace the normal definitions.
418--
419-- local hexhash  = setmetatableindex(function(t,k) local v = lpegmatch(p_hexcolor, k) t[k] = v return v end)  -- per file
420-- local hexhash3 = setmetatableindex(function(t,k) local v = lpegmatch(p_hexcolor3,k) t[k] = v return v end)  -- per file
421--
422-- local function hexcolor (c) return hexhash [c] end -- directly do hexhash [c]
423-- local function hexcolor3(c) return hexhash3[c] end -- directly do hexhash3[c]
424
425local colormap  = false
426
427local function prepared(t)
428    if type(t) == "table" then
429        local mapping = t.mapping or { }
430        local mapper  = t.mapper
431        local colormap = setmetatableindex(mapping)
432        if mapper then
433            setmetatableindex(colormap,function(t,k)
434                local v = mapper(k)
435                t[k] = v or k
436                return v
437            end)
438        end
439        return colormap
440    else
441        return false
442    end
443end
444
445local colormaps = setmetatableindex(function(t,k)
446    local v = false
447    if type(k) == "string" then
448        v = prepared(table.load(k)) -- todo: same path as svg file
449    elseif type(k) == "table" then
450        v = prepared(k)
451        k = k.name or k
452    end
453    t[k] = v
454    return v
455end)
456
457function metapost.svgcolorremapper(colormap)
458    return colormaps[colormap]
459end
460
461-- todo: cache colors per image / remapper
462
463local colorcomponents, withcolor, thecolor, usedcolors  do
464
465    local svgcolors = {
466        aliceblue       = 0xF0F8FF, antiquewhite      = 0xFAEBD7, aqua                  = 0x00FFFF, aquamarine       = 0x7FFFD4,
467        azure           = 0xF0FFFF, beige             = 0xF5F5DC, bisque                = 0xFFE4C4, black            = 0x000000,
468        blanchedalmond  = 0xFFEBCD, blue              = 0x0000FF, blueviolet            = 0x8A2BE2, brown            = 0xA52A2A,
469        burlywood       = 0xDEB887, cadetblue         = 0x5F9EA0, hartreuse             = 0x7FFF00, chocolate        = 0xD2691E,
470        coral           = 0xFF7F50, cornflowerblue    = 0x6495ED, cornsilk              = 0xFFF8DC, crimson          = 0xDC143C,
471        cyan            = 0x00FFFF, darkblue          = 0x00008B, darkcyan              = 0x008B8B, darkgoldenrod    = 0xB8860B,
472        darkgray        = 0xA9A9A9, darkgreen         = 0x006400, darkgrey              = 0xA9A9A9, darkkhaki        = 0xBDB76B,
473        darkmagenta     = 0x8B008B, darkolivegreen    = 0x556B2F, darkorange            = 0xFF8C00, darkorchid       = 0x9932CC,
474        darkred         = 0x8B0000, darksalmon        = 0xE9967A, darkseagreen          = 0x8FBC8F, darkslateblue    = 0x483D8B,
475        darkslategray   = 0x2F4F4F, darkslategrey     = 0x2F4F4F, darkturquoise         = 0x00CED1, darkviolet       = 0x9400D3,
476        deeppink        = 0xFF1493, deepskyblue       = 0x00BFFF, dimgray               = 0x696969, dimgrey          = 0x696969,
477        dodgerblue      = 0x1E90FF, firebrick         = 0xB22222, floralwhite           = 0xFFFAF0, forestgreen      = 0x228B22,
478        fuchsia         = 0xFF00FF, gainsboro         = 0xDCDCDC, ghostwhite            = 0xF8F8FF, gold             = 0xFFD700,
479        goldenrod       = 0xDAA520, gray              = 0x808080, green                 = 0x008000, greenyellow      = 0xADFF2F,
480        grey            = 0x808080, honeydew          = 0xF0FFF0, hotpink               = 0xFF69B4, indianred        = 0xCD5C5C,
481        indigo          = 0x4B0082, ivory             = 0xFFFFF0, khaki                 = 0xF0E68C, lavender         = 0xE6E6FA,
482        lavenderblush   = 0xFFF0F5, lawngreen         = 0x7CFC00, lemonchiffon          = 0xFFFACD, lightblue        = 0xADD8E6,
483        lightcoral      = 0xF08080, lightcyan         = 0xE0FFFF, lightgoldenrodyellow  = 0xFAFAD2, lightgray        = 0xD3D3D3,
484        lightgreen      = 0x90EE90, lightgrey         = 0xD3D3D3, lightpink             = 0xFFB6C1, lightsalmon      = 0xFFA07A,
485        lightseagreen   = 0x20B2AA, lightskyblue      = 0x87CEFA, lightslategray        = 0x778899, lightslategrey   = 0x778899,
486        lightsteelblue  = 0xB0C4DE, lightyellow       = 0xFFFFE0, lime                  = 0x00FF00, limegreen        = 0x32CD32,
487        linen           = 0xFAF0E6, magenta           = 0xFF00FF, maroon                = 0x800000, mediumaquamarine = 0x66CDAA,
488        mediumblue      = 0x0000CD, mediumorchid      = 0xBA55D3, mediumpurple          = 0x9370DB, mediumseagreen   = 0x3CB371,
489        mediumslateblue = 0x7B68EE, mediumspringgreen = 0x00FA9A, mediumturquoise       = 0x48D1CC, mediumvioletred  = 0xC71585,
490        midnightblue    = 0x191970, mintcream         = 0xF5FFFA, mistyrose             = 0xFFE4E1, moccasin         = 0xFFE4B5,
491        navajowhite     = 0xFFDEAD, navy              = 0x000080, oldlace               = 0xFDF5E6, olive            = 0x808000,
492        olivedrab       = 0x6B8E23, orange            = 0xFFA500, orangered             = 0xFF4500, orchid           = 0xDA70D6,
493        palegoldenrod   = 0xEEE8AA, palegreen         = 0x98FB98, paleturquoise         = 0xAFEEEE, palevioletred    = 0xDB7093,
494        papayawhip      = 0xFFEFD5, peachpuff         = 0xFFDAB9, peru                  = 0xCD853F, pink             = 0xFFC0CB,
495        plum            = 0xDDA0DD, powderblue        = 0xB0E0E6, purple                = 0x800080, red              = 0xFF0000,
496        rosybrown       = 0xBC8F8F, royalblue         = 0x4169E1, saddlebrown           = 0x8B4513, salmon           = 0xFA8072,
497        sandybrown      = 0xF4A460, seagreen          = 0x2E8B57, seashell              = 0xFFF5EE, sienna           = 0xA0522D,
498        silver          = 0xC0C0C0, skyblue           = 0x87CEEB, slateblue             = 0x6A5ACD, slategray        = 0x708090,
499        slategrey       = 0x708090, snow              = 0xFFFAFA, springgreen           = 0x00FF7F, steelblue        = 0x4682B4,
500        tan             = 0xD2B48C, teal              = 0x008080, thistle               = 0xD8BFD8, tomato           = 0xFF6347,
501        turquoise       = 0x40E0D0, violet            = 0xEE82EE, wheat                 = 0xF5DEB3, white            = 0xFFFFFF,
502        whitesmoke      = 0xF5F5F5, yellow            = 0xFFFF00, yellowgreen           = 0x9ACD32,
503    }
504
505    local f_rgb      = formatters['    withcolor svgcolor(%.3N,%.3N,%.3N)']
506    local f_cmyk     = formatters['    withcolor svgcmyk(%.3N,%.3N,%.3N,%.3N)']
507    local f_gray     = formatters['    withcolor svggray(%.3N)']
508    local f_rgba     = formatters['    withcolor svgcolor(%.3N,%.3N,%.3N) withopacity %.3N']
509    local f_graya    = formatters['    withcolor svggray(%.3N) withopacity %.3N']
510    local f_name     = formatters['    withcolor "%s"']
511    local f_svgrgb   = formatters['svgcolor(%.3N,%.3N,%.3N)']
512    local f_svgcmyk  = formatters['svgcmyk(%.3N,%.3N,%.3N,%.3N)']
513    local f_svggray  = formatters['svggray(%.3N)']
514    local f_svgname  = formatters['"%s"']
515
516    local triplets = setmetatableindex(function(t,k)
517        -- we delay building all these strings
518        local v = svgcolors[k]
519        if v then
520            v = { ((v>>16)&0xFF)/0xFF, ((v>>8)&0xFF)/0xFF, ((v>>0)&0xFF)/0xFF }
521        else
522            v = false
523        end
524        t[k] = v
525        return v
526    end)
527
528    local p_fraction  = C(p_number) * C("%")^-1  / function(a,b) return tonumber(a) / (b and 100 or 255) end
529    local p_angle     = C(p_number) * P("deg")^0 / function(a)   return tonumber(a) end
530    local p_percent   = C(p_number) * P("%")     / function(a)   return tonumber(a) / 100 end
531    local p_absolute  = C(p_number)              / tonumber
532
533    local p_left      = P("(")
534    local p_right     = P(")")
535    local p_a         = P("a")^-1
536    local p_r_a_color = p_left
537                      * (p_fraction * p_separator^-1)^-3
538                      * p_absolute^0
539                      * p_right
540    local p_c_k_color = p_left
541                      * (p_absolute + p_separator^-1)^-4
542                      * p_right
543    local p_h_a_color = p_left
544                      * p_angle
545                      * p_separator   * p_percent
546                      * p_separator   * p_percent
547                      * p_separator^0 * p_absolute^0
548                      * p_right
549
550    local colors      = attributes.colors
551    local colorvalues = colors.values
552    local colorindex  = attributes.list[attributes.private('color')]
553    local hsvtorgb    = colors.hsvtorgb
554    local hwbtorgb    = colors.hwbtorgb
555    local forcedmodel = colors.forcedmodel
556
557    local p_splitcolor = -- offet lowercase ff
558        P("#") * C(p_hexdigit*p_hexdigit)^1 / function(r,g,b)
559            if not r then
560                return "gray", 0
561            elseif not (g and b) then
562                return "gray",
563                    (r == "00" and 0) or (r == "ff" and 1) or (tonumber(r,16)/255)
564            else
565                return "rgb",
566                    (r == "00" and 0) or (r == "ff" and 1) or (tonumber(r,16)/255),
567                    (g == "00" and 0) or (g == "ff" and 1) or (tonumber(g,16)/255),
568                    (b == "00" and 0) or (b == "ff" and 1) or (tonumber(b,16)/255)
569            end
570        end
571      + P("rgb") * p_a
572      * p_r_a_color / function(r,g,b,a)
573            return "rgb", r or 0, g or 0, b or 0, a or false
574        end
575      + P("cmyk")
576      * p_c_k_color / function(c,m,y,k)
577            return "cmyk", c or 0, m or 0, y or 0, k or 0
578        end
579      + P("hsl") * p_a
580      * p_h_a_color / function(h,s,l,a)
581            local r, g, b = hsvtorgb(h,s,l,a)
582            return "rgb", r or 0, g or 0, b or 0, a or false
583        end
584      + P("hwb") * p_a
585      * p_h_a_color / function(h,w,b,a)
586            local r, g, b = hwbtorgb(h,w,b)
587            return "rgb", r or 0, g or 0, b or 0, a or false
588        end
589
590    function metapost.svgsplitcolor(color)
591        if type(color) == "string" then
592            local what, s1, s2, s3, s4 = lpegmatch(p_splitcolor,color)
593            if not what then
594                local t = triplets[color]
595                if t then
596                    what, s1, s2, s3 = "rgb", t[1], t[2], t[3]
597                end
598            end
599            return what, s1, s2, s3, s4
600        else
601            return "gray", 0, false
602        end
603    end
604
605    local function registeredcolor(name)
606        local color = colorindex[name]
607        if color then
608            local v = colorvalues[color]
609            local t = forcedmodel(v[1])
610            if t == 2 then
611                return "gray", v[2]
612            elseif t == 3 then
613                return "rgb", v[3], v[4], v[5]
614            elseif t == 4 then
615                return "cmyk", v[6], v[7], v[8], v[9]
616            else
617                --
618            end
619        end
620    end
621
622    -- we can have a fast check for #000000
623
624    local function validcolor(color)
625        if usedcolors then
626            usedcolors[color] = usedcolors[color] + 1
627        end
628        if colormap then
629            local c = colormap[color]
630            local t = type(c)
631            if t == "table" then
632                local what = t[1]
633                if what == "rgb" then
634                    return
635                        what,
636                        tonumber(t[2]) or 0,
637                        tonumber(t[3]) or 0,
638                        tonumber(t[4]) or 0,
639                        tonumber(t[5]) or false
640                elseif what == "cmyk" then
641                    return
642                        what,
643                        tonumber(t[2]) or 0,
644                        tonumber(t[3]) or 0,
645                        tonumber(t[4]) or 0,
646                        tonumber(t[5]) or 0
647                elseif what == "gray" then
648                    return
649                        what,
650                        tonumber(t[2]) or 0,
651                        tonumber(t[3]) or false
652                end
653            elseif t == "string" then
654                color = c
655            end
656        end
657        if color == "#000000" then
658            return "rgb", 0, 0, 0
659        elseif color == "#ffffff" then
660            return "rgb", 1, 1, 1
661        else
662            local what, s1, s2, s3, s4 = registeredcolor(color)
663            if not what then
664                what, s1, s2, s3, s4 = lpegmatch(p_splitcolor,color)
665                -- we could cache
666                if not what then
667                    local t = triplets[color]
668                    if t then
669                        s1, s2, s3 = t[1], t[2], t[3]
670                        what = "rgb"
671                    end
672                end
673            end
674            return what, s1, s2, s3, s4
675        end
676    end
677
678    colorcomponents = function(color)
679        local what, s1, s2, s3, s4 = validcolor(color)
680        return s1, s2, s3, s4 -- so 4 means cmyk
681    end
682
683    withcolor = function(color)
684        local what, s1, s2, s3, s4 = validcolor(color)
685        if what == "rgb" then
686            if s4 then
687                if s1 == s2 and s1 == s3 then
688                    return f_graya(s1,s4)
689                else
690                    return f_rgba(s1,s2,s3,s4)
691                end
692            else
693                if s1 == s2 and s1 == s3 then
694                    return f_gray(s1)
695                else
696                    return f_rgb(s1,s2,s3)
697                end
698            end
699        elseif what == "cmyk" then
700            return f_cmyk(s1,s2,s3,s4)
701        elseif what == "gray" then
702            if s2 then
703                return f_graya(s1,s2)
704            else
705                return f_gray(s1)
706            end
707        end
708        return f_name(color)
709    end
710
711    thecolor = function(color)
712        local what, s1, s2, s3, s4 = validcolor(color)
713        if what == "rgb" then
714            if s4 then
715                if s1 == s2 and s1 == s3 then
716                    return f_svggraya(s1,s4)
717                else
718                    return f_svgrgba(s1,s2,s3,s4)
719                end
720            else
721                if s1 == s2 and s1 == s3 then
722                    return f_svggray(s1)
723                else
724                    return f_svgrgb(s1,s2,s3)
725                end
726            end
727        elseif what == "cmyk" then
728            return f_cmyk(s1,s2,s3,s4)
729        elseif what == "gray" then
730            if s2 then
731                return f_svggraya(s1,s2)
732            else
733                return f_svggray(s1)
734            end
735        end
736        return f_svgname(color)
737    end
738
739end
740
741-- actually we can loop faster because we can go to the last one
742
743local grabpath, grablist  do
744
745    local f_moveto    = formatters['(%N,%N)']
746    local f_curveto_z = formatters['controls(%N,%N)and(%N,%N)..(%N,%N)']
747    local f_curveto_n = formatters['..controls(%N,%N)and(%N,%N)..(%N,%N)']
748    local f_lineto_z  = formatters['(%N,%N)']
749    local f_lineto_n  = formatters['--(%N,%N)']
750
751    local m = { __index = function() return 0 end }
752
753 -- local t      = { }    -- no real saving here if we share
754 -- local n      = 0
755
756    grabpath = function(str)
757        local p   = lpegmatch(p_path,str) or { }
758        local np  = #p
759        local all = { entries = np, closed = false, curve = false }
760        if np == 0 then
761            return all
762        end
763        setmetatable(p,m)
764        local t      = { }    -- no real saving here if we share
765        local n      = 0
766     -- n = 0
767        local a      = 0
768        local i      = 0
769        local last   = "M"
770        local prev   = last
771        local kind   = "L"
772        local x, y   = 0, 0
773        local x1, y1 = 0, 0
774        local x2, y2 = 0, 0
775        local rx, ry = 0, 0
776        local ar, al = 0, 0
777        local as, ac = 0, nil
778        local mx, my = 0, 0
779        while i < np do
780            i = i + 1
781            local pi = p[i]
782            if type(pi) ~= "number" then
783                last = pi
784                i    = i + 1
785                pi   = p[i]
786            end
787            -- most often
788            if last == "c" then
789                            x1 = x + pi
790                i = i + 1 ; y1 = y + p[i]
791                i = i + 1 ; x2 = x + p[i]
792                i = i + 1 ; y2 = y + p[i]
793                i = i + 1 ; x  = x + p[i]
794                i = i + 1 ; y  = y + p[i]
795                goto curveto
796            elseif last == "l" then
797                            x = x + pi
798                i = i + 1 ; y = y + p[i]
799                goto lineto
800            elseif last == "h" then
801                x = x + pi
802                goto lineto
803            elseif last == "v" then
804                y = y + pi
805                goto lineto
806            elseif last == "a" then
807                            x1 =     x
808                            y1 =     y
809                            rx =     pi
810                i = i + 1 ; ry =     p[i]
811                i = i + 1 ; ar =     p[i]
812                i = i + 1 ; al =     p[i]
813                i = i + 1 ; as =     p[i]
814                i = i + 1 ; x  = x + p[i]
815                i = i + 1 ; y  = y + p[i]
816                goto arc
817            elseif last == "s" then
818                if prev == "C" then
819                    x1 = 2 * x - x2
820                    y1 = 2 * y - y2
821                else
822                    x1 = x
823                    y1 = y
824                end
825                            x2 = x + pi
826                i = i + 1 ; y2 = y + p[i]
827                i = i + 1 ; x  = x + p[i]
828                i = i + 1 ; y  = y + p[i]
829                goto curveto
830            elseif last == "m" then
831                if n > 0 then
832                    a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0
833                end
834                            x = x + pi
835                i = i + 1 ; y = y + p[i]
836                goto moveto
837            elseif last == "z" then
838                goto close
839            -- less frequent
840            elseif last == "C" then
841                            x1 = pi
842                i = i + 1 ; y1 = p[i]
843                i = i + 1 ; x2 = p[i]
844                i = i + 1 ; y2 = p[i]
845                i = i + 1 ; x  = p[i]
846                i = i + 1 ; y  = p[i]
847                goto curveto
848            elseif last == "L" then
849                            x = pi
850                i = i + 1 ; y = p[i]
851                goto lineto
852            elseif last == "H" then
853                x = pi
854                goto lineto
855            elseif last == "V" then
856                y = pi
857                goto lineto
858            elseif last == "A" then
859                            x1 = x
860                            y1 = y
861                            rx = pi
862                i = i + 1 ; ry = p[i]
863                i = i + 1 ; ar = p[i]
864                i = i + 1 ; al = p[i]
865                i = i + 1 ; as = p[i]
866                i = i + 1 ; x  = p[i]
867                i = i + 1 ; y  = p[i]
868                goto arc
869            elseif last == "S" then
870                if prev == "C" then
871                    x1 = 2 * x - x2
872                    y1 = 2 * y - y2
873                else
874                    x1 = x
875                    y1 = y
876                end
877                            x2 = pi
878                i = i + 1 ; y2 = p[i]
879                i = i + 1 ; x  = p[i]
880                i = i + 1 ; y  = p[i]
881                goto curveto
882            elseif last == "M" then
883                if n > 0 then
884                    a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0
885                end
886                            x = pi ;
887                i = i + 1 ; y = p[i]
888                goto moveto
889            elseif last == "Z" then
890                goto close
891            -- very seldom
892            elseif last == "q" then
893                            x1 = x + pi
894                i = i + 1 ; y1 = y + p[i]
895                i = i + 1 ; x2 = x + p[i]
896                i = i + 1 ; y2 = y + p[i]
897                goto quadratic
898            elseif last == "t" then
899                if prev == "C" then
900                    x1 = 2 * x - x1
901                    y1 = 2 * y - y1
902                else
903                    x1 = x
904                    y1 = y
905                end
906                            x2 = x + pi
907                i = i + 1 ; y2 = y + p[i]
908                goto quadratic
909            elseif last == "Q" then
910                            x1 = pi
911                i = i + 1 ; y1 = p[i]
912                i = i + 1 ; x2 = p[i]
913                i = i + 1 ; y2 = p[i]
914                goto quadratic
915            elseif last == "T" then
916                if prev == "C" then
917                    x1 = 2 * x - x1
918                    y1 = 2 * y - y1
919                else
920                    x1 = x
921                    y1 = y
922                end
923                            x2 = pi
924                i = i + 1 ; y2 = p[i]
925                goto quadratic
926            else
927                goto continue
928            end
929            ::moveto::
930                n = n + 1 ; t[n] = f_moveto(x,y)
931                last = last == "M" and "L" or "l"
932                prev = "M"
933                mx = x
934                my = y
935                goto continue
936            ::lineto::
937                n = n + 1 ; t[n] = (n > 0 and f_lineto_n or f_lineto_z)(x,y)
938                prev = "L"
939                goto continue
940            ::curveto::
941                n = n + 1 ; t[n] = (n > 0 and f_curveto_n or f_curveto_z)(x1,y1,x2,y2,x,y)
942                prev = "C"
943                goto continue
944            ::arc::
945                ac = a2c(x1,y1,rx,ry,ar,al,as,x,y)
946                for i=1,#ac,6 do
947                    n = n + 1 ; t[n] = (n > 0 and f_curveto_n or f_curveto_z)(
948                        ac[i],ac[i+1],ac[i+2],ac[i+3],ac[i+4],ac[i+5]
949                    )
950                end
951                prev = "A"
952                goto continue
953            ::quadratic::
954                n = n + 1 ; t[n] = (n > 0 and f_curveto_n or f_curveto_z)(
955                    x  + 2/3 * (x1-x ), y  + 2/3 * (y1-y ),
956                    x2 + 2/3 * (x1-x2), y2 + 2/3 * (y1-y2),
957                    x2,                 y2
958                )
959                x = x2
960                y = y2
961                prev = "C"
962                goto continue
963            ::close::
964            --  n = n + 1 ; t[n] = prev == "C" and "..cycle" or "--cycle"
965                n = n + 1 ; t[n] = "--cycle"
966                if n > 0 then
967                    a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0
968                end
969                if i == np then
970                    break
971                else
972                    i = i - 1
973                end
974                kind = prev
975                prev = "Z"
976                -- this is kind of undocumented: a close also moves back
977                x = mx
978                y = my
979            ::continue::
980        end
981        if n > 0 then
982            a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0
983        end
984        if prev == "Z" then
985            all.closed = true
986        end
987        all.curve = (kind == "C" or kind == "A")
988        return all, p
989    end
990
991    -- this is a bit tricky as what are points for a mark ... the next can be simplified
992    -- a lot
993
994    grablist = function(p)
995        local np  = #p
996        if np == 0 then
997            return nil
998        end
999        local t      = { }
1000        local n      = 0
1001        local a      = 0
1002        local i      = 0
1003        local last   = "M"
1004        local prev   = last
1005        local kind   = "L"
1006        local x, y   = 0, 0
1007        local x1, y1 = 0, 0
1008        local x2, y2 = 0, 0
1009        local rx, ry = 0, 0
1010        local ar, al = 0, 0
1011        local as, ac = 0, nil
1012        local mx, my = 0, 0
1013        while i < np do
1014            i = i + 1
1015            local pi = p[i]
1016            if type(pi) ~= "number" then
1017                last = pi
1018                i    = i + 1
1019                pi   = p[i]
1020            end
1021            -- most often
1022            if last == "c" then
1023                            x1 = x + pi
1024                i = i + 1 ; y1 = y + p[i]
1025                i = i + 1 ; x2 = x + p[i]
1026                i = i + 1 ; y2 = y + p[i]
1027                i = i + 1 ; x  = x + p[i]
1028                i = i + 1 ; y  = y + p[i]
1029                goto curveto
1030            elseif last == "l" then
1031                            x = x + pi
1032                i = i + 1 ; y = y + p[i]
1033                goto lineto
1034            elseif last == "h" then
1035                x = x + pi
1036                goto lineto
1037            elseif last == "v" then
1038                y = y + pi
1039                goto lineto
1040            elseif last == "a" then
1041                            x1 =     x
1042                            y1 =     y
1043                            rx =     pi
1044                i = i + 1 ; ry =     p[i]
1045                i = i + 1 ; ar =     p[i]
1046                i = i + 1 ; al =     p[i]
1047                i = i + 1 ; as =     p[i]
1048                i = i + 1 ; x  = x + p[i]
1049                i = i + 1 ; y  = y + p[i]
1050                goto arc
1051            elseif last == "s" then
1052                if prev == "C" then
1053                    x1 = 2 * x - x2
1054                    y1 = 2 * y - y2
1055                else
1056                    x1 = x
1057                    y1 = y
1058                end
1059                            x2 = x + pi
1060                i = i + 1 ; y2 = y + p[i]
1061                i = i + 1 ; x  = x + p[i]
1062                i = i + 1 ; y  = y + p[i]
1063                goto curveto
1064            elseif last == "m" then
1065                            x = x + pi
1066                i = i + 1 ; y = y + p[i]
1067                goto moveto
1068            elseif last == "z" then
1069                goto close
1070            -- less frequent
1071            elseif last == "C" then
1072                            x1 = pi
1073                i = i + 1 ; y1 = p[i]
1074                i = i + 1 ; x2 = p[i]
1075                i = i + 1 ; y2 = p[i]
1076                i = i + 1 ; x  = p[i]
1077                i = i + 1 ; y  = p[i]
1078                goto curveto
1079            elseif last == "L" then
1080                            x = pi
1081                i = i + 1 ; y = p[i]
1082                goto lineto
1083            elseif last == "H" then
1084                x = pi
1085                goto lineto
1086            elseif last == "V" then
1087                y = pi
1088                goto lineto
1089            elseif last == "A" then
1090                            x1 = x
1091                            y1 = y
1092                            rx = pi
1093                i = i + 1 ; ry = p[i]
1094                i = i + 1 ; ar = p[i]
1095                i = i + 1 ; al = p[i]
1096                i = i + 1 ; as = p[i]
1097                i = i + 1 ; x  = p[i]
1098                i = i + 1 ; y  = p[i]
1099                goto arc
1100            elseif last == "S" then
1101                if prev == "C" then
1102                    x1 = 2 * x - x2
1103                    y1 = 2 * y - y2
1104                else
1105                    x1 = x
1106                    y1 = y
1107                end
1108                            x2 = pi
1109                i = i + 1 ; y2 = p[i]
1110                i = i + 1 ; x  = p[i]
1111                i = i + 1 ; y  = p[i]
1112                goto curveto
1113            elseif last == "M" then
1114                            x = pi ;
1115                i = i + 1 ; y = p[i]
1116                goto moveto
1117            elseif last == "Z" then
1118                goto close
1119            -- very seldom
1120            elseif last == "q" then
1121                            x1 = x + pi
1122                i = i + 1 ; y1 = y + p[i]
1123                i = i + 1 ; x2 = x + p[i]
1124                i = i + 1 ; y2 = y + p[i]
1125                goto quadratic
1126            elseif last == "t" then
1127                if prev == "C" then
1128                    x1 = 2 * x - x1
1129                    y1 = 2 * y - y1
1130                else
1131                    x1 = x
1132                    y1 = y
1133                end
1134                            x2 = x + pi
1135                i = i + 1 ; y2 = y + p[i]
1136                goto quadratic
1137            elseif last == "Q" then
1138                            x1 = pi
1139                i = i + 1 ; y1 = p[i]
1140                i = i + 1 ; x2 = p[i]
1141                i = i + 1 ; y2 = p[i]
1142                goto quadratic
1143            elseif last == "T" then
1144                if prev == "C" then
1145                    x1 = 2 * x - x1
1146                    y1 = 2 * y - y1
1147                else
1148                    x1 = x
1149                    y1 = y
1150                end
1151                            x2 = pi
1152                i = i + 1 ; y2 = p[i]
1153                goto quadratic
1154            else
1155                goto continue
1156            end
1157            ::moveto::
1158                n = n + 1 ; t[n] = x
1159                n = n + 1 ; t[n] = y
1160                last = last == "M" and "L" or "l"
1161                prev = "M"
1162                mx = x
1163                my = y
1164                goto continue
1165            ::lineto::
1166                n = n + 1 ; t[n] = x
1167                n = n + 1 ; t[n] = y
1168                prev = "L"
1169                goto continue
1170            ::curveto::
1171                n = n + 1 ; t[n] = x
1172                n = n + 1 ; t[n] = y
1173                prev = "C"
1174                goto continue
1175            ::arc::
1176                ac = a2c(x1,y1,rx,ry,ar,al,as,x,y)
1177                for i=1,#ac,6 do
1178                    n = n + 1 ; t[n] = ac[i+4]
1179                    n = n + 1 ; t[n] = ac[i+5]
1180                end
1181                prev = "A"
1182                goto continue
1183            ::quadratic::
1184                n = n + 1 ; t[n] = x2
1185                n = n + 1 ; t[n] = y2
1186                x = x2
1187                y = y2
1188                prev = "C"
1189                goto continue
1190            ::close::
1191                n = n + 1 ; t[n] = mx
1192                n = n + 1 ; t[n] = my
1193                if i == np then
1194                    break
1195                end
1196                kind = prev
1197                prev = "Z"
1198                x = mx
1199                y = my
1200            ::continue::
1201        end
1202        return t
1203    end
1204
1205end
1206
1207-- todo: viewbox helper
1208
1209local s_wrapped_start <const> = "draw image ("
1210local f_wrapped_stop          = formatters[") shifted (0,%N) scaled %N ;"]
1211
1212local handletransform, handleviewbox  do
1213
1214    local sind = math.sind
1215
1216 -- local f_rotatedaround           = formatters["svg_p := svg_p rotatedaround((%N,%N),%N) ;"]
1217 -- local f_rotated                 = formatters["svg_p := svg_p rotated(%N) ;"]
1218 -- local f_shifted                 = formatters["svg_p := svg_p shifted(%N,%N) ;"]
1219 -- local f_slanted_x               = formatters["svg_p := svg_p xslanted(%N) ;"]
1220 -- local f_slanted_y               = formatters["svg_p := svg_p yslanted(%N) ;"]
1221 -- local f_scaled                  = formatters["svg_p := svg_p scaled(%N) ;"]
1222 -- local f_xyscaled                = formatters["svg_p := svg_p xyscaled(%N,%N) ;"]
1223 -- local f_matrix                  = formatters["svg_p := svg_p transformed bymatrix(%N,%N,%N,%N,%N,%N) ;"]
1224 -- local s_transform_start <const> = "draw image ( begingroup ; save svg_p ; picture svg_p ; svg_p := image ( "
1225 -- local f_transform_stop          = formatters[" ; ) ; %s ; draw svg_p ; endgroup ; ) ; "]
1226
1227    local f_rotatedaround           = formatters["rotatedaround((%N,%N),%N) "]
1228    local f_rotated                 = formatters["rotated(%N) "]
1229    local f_shifted                 = formatters["shifted(%N,%N) "]
1230    local f_slanted_x               = formatters["xslanted(%N) "]
1231    local f_slanted_y               = formatters["yslanted(%N) "]
1232    local f_scaled                  = formatters["scaled(%N) "]
1233    local f_xyscaled                = formatters["xyscaled(%N,%N) "]
1234    local f_matrix                  = formatters["transformed bymatrix(%N,%N,%N,%N,%N,%N) "]
1235    local s_transform_start <const> = "draw image ( "
1236    local f_transform_stop          = formatters[") %s ; "]
1237
1238    local transforms    = { }
1239    local noftransforms = 0
1240
1241    local function rotate(r,x,y)
1242        if r then
1243            noftransforms = noftransforms + 1
1244            if x then
1245                transforms[noftransforms] = f_rotatedaround(x,-(y or x),-r)
1246            else
1247                transforms[noftransforms] = f_rotated(-r)
1248            end
1249        end
1250    end
1251
1252    local function translate(x,y)
1253        if x == 0 then x = false end
1254        if y == 0 then y = false end
1255        if y then
1256            noftransforms = noftransforms + 1
1257            transforms[noftransforms] = f_shifted(x or 0,-y)
1258        elseif x then
1259            noftransforms = noftransforms + 1
1260            transforms[noftransforms] = f_shifted(x,0)
1261        end
1262    end
1263
1264    local function scale(x,y)
1265        if x == 1 then x = false end
1266        if y == 1 then y = false end
1267        if y then
1268            noftransforms = noftransforms + 1
1269            transforms[noftransforms] = f_xyscaled(x or 1,y)
1270        elseif x then
1271            noftransforms = noftransforms + 1
1272            transforms[noftransforms] = f_scaled(x)
1273        end
1274    end
1275
1276    local function skew(x,y)
1277     -- if x = 0 then x = false end
1278     -- if y = 0 then y = false end
1279        if x then
1280            noftransforms = noftransforms + 1
1281            transforms[noftransforms] = f_slanted_x(sind(-x))
1282        end
1283        if y then
1284            noftransforms = noftransforms + 1
1285            transforms[noftransforms] = f_slanted_y(sind(-y))
1286        end
1287    end
1288
1289    local function matrix(rx,sx,sy,ry,tx,ty)
1290        if not ty then
1291            ty = 0
1292        end
1293        if not tx then
1294            tx = 0
1295        end
1296        if not sx then
1297            sx = 0
1298        end
1299        if not sy then
1300            sy = 0
1301        end
1302        if not rx then
1303            rx = 1
1304        end
1305        if not ry then
1306            ry = 1
1307        end
1308        noftransforms = noftransforms + 1
1309     -- transforms[noftransforms] = f_matrix(rx, sx, sy, ry, tx, -ty)
1310        -- https://en.wikipedia.org/wiki/Rotation_matrix : we're counter clockwise
1311        transforms[noftransforms] = f_matrix(rx, -sy, -sx, ry, tx, -ty)
1312    end
1313
1314    local p_transform = (
1315            lpegpatterns.whitespace^0 * (
1316            P("translate")  * (p_numbers / translate)      -- maybe xy
1317          + P("scale")      * (p_numbers / scale)
1318          + P("rotate")     * (p_numbers / rotate)
1319          + P("matrix")     * (p_numbers / matrix)
1320          + P("skew")       * (p_numbers / skew)
1321          + P("translateX") * (p_numbers / translate)
1322          + P("translateY") * (Cc(false) * p_numbers / translate)
1323          + P("scaleX")     * (p_numbers / translate)
1324          + P("scaleY")     * (Cc(false) * p_numbers / translate)
1325          + P("skewX")      * (p_numbers / skew)
1326          + P("skewY")      * (Cc(false) * p_numbers / skew)
1327        )
1328    )^1
1329
1330    -- indeed, we need to reverse the order ... not that pretty and counter intuitive too
1331
1332    local function combined()
1333        if noftransforms == 1 then
1334            return transforms[1]
1335        elseif noftransforms == 2 then
1336            return transforms[2] .. transforms[1]
1337        elseif noftransforms == 3 then
1338            return transforms[3] .. transforms[2] .. transforms[1]
1339        else
1340            -- the rare case (but anything can happen in svg and it gets worse)
1341            local m = noftransforms + 1
1342            for i=1,noftransforms//2 do
1343                local j = m - i
1344                transforms[i], transforms[j] = transforms[j], transforms[i]
1345            end
1346            return concat(transforms,"",1,noftransforms)
1347        end
1348    end
1349
1350    handletransform = function(at)
1351        local t = at.transform
1352        if t then
1353            noftransforms = 0
1354            lpegmatch(p_transform,t)
1355            if noftransforms > 0 then
1356                -- currentpicture
1357                return s_transform_start, f_transform_stop(combined()), t
1358            end
1359        end
1360    end
1361
1362    handletransformstring = function(t)
1363        if t then
1364            noftransforms = 0
1365            lpegmatch(p_transform,t)
1366            return noftransforms > 0 and combined()
1367        end
1368    end
1369
1370    handleviewbox = function(v)
1371        if v then
1372            local x, y, w, h = lpegmatch(p_fournumbers,v)
1373            if h then
1374                return x, y, w, h
1375            end
1376        end
1377    end
1378
1379end
1380
1381local dashed  do
1382
1383    -- actually commas are mandate but we're tolerant
1384
1385    local f_dashed_n = formatters[" dashed dashpattern (%s ) "]
1386    local f_dashed_y = formatters[" dashed dashpattern (%s ) shifted (%N,0) "]
1387
1388    local p_number   = p_optseparator/"" * p_number_r
1389    local p_on       = Cc(" on ")  * p_number
1390    local p_off      = Cc(" off ") * p_number
1391    local p_dashed   = Cs((p_on * p_off^-1)^1)
1392
1393    dashed = function(s,o)
1394        if not find(s,",") then
1395            -- a bit of a hack:
1396            s = s .. " " .. s
1397        end
1398        return (o and f_dashed_y or f_dashed_n)(lpegmatch(p_dashed,s),o)
1399    end
1400
1401end
1402
1403do
1404
1405    local handlers    = { }
1406    local process     = false
1407    local root        = false
1408    local result      = false
1409    local r           = false
1410    local definitions = false
1411    local classstyles = false
1412    local tagstyles   = false
1413
1414    local tags = {
1415        ["a"]                  = true,
1416     -- ["altgGlyph"]          = true,
1417     -- ["altgGlyphDef"]       = true,
1418     -- ["altgGlyphItem"]      = true,
1419     -- ["animate"]            = true,
1420     -- ["animateColor"]       = true,
1421     -- ["animateMotion"]      = true,
1422     -- ["animateTransform"]   = true,
1423        ["circle"]             = true,
1424        ["clipPath"]           = true,
1425     -- ["color-profile"]      = true,
1426     -- ["cursor"]             = true,
1427        ["defs"]               = true,
1428     -- ["desc"]               = true,
1429        ["ellipse"]            = true,
1430     -- ["filter"]             = true,
1431     -- ["font"]               = true,
1432     -- ["font-face"]          = true,
1433     -- ["font-face-format"]   = true,
1434     -- ["font-face-name"]     = true,
1435     -- ["font-face-src"]      = true,
1436     -- ["font-face-uri"]      = true,
1437     -- ["foreignObject"]      = true,
1438        ["g"]                  = true,
1439     -- ["glyph"]              = true,
1440     -- ["glyphRef"]           = true,
1441     -- ["hkern"]              = true,
1442        ["image"]              = true,
1443        ["line"]               = true,
1444        ["linearGradient"]     = true,
1445        ["marker"]             = true,
1446     -- ["mask"]               = true,
1447     -- ["metadata"]           = true,
1448     -- ["missing-glyph"]      = true,
1449     -- ["mpath"]              = true,
1450        ["path"]               = true,
1451        ["pattern"]            = true,
1452        ["polygon"]            = true,
1453        ["polyline"]           = true,
1454        ["radialGradient"]     = true,
1455        ["rect"]               = true,
1456     -- ["script"]             = true,
1457     -- ["set"]                = true,
1458        ["stop"]               = true,
1459        ["style"]              = true,
1460        ["svg"]                = true,
1461     -- ["switch"]             = true,
1462        ["symbol"]             = true,
1463        ["text"]               = true,
1464     -- ["textPath"]           = true,
1465     -- ["title"]              = true,
1466        ["tspan"]              = true,
1467        ["use"]                = true,
1468     -- ["view"]               = true,
1469     -- ["vkern"]              = true,
1470    }
1471
1472    local usetags = {
1473        ["circle"]             = true,
1474        ["ellipse"]            = true,
1475        ["g"]                  = true,
1476        ["image"]              = true,
1477        ["line"]               = true,
1478        ["path"]               = true,
1479        ["polygon"]            = true,
1480        ["polyline"]           = true,
1481        ["rect"]               = true,
1482     -- ["text"]               = true,
1483     -- ["tspan"]              = true,
1484    }
1485
1486    local pathtracer = {
1487        ["stroke"]         = "darkred",
1488        ["stroke-opacity"] = ".5",
1489        ["stroke-width"]   = ".5",
1490        ["fill"]           = "darkgray",
1491        ["fill-opacity"]   = ".75",
1492    }
1493
1494    local function handlechains(c)
1495        if tags[c.tg] then
1496            local at = c.at
1497            local dt = c.dt
1498            if at and dt then
1499             -- at["inkscape:connector-curvature"] = nil -- cleare entry and might prevent table growth
1500                local estyle = rawget(at,"style")
1501                if estyle and estyle ~= "" then
1502                    for k, v in gmatch(estyle,"%s*([^:]+):%s*([^;]+);?") do
1503                        at[k] = v
1504                    end
1505                end
1506                local eclass = rawget(at,"class")
1507                if eclass and eclass ~= "" then
1508                    for c in gmatch(eclass,"[^ ]+") do
1509                        local s = classstyles[c]
1510                        if s then
1511                            for k, v in next, s do
1512                                at[k] = v
1513                            end
1514                        end
1515                    end
1516                end
1517                local tstyle = tagstyles[tag]
1518                if tstyle then
1519                    for k, v in next, tstyle do
1520                        at[k] = v
1521                    end
1522                end
1523                if trace_path and pathtracer then
1524                    for k, v in next, pathtracer do
1525                        at[k] = v
1526                    end
1527                end
1528                for i=1,#dt do
1529                    local dti = dt[i]
1530                    if type(dti) == "table" then
1531                        handlechains(dti)
1532                    end
1533                end
1534            end
1535        end
1536    end
1537
1538    local handlestyle  do
1539
1540        -- It can also be CDATA but that is probably dealt with because we only
1541        -- check for style entries and ignore the rest. But maybe we also need
1542        -- to check a style at the outer level?
1543
1544        local p_key   = C((R("az","AZ","09","__","--")^1))
1545        local p_spec  = P("{") * C((1-P("}"))^1) * P("}")
1546        local p_valid = Carg(1) * P(".") * p_key + Carg(2) * p_key
1547        local p_grab  = ((p_valid * p_space^0 * p_spec / rawset) + p_space^1 + P(1))^1
1548
1549        local fontspecification = css.fontspecification
1550
1551        handlestyle = function(c)
1552            local s = xmltext(c)
1553            lpegmatch(p_grab,s,1,classstyles,tagstyles)
1554            for k, v in next, classstyles do
1555                local t = { }
1556                for k, v in gmatch(v,"%s*([^:]+):%s*([^;]+);?") do
1557                    if k == "font" then
1558                        local s = fontspecification(v)
1559                        for k, v in next, s do
1560                            t["font-"..k] = v
1561                        end
1562                    else
1563                        t[k] = v
1564                    end
1565                end
1566                classstyles[k] = t
1567            end
1568            for k, v in next, tagstyles do
1569                local t = { }
1570                for k, v in gmatch(v,"%s*([^:]+):%s*([^;]+);?") do
1571                    if k == "font" then
1572                        local s = fontspecification(v)
1573                        for k, v in next, s do
1574                            t["font-"..k] = v
1575                        end
1576                    else
1577                        t[k] = v
1578                    end
1579                end
1580                tagstyles[k] = t
1581            end
1582        end
1583
1584        function handlers.style()
1585            -- ignore
1586        end
1587
1588    end
1589
1590    -- We can have root in definitions and then do a metatable lookup but use
1591    -- is not used that often I guess.
1592
1593    local function locate(id,c)
1594        if id == none then
1595            return
1596        end
1597        local res = definitions[id]
1598        local ref
1599        if res then
1600            return res
1601        end
1602        ref = gsub(id,"^url%(#(.-)%)$","%1")
1603        ref = gsub(ref,"^#","")
1604        -- we can make a fast id lookup
1605        res = xmlfirst(root,"**[@id='"..ref.."']")
1606        if res then
1607            definitions[id] = res
1608            return res
1609        end
1610        -- we expect resource paths to be specified but for now we want
1611        -- them on the same path .. we could use the url splitter .. todo
1612        ref = url.hashed(id)
1613        if not ref.nosheme and ref.scheme == "file" then
1614            local filename = ref.filename
1615            local fragment = ref.fragment
1616            if filename and filename ~= "" then
1617                local fullname = resolvers.findbinfile(filename)
1618                if lfs.isfile(fullname) then
1619                    report("loading use file: %s",fullname)
1620                    local root = xml.load(fullname)
1621                    res = xmlfirst(root,"**[@id='"..fragment.."']")
1622                    if res then
1623                        xmlinheritattributes(res,c) -- tricky
1624                        setmetatableindex(res.at,c.at)
1625                        definitions[id] = res
1626                        return res
1627                    end
1628                end
1629            end
1630        end
1631        return res
1632    end
1633
1634    -- also locate
1635
1636    local function handleclippath(at)
1637        local clippath = at["clip-path"]
1638
1639        if not clippath then
1640            return
1641        end
1642
1643        local spec = definitions[clippath] or locate(clippath)
1644
1645        -- do we really need this crap
1646        if not spec then
1647            local index = match(clippath,"(%d+)")
1648            if index then
1649                spec = xmlfirst(root,"clipPath["..tostring(tonumber(index) or 0).."]")
1650            end
1651        end
1652        -- so far for the crap
1653
1654        if not spec then
1655            report("unknown clip %a",clippath)
1656            return
1657        elseif spec.tg ~= "clipPath" then
1658            report("bad clip %a",clippath)
1659            return
1660        end
1661
1662      ::again::
1663        for c in xmlcollected(spec,"/(path|use|g)") do
1664            local tg = c.tg
1665            if tg == "use" then
1666                local ca = c.at
1667                local id = ca["xlink:href"]
1668                if id then
1669                    spec = locate(id)
1670                    if spec then
1671                        local sa = spec.at
1672                        setmetatableindex(sa,ca)
1673                        if spec.tg == "path" then
1674                            local d = sa.d
1675                            if d then
1676                                local p = grabpath(d)
1677                                p.evenodd = sa["clip-rule"] == "evenodd"
1678                                p.close = true
1679                                return p, clippath
1680                            else
1681                                return
1682                            end
1683                        else
1684                            goto again
1685                        end
1686                    end
1687                end
1688             -- break
1689            elseif tg == "path" then
1690                local ca = c.at
1691                local d  = rawget(ca,"d")
1692                if d then
1693                    local p = grabpath(d)
1694                    p.evenodd = ca["clip-rule"] == "evenodd"
1695                    p.close   = true
1696                    local transform = rawget(ca,"transform")
1697                    if transform then
1698                        transform = handletransformstring(transform)
1699                    end
1700                    return p, clippath, transform
1701                else
1702                    return
1703                end
1704            else
1705                -- inherit?
1706            end
1707        end
1708    end
1709
1710    -- todo: clip = [ auto | rect(llx,lly,urx,ury) ]
1711
1712    local s_rotation_start <const> = "draw image ( "
1713    local f_rotation_stop          = formatters[") rotatedaround((0,0),-angle((%N,%N))) ;"]
1714    local f_rotation_angle         = formatters[") rotatedaround((0,0),-%N) ;"]
1715
1716    local s_offset_start   <const> = "draw image ( "
1717    local f_offset_stop            = formatters[") shifted (%N,%N) ;"]
1718    local s_size_start     <const> = "draw image ( "
1719    local f_size_stop              = formatters[") xysized (%N,%N) ;"]
1720
1721    local handleoffset, handlesize do
1722
1723        handleoffset = function(at)
1724            local x = asnumber_vx(rawget(at,"x"))
1725            local y = asnumber_vy(rawget(at,"y"))
1726            if x ~= 0 or y ~= 0 then
1727                return s_offset_start, f_offset_stop(x,y)
1728            end
1729        end
1730
1731        handlesize = function(at)
1732            local width  = asnumber_x(rawget(at,"width"))
1733            local height = asnumber_y(rawget(at,"height"))
1734            if width == 0 or height == 0 then
1735                -- bad scaling
1736            elseif width == 1 and height == 1 then
1737                -- no need for scaling
1738            else
1739                return s_size_start, f_size_stop(width,height)
1740            end
1741        end
1742
1743    end
1744
1745    function handlers.symbol(c)
1746        local at  = c.at
1747        -- x y refX refY
1748        local boffset, eoffset = handleoffset(at)
1749        local bsize, esize = handlesize(at)
1750        local btransform, etransform, transform = handletransform(at)
1751
1752        if boffset then
1753            r = r + 1 result[r] = boffset
1754        end
1755        if btransform then
1756            r = r + 1 result[r] = btransform
1757        end
1758        if bsize then
1759            r = r + 1 ; result[r] = bsize
1760        end
1761
1762-- local _x = at.x       at.x      = 0
1763-- local _y = at.y       at.y      = 0
1764-- local _w = at.width   at.width  = 0
1765-- local _h = at.height  at.height = 0
1766
1767        process(c,"/*")
1768-- at.x      = _x
1769-- at.y      = _y
1770-- at.width  = _w
1771-- at.height = _h
1772
1773        if esize then
1774            r = r + 1 result[r] = esize
1775        end
1776        if etransform then
1777            r = r + 1 ; result[r] = etransform
1778        end
1779        if eoffset then
1780            r = r + 1 result[r] = eoffset
1781        end
1782    end
1783
1784    -- do
1785
1786    local s_shade_linear       =            '    withshademethod "linear" '
1787    local s_shade_circular     =            '    withshademethod "circular" '
1788    local f_color              = formatters['    withcolor "%s"']
1789    local f_opacity            = formatters['    withopacity %N']
1790    local f_pen                = formatters['    withpen pencircle scaled %N']
1791
1792
1793    -- todo: gradient unfinished
1794    -- todo: opacity but first we need groups in mp
1795
1796    -- this is rather hard to deal with because browsers differ (at the time of writing)
1797    -- and what they show on screen comes out different (or not at all) in print
1798
1799    -- todo: gradientUnits = "userSpaceOnUse" : use units instead of ratios
1800
1801    -- spreadMethod = "pad"     : default
1802    -- spreadMethod = "repeat"  : crap
1803    -- spreadMethod = "reflect" : crap
1804
1805    -- stop-opacity = "0"       : strange, just use steps for that
1806
1807    -- todo: test for kind independently in caller, make a plug instead
1808
1809    local function pattern(id)
1810        local c = definitions[id] -- no locate !
1811        if c and c.tg == "pattern" then
1812            -- just use result and then prune
1813            local _r      = r
1814            local _result = result
1815            r      = 0
1816            result = { }
1817            --
1818         -- handlers.pattern(spec)
1819            --
1820            -- inlined because of width
1821            --
1822            local at  = c.at
1823
1824            local width  = asnumber_x(rawget(at,"width"))
1825            local height = asnumber_y(rawget(at,"height"))
1826            if width == 0 or height == 0 then
1827                -- bad scaling
1828                width  = nil
1829                height = nil
1830            elseif width == 1 and height == 1 then
1831                -- no need for scaling
1832                width  = nil
1833                height = nil
1834            else
1835                -- for now only relative
1836            end
1837
1838            local boffset, eoffset = handleoffset(at)
1839         -- local bsize, esize = handlesize(at)
1840            local btransform, etransform, transform = handletransform(at)
1841
1842            if boffset then
1843                r = r + 1 result[r] = boffset
1844            end
1845            if btransform then
1846                r = r + 1 result[r] = btransform
1847            end
1848         -- if bsize then
1849         --     r = r + 1 ; result[r] = bsize
1850         -- end
1851
1852            local _x = at.x       at.x      = 0
1853            local _y = at.y       at.y      = 0
1854            local _w = at.width   at.width  = 0
1855            local _h = at.height  at.height = 0
1856
1857            process(c,"/*")
1858
1859            at.x      = _x
1860            at.y      = _y
1861            at.width  = _w
1862            at.height = _h
1863
1864         -- if esize then
1865         --     r = r + 1 result[r] = esize
1866         -- end
1867            if etransform then
1868                r = r + 1 ; result[r] = etransform
1869            end
1870            if eoffset then
1871                r = r + 1 result[r] = eoffset
1872            end
1873            --
1874            local okay
1875            if width and height then
1876                okay = formatters["    withpattern image ( % t )\n    withpatternscale(%N,%N)"](result,width,height)
1877            else
1878                okay = formatters["    withpattern image ( % t )"](result)
1879            end
1880            r      = _r
1881            result = _result
1882            return okay
1883        end
1884    end
1885
1886    local gradient do
1887
1888        local f_shade_step         = formatters['withshadestep ( withshadefraction %N withshadecolors (%s,%s) )']
1889        local f_shade_step_opacity = formatters['withshadestep ( withshadefraction %N withshadecolors (%s,%s) withshadeopacity %N )']
1890        local f_shade_center       = formatters['withshadecenter (%N,%N)']
1891        local f_shade_center_f     = formatters['withshadecenterfraction (%N,%N)']
1892        local f_shade_radius       = formatters['withshaderadius (%N,%N) ']
1893        local f_shade_radius_f     = formatters['withshaderadiusfraction %N']
1894        local f_shade_center_one   = formatters['withshadecenterone (%N,%N)']
1895        local f_shade_center_two   = formatters['withshadecentertwo (%N,%N)']
1896        local f_shade_center_one_f = formatters['withshadecenteronefraction (%N,%N)']
1897        local f_shade_center_two_f = formatters['withshadecentertwofraction (%N,%N)']
1898
1899        gradient = function(id)
1900            local spec = definitions[id] -- no locate !
1901            if spec then
1902                local kind  = spec.tg
1903                local shade = nil
1904                local n     = 1
1905                local a     = spec.at
1906                -- bah
1907                local gu = rawget(a, "gradientUnits")         -- userSpaceOnUse
1908                local gt = rawget(a, "gradientTransform")
1909                local sm = rawget(a, "spreadMethod")
1910                --
1911                local userspace = gu == "userSpaceOnUse"
1912                --
1913                if kind == "linearGradient" then
1914                    shade = { s_shade_linear }
1915                    --
1916                    local x1 = rawget(a,"x1")
1917                    local y1 = rawget(a,"y1")
1918                    local x2 = rawget(a,"x2")
1919                    local y2 = rawget(a,"y2")
1920                    if x1 and y1 then
1921                        n = n + 1 ; shade[n] = f_shade_center_one_f(asnumber_p(x1),1-asnumber_p(y1))
1922                    end
1923                    if x2 and y2 then
1924                        n = n + 1 ; shade[n] = f_shade_center_two_f(asnumber_p(x2),1-asnumber_p(y2))
1925                    end
1926                    --
1927                elseif kind == "radialGradient" then
1928                    shade = { s_shade_circular }
1929                    --
1930                    local cx = rawget(a,"cx") -- x center
1931                    local cy = rawget(a,"cy") -- y center
1932                    local r  = rawget(a,"r" ) -- radius
1933                    local fx = rawget(a,"fx") -- focal points
1934                    local fy = rawget(a,"fy") -- focal points
1935                    --
1936                    if userspace then
1937                        if cx and cy then
1938                            n = n + 1 ; shade[n] = f_shade_center(asnumber_p(cx),asnumber_p(cy))
1939                        end
1940                        if fx and fy then
1941                            n = n + 1 ; shade[n] = f_shade_center_one(asnumber_p(fx),-asnumber_p(fy))
1942                        end
1943                        if r then
1944                            n = n + 1 ; shade[n] = f_shade_radius(asnumber_p(r))
1945                        end
1946                        if fx and fy then
1947                            -- todo
1948                        end
1949                    else
1950                        if cx and cy then
1951                            n = n + 1 ; shade[n] = f_shade_center_f(asnumber_p(cx),1-asnumber_p(cy))
1952                        end
1953                        if fx and fy then
1954                            n = n + 1 ; shade[n] = f_shade_center_one_f(asnumber_p(fx),1-asnumber_p(fy))
1955                        end
1956                        if r then
1957                            n = n + 1 ; shade[n] = f_shade_radius_f(asnumber_p(r))
1958                        end
1959                        if fx and fy then
1960                            -- todo
1961                        end
1962                    end
1963                else
1964                    return
1965                end
1966                local colora, colorb
1967                -- startcolor ?
1968                for c in xmlcollected(spec,"/stop") do
1969                    local a       = c.at
1970                    local offset  = rawget(a,"offset")
1971                    local colorb  = rawget(a,"stop-color")
1972                 -- local opacity = rawget(a,"stop-opacity") -- not in pdf for steps
1973                    if not colora then
1974                        colora = colorb
1975                    end
1976                    -- what if no percentage
1977--                     local fraction = offset and asnumber_r(offset) -- asnumber_p ?
1978local fraction = offset and asnumber_p(offset)
1979                    if not fraction then
1980                        -- for now
1981                        fraction = xmlcount(spec,"/stop")/100      -- asnumber_p ?
1982                    end
1983                    if colora and colorb and colora ~= "" and colorb ~= "" then
1984                        n = n + 1
1985                     -- if opacity then
1986                     --     shade[n] = f_shade_step_opacity(fraction,thecolor(colora),thecolor(colorb),asnumber(o))
1987                     -- else
1988                            if userspace then
1989                                shade[n] = f_shade_step(fraction,thecolor(colora),thecolor(colorb))
1990                            else
1991                                shade[n] = f_shade_step(fraction,thecolor(colora),thecolor(colorb))
1992                            end
1993                     -- end
1994                    end
1995                    colora = colorb
1996                end
1997                return concat(shade,"\n    ")
1998            end
1999        end
2000
2001    end
2002
2003    local function drawproperties(stroke,at,opacity)
2004        local p = at["stroke-width"]
2005        if p then
2006            p = f_pen(asnumber_r(p))
2007        end
2008        local d = at["stroke-dasharray"]
2009        if d == "none" then
2010            d = nil
2011        elseif d then
2012            local o = at["stroke-dashoffset"]
2013            if o and o ~= "none" then
2014                o = asnumber_r(o)
2015            else
2016                o = false
2017            end
2018            d = dashed(d,o)
2019        end
2020        local c = withcolor(stroke)
2021        local o = at["stroke-opacity"] or (opacity and at["opacity"])
2022        if o == "none" then
2023            o = nil
2024        elseif o == "transparent" then
2025            o = f_opacity(0)
2026        elseif o then
2027            o = asnumber_r(o)
2028            if o == ignoredopacity then
2029                o = nil
2030            elseif o then
2031                o = f_opacity(o)
2032            else
2033                o = nil
2034            end
2035        end
2036        return p, d, c, o
2037    end
2038
2039    local s_opacity_start   <const> = "draw image ("
2040    local f_opacity_content         = formatters["setgroup currentpicture to boundingbox currentpicture withopacity %N;"]
2041    local s_opacity_stop    <const> = ") ;"
2042
2043    local function sharedopacity(at)
2044        local o = at["opacity"]
2045        if o and o ~= "none" then
2046            o = asnumber_r(o)
2047            if o == ignoredopacity then
2048                return
2049            end
2050            if o then
2051                return s_opacity_start, f_opacity_content(o), s_opacity_stop
2052            end
2053        end
2054    end
2055
2056    -- it looks like none and transparent are both used (mozilla examples)
2057
2058    local function fillproperties(fill,at,opacity)
2059        local o = at["fill-opacity"] or (opacity and at["opacity"])
2060        local c = nil
2061        if c ~= "none" then
2062            c = gradient(fill)
2063            if not c then
2064                c = pattern(fill)
2065                if c then
2066                    if o and o ~= "none" then
2067                        o = asnumber_r(o)
2068                        if o ~= ignoredopacity then
2069                            return c, f_opacity(o), "pattern"
2070                        end
2071                    end
2072                    return c, false, "pattern"
2073                else
2074                    c = withcolor(fill)
2075                end
2076            end
2077        end
2078        if not o and fill == "transparent" then
2079            return nil, f_opacity(0), true
2080        elseif o and o ~= "none" then
2081            o = asnumber_r(o)
2082            if o == ignoredopacity then
2083                return c
2084            end
2085            if o then
2086                return c, f_opacity(o), (o == 1 and "invisible")
2087            end
2088        end
2089        return c
2090    end
2091
2092    local viewport do
2093
2094        local s_viewport_start  <const> = "draw image ("
2095        local s_viewport_stop   <const> = ") ;"
2096        local f_viewport_shift          = formatters["currentpicture := currentpicture shifted (%N,%N);"]
2097        local f_viewport_scale          = formatters["currentpicture := currentpicture xysized (%N,%N);"]
2098        local f_viewport_clip           = formatters["clip currentpicture to (unitsquare xyscaled (%N,%N));"]
2099
2100        viewport = function(x,y,w,h,noclip,scale)
2101            r = r + 1 ; result[r] = s_viewport_start
2102            return function()
2103                local okay = w ~= 0 and h ~= 0
2104                if okay and scale then
2105                    r = r + 1 ; result[r] = f_viewport_scale(w,h)
2106                end
2107                if x ~= 0 or y ~= 0 then
2108                    r = r + 1 ; result[r] = f_viewport_shift(-x,y)
2109                end
2110                if okay and not noclip then
2111                    r = r + 1 ; result[r] = f_viewport_clip(w,-h)
2112                end
2113
2114                r = r + 1 ; result[r] = s_viewport_stop
2115            end
2116        end
2117
2118    end
2119
2120    -- maybe forget about defs and just always locate (and then backtrack
2121    -- over <g> if needed) .. so, only store after locating
2122
2123    function handledefinitions(c)
2124        for c in xmlcollected(c,"defs/*") do
2125            local a = c.at
2126            if a then
2127                local id = rawget(a,"id")
2128                if id then
2129                    definitions["#"     .. id       ] = c
2130                    definitions["url(#" .. id .. ")"] = c
2131                end
2132            end
2133        end
2134        for c in xmlcollected(c,"(symbol|radialGradient|linearGradient)") do
2135            local id = rawget(c.at,"id")
2136            if id then
2137                definitions["#"     .. id       ] = c
2138                definitions["url(#" .. id .. ")"] = c
2139            end
2140        end
2141    end
2142
2143 --  function handlers.defs(c)
2144 --     for c in xmlcollected(c,"/*") do
2145 --         local a = c.at
2146 --         if a then
2147 --             local id = rawget(a,"id")
2148 --             if id then
2149 --                 definitions["#"     .. id       ] = c
2150 --                 definitions["url(#" .. id .. ")"] = c
2151 --             end
2152 --         end
2153 --     end
2154 -- end
2155
2156    -- lots of stuff todo: transform
2157
2158    local uselevel = 0
2159
2160    function handlers.use(c)
2161        local at  = c.at
2162        local id  = rawget(at,"href") or rawget(at,"xlink:href") -- better a rawget
2163        local res = locate(id,c)
2164        if res then
2165            uselevel = uselevel + 1
2166            local boffset, eoffset = handleoffset(at)
2167            local btransform, etransform, transform = handletransform(at)
2168
2169            if boffset then
2170                r = r + 1 result[r] = boffset
2171            end
2172
2173         -- local clippath  = at.clippath
2174
2175            if btransform then
2176                r = r + 1 result[r] = btransform
2177            end
2178
2179            local _transform = transform
2180            local _clippath  = clippath
2181            at["transform"] = false
2182         -- at["clip-path"] = false
2183
2184setmetatableindex(res.at,at)
2185
2186            local tg = res.tg
2187--             if usetags[tg] then
2188                process(res,".")
2189--             else
2190--                 process(res,"/*")
2191--             end
2192
2193            at["transform"] = _transform
2194         -- at["clip-path"] = _clippath
2195
2196            if etransform then
2197                r = r + 1 ; result[r] = etransform
2198            end
2199
2200            if eoffset then
2201                r = r + 1 result[r] = eoffset
2202            end
2203
2204            uselevel = uselevel - 1
2205        else
2206            report("use: unknown definition %a",id)
2207        end
2208    end
2209
2210    local f_no_draw               = formatters['  nodraw (%s)']
2211    local f_do_draw               = formatters['  draw (%s)']
2212    local f_no_fill_c             = formatters['  nofill closedcurve(%s)']
2213    local f_do_fill_c             = formatters['  fill closedcurve(%s)']
2214    local f_eo_fill_c             = formatters['  eofill closedcurve(%s)']
2215    local f_no_fill_l             = formatters['  nofill closedlines(%s)']
2216    local f_do_fill_l             = formatters['  fill closedlines(%s)']
2217    local f_eo_fill_l             = formatters['  eofill closedlines(%s)']
2218    local f_closed_draw           = formatters['  draw closedcurve(%s)']
2219    local f_do_fill               = f_do_fill_c
2220    local f_eo_fill               = f_eo_fill_c
2221    local f_no_fill               = f_no_fill_c
2222    local s_clip_start    <const> = 'save p ; picture p ; p := image ('
2223    local f_clip_stop_c           = formatters[') ; clip p to closedcurve(%s) %s ; draw p ;']
2224    local f_clip_stop_l           = formatters[') ; clip p to closedlines(%s) %s ; draw p ;']
2225    local f_clip_stop             = f_clip_stop_c
2226    local f_eoclip_stop_c         = formatters[') ; eoclip p to closedcurve(%s) %s ; draw p ;']
2227    local f_eoclip_stop_l         = formatters[') ; eoclip p to closedlines(%s) %s ; draw p ;']
2228    local f_eoclip_stop           = f_eoclip_stop_c
2229
2230    -- could be shared and then beginobject | endobject
2231
2232    local function flushobject(object,at,c,o)
2233        local btransform, etransform = handletransform(at)
2234        local cpath, _, ctransform = handleclippath(at)
2235
2236        if cpath then
2237            r = r + 1 ; result[r] = s_clip_start
2238        end
2239
2240        if btransform then
2241            r = r + 1 ; result[r] = btransform
2242        end
2243
2244        r = r + 1 ; result[r] = f_do_draw(object)
2245
2246        if c then
2247            r = r + 1 ; result[r] = c
2248        end
2249
2250        if o then
2251            r = r + 1 ; result[r] = o
2252        end
2253
2254        if etransform then
2255            r = r + 1 ; result[r] = etransform
2256        end
2257
2258        r = r + 1 ; result[r] = "  ;"
2259
2260        if cpath then
2261            local f_done = cpath.evenodd
2262            if cpath.curve then
2263                f_done = f_done and f_eoclip_stop_c or f_clip_stop_c
2264            else
2265                f_done = f_done and f_eoclip_stop_l or f_clip_stop_l
2266            end
2267            r = r + 1 ; result[r] = f_done(cpath[1],ctransform or "")
2268        end
2269    end
2270
2271    do
2272
2273        local flush
2274
2275        local f_linecap    = formatters["  interim linecap := %s ;"]
2276        local f_linejoin   = formatters["  interim linejoin := %s ;"]
2277        local f_miterlimit = formatters["  interim miterlimit := %s ;"]
2278
2279        local s_begingroup <const> = "begingroup;"
2280        local s_endgroup   <const> = "endgroup;"
2281
2282        local linecaps  = { butt  = "butt",    square = "squared", round = "rounded" }
2283        local linejoins = { miter = "mitered", bevel  = "beveled", round = "rounded" }
2284
2285        local function startlineproperties(at)
2286            local cap   = at["stroke-linecap"]
2287            local join  = at["stroke-linejoin"]
2288            local limit = at["stroke-miterlimit"]
2289            cap   = cap   and linecaps [cap]
2290            join  = join  and linejoins[join]
2291            limit = limit and asnumber_r(limit)
2292            if cap or join or limit then
2293                r = r + 1 ; result[r] = s_begingroup
2294                if cap then
2295                    r = r + 1 ; result[r] = f_linecap(cap)
2296                end
2297                if join then
2298                    r = r + 1 ; result[r] = f_linejoin(join)
2299                end
2300                if limit then
2301                    r = r + 1 ; result[r] = f_miterlimit(limit)
2302                end
2303                return function()
2304                    at["stroke-linecap"]    = false
2305                    at["stroke-linejoin"]   = false
2306                    at["stroke-miterlimit"] = false
2307                    r = r + 1 ; result[r] = s_endgroup
2308                    at["stroke-linecap"]    = cap
2309                    at["stroke-linejoin"]   = join
2310                    at["stroke-miterlimit"] = limit
2311                end
2312            end
2313        end
2314
2315        -- markers are a quite rediculous thing .. let's assume simple usage for now
2316
2317        function handlers.marker()
2318            -- todo: is just a def too
2319        end
2320
2321        -- kind of local svg ... so make a generic one
2322        --
2323        -- todo: combine more (offset+scale+rotation)
2324
2325        local function makemarker(where,c,x1,y1,x2,y2,x3,y3,parentat)
2326            local at     = c.at
2327            local refx   = rawget(at,"refX")
2328            local refy   = rawget(at,"refY")
2329            local width  = rawget(at,"markerWidth")
2330            local height = rawget(at,"markerHeight")
2331            local units  = rawget(at,"markerUnits") -- no parentat["stroke-width"], bad for m4mbo
2332            local view   = rawget(at,"viewBox")
2333            local orient = rawget(at,"orient")
2334         -- local ratio  = rawget(at,"preserveAspectRatio")
2335            local units  = units and asnumber(units) or 1
2336
2337            local angx   = 0
2338            local angy   = 0
2339            local angle  = 0
2340
2341            if where == "beg" then
2342                if orient == "auto" then -- unchecked
2343                    -- no angle
2344                    angx = abs(x2 - x3)
2345                    angy = abs(y2 - y3)
2346                elseif orient == "auto-start-reverse" then -- checked
2347                    -- points to start
2348                    angx = -abs(x2 - x3)
2349                    angy = -abs(y2 - y3)
2350                elseif orient then -- unchecked
2351                    angle = asnumber_r(orient)
2352                end
2353            elseif where == "end" then
2354                -- funny standard .. bug turned feature?
2355                if orient == "auto" or orient == "auto-start-reverse" then
2356                    angx = abs(x1 - x2)
2357                    angy = abs(y1 - y2)
2358                elseif orient then -- unchecked
2359                    angle = asnumber_r(orient)
2360                end
2361            elseif orient then -- unchecked
2362                angle = asnumber_r(orient)
2363            end
2364            -- what wins: viewbox or w/h
2365
2366            refx = asnumber_x(refx)
2367            refy = asnumber_y(refy)
2368
2369            width  = (width  and asnumber_x(width)  or 3) * units
2370            height = (height and asnumber_y(height) or 3) * units
2371
2372            local x = 0
2373            local y = 0
2374            local w = width
2375            local h = height
2376
2377            -- kind of like the main svg
2378
2379            r = r + 1 ; result[r] = s_offset_start
2380
2381            local wrapupviewport
2382-- todo : better viewbox code
2383            local xpct, ypct, rpct
2384            if view then
2385                x, y, w, h = handleviewbox(view)
2386            end
2387
2388            if width ~= 0 then
2389                w = width
2390            end
2391            if height ~= 0 then
2392                h = height
2393            end
2394
2395            if h then
2396                xpct           = percentage_x
2397                ypct           = percentage_y
2398                rpct           = percentage_r
2399                percentage_x   = w / 100
2400                percentage_y   = h / 100
2401                percentage_r   = (sqrt(w^2 + h^2) / sqrt(2)) / 100
2402                wrapupviewport = viewport(x,y,w,h,true,true) -- no clip
2403            end
2404
2405            -- we can combine a lot here:
2406
2407            local hasref = refx ~= 0 or refy ~= 0
2408            local hasrot = angx ~= 0 or angy ~= 0 or angle ~= 0
2409
2410            local btransform, etransform, transform = handletransform(at)
2411
2412            if btransform then
2413                r = r + 1 ; result[r] = btransform
2414            end
2415
2416            if hasrot then
2417                r = r + 1 ; result[r] = s_rotation_start
2418            end
2419
2420            if hasref then
2421                r = r + 1 ; result[r] = s_offset_start
2422            end
2423
2424            local _transform = transform
2425            at["transform"] = false
2426
2427            handlers.g(c)
2428
2429            at["transform"] = _transform
2430
2431            if hasref then
2432                r = r + 1 ; result[r] = f_offset_stop(-refx,refy)
2433            end
2434
2435            if hasrot then
2436                if angle ~= 0 then
2437                    r = r + 1 ; result[r] = f_rotation_angle(angle)
2438                else
2439                    r = r + 1 ; result[r] = f_rotation_stop(angx,angy)
2440                end
2441            end
2442
2443            if etransform then
2444                r = r + 1 ; result[r] = etransform
2445            end
2446
2447            if h then
2448                percentage_x = xpct
2449                percentage_y = ypct
2450                percentage_r = rpct
2451                if wrapupviewport then
2452                    wrapupviewport()
2453                end
2454            end
2455            r = r + 1 ; result[r] = f_offset_stop(x2,y2)
2456
2457        end
2458
2459        -- do we need to metatable the attributes here?
2460
2461        local function addmarkers(list,begmarker,midmarker,endmarker,at)
2462            local n = #list
2463            if n > 3 then
2464                if begmarker then
2465                    local m = locate(begmarker)
2466                    if m then
2467                        makemarker("beg",m,false,false,list[1],list[2],list[3],list[4],at)
2468                    end
2469                end
2470                if midmarker then
2471                    local m = locate(midmarker)
2472                    if m then
2473                        for i=3,n-2,2 do
2474                            makemarker("mid",m,list[i-2],list[i-1],list[i],list[i+1],list[i+2],list[i+3],at)
2475                        end
2476                    end
2477                end
2478                if endmarker then
2479                    local m = locate(endmarker)
2480                    if m then
2481                        makemarker("end",m,list[n-3],list[n-2],list[n-1],list[n],false,false,at)
2482                    end
2483                end
2484            else
2485                -- no line
2486            end
2487        end
2488
2489        local function flush(shape,dofill,at,list,begmarker,midmarker,endmarker)
2490
2491            local fill   = dofill and (at["fill"] or "black")
2492            local stroke = at["stroke"] or "none"
2493
2494            local btransform, etransform = handletransform(at)
2495            local cpath, _, ctransform = handleclippath(at)
2496
2497            if cpath then
2498                r = r + 1 ; result[r] = s_clip_start
2499            end
2500
2501            local has_stroke = stroke and stroke ~= "none"
2502            local has_fill   = fill and fill ~= "none"
2503
2504            local bopacity, copacity, eopacity
2505            if has_stroke and has_fill then
2506                bopacity, copacity, eopacity = sharedopacity(at)
2507            end
2508
2509            if copacity then
2510                r = r + 1 ; result[r] = bopacity
2511            end
2512
2513            if has_fill then
2514                local color, opacity, option = fillproperties(fill,at,not has_stroke)
2515                local f_xx_fill = at["fill-rule"] == "evenodd" and f_eo_fill or f_do_fill
2516                if btransform then
2517                    r = r + 1 ; result[r] = btransform
2518                end
2519                if option == "pattern" then
2520                    r = r + 1 result[r] = f_closed_draw(shape)
2521                else
2522                    r = r + 1 result[r] = f_xx_fill(shape)
2523                end
2524                if color   then
2525                    r = r + 1 ; result[r] = color
2526                end
2527                if opacity then
2528                    r = r + 1 ; result[r] = opacity
2529                end
2530                r = r + 1 ; result[r] = etransform or "  ;"
2531            end
2532
2533            if has_stroke then
2534                local wrapup = startlineproperties(at)
2535                local pen, dashing, color, opacity = drawproperties(stroke,at,not has_fill)
2536                if btransform then
2537                    r = r + 1 ; result[r] = btransform
2538                end
2539                r = r + 1 ; result[r] = f_do_draw(shape)
2540                if pen     then
2541                    r = r + 1 ; result[r] = pen
2542                end
2543                if dashing then
2544                    r = r + 1 ; result[r] = dashing
2545                end
2546                if color then
2547                    r = r + 1 ; result[r] = color
2548                end
2549                if opacity then
2550                    r = r + 1 ; result[r] = opacity
2551                end
2552                r = r + 1 ; result[r] = etransform or "  ;"
2553                --
2554                if list then
2555                    addmarkers(list,begmarker,midmarker,endmarker,at)
2556                end
2557                --
2558                if wrapup then
2559                    wrapup()
2560                end
2561            end
2562
2563            if copacity then
2564                r = r + 1 ; result[r] = copacity
2565                r = r + 1 ; result[r] = eopacity
2566            end
2567
2568            if cpath then
2569                r = r + 1 ; result[r] = (cpath.evenodd and f_eoclip_stop or f_clip_stop)(cpath[1],ctransform)
2570            end
2571
2572        end
2573
2574        local f_rectangle = formatters['unitsquare xyscaled (%N,%N) shifted (%N,%N)']
2575        local f_rounded   = formatters['roundedsquarexy(%N,%N,%N,%N) shifted (%N,%N)']
2576        local f_line      = formatters['((%N,%N)--(%N,%N))']
2577        local f_ellipse   = formatters['(fullcircle xyscaled (%N,%N) shifted (%N,%N))']
2578        local f_circle    = formatters['(fullcircle scaled %N shifted (%N,%N))']
2579
2580        function handlers.line(c)
2581            local at = c.at
2582            local x1 = rawget(at,"x1")
2583            local y1 = rawget(at,"y1")
2584            local x2 = rawget(at,"x2")
2585            local y2 = rawget(at,"y2")
2586
2587            x1 = x1 and asnumber_vx(x1) or 0
2588            y1 = y1 and asnumber_vy(y1) or 0
2589            x2 = x2 and asnumber_vx(x2) or 0
2590            y2 = y2 and asnumber_vy(y2) or 0
2591
2592            flush(f_line(x1,y1,x2,y2),false,at)
2593        end
2594
2595        function handlers.rect(c)
2596            local at     = c.at
2597            local width  = rawget(at,"width")
2598            local height = rawget(at,"height")
2599            local x      = rawget(at,"x")
2600            local y      = rawget(at,"y")
2601            local rx     = rawget(at,"rx")
2602            local ry     = rawget(at,"ry")
2603
2604            width  = width  and asnumber_x(width)  or 0
2605            height = height and asnumber_y(height) or 0
2606            x      = x      and asnumber_vx(x) or 0
2607            y      = y      and asnumber_vy(y) or 0
2608
2609            y      = y - height
2610
2611            if rx then rx = asnumber_x(rx) end
2612            if ry then ry = asnumber_y(ry) end
2613
2614            if rx or ry then
2615                if not rx then rx = ry end
2616                if not ry then ry = rx end
2617                flush(f_rounded(width,height,rx,ry,x,y),true,at)
2618            else
2619                flush(f_rectangle(width,height,x,y),true,at)
2620            end
2621        end
2622
2623        function handlers.ellipse(c)
2624            local at = c.at
2625            local cx = rawget(at,"cx")
2626            local cy = rawget(at,"cy")
2627            local rx = rawget(at,"rx")
2628            local ry = rawget(at,"ry")
2629
2630            cx = cx and asnumber_vx(cx) or 0
2631            cy = cy and asnumber_vy(cy) or 0
2632            rx = rx and asnumber_r (rx) or 0
2633            ry = ry and asnumber_r (ry) or 0
2634
2635            flush(f_ellipse(2*rx,2*ry,cx,cy),true,at)
2636        end
2637
2638        function handlers.circle(c)
2639            local at = c.at
2640            local cx = rawget(at,"cx")
2641            local cy = rawget(at,"cy")
2642            local r  = rawget(at,"r")
2643
2644            cx = cx and asnumber_vx(cx) or 0
2645            cy = cy and asnumber_vy(cy) or 0
2646            r  = r  and asnumber_r (r)  or 0
2647
2648            flush(f_circle(2*r,cx,cy),true,at)
2649        end
2650
2651        local f_lineto_z  = formatters['(%N,%N)']
2652        local f_lineto_n  = formatters['--(%N,%N)']
2653
2654        local p_pair     = p_optseparator * p_number_vx * p_optseparator * p_number_vy
2655        local p_open     = Cc("(")
2656        local p_close    = Carg(1) * P(true) / function(s) return s end
2657        local p_polyline = Cs(p_open * (p_pair / f_lineto_z) * (p_pair / f_lineto_n)^0 * p_close)
2658        local p_polypair = Ct(p_pair^0)
2659
2660        local function poly(c,final)
2661            local at     = c.at
2662            local points = rawget(at,"points")
2663            if points then
2664                local path = lpegmatch(p_polyline,points,1,final)
2665                local list = nil
2666                local begmarker = rawget(at,"marker-start")
2667                local midmarker = rawget(at,"marker-mid")
2668                local endmarker = rawget(at,"marker-end")
2669                if begmarker or midmarker or endmarker then
2670                    list = lpegmatch(p_polypair,points)
2671                end
2672                flush(path,true,at,list,begmarker,midmarker,endmarker)
2673            end
2674        end
2675
2676        function handlers.polyline(c) poly(c,       ")") end
2677        function handlers.polygon (c) poly(c,"--cycle)") end
2678
2679        local s_image_start <const> = "draw image ("
2680        local s_image_stop  <const> = ") ;"
2681
2682        function handlers.path(c)
2683            local at = c.at
2684            local d  = rawget(at,"d")
2685            if d then
2686                local shape, l = grabpath(d)
2687                local fill     = at["fill"] or "black"
2688                local stroke   = at["stroke"] or "none"
2689                local n        = #shape
2690
2691                local btransform, etransform = handletransform(at)
2692                local cpath = handleclippath(at)
2693                if cpath then
2694                    r = r + 1 ; result[r] = s_clip_start
2695                end
2696
2697                -- todo: image (nicer for transform too)
2698
2699                if fill and fill ~= "none" then
2700                    local color, opacity, option = fillproperties(fill,at)
2701                    local f_xx_fill = at["fill-rule"] == "evenodd"
2702                    if option == "pattern" then
2703                        f_xx_fill = f_closed_draw
2704                    elseif shape.closed then
2705                        f_xx_fill = f_xx_fill and f_eo_fill   or f_do_fill
2706                    elseif shape.curve then
2707                        f_xx_fill = f_xx_fill and f_eo_fill_c or f_do_fill_c
2708                    else
2709                        f_xx_fill = f_xx_fill and f_eo_fill_l or f_do_fill_l
2710                    end
2711                    if n == 1 then
2712                        if btransform then
2713                            r = r + 1 ; result[r] = btransform
2714                        end
2715                        r = r + 1 result[r] = f_xx_fill(shape[1])
2716                        if color then
2717                            r = r + 1 ; result[r] = color
2718                        end
2719                        if opacity then
2720                            r = r + 1 ; result[r] = opacity
2721                        end
2722                        r = r + 1 ; result[r] = etransform or "  ;"
2723                    else
2724                        r = r + 1 ; result[r] = btransform or s_image_start
2725                        for i=1,n do
2726                            if i == n then
2727                                r = r + 1 ; result[r] = f_xx_fill(shape[i])
2728                                if color then
2729                                    r = r + 1 ; result[r] = color
2730                                end
2731                                if opacity then
2732                                    r = r + 1 ; result[r] = opacity
2733                                end
2734                            else
2735                                r = r + 1 ; result[r] = f_no_fill(shape[i])
2736                            end
2737                            r = r + 1 ; result[r] = "  ;"
2738                        end
2739                        r = r + 1 ; result[r] = etransform or s_image_stop
2740                    end
2741                end
2742
2743                if stroke and stroke ~= "none" then
2744                    local begmarker = rawget(at,"marker-start")
2745                    local midmarker = rawget(at,"marker-mid")
2746                    local endmarker = rawget(at,"marker-end")
2747                    if begmarker or midmarker or endmarker then
2748                        list = grablist(l)
2749                    end
2750                    local wrapup = startlineproperties(at)
2751                    local pen, dashing, color, opacity = drawproperties(stroke,at)
2752                    if n == 1 and not list then
2753                        if btransform then
2754                            r = r + 1 ; result[r] = btransform
2755                        end
2756                        r = r + 1 result[r] = f_do_draw(shape[1])
2757                        if pen then
2758                            r = r + 1 ; result[r] = pen
2759                        end
2760                        if dashing then
2761                            r = r + 1 ; result[r] = dashing
2762                        end
2763                        if color then
2764                            r = r + 1 ; result[r] = color
2765                        end
2766                        if opacity then
2767                            r = r + 1 ; result[r] = opacity
2768                        end
2769                        r = r + 1 result[r] = etransform or "  ;"
2770                    else
2771                        r = r + 1 result[r] = btransform or s_draw_image_start
2772                        for i=1,n do
2773                            r = r + 1 result[r] = f_do_draw(shape[i])
2774                            if pen then
2775                                r = r + 1 ; result[r] = pen
2776                            end
2777                            if dashing then
2778                                r = r + 1 ; result[r] = dashing
2779                            end
2780                            if color then
2781                                r = r + 1 ; result[r] = color
2782                            end
2783                            if opacity then
2784                                r = r + 1 ; result[r] = opacity
2785                            end
2786                            r = r + 1 ; result[r] = "  ;"
2787                        end
2788                        if list then
2789                            addmarkers(list,begmarker,midmarker,endmarker,at)
2790                        end
2791                        r = r + 1 ; result[r] = etransform or s_draw_image_stop
2792                    end
2793                    if wrapup then
2794                        wrapup()
2795                    end
2796                end
2797
2798                if cpath then
2799                    r = r + 1 ; result[r] = f_clip_stop(cpath[1],"")
2800                end
2801
2802            end
2803        end
2804
2805    end
2806
2807    -- kind of special
2808
2809    do
2810
2811        -- some day:
2812        --
2813        -- specification = identifiers.jpg(data."string")
2814        -- specification.data = data
2815        -- inclusion takes from data
2816        -- specification.data = false
2817
2818     -- local f_image = formatters[ [[figure("%s") xysized (%N,%N) shifted (%N,%N)]] ]
2819        local f_image = formatters[ [[svgembeddedfigure(%i) xysized (%N,%N) shifted (%N,%N)]] ]
2820
2821     -- local nofimages = 0
2822
2823        function handlers.image(c)
2824            local at = c.at
2825            local im = rawget(at,"xlink:href")
2826            if im then
2827                local kind, data = match(im,"^data:image/([a-z]+);base64,(.*)$")
2828                if kind == "png" then
2829                    -- ok
2830                elseif kind == "jpeg" then
2831                    kind = "jpg"
2832                else
2833                    kind = false
2834                end
2835                if kind and data then
2836                    local w  = rawget(at,"width")
2837                    local h  = rawget(at,"height")
2838                    local x  = rawget(at,"x")
2839                    local y  = rawget(at,"y")
2840                    w = w and asnumber_x(w)
2841                    h = h and asnumber_y(h)
2842                    x = x and asnumber_vx(x) or 0
2843                    y = y and asnumber_vy(y) or 0
2844                    local data = basexx.decode64(data)
2845                 -- local name  = "temp-svg-image-" .. nofimages .. "." .. kind
2846                    local index = images.storedata("svg", {
2847                        kind = kind,
2848                        data = data,
2849                        info = graphics.identifiers[kind](data,"string"),
2850                    })
2851                 -- io.savedata(name,data)
2852                    if not w or not h then
2853                        if info then
2854                            -- todo: keep aspect ratio attribute
2855                            local xsize = info.xsize
2856                            local ysize = info.ysize
2857                            if not w then
2858                                if not h then
2859                                    w = xsize
2860                                    h = ysize
2861                                else
2862                                    w = (h / ysize) * xsize
2863                                end
2864                            else
2865                                h = (w / xsize) * ysize
2866                            end
2867                        end
2868                    end
2869                    -- safeguard:
2870                    if not w then w = h or 1 end
2871                    if not h then h = w or 1 end
2872                 -- luatex.registertempfile(name)
2873                 -- flushobject(f_image(name,w,h,x,y - h),at)
2874                    flushobject(f_image(index,w,h,x,y - h),at)
2875                else
2876                    -- nothing done
2877                end
2878            end
2879        end
2880
2881    end
2882
2883    -- these transform: g a text svg symbol
2884
2885    do
2886
2887        function handlers.a(c)
2888            process(c,"/*")
2889        end
2890
2891        function handlers.g(c) -- much like flushobject so better split and share
2892            local at = c.at
2893
2894            local btransform, etransform, transform = handletransform(at)
2895            local cpath, clippath, ctransform = handleclippath(at)
2896
2897            if cpath then
2898                r = r + 1 ; result[r] = s_clip_start
2899            end
2900
2901            if btransform then
2902                r= r + 1 result[r] = btransform
2903            end
2904
2905            local _transform = transform
2906            local _clippath  = clippath
2907            at["transform"] = false
2908            at["clip-path"] = false
2909
2910            process(c,"/!(defs|symbol)") -- /*
2911
2912            at["transform"] = _transform
2913            at["clip-path"] = _clippath
2914
2915            if etransform then
2916                r = r + 1 ; result[r] = etransform
2917            end
2918
2919            if cpath then
2920                local f_done = cpath.evenodd
2921                if cpath.curve then
2922                    f_done = f_done and f_eoclip_stop_c or f_clip_stop_c
2923                else
2924                    f_done = f_done and f_eoclip_stop_l or f_clip_stop_l
2925                end
2926                r = r + 1 ; result[r] = f_done(cpath[1],ctransform or "")
2927            end
2928        end
2929
2930        -- this will never really work out
2931        --
2932        -- todo: register text in lua in mapping with id, then draw mapping unless overloaded
2933        --       using lmt_svglabel with family,style,weight,size,id passed
2934
2935        -- nested tspans are messy: they can have displacements but in inkscape we also
2936        -- see x and y (inner and outer element)
2937
2938        -- The size is a bit of an issue. I assume that the specified size relates to the
2939        -- designsize but we want to be able to use other fonts.
2940
2941        -- a mix of text and spans and possibly wrap (where xy is to be ignored) ... bah ...
2942        -- it's fuzzy when we have a span with positions mixed with text ... basically that
2943        -- is a box and we can assume that an editor then has all positioned
2944
2945        do
2946
2947            local s_start     <const> = "\\svgstart "
2948            local s_stop      <const> = "\\svgstop "
2949            local f_set               = formatters["\\svgset{%N}{%N}"] -- we need a period
2950            local f_color_c           = formatters["\\svgcolorc{%.3N}{%.3N}{%.3N}{"]
2951            local f_color_o           = formatters["\\svgcoloro{%.3N}{"]
2952            local f_color_b           = formatters["\\svgcolorb{%.3N}{%.3N}{%.3N}{%.3N}{"]
2953            local f_poscode           = formatters["\\svgpcode{%N}{%N}{%s}"]
2954            local f_poschar           = formatters["\\svgpchar{%N}{%N}{%s}"]
2955            local f_posspace          = formatters["\\svgpspace{%N}{%N}"]
2956            local f_code              = formatters["\\svgcode{%s}"]
2957            local f_char              = formatters["\\svgchar{%s}"]
2958            local s_space     <const> = "\\svgspace "
2959            local f_size              = formatters["\\svgsize{%0.6f}"]             -- we need a period
2960            local f_font              = formatters["\\svgfont{%s}{%s}{%s}"]
2961            local f_hashed            = formatters["\\svghashed{%s}"]
2962
2963            ----- p_texescape = lpegpatterns.texescape
2964
2965            local anchors = {
2966                ["start"]  = "drt",
2967                ["end"]    = "dlft",
2968                ["middle"] = "d",
2969            }
2970
2971            -- we can now just use the lmt maptext feature
2972
2973            local f_text_normal_svg   = formatters['(onetimetextext.%s("%s") shifted (%N,%N))']
2974            local f_text_simple_svg   = formatters['onetimetextext.%s("%s")']
2975            local f_mapped_normal_svg = formatters['(svgtext("%s") shifted (%N,%N))']
2976            local f_mapped_simple_svg = formatters['svgtext("%s")']
2977
2978            local cssfamily  = css.family
2979            local cssstyle   = css.style
2980            local cssweight  = css.weight
2981            local csssize    = css.size
2982
2983            local usedfonts  = setmetatableindex(function(t,k)
2984                local v = setmetatableindex("table")
2985                t[k] = v
2986                return v
2987            end)
2988
2989            -- For now as I need it for my (some 1500) test files.
2990
2991            local function checkedfamily(name)
2992                if find(name,"^.-verdana.-$") then
2993                    name = "verdana"
2994                end
2995                return name
2996            end
2997
2998            -- todo: only escape some chars and handle space
2999
3000         -- An arbitrary mix of text and spans with x/y is asking for troubles. The fact that the
3001         -- description in the (proposed) standard is so complex indicates this (its also looks
3002         -- like reveng application specs and doesn't aim at simplicity. Basically we have two
3003         -- cases: positioned lines and words and such (text & span with xy), or just stripes of
3004         -- text and span. Free flow automatically broken into lines text is kind of strange in
3005         -- svg and the fact that glyph placement is dropped is both an indication that svg lost
3006         -- part of its purpose and probably also that it never really was a standard (although
3007         -- maybe today standards are just short term specifications. Who knows.
3008
3009         -- text with spans, all with x/y
3010         -- text mixed with spans, no xy in inner elements
3011         --
3012         -- the spec says that nested x/y are absolute
3013
3014            local defaultsize = 10
3015
3016            local sensitive = { -- todo: characters.sensitive
3017                ["#"] = true,
3018                ["$"] = true,
3019                ["%"] = true,
3020                ["&"] = true,
3021               ["\\"] = true,
3022                ["{"] = true,
3023                ["|"] = true,
3024                ["}"] = true,
3025                ["~"] = true,
3026            }
3027
3028            -- messy: in nested spans (they happen) the x/y are not accumulated
3029
3030            local function validdelta(usedscale,d)
3031                if d then
3032                    local value, unit = match(d,"^([%A]-)(%a+)")
3033                    value = tonumber(value) or 0
3034                    if not unit then
3035                        return value .. "bp"
3036                    elseif unit == "ex" or unit == "em" then
3037                        return (usedscale * value) .. unit
3038                    else
3039                        return value .. "bp"
3040                    end
3041                else
3042                    return "0bp"
3043                end
3044            end
3045
3046            local cleanfontname = fonts.names.cleanname
3047
3048            local x_family = false
3049            local x_weight = false
3050            local x_style  = false
3051
3052            local function collect(parent,t,c,x,y,size,scale,family,tx,ty,tdx,tdy)
3053                if c.special then
3054                    return nil
3055                end
3056                local dt           = c.dt
3057                local nt           = #dt
3058                local at           = c.at
3059                local tg           = c.tg
3060                local ax           = rawget(at,"x")
3061                local ay           = rawget(at,"y")
3062                local v_opacity    = tonumber(at["fill-opacity"])
3063                local v_fill       = at["fill"]
3064                local v_family     = at["font-family"]
3065                local v_style      = at["font-style"]
3066                local v_weight     = at["font-weight"]
3067                local v_size       = at["font-size"]
3068                local v_lineheight = at["line-height"]
3069                --
3070                ax = ax and asnumber_vx(ax) or x
3071                ay = ay and asnumber_vy(ay) or y
3072                --
3073                if v_family then v_family = cssfamily(v_family) end
3074                if v_style  then v_style  = cssstyle (v_style)  end
3075                if v_weight then v_weight = cssweight(v_weight) end
3076                if v_size   then v_size   = csssize  (v_size,factors,size/100) or tonumber(v_size) end
3077                --
3078                if not v_family then v_family = family   end
3079                if not v_weight then v_weight = "normal" end
3080                if not v_style  then v_style  = "normal" end
3081                --
3082                if v_family then
3083                    v_family = cleanfontname(v_family)
3084                    v_family = checkedfamily(v_family)
3085                end
3086                --
3087                usedfonts[v_family][v_weight][v_style] = true
3088                --
3089                local lh = v_lineheight and asnumber_vx(v_lineheight) or false
3090                --
3091                ax = ax - x
3092                ay = ay - y
3093                --
3094                local usedsize  = v_size or defaultsize
3095                local usedscale = usedsize / defaultsize
3096                --
3097                -- todo: rotate            : list of numbers
3098                -- todo: lengthAdjust      : spacing|spacingAndGlyphs
3099                -- todo: textLength        : scale to width
3100                -- toto: font-size-adjust
3101                -- toto: font-stretch
3102                -- letter-spacing
3103                -- word-spacing
3104                -- writing-mode:lr-tb
3105                --
3106                local newfont = v_family ~= x_family or v_weight ~= x_weight or v_style ~= x_style
3107                if newfont then
3108                    x_family = v_family
3109                    x_weight = v_weight
3110                    x_style  = v_style
3111                    t[#t+1] = f_font(v_family,v_weight,v_style)
3112                    t[#t+1] = "{"
3113                end
3114                t[#t+1] = f_size(usedscale)
3115                t[#t+1] = "{"
3116                --
3117                if trace_fonts then
3118                    -- we can hash and keep it when no change
3119                    report("element        : %s",c.tg)
3120                    report("  font family  : %s",v_family)
3121                    report("  font weight  : %s",v_weight)
3122                    report("  font style   : %s",v_style)
3123                    report("  parent size  : %s",size)
3124                 -- report("  parent scale : %s",scale)
3125                    report("  used size    : %s",v_size or defaultsize)
3126                end
3127                --
3128                local ecolored = v_fill    ~= ""             and v_fill    or false
3129                local opacity  = v_opacity ~= ignoredopacity and v_opacity or false
3130                --
3131                -- todo cmyk
3132                --
3133                if ecolored then
3134                    local r, g, b = colorcomponents(v_fill)
3135                    if r and g and b then
3136                        if opacity then
3137                            t[#t+1] = f_color_b(r,g,b,opacity)
3138                        else
3139                            t[#t+1] = f_color_c(r,g,b)
3140                        end
3141                    elseif opacity then
3142                        t[#t+1] = f_color_o(opacity)
3143                    else
3144                        ecolored = false
3145                    end
3146                elseif opacity then
3147                    t[#t+1] = f_color_o(opacity)
3148                end
3149                --
3150                local hasa = ax ~= 0 or ay ~= 0
3151                if hasa then
3152                    -- we abuse the fact that flushing layers can be nested
3153                    t[#t+1] = f_set(ax or 0,ay or 0)
3154                    t[#t+1] = "{"
3155                end
3156                for i=1,nt do
3157                    local di = dt[i]
3158                    if type(di) == "table" then
3159                        -- when x or y then absolute else inline
3160                        if #di.dt > 0 then
3161                            collect(tg,t,di,x,y,usedsize,usedscale,v_family)
3162                        end
3163                    else
3164                        -- check for preserve
3165                        if i == 1 then
3166                            di = gsub(di,"^%s+","")
3167                        end
3168                        if i == nt then
3169                            di = gsub(di,"%s+$","")
3170                        end
3171                        local chars = utfsplit(di)
3172                        if svghash then
3173                            -- dx dy
3174                            di = f_hashed(svghash[di])
3175                        else
3176                            if tx or ty or tdx or tdy then
3177                                local txi, tyi, tdxi, tdyi
3178                                for i=1,#chars do
3179                                    txi  = tx  and (tx [i] or txi )
3180                                    tyi  = ty  and (ty [i] or tyi )
3181                                    tdxi = tdx and (tdx[i] or tdxi) or 0
3182                                    tdyi = tdy and (tdy[i] or tdyi) or 0
3183                                    local dx = (txi and (txi - x) or 0) + tdxi
3184                                    local dy = (tyi and (tyi - y) or 0) + tdyi
3185                                    local ci = chars[i]
3186                                    if ci == " " then
3187                                        chars[i] = f_posspace(dx, dy)
3188                                    elseif sensitive[ci] then
3189                                        chars[i] = f_poscode(dx, dy, utfbyte(ci))
3190                                    else
3191                                        chars[i] = f_poschar(dx, dy, ci)
3192                                    end
3193                                end
3194                                di = "{" .. concat(chars) .. "}"
3195                                t[#t+1] = di
3196                            else
3197                                -- this needs to be texescaped ! and even quotes and newlines
3198                                -- or we could register it but that's a bit tricky as we nest
3199                                -- and don't know what we can expect here
3200                             -- di = lpegmatch(p_texescape,di) or di
3201                                for i=1,#chars do
3202                                    local ci = chars[i]
3203                                    if ci == " " then
3204                                        chars[i] = s_space
3205                                    elseif sensitive[ci] then
3206                                        chars[i] = f_code(utfbyte(ci))
3207                                    else
3208                                        chars[i] = f_char(ci)
3209                                     -- chars[i] = ci
3210                                    end
3211                                end
3212                                di = concat(chars)
3213                                t[#t+1] = di
3214                            end
3215                        end
3216                    end
3217                end
3218                if hasa then
3219                    if t[#t] == "{" then
3220                        t[#t] = nil
3221                        t[#t] = nil
3222                    else
3223                        t[#t+1] = "}"
3224                    end
3225                end
3226                --
3227                if opacity or ecolored then
3228                    t[#t+1] = "}"
3229                end
3230                --
3231                t[#t+1] = "}"
3232                --
3233                if newfont then
3234                    t[#t+1] = "}"
3235                end
3236                --
3237                return t
3238            end
3239
3240            -- case 1: just text, maybe with spans
3241            -- case 2: only positioned spans
3242            -- case 3: just text, seen as label
3243
3244            local textlevel = 0
3245
3246            function handlers.text(c)
3247                if textlevel == 0 then
3248                    x_family = v_family
3249                    x_weight = v_weight
3250                    x_style  = v_style
3251                end
3252                --
3253                textlevel = textlevel + 1
3254                -- analyze
3255                local only = fullstrip(xmltextonly(c))
3256                local at   = c.at
3257                local x    = rawget(at,"x")
3258                local y    = rawget(at,"y")
3259
3260                local dx   = rawget(at,"dx")
3261                local dy   = rawget(at,"dy")
3262
3263                local tx   = asnumber_vx_t(x)
3264                local ty   = asnumber_vy_t(y)
3265
3266                local tdx  = asnumber_vx_t(dx)
3267                local tdy  = asnumber_vy_t(dy)
3268
3269                x  = tx[1]  or 0 -- catch bad x/y spec
3270                y  = ty[1]  or 0 -- catch bad x/y spec
3271
3272                dx = tdx[1] or 0 -- catch bad x/y spec
3273                dy = tdy[1] or 0 -- catch bad x/y spec
3274
3275                local v_fill = at["fill"]
3276                if not v_fill or v_fill == "none" then
3277                    v_fill = "black"
3278                end
3279                local color, opacity, option = fillproperties(v_fill,at)
3280                local anchor = anchors[at["text-anchor"] or "start"] or "drt"
3281                local remap = metapost.remappedtext(only)
3282                -- x = x + dx
3283                -- y = y + dy
3284                if remap then
3285                    if x == 0 and y == 0 then
3286                        only = f_mapped_simple_svg(remap.index)
3287                    else
3288                        only = f_mapped_normal_svg(remap.index,x,y)
3289                    end
3290                    flushobject(only,at,color,opacity)
3291                    if trace_text then
3292                        report("text: %s",only)
3293                    end
3294                elseif option == "invisible" then
3295                    if trace_text then
3296                        report("invisible text: %s",only)
3297                    end
3298                else
3299                    local scale  = 1
3300                    local textid = 0
3301                    local result = { }
3302                    local nx     = #tx
3303                    local ny     = #ty
3304                    local ndx    = #tdx
3305                    local ndy    = #tdy
3306                    --
3307                    local t = { }
3308                    t[#t+1] = s_start
3309                    if nx > 1 or ny > 1 or ndx > 1 or ndy > 1 then
3310                        collect(tg,t,c,x,y,defaultsize,1,"serif",tx,ty,tdx,tdy)
3311                    else
3312                        collect(tg,t,c,x,y,defaultsize,1,"serif")
3313                    end
3314                    t[#t+1] = s_stop
3315                    t = concat(t)
3316                    if x == 0 and y == 0 then
3317                        t = f_text_simple_svg(anchor,t)
3318                    else -- dx dy
3319                        t = f_text_normal_svg(anchor,t,x,y)
3320                    end
3321                 -- flushobject(t,at,color,opacity) -- otherwise mixup with transparency
3322                    flushobject(t,at,false,false)
3323                    if trace_text then
3324                        report("text: %s",result)
3325                    end
3326                end
3327                --
3328                textlevel = textlevel - 1
3329            end
3330
3331            function metapost.reportsvgfonts()
3332                for family, weights in sortedhash(usedfonts) do
3333                    for weight, styles in sortedhash(weights) do
3334                        for style in sortedhash(styles) do
3335                            report("used font: %s-%s-%s",family,weight,style)
3336                        end
3337                    end
3338                end
3339            end
3340
3341            statistics.register("used svg fonts",function()
3342                if next(usedfonts) then
3343                    -- also in log file
3344                    logs.startfilelogging(report,"used svg fonts")
3345                    local t = { }
3346                    for family, weights in sortedhash(usedfonts) do
3347                        for weight, styles in sortedhash(weights) do
3348                            for style in sortedhash(styles) do
3349                                report("%s-%s-%s",family,weight,style)
3350                                t[#t+1] = formatters["%s-%s-%s"](family,weight,style)
3351                            end
3352                        end
3353                    end
3354                    logs.stopfilelogging()
3355                    return concat(t," ")
3356                end
3357            end)
3358
3359        end
3360
3361        function handlers.svg(c,x,y,w,h,noclip,notransform,normalize)
3362            local at = c.at
3363
3364            local wrapupviewport
3365            local bhacked
3366            local ehacked
3367            local wd = w
3368         -- local ex, em
3369            local xpct, ypct, rpct
3370
3371            local btransform, etransform, transform = handletransform(at)
3372
3373            if trace then
3374                report("view: %s, xpct %N, ypct %N","before",percentage_x,percentage_y)
3375            end
3376
3377            local viewbox = at.viewBox
3378
3379            if viewbox then
3380                x, y, w, h = handleviewbox(viewbox)
3381                if trace then
3382                    report("viewbox: x %N, y %N, width %N, height %N",x,y,w,h)
3383                end
3384            end
3385            if not w or not h or w == 0 or h == 0 then
3386                noclip = true
3387            end
3388            if h then
3389                --
3390             -- em = factors["em"]
3391             -- ex = factors["ex"]
3392             -- factors["em"] = em
3393             -- factors["ex"] = ex
3394                --
3395                xpct = percentage_x
3396                ypct = percentage_y
3397                rpct = percentage_r
3398                percentage_x = w / 100
3399                percentage_y = h / 100
3400                percentage_r = (sqrt(w^2 + h^2) / sqrt(2)) / 100
3401                if trace then
3402                    report("view: %s, xpct %N, ypct %N","inside",percentage_x,percentage_y)
3403                end
3404                wrapupviewport = viewport(x,y,w,h,noclip)
3405            end
3406            -- todo: combine transform and offset here
3407
3408            -- some fonts need this (bad transforms + viewbox)
3409            if v and normalize and w and wd and w ~= wd and w > 0 and wd > 0 then
3410                bhacked = s_wrapped_start
3411                ehacked = f_wrapped_stop(y or 0,wd/w)
3412            end
3413            if btransform then
3414                r = r + 1 ; result[r] = btransform
3415            end
3416            if bhacked then
3417                r = r + 1 ; result[r] = bhacked
3418            end
3419            local boffset, eoffset = handleoffset(at)
3420            if boffset then
3421                r = r + 1 result[r] = boffset
3422            end
3423
3424            at["transform"] = false
3425            at["viewBox"]   = false
3426
3427            process(c,"/!(defs|symbol)")
3428
3429            at["transform"] = transform
3430            at["viewBox"]   = viewbox
3431
3432            if eoffset then
3433                r = r + 1 result[r] = eoffset
3434            end
3435            if ehacked then
3436                r = r + 1 ; result[r] = ehacked
3437            end
3438            if etransform then
3439                r = r + 1 ; result[r] = etransform
3440            end
3441            if h then
3442                --
3443             -- factors["em"] = em
3444             -- factors["ex"] = ex
3445                --
3446                percentage_x = xpct
3447                percentage_y = ypct
3448                percentage_r = rpct
3449                if wrapupviewport then
3450                    wrapupviewport()
3451                end
3452            end
3453            if trace then
3454                report("view: %s, xpct %N, ypct %N","after",percentage_x,percentage_y)
3455            end
3456        end
3457
3458    end
3459
3460    process = function(x,p)
3461        for c in xmlcollected(x,p) do
3462            local tg = c.tg
3463            local h  = handlers[c.tg]
3464            if h then
3465                h(c)
3466            end
3467        end
3468    end
3469
3470    -- For huge inefficient files there can be lots of garbage to collect so
3471    -- maybe we should run the collector when a file is larger than say 50K.
3472
3473    function metapost.svgtomp(specification,pattern,notransform,normalize)
3474        local mps = ""
3475        local svg = specification.data
3476        images.resetstore("svg")
3477        if type(svg) == "string" then
3478            svg = xmlconvert(svg)
3479        end
3480        if svg then
3481            local c = xmlfirst(svg,pattern or "/svg")
3482            if c then
3483                root        = svg
3484                result      = { }
3485                r           = 0
3486                definitions = { }
3487                tagstyles   = { }
3488                classstyles = { }
3489                colormap    = specification.colormap
3490                usedcolors  = trace_colors and setmetatableindex("number") or false
3491                for s in xmlcollected(c,"style") do -- can also be in a def, so let's play safe
3492                    handlestyle(c)
3493                end
3494                handlechains(c)
3495                xmlinheritattributes(c) -- put this in handlechains
3496                handledefinitions(c)
3497                handlers.svg (
3498                    c,
3499                    specification.x,
3500                    specification.y,
3501                    specification.width,
3502                    specification.height,
3503                    specification.noclip,
3504                    notransform,
3505                    normalize,
3506                    specification.remap
3507                )
3508                if trace_result == "file" then
3509                    io.savedata(
3510                        tex.jobname .. "-svg-to-mp.tex",
3511                        "\\startMPpage[instance=doublefun]\n" .. concat(result,"\n") .. "\n\\stopMPpage\n"
3512                    )
3513                elseif trace_result then
3514                    report("result graphic:\n    %\n    t",result)
3515                end
3516                if usedcolors and next(usedcolors) then
3517                    report("graphic %a uses colors: %s",specification.id or "unknown",table.sequenced(usedcolors))
3518                end
3519                mps         = concat(result," ")
3520                root        = false
3521                result      = false
3522                r           = false
3523                definitions = false
3524                tagstyles   = false
3525                classstyles = false
3526                colormap    = false
3527            else
3528                report("missing svg root element")
3529            end
3530        else
3531            report("bad svg blob")
3532        end
3533        return mps
3534    end
3535
3536end
3537
3538-- These helpers might move to their own module .. some day ... also they will become
3539-- a bit more efficient, because we now go to mp and back which is kind of redundant,
3540-- but for now it will do.
3541
3542do
3543
3544    local bpfactor = number.dimenfactors.bp
3545
3546    function metapost.includesvgfile(filename,offset) -- offset in sp
3547        local fullname = resolvers.findbinfile(filename)
3548        if lfs.isfile(fullname) then
3549            context.startMPcode("doublefun")
3550                context('draw lmt_svg [ filename = "%s", offset = %N ] ;',filename,(offset or 0)*bpfactor)
3551            context.stopMPcode()
3552        end
3553    end
3554
3555    function metapost.includesvgbuffer(name,offset) -- offset in sp
3556        context.startMPcode("doublefun")
3557            context('draw lmt_svg [ buffer = "%s", offset = %N ] ;',name or "",(offset or 0)*bpfactor)
3558        context.stopMPcode()
3559    end
3560
3561    interfaces.implement {
3562        name      = "includesvgfile",
3563        actions   = metapost.includesvgfile,
3564        arguments = { "string", "dimension" },
3565    }
3566
3567    interfaces.implement {
3568        name      = "includesvgbuffer",
3569        actions   = metapost.includesvgbuffer,
3570        arguments = { "string", "dimension" },
3571    }
3572
3573    function metapost.showsvgpage(data)
3574        local dd = data.data
3575        if not dd then
3576            local filename = data.filename
3577            local fullname = filename and resolvers.findbinfile(filename)
3578            dd = fullname and table.load(fullname)
3579        end
3580        if type(dd) == "table" then
3581            local comment = data.comment
3582            local offset  = data.pageoffset
3583            local index   = data.index
3584            local first   = math.max(index or 1,1)
3585            local last    = math.min(index or #dd,#dd)
3586            for i=first,last do
3587                local d = setmetatableindex( {
3588                    data       = dd[i],
3589                    comment    = comment and i or false,
3590                    pageoffset = offset or nil,
3591                }, data)
3592                metapost.showsvgpage(d)
3593            end
3594        elseif data.method == "code" then
3595            context.startMPcode(doublefun)
3596                context(metapost.svgtomp(data))
3597            context.stopMPcode()
3598        else
3599            context.startMPpage { instance = "doublefun", offset = data.pageoffset or nil }
3600                context(metapost.svgtomp(data))
3601                local comment = data.comment
3602                if comment then
3603                    context("draw boundingbox currentpicture withcolor .6red ;")
3604                    context('draw textext.bot("\\strut\\tttf %s") ysized (10pt) shifted center bottomboundary currentpicture ;',comment)
3605                end
3606            context.stopMPpage()
3607        end
3608    end
3609
3610    function metapost.typesvgpage(data)
3611        local dd = data.data
3612        if not dd then
3613            local fn = data.filename
3614            dd = fn and table.load(fn)
3615        end
3616        if type(dd) == "table" then
3617            local index = data.index
3618            if index and index > 0 and index <= #dd then
3619                data = dd[index]
3620            else
3621                data = nil
3622            end
3623        end
3624        if type(data) == "string" and data ~= "" then
3625            buffers.assign("svgpage",data)
3626            context.typebuffer ({ "svgpage" }, { option = "XML", strip = "yes" })
3627        end
3628    end
3629
3630    function metapost.svgtopdf(data,...)
3631        local mps = metapost.svgtomp(data,...)
3632        if mps then
3633            -- todo: special instance, only basics needed
3634            local pdf = metapost.simple("metafun",mps,true,false,"svg")
3635            if pdf then
3636                return pdf
3637            else
3638                -- message
3639            end
3640        else
3641            -- message
3642        end
3643    end
3644
3645end
3646
3647do
3648
3649    local runner = sandbox.registerrunner {
3650        name     = "otfsvg2pdf",
3651        program  = "context",
3652        template = "--batchmode --purgeall --runs=2 %filename%",
3653        reporter = report_svg,
3654    }
3655
3656    -- By using an independent pdf file instead of pdf streams we can use resources and still
3657    -- cache. This is the old method updated. Maybe a future version will just do this runtime
3658    -- but for now this is the most efficient method.
3659
3660    local decompress = gzip.decompress
3661    local compress   = gzip.compress
3662
3663    function metapost.svgshapestopdf(svgshapes,pdftarget,report_svg)
3664        local texname   = "temp-otf-svg-to-pdf.tex"
3665        local pdfname   = "temp-otf-svg-to-pdf.pdf"
3666        local tucname   = "temp-otf-svg-to-pdf.tuc"
3667        local nofshapes = #svgshapes
3668        local pdfpages  = { filename = pdftarget }
3669        local pdfpage   = 0
3670        local t         = { }
3671        local n         = 0
3672        --
3673        os.remove(texname)
3674        os.remove(pdfname)
3675        os.remove(tucname)
3676        --
3677        if report_svg then
3678            report_svg("processing %i svg containers",nofshapes)
3679            statistics.starttiming(pdfpages)
3680        end
3681        --
3682        -- can be option:
3683        --
3684     -- n = n + 1 ; t[n] = "\\nopdfcompression"
3685        --
3686        n = n + 1 ; t[n] = "\\starttext"
3687        n = n + 1 ; t[n] = "\\setupMPpage[alternative=offset,instance=doublefun]"
3688        --
3689        for i=1,nofshapes do
3690            local entry = svgshapes[i]
3691            local data  = entry.data
3692            if decompress then
3693                data = decompress(data) or data
3694            end
3695            local specification = {
3696                data   = xmlconvert(data),
3697                x      = 0,
3698                y      = 1000,
3699                width  = 1000,
3700                height = 1000,
3701                noclip = true,
3702            }
3703            for index=entry.first,entry.last do
3704                if not pdfpages[index] then
3705                    pdfpage = pdfpage + 1
3706                    pdfpages[index] = pdfpage
3707                    local pattern = "/svg[@id='glyph" .. index .. "']"
3708                    n = n + 1 ; t[n] = "\\startMPpage"
3709                    n = n + 1 ; t[n] = metapost.svgtomp(specification,pattern,true,true) or ""
3710                    n = n + 1 ; t[n] = "\\stopMPpage"
3711                end
3712            end
3713        end
3714        n = n + 1 ; t[n] = "\\stoptext"
3715        io.savedata(texname,concat(t,"\n"))
3716        runner { filename = texname }
3717        os.remove(pdftarget)
3718        file.copy(pdfname,pdftarget)
3719        if report_svg then
3720            statistics.stoptiming(pdfpages)
3721            report_svg("svg conversion time %s",statistics.elapsedseconds(pdfpages))
3722        end
3723        os.remove(texname)
3724        os.remove(pdfname)
3725        os.remove(tucname)
3726        return pdfpages
3727    end
3728
3729    function metapost.svgshapestomp(svgshapes,report_svg)
3730        local nofshapes = #svgshapes
3731        local mpshapes = { }
3732        if report_svg then
3733            report_svg("processing %i svg containers",nofshapes)
3734            statistics.starttiming(mpshapes)
3735        end
3736        for i=1,nofshapes do
3737            local entry = svgshapes[i]
3738            local data  = entry.data
3739            if decompress then
3740                data = decompress(data) or data
3741            end
3742            local specification = {
3743                data   = xmlconvert(data),
3744                x      = 0,
3745                y      = 1000,
3746                width  = 1000,
3747                height = 1000,
3748                noclip = true,
3749            }
3750            for index=entry.first,entry.last do
3751                if not mpshapes[index] then
3752                    local pattern = "/svg[@id='glyph" .. index .. "']"
3753                    local mpcode  = metapost.svgtomp(specification,pattern,true,true) or ""
3754                    if mpcode ~= "" and compress then
3755                        mpcode = compress(mpcode) or mpcode
3756                    end
3757                    mpshapes[index] = mpcode
3758                end
3759            end
3760        end
3761        if report_svg then
3762            statistics.stoptiming(mpshapes)
3763            report_svg("svg conversion time %s",statistics.elapsedseconds(mpshapes))
3764        end
3765        return mpshapes
3766    end
3767
3768    function metapost.svgglyphtomp(fontname,unicode)
3769        if fontname and unicode then
3770            local id = fonts.definers.internal { name = fontname }
3771            if id then
3772                local tfmdata = fonts.hashes.identifiers[id]
3773                if tfmdata then
3774                    local properties = tfmdata.properties
3775                    local svg        = properties.svg
3776                    local hash       = svg and svg.hash
3777                    local timestamp  = svg and svg.timestamp
3778                    if hash then
3779                        local svgfile   = containers.read(fonts.handlers.otf.svgcache,hash)
3780                        local svgshapes = svgfile and svgfile.svgshapes
3781                        if svgshapes then
3782                            if type(unicode) == "string" then
3783                                unicode = utfbyte(unicode)
3784                            end
3785                            local chardata = tfmdata.characters[unicode]
3786                            local index    = chardata and chardata.index
3787                            if index then
3788                                for i=1,#svgshapes do
3789                                    local entry = svgshapes[i]
3790                                    if index >= entry.first and index <= entry.last then
3791                                        local data  = entry.data
3792                                        if data then
3793                                            local root = xml.convert(gzip.decompress(data) or data)
3794                                            return metapost.svgtomp (
3795                                                {
3796                                                    data   = root,
3797                                                    x      = 0,
3798                                                    y      = 1000,
3799                                                    width  = 1000,
3800                                                    height = 1000,
3801                                                    noclip = true,
3802                                                },
3803                                                "/svg[@id='glyph" .. index .. "']",
3804                                                true,
3805                                                true
3806                                            )
3807                                        end
3808                                    end
3809                                end
3810                            end
3811                        end
3812                    end
3813                end
3814            end
3815        end
3816    end
3817
3818end
3819