diff options
Diffstat (limited to 'Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua')
-rw-r--r-- | Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua | 3118 |
1 files changed, 3118 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua b/Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua new file mode 100644 index 00000000000..f4b3117a62f --- /dev/null +++ b/Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua @@ -0,0 +1,3118 @@ +if not modules then modules = { } end modules ['mlib-svg'] = { + version = 1.001, + comment = "companion to mlib-ctx.mkiv", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", + copyright = "PRAGMA ADE / ConTeXt Development Team", + license = "see context related readme files", +} + +-- Just a few notes: +-- +-- There is no real need to boost performance here .. we can always make a fast +-- variant when really needed. I will also do some of the todo's when I run into +-- proper fonts. I need to optimize this a bit but will do that once I'm satisfied +-- with the outcome and don't need more hooks and plugs. At some point I will +-- optimize the MetaPost part because now we probably have more image wrapping +-- than needed. +-- +-- As usual with these standards, things like a path can be very compact while the +-- rest is very verbose which defeats the point. This is a first attempt. There will +-- be a converter to MP as well as directly to PDF. This module was made for one of +-- the dangerous curves talks at the 2019 CTX meeting. I will do the font when I +-- need it (not that hard). +-- +-- The fact that in the more recent versions of SVG the older text related elements +-- are depricated and not even supposed to be supported, combined with the fact that +-- the text element assumes css styling, demonstrates that there is not so much as a +-- standard. It basically means that whatever technology dominates at some point +-- (probably combined with some libraries that at that point exist) determine what +-- is standard. Anyway, it probably also means that these formats are not that +-- suitable for long term archival purposes. So don't take the next implementation +-- too serious. So in the end we now have (1) attributes for properties (which is +-- nice and clean and what attributes are for, (2) a style attribute that needs to +-- be parsed, (3) classes that map to styles and (4) element related styles, plus a +-- kind of inheritance (given the limited number of elements sticking to only <g> as +-- wrapper would have made much sense. Anyway, we need to deal with it. With all +-- these style things going on, one can wonder where it will end. Basically svg +-- became just a html element that way and less clean too. The same is true for +-- tspan, which means that text itself is nested xml. +-- +-- We can do a direct conversion to PDF but then we also loose the abstraction which +-- in the future will be used, and for fonts we need to spawn out to TeX anyway, so +-- the little overhead of calling MetaPost is okay I guess. Also, we want to +-- overload labels, share fonts with the main document, etc. and are not aiming at a +-- general purpose SVG converter. For going to PDF one can just use InkScape. +-- +-- Written with Anne Clark on speakers as distraction. +-- +-- Todo when I run into an example (but ony when needed and reasonable): +-- +-- var(color,color) +-- --color<decimal> +-- currentColor : when i run into an example +-- a bit more shading +-- clip = [ auto | rect(llx,lly,urx,ury) ] (in svg) +-- xlink url ... whatever +-- masks +-- opacity per group (i need to add that to metafun first, inefficient pdf but +-- maybe filldraw can help here) +-- +-- Maybe in metafun: +-- +-- penciled n -> withpen pencircle scaled n +-- applied (...) -> transformed bymatrix (...) +-- withopacity n -> withtransparency (1,n) + +-- When testing mbo files: +-- +-- empty paths +-- missing control points +-- funny fontnames like abcdefverdana etc +-- paths representing glyphs but also with style specs +-- all kind of attributes +-- very weird and inefficient shading + +-- One can run into pretty crazy images, like lines that are fills being clipped +-- to some width. That's the danger of hiding yourself behind an interface I guess. + +local rawget, rawset, type, tonumber, tostring, next, setmetatable = rawget, rawset, type, tonumber, tostring, next, setmetatable + +local 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 + +local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns +local sqrt = math.sqrt +local concat, setmetatableindex, sortedhash = table.concat, table.setmetatableindex, table.sortedhash +local gmatch, gsub, find, match, rep = string.gmatch, string.gsub, string.find, string.match, string.rep +local formatters, fullstrip = string.formatters, string.fullstrip +local utfsplit, utfbyte = utf.split, utf.byte + +local xmlconvert, xmlcollected, xmlcount, xmlfirst, xmlroot = xml.convert, xml.collected, xml.count, xml.first, xml.root +local xmltext, xmltextonly = xml.text, xml.textonly +local css = xml.css or { } -- testing + +local function xmlinheritattributes(c,pa) + local at = c.at + local dt = c.dt + if at and dt then + if pa then + setmetatableindex(at,pa) + end + for i=1,#dt do + local dti = dt[i] + if type(dti) == "table" then + xmlinheritattributes(dti,at) + end + end + else + -- comment of so + end +end + +xml.inheritattributes = xmlinheritattributes + +-- Maybe some day helpers will move to the metapost.svg namespace! + +metapost = metapost or { } +local metapost = metapost +local context = context + +local report = logs.reporter("metapost","svg") + +local trace = false trackers.register("metapost.svg", function(v) trace = v end) +local trace_text = false trackers.register("metapost.svg.text", function(v) trace_text = v end) +local trace_path = false trackers.register("metapost.svg.path", function(v) trace_path = v end) +local trace_result = false trackers.register("metapost.svg.result", function(v) trace_result = v end) + +local pathtracer = { + ["stroke"] = "darkred", + ["stroke-opacity"] = ".5", + ["stroke-width"] = ".5", + ["fill"] = "darkgray", + ["fill-opacity"] = ".75", +} + +-- This is just an experiment. Todo: reset hash etc. Also implement +-- an option handler. + +local svghash = false do + + local svglast = 0 + local svglist = false + + local function checkhash(t,k) + local n = svglast + 1 + svglast = n + svglist[n] = k + t[k] = n + return n + end + + function metapost.startsvghashing() + svglast = 0 + svglist = { } + svghash = setmetatableindex(checkhash) + end + + function metapost.stopsvghashing() + svglast = 0 + svglist = false + svghash = false + end + + interfaces.implement { + name = "svghashed", + arguments = "integer", + actions = function(n) + local t = svglist and svglist[n] + if t then + context(t) + end + end + } + +end + +-- We have quite some closures because otherwise we run into the local variable +-- limitations. It doesn't always look pretty now, sorry. I'll clean up this mess +-- some day (the usual nth iteration of code). +-- +-- Most of the conversion is rather trivial code till I ran into a file with arcs. A +-- bit of searching lead to the a2c javascript function but it has some puzzling +-- thingies (like sin and cos definitions that look like leftovers and possible +-- division by zero). Anyway, we can if needed optimize it a bit more. Here does it +-- come from: + +-- http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes +-- https://github.com/adobe-webplatform/Snap.svg/blob/b242f49e6798ac297a3dad0dfb03c0893e394464/src/path.js + +local a2c do + + local pi, sin, cos, tan, asin, abs = math.pi, math.sin, math.cos, math.tan, math.asin, math.abs + + local d120 = (pi * 120) / 180 + local pi2 = 2 * pi + + a2c = function(x1, y1, rx, ry, angle, large, sweep, x2, y2, f1, f2, cx, cy) + + if (rx == 0 or ry == 0 ) or (x1 == x2 and y1 == y2) then + return { x1, y1, x2, y2, x2, y2 } + end + + local recursive = f1 + local rad = pi / 180 * angle + local res = nil + local cosrad = cos(-rad) -- local cosrad = cosd(angle) + local sinrad = sin(-rad) -- local sinrad = sind(angle) + + if not recursive then + + x1, y1 = x1 * cosrad - y1 * sinrad, x1 * sinrad + y1 * cosrad + x2, y2 = x2 * cosrad - y2 * sinrad, x2 * sinrad + y2 * cosrad + + local x = (x1 - x2) / 2 + local y = (y1 - y2) / 2 + local xx = x * x + local yy = y * y + local h = xx / (rx * rx) + yy / (ry * ry) + + if h > 1 then + h = sqrt(h) + rx = h * rx + ry = h * ry + end + + local rx2 = rx * rx + local ry2 = ry * ry + local ry2xx = ry2 * xx + local rx2yy = rx2 * yy + local total = rx2yy + ry2xx -- otherwise overflow + + local k = total == 0 and 0 or sqrt(abs((rx2 * ry2 - rx2yy - ry2xx) / total)) + + if large == sweep then + k = -k + end + + cx = k * rx * y / ry + (x1 + x2) / 2 + cy = k * -ry * x / rx + (y1 + y2) / 2 + + f1 = (y1 - cy) / ry -- otherwise crash on a tiny eps + f2 = (y2 - cy) / ry -- otherwise crash on a tiny eps + + f1 = asin((f1 < -1.0 and -1.0) or (f1 > 1.0 and 1.0) or f1) + f2 = asin((f2 < -1.0 and -1.0) or (f2 > 1.0 and 1.0) or f2) + + if x1 < cx then f1 = pi - f1 end + if x2 < cx then f2 = pi - f2 end + + if f1 < 0 then f1 = pi2 + f1 end + if f2 < 0 then f2 = pi2 + f2 end + + if sweep ~= 0 and f1 > f2 then f1 = f1 - pi2 end + if sweep == 0 and f2 > f1 then f2 = f2 - pi2 end + + end + + if abs(f2 - f1) > d120 then + local f2old = f2 + local x2old = x2 + local y2old = y2 + f2 = f1 + d120 * ((sweep ~= 0 and f2 > f1) and 1 or -1) + x2 = cx + rx * cos(f2) + y2 = cy + ry * sin(f2) + res = a2c(x2, y2, rx, ry, angle, 0, sweep, x2old, y2old, f2, f2old, cx, cy) + end + + local c1 = cos(f1) + local s1 = sin(f1) + local c2 = cos(f2) + local s2 = sin(f2) + + local t = tan((f2 - f1) / 4) + local hx = 4 * rx * t / 3 + local hy = 4 * ry * t / 3 + + local r = { x1 - hx * s1, y1 + hy * c1, x2 + hx * s2, y2 - hy * c2, x2, y2, unpack(res or { }) } + + if not recursive then -- we can also check for sin/cos being 0/1 + cosrad = cos(rad) + sinrad = sin(rad) + -- cosrad = cosd(angle) + -- sinrad = sind(angle) + for i0=1,#r,2 do + local i1 = i0 + 1 + local x = r[i0] + local y = r[i1] + r[i0] = x * cosrad - y * sinrad + r[i1] = x * sinrad + y * cosrad + end + end + + return r + end + +end + +-- We share some patterns. + +local p_digit = lpegpatterns.digit +local p_hexdigit = lpegpatterns.hexdigit +local p_space = lpegpatterns.whitespace + +local factors = { + ["pt"] = 1.25, + ["mm"] = 3.543307, + ["cm"] = 35.43307, + ["px"] = 1, + ["pc"] = 15, + ["in"] = 90, + ["em"] = 12 * 1.25, + ["ex"] = 8 * 1.25, +} + +local percentage_r = 1/100 +local percentage_x = percentage_r +local percentage_y = percentage_r + +-- incredible: we can find .123.456 => 0.123 0.456 ... + +local p_command_x = C(S("Hh")) +local p_command_y = C(S("Vv")) +local p_command_xy = C(S("CcLlMmQqSsTt")) +local p_command_a = C(S("Aa")) +local p_command = C(S("Zz")) + +local p_optseparator = S("\t\n\r ,")^0 +local p_separator = S("\t\n\r ,")^1 +local p_number = (S("+-")^0 * (p_digit^0 * P(".") * p_digit^1 + p_digit^1 * P(".") + p_digit^1)) + * (P("e") * S("+-")^0 * p_digit^1)^-1 + +local function convert (n) n = tonumber(n) return n end +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 +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 +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 +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 +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 + +local p_unit = (P("p") * S("txc") + P("e") * S("xm") + S("mc") * P("m") + P("in")) / factors +local p_percent = P("%") * Cc(true) + +local c_number_n = C(p_number) +local c_number_u = C(p_number) * (p_unit + p_percent)^-1 + +local p_number_n = c_number_n / convert +local p_number_x = c_number_u / convert_x +local p_number_vx = c_number_u / convert_vx +local p_number_y = c_number_u / convert_y +local p_number_vy = c_number_u / convert_vy +local p_number_r = c_number_u / convert_r + +local function asnumber (s) return s and lpegmatch(p_number, s) or 0 end +local function asnumber_r (s) return s and lpegmatch(p_number_r, s) or 0 end +local function asnumber_x (s) return s and lpegmatch(p_number_x, s) or 0 end +local function asnumber_y (s) return s and lpegmatch(p_number_y, s) or 0 end +local function asnumber_vx(s) return s and lpegmatch(p_number_vx,s) or 0 end +local function asnumber_vy(s) return s and lpegmatch(p_number_vy,s) or 0 end + +local p_number_vx_t = Ct { (p_number_vx + p_separator)^1 } +local p_number_vy_t = Ct { (p_number_vy + p_separator)^1 } + +local zerotable = { 0 } + +local function asnumber_vx_t(s) return s and lpegmatch(p_number_vx_t,s) or zerotable end +local function asnumber_vy_t(s) return s and lpegmatch(p_number_vy_t,s) or zerotable end + +local p_numbersep = p_number_n + p_separator +local p_numbers = p_optseparator * P("(") * p_numbersep^0 * p_optseparator * P(")") +local p_fournumbers = p_numbersep^4 +local p_path = Ct ( ( + p_command_xy * (p_optseparator * p_number_vx * + p_optseparator * p_number_vy )^1 + + p_command_x * (p_optseparator * p_number_vx )^1 + + p_command_y * (p_optseparator * p_number_vy )^1 + + p_command_a * (p_optseparator * p_number_vx * + p_optseparator * p_number_vy * + p_optseparator * p_number_r * + p_optseparator * p_number_n * -- flags + p_optseparator * p_number_n * -- flags + p_optseparator * p_number_vx * + p_optseparator * p_number_vy )^1 + + p_command + + p_separator +)^1 ) + +-- We can actually use the svg color definitions from the tex end but maybe a user +-- doesn't want those replace the normal definitions. +-- +-- local hexhash = setmetatableindex(function(t,k) local v = lpegmatch(p_hexcolor, k) t[k] = v return v end) -- per file +-- local hexhash3 = setmetatableindex(function(t,k) local v = lpegmatch(p_hexcolor3,k) t[k] = v return v end) -- per file +-- +-- local function hexcolor (c) return hexhash [c] end -- directly do hexhash [c] +-- local function hexcolor3(c) return hexhash3[c] end -- directly do hexhash3[c] + +local rgbcomponents, withcolor, thecolor do + + local svgcolors = { + aliceblue = 0xF0F8FF, antiquewhite = 0xFAEBD7, aqua = 0x00FFFF, aquamarine = 0x7FFFD4, + azure = 0xF0FFFF, beige = 0xF5F5DC, bisque = 0xFFE4C4, black = 0x000000, + blanchedalmond = 0xFFEBCD, blue = 0x0000FF, blueviolet = 0x8A2BE2, brown = 0xA52A2A, + burlywood = 0xDEB887, cadetblue = 0x5F9EA0, hartreuse = 0x7FFF00, chocolate = 0xD2691E, + coral = 0xFF7F50, cornflowerblue = 0x6495ED, cornsilk = 0xFFF8DC, crimson = 0xDC143C, + cyan = 0x00FFFF, darkblue = 0x00008B, darkcyan = 0x008B8B, darkgoldenrod = 0xB8860B, + darkgray = 0xA9A9A9, darkgreen = 0x006400, darkgrey = 0xA9A9A9, darkkhaki = 0xBDB76B, + darkmagenta = 0x8B008B, darkolivegreen = 0x556B2F, darkorange = 0xFF8C00, darkorchid = 0x9932CC, + darkred = 0x8B0000, darksalmon = 0xE9967A, darkseagreen = 0x8FBC8F, darkslateblue = 0x483D8B, + darkslategray = 0x2F4F4F, darkslategrey = 0x2F4F4F, darkturquoise = 0x00CED1, darkviolet = 0x9400D3, + deeppink = 0xFF1493, deepskyblue = 0x00BFFF, dimgray = 0x696969, dimgrey = 0x696969, + dodgerblue = 0x1E90FF, firebrick = 0xB22222, floralwhite = 0xFFFAF0, forestgreen = 0x228B22, + fuchsia = 0xFF00FF, gainsboro = 0xDCDCDC, ghostwhite = 0xF8F8FF, gold = 0xFFD700, + goldenrod = 0xDAA520, gray = 0x808080, green = 0x008000, greenyellow = 0xADFF2F, + grey = 0x808080, honeydew = 0xF0FFF0, hotpink = 0xFF69B4, indianred = 0xCD5C5C, + indigo = 0x4B0082, ivory = 0xFFFFF0, khaki = 0xF0E68C, lavender = 0xE6E6FA, + lavenderblush = 0xFFF0F5, lawngreen = 0x7CFC00, lemonchiffon = 0xFFFACD, lightblue = 0xADD8E6, + lightcoral = 0xF08080, lightcyan = 0xE0FFFF, lightgoldenrodyellow = 0xFAFAD2, lightgray = 0xD3D3D3, + lightgreen = 0x90EE90, lightgrey = 0xD3D3D3, lightpink = 0xFFB6C1, lightsalmon = 0xFFA07A, + lightseagreen = 0x20B2AA, lightskyblue = 0x87CEFA, lightslategray = 0x778899, lightslategrey = 0x778899, + lightsteelblue = 0xB0C4DE, lightyellow = 0xFFFFE0, lime = 0x00FF00, limegreen = 0x32CD32, + linen = 0xFAF0E6, magenta = 0xFF00FF, maroon = 0x800000, mediumaquamarine = 0x66CDAA, + mediumblue = 0x0000CD, mediumorchid = 0xBA55D3, mediumpurple = 0x9370DB, mediumseagreen = 0x3CB371, + mediumslateblue = 0x7B68EE, mediumspringgreen = 0x00FA9A, mediumturquoise = 0x48D1CC, mediumvioletred = 0xC71585, + midnightblue = 0x191970, mintcream = 0xF5FFFA, mistyrose = 0xFFE4E1, moccasin = 0xFFE4B5, + navajowhite = 0xFFDEAD, navy = 0x000080, oldlace = 0xFDF5E6, olive = 0x808000, + olivedrab = 0x6B8E23, orange = 0xFFA500, orangered = 0xFF4500, orchid = 0xDA70D6, + palegoldenrod = 0xEEE8AA, palegreen = 0x98FB98, paleturquoise = 0xAFEEEE, palevioletred = 0xDB7093, + papayawhip = 0xFFEFD5, peachpuff = 0xFFDAB9, peru = 0xCD853F, pink = 0xFFC0CB, + plum = 0xDDA0DD, powderblue = 0xB0E0E6, purple = 0x800080, red = 0xFF0000, + rosybrown = 0xBC8F8F, royalblue = 0x4169E1, saddlebrown = 0x8B4513, salmon = 0xFA8072, + sandybrown = 0xF4A460, seagreen = 0x2E8B57, seashell = 0xFFF5EE, sienna = 0xA0522D, + silver = 0xC0C0C0, skyblue = 0x87CEEB, slateblue = 0x6A5ACD, slategray = 0x708090, + slategrey = 0x708090, snow = 0xFFFAFA, springgreen = 0x00FF7F, steelblue = 0x4682B4, + tan = 0xD2B48C, teal = 0x008080, thistle = 0xD8BFD8, tomato = 0xFF6347, + turquoise = 0x40E0D0, violet = 0xEE82EE, wheat = 0xF5DEB3, white = 0xFFFFFF, + whitesmoke = 0xF5F5F5, yellow = 0xFFFF00, yellowgreen = 0x9ACD32, + } + + local f_rgb = formatters['withcolor svgcolor(%.3N,%.3N,%.3N)'] + local f_gray = formatters['withcolor svggray(%.3N)'] + local f_rgba = formatters['withcolor svgcolor(%.3N,%.3N,%.3N) withtransparency (1,%.3N)'] + local f_graya = formatters['withcolor svggray(%.3N) withtransparency (1,%.3N)'] + local f_name = formatters['withcolor "%s"'] + local f_svgcolor = formatters['svgcolor(%.3N,%.3N,%.3N)'] + local f_svggray = formatters['svggray(%.3N)'] + local f_svgname = formatters['"%s"'] + + local extract = bit32.extract + + local triplets = setmetatableindex(function(t,k) + -- we delay building all these strings + local v = svgcolors[k] + if v then + v = { extract(v,16,8)/255, extract(v,8,8)/255, extract(v,0,8)/255 } + else + v = false + end + t[k] = v + return v + end) + + local p_fraction = C(p_number) * C("%")^-1 / function(a,b) + a = tonumber(a) return a / (b and 100 or 255) + end + local p_hexcolor = P("#") * C(p_hexdigit*p_hexdigit)^1 / function(r,g,b) + return r and tonumber(r,16)/255 or nil, g and tonumber(g,16)/255 or nil, b and tonumber(b,16)/255 or nil + end + local p_rgbacolor = P("rgb") * (P("a")^-1) * P("(") * (p_fraction + p_separator)^1 * P(")") + + rgbcomponents = function(color) + local h = lpegmatch(p_hexcolor,color) + if h then + return h + end + local r, g, b, a = lpegmatch(p_rgbacolor,color) + if r then + return r, g or r, b or r + end + local t = triplets[color] + return t[1], t[2], t[3] + + end + + withcolor = function(color) + local r, g, b = lpegmatch(p_hexcolor,color) + if b and not (r == g and g == b) then + return f_rgb(r,g,b) + elseif r then + return f_gray(r) + end + local r, g, b, a = lpegmatch(p_rgbacolor,color) + if a then + if a == 1 then + if r == g and g == b then + return f_gray(r) + else + return f_rgb(r,g,b) + end + else + if r == g and g == b then + return f_graya(r,a) + else + return f_rgba(r,g,b,a) + end + end + end + if not r then + local t = triplets[color] + if t then + r, g, b = t[1], t[2], t[3] + end + end + if r then + if r == g and g == b then + return f_gray(r) + elseif g and b then + return f_rgb(r,g,b) + else + return f_gray(r) + end + end + return f_name(color) + end + + thecolor = function(color) + local h = lpegmatch(p_hexcolor,color) + if h then + return h + end + local r, g, b, a = lpegmatch(p_rgbacolor,color) + if not r then + local t = triplets[color] + if t then + r, g, b = t[1], t[2], t[3] + end + end + if r then + if r == g and g == b then + return f_svggray(r) + elseif g and b then + return f_svgcolor(r,g,b) + else + return f_svggray(r) + end + end + return f_svgname(color) + end + +end + +-- actually we can loop faster because we can go to the last one + +local grabpath, grablist do + + local f_moveto = formatters['(%N,%N)'] + local f_curveto_z = formatters['controls(%N,%N)and(%N,%N)..(%N,%N)'] + local f_curveto_n = formatters['..controls(%N,%N)and(%N,%N)..(%N,%N)'] + local f_lineto_z = formatters['(%N,%N)'] + local f_lineto_n = formatters['--(%N,%N)'] + + local m = { __index = function() return 0 end } + + grabpath = function(str) + local p = lpegmatch(p_path,str) or { } + local np = #p + local all = { entries = np, closed = false, curve = false } + if np == 0 then + return all + end + setmetatable(p,m) + local t = { } -- no real saving here if we share + local n = 0 + local a = 0 + local i = 0 + local last = "M" + local prev = last + local kind = "L" + local x, y = 0, 0 + local x1, y1 = 0, 0 + local x2, y2 = 0, 0 + local rx, ry = 0, 0 + local ar, al = 0, 0 + local as, ac = 0, nil + local mx, my = 0, 0 + while i < np do + i = i + 1 + local pi = p[i] + if type(pi) ~= "number" then + last = pi + i = i + 1 + pi = p[i] + end + -- most often + if last == "c" then + x1 = x + pi + i = i + 1 ; y1 = y + p[i] + i = i + 1 ; x2 = x + p[i] + i = i + 1 ; y2 = y + p[i] + i = i + 1 ; x = x + p[i] + i = i + 1 ; y = y + p[i] + goto curveto + elseif last == "l" then + x = x + pi + i = i + 1 ; y = y + p[i] + goto lineto + elseif last == "h" then + x = x + pi + goto lineto + elseif last == "v" then + y = y + pi + goto lineto + elseif last == "a" then + x1 = x + y1 = y + rx = pi + i = i + 1 ; ry = p[i] + i = i + 1 ; ar = p[i] + i = i + 1 ; al = p[i] + i = i + 1 ; as = p[i] + i = i + 1 ; x = x + p[i] + i = i + 1 ; y = y + p[i] + goto arc + elseif last == "s" then + if prev == "C" then + x1 = 2 * x - x2 + y1 = 2 * y - y2 + else + x1 = x + y1 = y + end + x2 = x + pi + i = i + 1 ; y2 = y + p[i] + i = i + 1 ; x = x + p[i] + i = i + 1 ; y = y + p[i] + goto curveto + elseif last == "m" then + if n > 0 then + a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0 + end + x = x + pi + i = i + 1 ; y = y + p[i] + goto moveto + elseif last == "z" then + goto close + -- less frequent + elseif last == "C" then + x1 = pi + i = i + 1 ; y1 = p[i] + i = i + 1 ; x2 = p[i] + i = i + 1 ; y2 = p[i] + i = i + 1 ; x = p[i] + i = i + 1 ; y = p[i] + goto curveto + elseif last == "L" then + x = pi + i = i + 1 ; y = p[i] + goto lineto + elseif last == "H" then + x = pi + goto lineto + elseif last == "V" then + y = pi + goto lineto + elseif last == "A" then + x1 = x + y1 = y + rx = pi + i = i + 1 ; ry = p[i] + i = i + 1 ; ar = p[i] + i = i + 1 ; al = p[i] + i = i + 1 ; as = p[i] + i = i + 1 ; x = p[i] + i = i + 1 ; y = p[i] + goto arc + elseif last == "S" then + if prev == "C" then + x1 = 2 * x - x2 + y1 = 2 * y - y2 + else + x1 = x + y1 = y + end + x2 = pi + i = i + 1 ; y2 = p[i] + i = i + 1 ; x = p[i] + i = i + 1 ; y = p[i] + goto curveto + elseif last == "M" then + if n > 0 then + a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0 + end + x = pi ; + i = i + 1 ; y = p[i] + goto moveto + elseif last == "Z" then + goto close + -- very seldom + elseif last == "q" then + x1 = x + pi + i = i + 1 ; y1 = y + p[i] + i = i + 1 ; x2 = x + p[i] + i = i + 1 ; y2 = y + p[i] + goto quadratic + elseif last == "t" then + if prev == "C" then + x1 = 2 * x - x1 + y1 = 2 * y - y1 + else + x1 = x + y1 = y + end + x2 = x + pi + i = i + 1 ; y2 = y + p[i] + goto quadratic + elseif last == "Q" then + x1 = pi + i = i + 1 ; y1 = p[i] + i = i + 1 ; x2 = p[i] + i = i + 1 ; y2 = p[i] + goto quadratic + elseif last == "T" then + if prev == "C" then + x1 = 2 * x - x1 + y1 = 2 * y - y1 + else + x1 = x + y1 = y + end + x2 = pi + i = i + 1 ; y2 = p[i] + goto quadratic + else + goto continue + end + ::moveto:: + n = n + 1 ; t[n] = f_moveto(x,y) + last = last == "M" and "L" or "l" + prev = "M" + mx = x + my = y + goto continue + ::lineto:: + n = n + 1 ; t[n] = (n > 0 and f_lineto_n or f_lineto_z)(x,y) + prev = "L" + goto continue + ::curveto:: + n = n + 1 ; t[n] = (n > 0 and f_curveto_n or f_curveto_z)(x1,y1,x2,y2,x,y) + prev = "C" + goto continue + ::arc:: + ac = a2c(x1,y1,rx,ry,ar,al,as,x,y) + for i=1,#ac,6 do + n = n + 1 ; t[n] = (n > 0 and f_curveto_n or f_curveto_z)( + ac[i],ac[i+1],ac[i+2],ac[i+3],ac[i+4],ac[i+5] + ) + end + prev = "A" + goto continue + ::quadratic:: + n = n + 1 ; t[n] = (n > 0 and f_curveto_n or f_curveto_z)( + x + 2/3 * (x1-x ), y + 2/3 * (y1-y ), + x2 + 2/3 * (x1-x2), y2 + 2/3 * (y1-y2), + x2, y2 + ) + x = x2 + y = y2 + prev = "C" + goto continue + ::close:: + -- n = n + 1 ; t[n] = prev == "C" and "..cycle" or "--cycle" + n = n + 1 ; t[n] = "--cycle" + if n > 0 then + a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0 + end + if i == np then + break + else + i = i - 1 + end + kind = prev + prev = "Z" + -- this is kind of undocumented: a close also moves back + x = mx + y = my + ::continue:: + end + if n > 0 then + a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0 + end + if prev == "Z" then + all.closed = true + end + all.curve = (kind == "C" or kind == "A") + return all, p + end + + -- this is a bit tricky as what are points for a mark ... the next can be simplified + -- a lot + + grablist = function(p) + local np = #p + if np == 0 then + return nil + end + local t = { } + local n = 0 + local a = 0 + local i = 0 + local last = "M" + local prev = last + local kind = "L" + local x, y = 0, 0 + local x1, y1 = 0, 0 + local x2, y2 = 0, 0 + local rx, ry = 0, 0 + local ar, al = 0, 0 + local as, ac = 0, nil + local mx, my = 0, 0 + while i < np do + i = i + 1 + local pi = p[i] + if type(pi) ~= "number" then + last = pi + i = i + 1 + pi = p[i] + end + -- most often + if last == "c" then + x1 = x + pi + i = i + 1 ; y1 = y + p[i] + i = i + 1 ; x2 = x + p[i] + i = i + 1 ; y2 = y + p[i] + i = i + 1 ; x = x + p[i] + i = i + 1 ; y = y + p[i] + goto curveto + elseif last == "l" then + x = x + pi + i = i + 1 ; y = y + p[i] + goto lineto + elseif last == "h" then + x = x + pi + goto lineto + elseif last == "v" then + y = y + pi + goto lineto + elseif last == "a" then + x1 = x + y1 = y + rx = pi + i = i + 1 ; ry = p[i] + i = i + 1 ; ar = p[i] + i = i + 1 ; al = p[i] + i = i + 1 ; as = p[i] + i = i + 1 ; x = x + p[i] + i = i + 1 ; y = y + p[i] + goto arc + elseif last == "s" then + if prev == "C" then + x1 = 2 * x - x2 + y1 = 2 * y - y2 + else + x1 = x + y1 = y + end + x2 = x + pi + i = i + 1 ; y2 = y + p[i] + i = i + 1 ; x = x + p[i] + i = i + 1 ; y = y + p[i] + goto curveto + elseif last == "m" then + x = x + pi + i = i + 1 ; y = y + p[i] + goto moveto + elseif last == "z" then + goto close + -- less frequent + elseif last == "C" then + x1 = pi + i = i + 1 ; y1 = p[i] + i = i + 1 ; x2 = p[i] + i = i + 1 ; y2 = p[i] + i = i + 1 ; x = p[i] + i = i + 1 ; y = p[i] + goto curveto + elseif last == "L" then + x = pi + i = i + 1 ; y = p[i] + goto lineto + elseif last == "H" then + x = pi + goto lineto + elseif last == "V" then + y = pi + goto lineto + elseif last == "A" then + x1 = x + y1 = y + rx = pi + i = i + 1 ; ry = p[i] + i = i + 1 ; ar = p[i] + i = i + 1 ; al = p[i] + i = i + 1 ; as = p[i] + i = i + 1 ; x = p[i] + i = i + 1 ; y = p[i] + goto arc + elseif last == "S" then + if prev == "C" then + x1 = 2 * x - x2 + y1 = 2 * y - y2 + else + x1 = x + y1 = y + end + x2 = pi + i = i + 1 ; y2 = p[i] + i = i + 1 ; x = p[i] + i = i + 1 ; y = p[i] + goto curveto + elseif last == "M" then + x = pi ; + i = i + 1 ; y = p[i] + goto moveto + elseif last == "Z" then + goto close + -- very seldom + elseif last == "q" then + x1 = x + pi + i = i + 1 ; y1 = y + p[i] + i = i + 1 ; x2 = x + p[i] + i = i + 1 ; y2 = y + p[i] + goto quadratic + elseif last == "t" then + if prev == "C" then + x1 = 2 * x - x1 + y1 = 2 * y - y1 + else + x1 = x + y1 = y + end + x2 = x + pi + i = i + 1 ; y2 = y + p[i] + goto quadratic + elseif last == "Q" then + x1 = pi + i = i + 1 ; y1 = p[i] + i = i + 1 ; x2 = p[i] + i = i + 1 ; y2 = p[i] + goto quadratic + elseif last == "T" then + if prev == "C" then + x1 = 2 * x - x1 + y1 = 2 * y - y1 + else + x1 = x + y1 = y + end + x2 = pi + i = i + 1 ; y2 = p[i] + goto quadratic + else + goto continue + end + ::moveto:: + n = n + 1 ; t[n] = x + n = n + 1 ; t[n] = y + last = last == "M" and "L" or "l" + prev = "M" + mx = x + my = y + goto continue + ::lineto:: + n = n + 1 ; t[n] = x + n = n + 1 ; t[n] = y + prev = "L" + goto continue + ::curveto:: + n = n + 1 ; t[n] = x + n = n + 1 ; t[n] = y + prev = "C" + goto continue + ::arc:: + ac = a2c(x1,y1,rx,ry,ar,al,as,x,y) + for i=1,#ac,6 do + n = n + 1 ; t[n] = ac[i+4] + n = n + 1 ; t[n] = ac[i+5] + end + prev = "A" + goto continue + ::quadratic:: + n = n + 1 ; t[n] = x2 + n = n + 1 ; t[n] = y2 + x = x2 + y = y2 + prev = "C" + goto continue + ::close:: + n = n + 1 ; t[n] = mx + n = n + 1 ; t[n] = my + if i == np then + break + end + kind = prev + prev = "Z" + x = mx + y = my + ::continue:: + end + return t + end + +end + +-- todo: viewbox helper + +local s_wrapped_start = "draw image (" +local f_wrapped_stop = formatters[") shifted (0,%N) scaled %N ;"] + +local handletransform, handleviewbox do + + local sind = math.sind + + --todo: better lpeg + + local f_rotatedaround = formatters[" rotatedaround((%N,%N),%N)"] + local f_rotated = formatters[" rotated(%N)"] + local f_shifted = formatters[" shifted(%N,%N)"] + local f_slanted_x = formatters[" xslanted(%N)"] + local f_slanted_y = formatters[" yslanted(%N)"] + local f_scaled = formatters[" scaled(%N)"] + local f_xyscaled = formatters[" xyscaled(%N,%N)"] + local f_matrix = formatters[" transformed bymatrix(%N,%N,%N,%N,%N,%N)"] + + local s_transform_start = "draw image ( " + local f_transform_stop = formatters[")%s ;"] + + local function rotate(r,x,y) + if x then + return r and f_rotatedaround(x,-(y or x),-r) + elseif r then + return f_rotated(-r) + else + return "" + end + end + + local function translate(x,y) + if y then + return f_shifted(x,-y) + elseif x then + return f_shifted(x,0) + else + return "" + end + end + + local function scale(x,y) + if y then + return f_xyscaled(x,y) + elseif x then + return f_scaled(x) + else + return "" + end + end + + local function skewx(x) + if x then + return f_slanted_x(sind(-x)) + else + return "" + end + end + + local function skewy(y) + if y then + return f_slanted_y(sind(-y)) + else + return "" + end + end + + local function matrix(rx,sx,sy,ry,tx,ty) + return f_matrix(rx or 1, sx or 0, sy or 0, ry or 1, tx or 0, - (ty or 0)) + end + + -- How to deal with units here? Anyway, order seems to matter. + + local p_transform = Cf ( Ct("") * ( + lpegpatterns.whitespace^0 * Cg( + C("translate") * (p_numbers / translate) -- maybe xy + + C("scale") * (p_numbers / scale) + + C("rotate") * (p_numbers / rotate) + + C("matrix") * (p_numbers / matrix) + + C("skewX") * (p_numbers / skewx) + + C("skewY") * (p_numbers / skewy) + ) + )^1, rawset) + + handletransform = function(at) + local t = at.transform + if t then + local e = lpegmatch(p_transform,t) + if e then + e = concat({ + e.rotate or "", + e.skewX or "", + e.skewY or "", + e.scale or "", + e.translate or "", + e.matrix or "", + }, " ") + return s_transform_start, f_transform_stop(e), t + end + end + end + + handleviewbox = function(v) + if v then + local x, y, w, h = lpegmatch(p_fournumbers,v) + if h then + return x, y, w, h + end + end + end + +end + +local dashed do + + -- actually commas are mandate but we're tolerant + + local f_dashed_n = formatters[" dashed dashpattern (%s ) "] + local f_dashed_y = formatters[" dashed dashpattern (%s ) shifted (%N,0) "] + + local p_number = p_optseparator/"" * p_number_r + local p_on = Cc(" on ") * p_number + local p_off = Cc(" off ") * p_number + local p_dashed = Cs((p_on * p_off^-1)^1) + + dashed = function(s,o) + if not find(s,",") then + -- a bit of a hack: + s = s .. " " .. s + end + return (o and f_dashed_y or f_dashed_n)(lpegmatch(p_dashed,s),o) + end + +end + +do + + local handlers = { } + local process = false + local root = false + local result = false + local r = false + local definitions = false + local classstyles = false + local tagstyles = false + + local tags = { + ["a"] = true, + -- ["altgGlyph"] = true, + -- ["altgGlyphDef"] = true, + -- ["altgGlyphItem"] = true, + -- ["animate"] = true, + -- ["animateColor"] = true, + -- ["animateMotion"] = true, + -- ["animateTransform"] = true, + ["circle"] = true, + ["clipPath"] = true, + -- ["color-profile"] = true, + -- ["cursor"] = true, + ["defs"] = true, + -- ["desc"] = true, + ["ellipse"] = true, + -- ["filter"] = true, + -- ["font"] = true, + -- ["font-face"] = true, + -- ["font-face-format"] = true, + -- ["font-face-name"] = true, + -- ["font-face-src"] = true, + -- ["font-face-uri"] = true, + -- ["foreignObject"] = true, + ["g"] = true, + -- ["glyph"] = true, + -- ["glyphRef"] = true, + -- ["hkern"] = true, + ["image"] = true, + ["line"] = true, + ["linearGradient"] = true, + ["marker"] = true, + -- ["mask"] = true, + -- ["metadata"] = true, + -- ["missing-glyph"] = true, + -- ["mpath"] = true, + ["path"] = true, + -- ["pattern"] = true, + ["polygon"] = true, + ["polyline"] = true, + ["radialGradient"] = true, + ["rect"] = true, + -- ["script"] = true, + -- ["set"] = true, + ["stop"] = true, + ["style"] = true, + ["svg"] = true, + -- ["switch"] = true, + ["symbol"] = true, + ["text"] = true, + -- ["textPath"] = true, + -- ["title"] = true, + ["tspan"] = true, + ["use"] = true, + -- ["view"] = true, + -- ["vkern"] = true, + } + + local function handlechains(c) + if tags[c.tg] then + local at = c.at + local dt = c.dt + if at and dt then + -- at["inkscape:connector-curvature"] = nil -- cleare entry and might prevent table growth + local estyle = rawget(at,"style") + if estyle and estyle ~= "" then + for k, v in gmatch(estyle,"%s*([^:]+):%s*([^;]+);?") do + at[k] = v + end + end + local eclass = rawget(at,"class") + if eclass and eclass ~= "" then + for c in gmatch(eclass,"[^ ]+") do + local s = classstyles[c] + if s then + for k, v in next, s do + at[k] = v + end + end + end + end + local tstyle = tagstyles[tag] + if tstyle then + for k, v in next, tstyle do + at[k] = v + end + end + if trace_path and pathtracer then + for k, v in next, pathtracer do + at[k] = v + end + end + for i=1,#dt do + local dti = dt[i] + if type(dti) == "table" then + handlechains(dti) + end + end + end + end + end + + local handlestyle do + + -- It can also be CDATA but that is probably dealt with because we only + -- check for style entries and ignore the rest. But maybe we also need + -- to check a style at the outer level? + + local p_key = C((R("az","AZ","09","__","--")^1)) + local p_spec = P("{") * C((1-P("}"))^1) * P("}") + local p_valid = Carg(1) * P(".") * p_key + Carg(2) * p_key + local p_grab = ((p_valid * p_space^0 * p_spec / rawset) + p_space^1 + P(1))^1 + + local fontspecification = css.fontspecification + + handlestyle = function(c) + local s = xmltext(c) + lpegmatch(p_grab,s,1,classstyles,tagstyles) + for k, v in next, classstyles do + local t = { } + for k, v in gmatch(v,"%s*([^:]+):%s*([^;]+);?") do + if k == "font" then + local s = fontspecification(v) + for k, v in next, s do + t["font-"..k] = v + end + else + t[k] = v + end + end + classstyles[k] = t + end + for k, v in next, tagstyles do + local t = { } + for k, v in gmatch(v,"%s*([^:]+):%s*([^;]+);?") do + if k == "font" then + local s = fontspecification(v) + for k, v in next, s do + t["font-"..k] = v + end + else + t[k] = v + end + end + tagstyles[k] = t + end + end + + function handlers.style() + -- ignore + end + + end + + -- We can have root in definitions and then do a metatable lookup but use + -- is not used that often I guess. + + local function locate(id) + local res = definitions[id] + if res then + return res + end + local ref = gsub(id,"^url%(#(.-)%)$","%1") + local ref = gsub(ref,"^#","") + -- we can make a fast id lookup + local res = xmlfirst(root,"**[@id='"..ref.."']") + if res then + definitions[id] = res + end + return res + end + + -- also locate + + local function handleclippath(at) + local clippath = at["clip-path"] + + if not clippath then + return + end + + local spec = definitions[clippath] or locate(clippath) + + -- do we really need thsi crap + if not spec then + local index = match(clippath,"(%d+)") + if index then + spec = xmlfirst(root,"clipPath["..tostring(tonumber(index) or 0).."]") + end + end + -- so far for the crap + + if not spec then + report("unknown clip %a",clippath) + return + elseif spec.tg ~= "clipPath" then + report("bad clip %a",clippath) + return + end + + ::again:: + for c in xmlcollected(spec,"/(path|use|g)") do + local tg = c.tg + if tg == "use" then + local ca = c.at + local id = ca["xlink:href"] + if id then + spec = locate(id) + if spec then + local sa = spec.at + setmetatableindex(sa,ca) + if spec.tg == "path" then + local d = sa.d + if d then + local p = grabpath(d) + p.evenodd = sa["clip-rule"] == "evenodd" + p.close = true + return p, clippath + else + return + end + else + goto again + end + end + end + -- break + elseif tg == "path" then + local ca = c.at + local d = ca.d + if d then + local p = grabpath(d) + p.evenodd = ca["clip-rule"] == "evenodd" + p.close = true + return p, clippath + else + return + end + else + -- inherit? + end + end + end + + local s_shade_linear = ' withshademethod "linear" ' + local s_shade_circular = ' withshademethod "circular" ' + local f_shade_step = formatters['withshadestep ( withshadefraction %N withshadecolors(%s,%s) )'] + local f_shade_one = formatters['withprescript "sh_center_a=%N %N"'] + local f_shade_two = formatters['withprescript "sh_center_b=%N %N"'] + + local f_color = formatters['withcolor "%s"'] + local f_opacity = formatters['withtransparency (1,%N)'] + local f_pen = formatters['withpen pencircle scaled %N'] + + -- todo: gradient unfinished + -- todo: opacity but first we need groups in mp + + local function gradient(id) + local spec = definitions[id] -- no locate ! + if spec then + local kind = spec.tg + local shade = nil + local n = 1 + local a = spec.at + if kind == "linearGradient" then + shade = { s_shade_linear } + -- + local x1 = rawget(a,"x1") + local y1 = rawget(a,"y1") + local x2 = rawget(a,"x2") + local y2 = rawget(a,"y2") + if x1 and y1 then + n = n + 1 ; shade[n] = f_shade_one(asnumber_vx(x1),asnumber_vy(y1)) + end + if x2 and y2 then + n = n + 1 ; shade[n] = f_shade_one(asnumber_vx(x2),asnumber_vy(y2)) + end + -- + elseif kind == "radialGradient" then + shade = { s_shade_circular } + -- + local cx = rawget(a,"cx") -- x center + local cy = rawget(a,"cy") -- y center + local r = rawget(a,"r" ) -- radius + local fx = rawget(a,"fx") -- focal points + local fy = rawget(a,"fy") -- focal points + -- + if cx and cy then + -- todo + end + if r then + -- todo + end + if fx and fy then + -- todo + end + else + report("unknown gradient %a",id) + return + end + -- local gu = a.gradientUnits + -- local gt = a.gradientTransform + -- local sm = a.spreadMethod + local colora, colorb + -- startcolor ? + for c in xmlcollected(spec,"/stop") do + local a = c.at + local offset = rawget(a,"offset") + local colorb = rawget(a,"stop-color") + local opacity = rawget(a,"stop-opacity") + if colorb then + colorb = thecolor(colorb) + end + if not colora then + colora = colorb + end + -- what if no percentage + + local fraction = offset and asnumber_r(offset) + if not fraction then + -- offset = tonumber(offset) + -- for now + fraction = xmlcount(spec,"/stop")/100 + end + + if colora and colorb and color_a ~= "" and color_b ~= "" then + n = n + 1 ; shade[n] = f_shade_step(fraction,colora,colorb) + end + + colora = colorb + end + return concat(shade," ") + end + end + + local function drawproperties(stroke,at,opacity) + local p = at["stroke-width"] + if p then + p = f_pen(asnumber_r(p)) + end + local d = at["stroke-dasharray"] + if d == "none" then + d = nil + elseif d then + local o = at["stroke-dashoffset"] + if o and o ~= "none" then + o = asnumber_r(o) + else + o = false + end + d = dashed(d,o) + end + local c = withcolor(stroke) + local o = at["stroke-opacity"] or (opacity and at["opacity"]) + if o == "none" then + o = nil + elseif o then + o = asnumber_r(o) + if o and o ~= 1 then + o = f_opacity(o) + else + o = nil + end + end + return p, d, c, o + end + + local s_opacity_start = "draw image (" + local f_opacity_stop = formatters["setgroup currentpicture to boundingbox currentpicture withtransparency (1,%N)) ;"] + + local function sharedopacity(at) + local o = at["opacity"] + if o and o ~= "none" then + o = asnumber_r(o) + if o and o ~= 1 then + return s_opacity_start, f_opacity_stop(o) + end + end + end + + local function fillproperties(fill,at,opacity) + local c = c ~= "none" and (gradient(fill) or withcolor(fill)) or nil + local o = at["fill-opacity"] or (opacity and at["opacity"]) + if o and o ~= "none" then + o = asnumber_r(o) + if o == 1 then + return c + elseif o then + return c, f_opacity(o), o == 0 + end + end + return c + end + + -- todo: clip = [ auto | rect(llx,lly,urx,ury) ] + + local s_offset_start = "draw image ( " + local f_offset_stop = formatters[") shifted (%N,%N) ;"] + local s_rotation_start = "draw image ( " + local f_rotation_stop = formatters[") rotatedaround((0,0),-angle((%N,%N))) ;"] + local f_rotation_angle = formatters[") rotatedaround((0,0),-%N) ;"] + + local function offset(at) + local x = asnumber_vx(rawget(at,"x")) + local y = asnumber_vy(rawget(at,"y")) + if x ~= 0 or y ~= 0 then + return s_offset_start, f_offset_stop(x,y) + end + end + + local s_viewport_start = "draw image (" + local s_viewport_stop = ") ;" + local f_viewport_shift = formatters["currentpicture := currentpicture shifted (%03N,%03N);"] + local f_viewport_scale = formatters["currentpicture := currentpicture xysized (%03N,%03N);"] + local f_viewport_clip = formatters["clip currentpicture to (unitsquare xyscaled (%03N,%03N));"] + + local function viewport(x,y,w,h,noclip,scale) + r = r + 1 ; result[r] = s_viewport_start + return function() + local okay = w ~= 0 and h ~= 0 + if okay and scale then + r = r + 1 ; result[r] = f_viewport_scale(w,h) + end + if x ~= 0 or y ~= 0 then + r = r + 1 ; result[r] = f_viewport_shift(-x,y) + end + if okay and not noclip then + r = r + 1 ; result[r] = f_viewport_clip(w,-h) + end + + r = r + 1 ; result[r] = s_viewport_stop + end + end + + -- maybe forget about defs and just always locate (and then backtrack + -- over <g> if needed) + + function handlers.defs(c) + for c in xmlcollected(c,"/*") do + local a = c.at + if a then + local id = rawget(a,"id") + if id then + definitions["#" .. id ] = c + definitions["url(#" .. id .. ")"] = c + end + end + end + end + + function handlers.symbol(c) + if uselevel == 0 then + local id = rawget(c.at,"id") + if id then + definitions["#" .. id ] = c + definitions["url(#" .. id .. ")"] = c + end + else + handlers.g(c) + end + end + + local uselevel = 0 + + function handlers.use(c) + local at = c.at + local id = rawget(at,"href") or rawget(at,"xlink:href") -- better a rawget + local res = locate(id) + if res then + -- width height ? + uselevel = uselevel + 1 + local boffset, eoffset = offset(at) + local btransform, etransform, transform = handletransform(at) + + if boffset then + r = r + 1 result[r] = boffset + end + + -- local clippath = at.clippath + + if btransform then + r = r + 1 result[r] = btransform + end + + local _transform = transform + local _clippath = clippath + at["transform"] = false + -- at["clip-path"] = false + + process(res,"/*") + + at["transform"] = _transform + -- at["clip-path"] = _clippath + + if etransform then + r = r + 1 ; result[r] = etransform + end + + if eoffset then + r = r + 1 result[r] = eoffset + end + + uselevel = uselevel - 1 + else + report("use: unknown definition %a",id) + end + end + + local f_no_draw = formatters['nodraw (%s)'] + local f_do_draw = formatters['draw (%s)'] + local f_no_fill_c = formatters['nofill (%s..cycle)'] + local f_do_fill_c = formatters['fill (%s..cycle)'] + local f_eo_fill_c = formatters['eofill (%s..cycle)'] + local f_no_fill_l = formatters['nofill (%s--cycle)'] + local f_do_fill_l = formatters['fill (%s--cycle)'] + local f_eo_fill_l = formatters['eofill (%s--cycle)'] + local f_do_fill = f_do_fill_c + local f_eo_fill = f_eo_fill_c + local f_no_fill = f_no_fill_c + local s_clip_start = 'draw image (' + local f_clip_stop_c = formatters[') ; clip currentpicture to (%s..cycle) ;'] + local f_clip_stop_l = formatters[') ; clip currentpicture to (%s--cycle) ;'] + local f_clip_stop = f_clip_stop_c + local f_eoclip_stop_c = formatters[') ; eoclip currentpicture to (%s..cycle) ;'] + local f_eoclip_stop_l = formatters[') ; eoclip currentpicture to (%s--cycle) ;'] + local f_eoclip_stop = f_eoclip_stop_c + + -- could be shared and then beginobject | endobject + + local function flushobject(object,at,c,o) + local btransform, etransform = handletransform(at) + local cpath = handleclippath(at) + + if cpath then + r = r + 1 ; result[r] = s_clip_start + end + + if btransform then + r = r + 1 ; result[r] = btransform + end + + r = r + 1 ; result[r] = f_do_draw(object) + + if c then + r = r + 1 ; result[r] = c + end + + if o then + r = r + 1 ; result[r] = o + end + + if etransform then + r = r + 1 ; result[r] = etransform + end + + r = r + 1 ; result[r] = ";" + + if cpath then + local f_done = cpath.evenodd + if cpath.curve then + f_done = f_done and f_eoclip_stop_c or f_clip_stop_c + else + f_done = f_done and f_eoclip_stop_l or f_clip_stop_l + end + r = r + 1 ; result[r] = f_done(cpath[1]) + end + end + + do + + local flush + + local f_linecap = formatters["interim linecap := %s ;"] + local f_linejoin = formatters["interim linejoin := %s ;"] + local f_miterlimit = formatters["interim miterlimit := %s ;"] + + local s_begingroup = "begingroup;" + local s_endgroup = "endgroup;" + + local linecaps = { butt = "butt", square = "squared", round = "rounded" } + local linejoins = { miter = "mitered", bevel = "beveled", round = "rounded" } + + local function startlineproperties(at) + local cap = at["stroke-linecap"] + local join = at["stroke-linejoin"] + local limit = at["stroke-miterlimit"] + cap = cap and linecaps [cap] + join = join and linejoins[join] + limit = limit and asnumber_r(limit) + if cap or join or limit then + r = r + 1 ; result[r] = s_begingroup + if cap then + r = r + 1 ; result[r] = f_linecap(cap) + end + if join then + r = r + 1 ; result[r] = f_linejoin(join) + end + if limit then + r = r + 1 ; result[r] = f_miterlimit(limit) + end + return function() + at["stroke-linecap"] = false + at["stroke-linejoin"] = false + at["stroke-miterlimit"] = false + r = r + 1 ; result[r] = s_endgroup + at["stroke-linecap"] = cap + at["stroke-linejoin"] = join + at["stroke-miterlimit"] = limit + end + end + end + + -- markers are a quite rediculous thing .. let's assume simple usage for now + + function handlers.marker() + -- todo: is just a def too + end + + -- kind of local svg ... so make a generic one + -- + -- todo: combine more (offset+scale+rotation) + + local function makemarker(where,c,x1,y1,x2,y2,x3,y3) + local at = c.at + local refx = rawget(at,"refX") + local refy = rawget(at,"refY") + local width = rawget(at,"markerWidth") + local height = rawget(at,"markerHeight") + local view = rawget(at,"viewBox") + local orient = rawget(at,"orient") + -- local ratio = rawget(at,"preserveAspectRatio") + local units = at["markerUnits"] + local height = at["markerHeight"] + local angx = 0 + local angy = 0 + local angle = 0 + + if where == "beg" then + if orient == "auto" then -- unchecked + -- no angle + angx = x2 - x3 + angy = y2 - y3 + elseif orient == "auto-start-reverse" then -- checked + -- points to start + angx = x3 - x2 + angy = y3 - y2 + elseif orient then -- unchecked + angle = asnumber_r(orient) + end + elseif where == "end" then + if orient == "auto" then -- unchecked + -- no angle ? + angx = x1 - x2 + angy = y1 - y2 + elseif orient == "auto-start-reverse" then -- unchecked + -- points to end + angx = x2 - x1 + angy = y2 - y1 + elseif orient then -- unchecked + angle = asnumber_r(orient) + end + elseif orient then -- unchecked + angle = asnumber_r(orient) + end + + -- what wins: viewbox or w/h + + refx = asnumber_x(refx) + refy = asnumber_y(refy) + + width = width and asnumber_x(width) or 3 -- defaults + height = height and asnumber_y(height) or 3 -- defaults + + local x = 0 + local y = 0 + local w = width + local h = height + + -- kind of like the main svg + + r = r + 1 ; result[r] = s_offset_start + + local wrapupviewport +-- todo : better viewbox code + local xpct, ypct, rpct + if view then + x, y, w, h = handleviewbox(view) + end + + if width ~= 0 then + w = width + end + if height ~= 0 then + h = height + end + + if h then + xpct = percentage_x + ypct = percentage_y + rpct = percentage_r + percentage_x = w / 100 + percentage_y = h / 100 + percentage_r = (sqrt(w^2 + h^2) / sqrt(2)) / 100 + wrapupviewport = viewport(x,y,w,h,true,true) -- no clip + end + + -- we can combine a lot here: + + local hasref = refx ~= 0 or refy ~= 0 + local hasrot = angx ~= 0 or angy ~= 0 or angle ~= 0 + + local btransform, etransform, transform = handletransform(at) + + if btransform then + r = r + 1 ; result[r] = btransform + end + + if hasrot then + r = r + 1 ; result[r] = s_rotation_start + end + + if hasref then + r = r + 1 ; result[r] = s_offset_start + end + + local _transform = transform + at["transform"] = false + + handlers.g(c) + + at["transform"] = _transform + + if hasref then + r = r + 1 ; result[r] = f_offset_stop(-refx,refy) + end + + if hasrot then + if angle ~= 0 then + r = r + 1 ; result[r] = f_rotation_angle(angle) + else + r = r + 1 ; result[r] = f_rotation_stop(angx,angy) + end + end + + if etransform then + r = r + 1 ; result[r] = etransform + end + + if h then + percentage_x = xpct + percentage_y = ypct + percentage_r = rpct + if wrapupviewport then + wrapupviewport() + end + end + r = r + 1 ; result[r] = f_offset_stop(x2,y2) + + end + + local function addmarkers(list,begmarker,midmarker,endmarker) + local n = #list + if n > 3 then + if begmarker then + local m = locate(begmarker) + if m then + makemarker("beg",m,false,false,list[1],list[2],list[3],list[4]) + end + end + if midmarker then + local m = locate(midmarker) + if m then + for i=3,n-2,2 do + makemarker("mid",m,list[i-2],list[i-1],list[i],list[i+1],list[i+2],list[i+3]) + end + end + end + if endmarker then + local m = locate(endmarker) + if m then + makemarker("end",m,list[n-3],list[n-2],list[n-1],list[n],false,false) + end + end + else + -- no line + end + end + + local function flush(shape,dofill,at,list,begmarker,midmarker,endmarker) + + local fill = dofill and (at["fill"] or "black") + local stroke = at["stroke"] or "none" + + local btransform, etransform = handletransform(at) + local cpath = handleclippath(at) + + if cpath then + r = r + 1 ; result[r] = s_clip_start + end + + local has_stroke = stroke and stroke ~= "none" + local has_fill = fill and fill ~= "none" + + local bopacity, eopacity + if has_stroke and has_fill then + bopacity, eopacity = sharedopacity(at) + end + + if bopacity then + r = r + 1 ; result[r] = bopacity + end + + if has_fill then + local color, opacity = fillproperties(fill,at,not has_stroke) + local f_xx_fill = at["fill-rule"] == "evenodd" and f_eo_fill or f_do_fill + if btransform then + r = r + 1 ; result[r] = btransform + end + r = r + 1 result[r] = f_xx_fill(shape) + if color then + r = r + 1 ; result[r] = color + end + if opacity then + r = r + 1 ; result[r] = opacity + end + r = r + 1 ; result[r] = etransform or ";" + end + + if has_stroke then + local wrapup = startlineproperties(at) + local pen, dashing, color, opacity = drawproperties(stroke,at,not has_fill) + if btransform then + r = r + 1 ; result[r] = btransform + end + r = r + 1 ; result[r] = f_do_draw(shape) + if pen then + r = r + 1 ; result[r] = pen + end + if dashing then + r = r + 1 ; result[r] = dashing + end + if color then + r = r + 1 ; result[r] = color + end + if opacity then + r = r + 1 ; result[r] = opacity + end + r = r + 1 ; result[r] = etransform or ";" + -- + if list then + addmarkers(list,begmarker,midmarker,endmarker) + end + -- + if wrapup then + wrapup() + end + end + + if eopacity then + r = r + 1 ; result[r] = eopacity + end + + if cpath then + r = r + 1 ; result[r] = (cpath.evenodd and f_eoclip_stop or f_clip_stop)(cpath[1]) + end + + end + + local f_rectangle = formatters['unitsquare xyscaled (%N,%N) shifted (%N,%N)'] + local f_rounded = formatters['roundedsquarexy(%N,%N,%N,%N) shifted (%N,%N)'] + local f_line = formatters['((%N,%N)--(%N,%N))'] + local f_ellipse = formatters['(fullcircle xyscaled (%N,%N) shifted (%N,%N))'] + local f_circle = formatters['(fullcircle scaled %N shifted (%N,%N))'] + + function handlers.line(c) + local at = c.at + local x1 = rawget(at,"x1") + local y1 = rawget(at,"y1") + local x2 = rawget(at,"x2") + local y2 = rawget(at,"y2") + + x1 = x1 and asnumber_vx(x1) or 0 + y1 = y1 and asnumber_vy(y1) or 0 + x2 = x2 and asnumber_vx(x2) or 0 + y2 = y2 and asnumber_vy(y2) or 0 + + flush(f_line(x1,y1,x2,y2),false,at) + end + + function handlers.rect(c) + local at = c.at + local width = rawget(at,"width") + local height = rawget(at,"height") + local x = rawget(at,"x") + local y = rawget(at,"y") + local rx = rawget(at,"rx") + local ry = rawget(at,"ry") + + width = width and asnumber_x(width) or 0 + height = height and asnumber_y(height) or 0 + x = x and asnumber_vx(x) or 0 + y = y and asnumber_vy(y) or 0 + + y = y - height + + if rx then rx = asnumber(rx) end + if ry then ry = asnumber(ry) end + + if rx or ry then + if not rx then rx = ry end + if not ry then ry = rx end + flush(f_rounded(width,height,rx,ry,x,y),true,at) + else + flush(f_rectangle(width,height,x,y),true,at) + end + end + + function handlers.ellipse(c) + local at = c.at + local cx = rawget(at,"cx") + local cy = rawget(at,"cy") + local rx = rawget(at,"rx") + local ry = rawget(at,"ry") + + cx = cx and asnumber_vx(cx) or 0 + cy = cy and asnumber_vy(cy) or 0 + rx = rx and asnumber_r (rx) or 0 + ry = ry and asnumber_r (ry) or 0 + + flush(f_ellipse(2*rx,2*ry,cx,cy),true,at) + end + + function handlers.circle(c) + local at = c.at + local cx = rawget(at,"cx") + local cy = rawget(at,"cy") + local r = rawget(at,"r") + + cx = cx and asnumber_vx(cx) or 0 + cy = cy and asnumber_vy(cy) or 0 + r = r and asnumber_r (r) or 0 + + flush(f_circle(2*r,cx,cy),true,at) + end + + local f_lineto_z = formatters['(%N,%N)'] + local f_lineto_n = formatters['--(%N,%N)'] + + local p_pair = p_optseparator * p_number_vx * p_optseparator * p_number_vy + local p_open = Cc("(") + local p_close = Carg(1) * P(true) / function(s) return s end + local p_polyline = Cs(p_open * (p_pair / f_lineto_z) * (p_pair / f_lineto_n)^0 * p_close) + local p_polypair = Ct(p_pair^0) + + local function poly(c,final) + local at = c.at + local points = rawget(at,"points") + if points then + local path = lpegmatch(p_polyline,points,1,final) + local list = nil + local begmarker = rawget(at,"marker-start") + local midmarker = rawget(at,"marker-mid") + local endmarker = rawget(at,"marker-end") + if begmarker or midmarker or endmarker then + list = lpegmatch(p_polypair,points) + end + flush(path,true,at,list,begmarker,midmarker,endmarker) + end + end + + function handlers.polyline(c) poly(c, ")") end + function handlers.polygon (c) poly(c,"--cycle)") end + + local s_image_start = "draw image (" + local s_image_stop = ") ;" + + function handlers.path(c) + local at = c.at + local d = rawget(at,"d") + if d then + local shape, l = grabpath(d) + local fill = at["fill"] or "black" + local stroke = at["stroke"] or "none" + local n = #shape + + local btransform, etransform = handletransform(at) + local cpath = handleclippath(at) + + if cpath then + r = r + 1 ; result[r] = s_clip_start + end + + -- todo: image (nicer for transform too) + + if fill and fill ~= "none" then + local color, opacity = fillproperties(fill,at) + local f_xx_fill = at["fill-rule"] == "evenodd" + if shape.closed then + f_xx_fill = f_xx_fill and f_eo_fill or f_do_fill + elseif shape.curve then + f_xx_fill = f_xx_fill and f_eo_fill_c or f_do_fill_c + else + f_xx_fill = f_xx_fill and f_eo_fill_l or f_do_fill_l + end + if n == 1 then + if btransform then + r = r + 1 ; result[r] = btransform + end + r = r + 1 result[r] = f_xx_fill(shape[1]) + if color then + r = r + 1 ; result[r] = color + end + if opacity then + r = r + 1 ; result[r] = opacity + end + r = r + 1 ; result[r] = etransform or ";" + else + r = r + 1 ; result[r] = btransform or s_image_start + for i=1,n do + if i == n then + r = r + 1 ; result[r] = f_xx_fill(shape[i]) + if color then + r = r + 1 ; result[r] = color + end + if opacity then + r = r + 1 ; result[r] = opacity + end + else + r = r + 1 ; result[r] = f_no_fill(shape[i]) + end + r = r + 1 ; result[r] = ";" + end + r = r + 1 ; result[r] = etransform or s_image_stop + end + end + + if stroke and stroke ~= "none" then + local begmarker = rawget(at,"marker-start") + local midmarker = rawget(at,"marker-mid") + local endmarker = rawget(at,"marker-end") + if begmarker or midmarker or endmarker then + list = grablist(l) + end + local wrapup = startlineproperties(at) + local pen, dashing, color, opacity = drawproperties(stroke,at) + if n == 1 and not list then + if btransform then + r = r + 1 ; result[r] = btransform + end + r = r + 1 result[r] = f_do_draw(shape[1]) + if pen then + r = r + 1 ; result[r] = pen + end + if dashing then + r = r + 1 ; result[r] = dashing + end + if color then + r = r + 1 ; result[r] = color + end + if opacity then + r = r + 1 ; result[r] = opacity + end + r = r + 1 result[r] = etransform or ";" + else + r = r + 1 result[r] = btransform or "draw image (" + for i=1,n do + r = r + 1 result[r] = f_do_draw(shape[i]) + if pen then + r = r + 1 ; result[r] = pen + end + if dashing then + r = r + 1 ; result[r] = dashing + end + if color then + r = r + 1 ; result[r] = color + end + if opacity then + r = r + 1 ; result[r] = opacity + end + r = r + 1 ; result[r] = ";" + end + if list then + addmarkers(list,begmarker,midmarker,endmarker) + end + r = r + 1 ; result[r] = etransform or ") ;" + end + if wrapup then + wrapup() + end + end + + if cpath then + r = r + 1 ; result[r] = f_clip_stop(cpath[1]) + end + + end + end + + end + + -- kind of special + + do + + -- some day: + -- + -- specification = identifiers.jpg(data."string") + -- specification.data = data + -- inclusion takes from data + -- specification.data = false + + local f_image = formatters[ [[figure("%s") xysized (%N,%N) shifted (%N,%N)]] ] + + local nofimages = 0 + + function handlers.image(c) + local at = c.at + local im = rawget(at,"xlink:href") + if im then + local kind, data = match(im,"^data:image/([a-z]+);base64,(.*)$") + if kind == "png" then + -- ok + elseif kind == "jpeg" then + kind = "jpg" + else + kind = false + end + if kind and data then + local w = rawget(at,"width") + local h = rawget(at,"height") + local x = rawget(at,"x") + local y = rawget(at,"y") + w = w and asnumber_x(w) + h = h and asnumber_y(h) + x = x and asnumber_vx(x) or 0 + y = y and asnumber_vy(y) or 0 + nofimages = nofimages + 1 + local name = "temp-svg-image-" .. nofimages .. "." .. kind + local data = mime.decode("base64")(data) + io.savedata(name,data) + if not w or not h then + local info = graphics.identifiers[kind](data,"string") + if info then + -- todo: keep aspect ratio attribute + local xsize = info.xsize + local ysize = info.ysize + if not w then + if not h then + w = xsize + h = ysize + else + w = (h / ysize) * xsize + end + else + h = (w / xsize) * ysize + end + end + end + -- safeguard: + if not w then w = h or 1 end + if not h then h = w or 1 end + luatex.registertempfile(name) + -- done: + flushobject(f_image(name,w,h,x,y - h),at) + else + -- nothing done + end + end + end + + end + + -- these transform: g a text svg symbol + + do + + function handlers.a(c) + process(c,"/*") + end + + function handlers.g(c) -- much like flushobject so better split and share + local at = c.at + + local btransform, etransform, transform = handletransform(at) + local cpath, clippath = handleclippath(at) + + if cpath then + r = r + 1 ; result[r] = s_clip_start + end + + if btransform then + r= r + 1 result[r] = btransform + end + + local _transform = transform + local _clippath = clippath + at["transform"] = false + at["clip-path"] = false + + process(c,"/*") + + at["transform"] = _transform + at["clip-path"] = _clippath + + if etransform then + r = r + 1 ; result[r] = etransform + end + + if cpath then + local f_done = cpath.evenodd + if cpath.curve then + f_done = f_done and f_eoclip_stop_c or f_clip_stop_c + else + f_done = f_done and f_eoclip_stop_l or f_clip_stop_l + end + r = r + 1 ; result[r] = f_done(cpath[1]) + end + end + + -- this will never really work out + -- + -- todo: register text in lua in mapping with id, then draw mapping unless overloaded + -- using lmt_svglabel with family,style,weight,size,id passed + + -- nested tspans are messy: they can have displacements but in inkscape we also + -- see x and y (inner and outer element) + + -- The size is a bit of an issue. I assume that the specified size relates to the + -- designsize but we want to be able to use other fonts. + + do + + local f_styled = formatters["\\svgstyled{%s}{%s}{%s}{%s}"] + local f_colored = formatters["\\svgcolored{%.3N}{%.3N}{%.3N}{"] + local f_placed = formatters["\\svgplaced{%.3N}{%.3N}{}{"] + local f_poschar = formatters["\\svgposchar{%.3N}{%.3N}{%s}"] + local f_char = formatters["\\svgchar{%s}"] + + local f_scaled = formatters["\\svgscaled{%N}{%s}{%s}{%s}"] + local f_normal = formatters["\\svgnormal{%s}{%s}{%s}"] + local f_hashed = formatters["\\svghashed{%s}"] + + -- We move to the outer (x,y) and when we have an inner offset we + -- (need to) compensate for that outer offset. + + -- local f_text_scaled_svg = formatters['(svgtext("%s") scaled %N shifted (%N,%N))'] + -- local f_text_normal_svg = formatters['(svgtext("%s") shifted (%N,%N))'] + -- local f_text_simple_svg = formatters['svgtext("%s")'] + + local anchors = { + ["start"] = "drt", + ["end"] = "dflt", + ["middle"] = "d", + } + + local f_text_normal_svg = formatters['(textext.%s("%s") shifted (%N,%N))'] + local f_text_simple_svg = formatters['textext.%s("%s")'] + + -- or just maptext + + local f_mapped_normal_svg = formatters['(svgtext("%s") shifted (%N,%N))'] + local f_mapped_simple_svg = formatters['svgtext("%s")'] + + local cssfamily = css.family + local cssstyle = css.style + local cssweight = css.weight + local csssize = css.size + + local usedfonts = setmetatableindex(function(t,k) + local v = setmetatableindex("table") + t[k] = v + return v + end) + + local p_texescape = lpegpatterns.texescape + + -- For now as I need it for my (some 1500) test files. + + local function checkedfamily(name) + if find(name,"^.-verdana.-$") then + name = "verdana" + end + return name + end + + -- todo: only escape some chars and handle space + + local defaultsize = 10 + + local function collect(t,c,x,y,size,scale,family,tx,ty) + local at = c.at + local ax = rawget(at,"x") + local ay = rawget(at,"y") + local dx = rawget(at,"dx") + local dy = rawget(at,"dy") + local v_fill = at["fill"] + local v_family = at["font-family"] + local v_style = at["font-style"] + local v_weight = at["font-weight"] + local v_size = at["font-size"] + -- + ax = ax and asnumber_vx(ax) or x + ay = ay and asnumber_vy(ay) or y + dx = dx and asnumber_vx(dx) or 0 + dy = dy and asnumber_vy(dy) or 0 + -- + if v_family then v_family = cssfamily(v_family) end + if v_style then v_style = cssstyle (v_style) end + if v_weight then v_weight = cssweight(v_weight) end + if v_size then v_size = csssize (v_size,factors) end + -- + ax = ax - x + ay = ay - y + -- + local elayered = ax ~= 0 or ay ~= 0 or false + local eplaced = dx ~= 0 or dy ~= 0 or false + + local usedsize, usedscaled + + if elayered then + -- we're now at the outer level again so we need to scale + -- back to the outer level values + t[#t+1] = formatters["\\svgsetlayer{%0N}{%0N}{"](ax,-ay) + usedsize = v_size or defaultsize + usedscale = usedsize / defaultsize + else + -- we're nested so we can be scaled + usedsize = v_size or size + usedscale = (usedsize / defaultsize) / scale + end + -- + -- print("element ",c.tg) + -- print(" layered ",elayered) + -- print(" font size ",v_size) + -- print(" parent size ",size) + -- print(" parent scale",scale) + -- print(" used size ",usedsize) + -- print(" used scale ",usedscale) + -- + if eplaced then + t[#t+1] = f_placed(dx,dy) + end + -- + if not v_family then v_family = family end + if not v_weight then v_weight = "normal" end + if not v_style then v_style = "normal" end + -- + if v_family then + v_family = fonts.names.cleanname(v_family) + v_family = checkedfamily(v_family) + end + -- + usedfonts[v_family][v_weight][v_style] = true + -- +-- if usedscale == 1 then +-- t[#t+1] = f_normal( v_family,v_weight,v_style) +-- else + t[#t+1] = f_scaled(usedscale,v_family,v_weight,v_style) +-- end + t[#t+1] = "{" + -- + local ecolored = v_fill and v_fill ~= "" or false + if ecolored then + -- todo + local r, g, b = rgbcomponents(v_fill) + if r and g and b then + t[#t+1] = f_colored(r,g,b) + else + ecolored = false + end + end + -- + local dt = c.dt + local nt = #dt + for i=1,nt do + local di = dt[i] + if type(di) == "table" then + -- can be a tspan (should we pass dx too) + collect(t,di,x,y,usedsize,usedscale,v_family) + else + if i == 1 then + di = gsub(di,"^%s+","") + end + if i == nt then + di = gsub(di,"%s+$","") + end + local chars = utfsplit(di) + if svghash then + di = f_hashed(svghash[di]) + elseif tx then + for i=1,#chars do + chars[i] = f_poschar( + (tx[i] or 0) - x, + (ty[i] or 0) - y, + utfbyte(chars[i]) + ) + end + di = "{" .. concat(chars) .. "}" + else + -- this needs to be texescaped ! and even quotes and newlines + -- or we could register it but that's a bit tricky as we nest + -- and don't know what we can expect here + -- di = lpegmatch(p_texescape,di) or di + for i=1,#chars do + chars[i] = f_char(utfbyte(chars[i])) + end + di = concat(chars) + end + t[#t+1] = di + end + end + -- + if ecolored then + t[#t+1] = "}" + end + -- + t[#t+1] = "}" + -- + if eplaced then + t[#t+1] = "}" + end + if elayered then + t[#t+1] = "}" + end + -- + return t + end + + local s_startlayer = "\\svgstartlayer " + local s_stoplayer = "\\svgstoplayer " + + function handlers.text(c) + local only = fullstrip(xmltextonly(c)) + -- if metapost.processing() then + local at = c.at + local x = rawget(at,"x") + local y = rawget(at,"y") + + local tx = asnumber_vx_t(x) + local ty = asnumber_vy_t(y) + + x = tx[1] or 0 -- catch bad x/y spec + y = ty[1] or 0 -- catch bad x/y spec + + local v_fill = at["fill"] + if not v_fill or v_fill == "none" then + v_fill = "black" + end + local color, opacity, invisible = fillproperties(v_fill,at) + local anchor = anchors[at["text-anchor"] or "start"] or "drt" + local r = metapost.remappedtext(only) + if r then + if x == 0 and y == 0 then + only = f_mapped_simple_svg(r.index) + else + only = f_mapped_normal_svg(r.index,x,y) + end + flushobject(only,at,color,opacity) + if trace_text then + report("text: %s",only) + end + elseif not invisible then -- can be an option + local scale = 1 + local textid = 0 + local result = { } + local nx = #tx + local ny = #ty + -- + result[#result+1] = s_startlayer + if nx > 1 or ny > 1 then + concat(collect(result,c,x,y,defaultsize,1,"serif",tx,ty)) + else + concat(collect(result,c,x,y,defaultsize,1,"serif")) + end + result[#result+1] = s_stoplayer + result = concat(result) + if x == 0 and y == 0 then + result = f_text_simple_svg(anchor,result) + else + result = f_text_normal_svg(anchor,result,x,y) + end + flushobject(result,at,color,opacity) + if trace_text then + report("text: %s",result) + end + elseif trace_text then + report("invisible text: %s",only) + end + -- elseif trace_text then + -- report("ignored text: %s",only) + -- end + end + + function metapost.reportsvgfonts() + for family, weights in sortedhash(usedfonts) do + for weight, styles in sortedhash(weights) do + for style in sortedhash(styles) do + report("used font: %s-%s-%s",family,weight,style) + end + end + end + end + + statistics.register("used svg fonts",function() + if next(usedfonts) then + -- also in log file + logs.startfilelogging(report,"used svg fonts") + local t = { } + for family, weights in sortedhash(usedfonts) do + for weight, styles in sortedhash(weights) do + for style in sortedhash(styles) do + report("%s-%s-%s",family,weight,style) + t[#t+1] = formatters["%s-%s-%s"](family,weight,style) + end + end + end + logs.stopfilelogging() + return concat(t," ") + end + end) + + end + + function handlers.svg(c,x,y,w,h,noclip,notransform,normalize,usetextindex) + local at = c.at + + local wrapupviewport + local bhacked + local ehacked + local wd = w + -- local ex, em + local xpct, ypct, rpct + + local btransform, etransform, transform = handletransform(at) + + if trace then + report("view: %s, xpct %N, ypct %N","before",percentage_x,percentage_y) + end + + local viewbox = at.viewBox + + if viewbox then + x, y, w, h = handleviewbox(viewbox) + if trace then + report("viewbox: x %N, y %N, width %N, height %N",x,y,w,h) + end + end + if not w or not h or w == 0 or h == 0 then + noclip = true + end + if h then + -- + -- em = factors["em"] + -- ex = factors["ex"] + -- factors["em"] = em + -- factors["ex"] = ex + -- + xpct = percentage_x + ypct = percentage_y + rpct = percentage_r + percentage_x = w / 100 + percentage_y = h / 100 + percentage_r = (sqrt(w^2 + h^2) / sqrt(2)) / 100 + if trace then + report("view: %s, xpct %N, ypct %N","inside",percentage_x,percentage_y) + end + wrapupviewport = viewport(x,y,w,h,noclip) + end + -- todo: combine transform and offset here + + -- some fonts need this (bad transforms + viewbox) + if v and normalize and w and wd and w ~= wd and w > 0 and wd > 0 then + bhacked = s_wrapped_start + ehacked = f_wrapped_stop(y or 0,wd/w) + end + if btransform then + r = r + 1 ; result[r] = btransform + end + if bhacked then + r = r + 1 ; result[r] = bhacked + end + local boffset, eoffset = offset(at) + if boffset then + r = r + 1 result[r] = boffset + end + textindex = usetextindex and 0 or false + + at["transform"] = false + at["viewBox"] = false + + process(c,"/*") + + at["transform"] = transform + at["viewBox"] = viewbox + + if eoffset then + r = r + 1 result[r] = eoffset + end + if ehacked then + r = r + 1 ; result[r] = ehacked + end + if etransform then + r = r + 1 ; result[r] = etransform + end + if h then + -- + -- factors["em"] = em + -- factors["ex"] = ex + -- + percentage_x = xpct + percentage_y = ypct + percentage_r = rpct + if wrapupviewport then + wrapupviewport() + end + end + if trace then + report("view: %s, xpct %N, ypct %N","after",percentage_x,percentage_y) + end + end + + end + + process = function(x,p) + for c in xmlcollected(x,p) do + local tg = c.tg + local h = handlers[c.tg] + if h then + h(c) + end + end + end + + -- For huge inefficient files there can be lots of garbage to collect so + -- maybe we should run the collector when a file is larger than say 50K. + + function metapost.svgtomp(specification,pattern,notransform,normalize) + local mps = "" + local svg = specification.data + if type(svg) == "string" then + svg = xmlconvert(svg) + end + if svg then + local c = xmlfirst(svg,pattern or "/svg") + if c then + root = svg + result = { } + r = 0 + definitions = { } + tagstyles = { } + classstyles = { } + for s in xmlcollected(c,"style") do -- can also be in a def, so let's play safe + handlestyle(c) + end + handlechains(c) + xmlinheritattributes(c) -- put this in handlechains + handlers.svg ( + c, + specification.x, + specification.y, + specification.width, + specification.height, + specification.noclip, + notransform, + normalize, + specification.remap + ) + if trace_result then + report("result graphic:\n %\n t",result) + end + mps = concat(result," ") + root, result, r, definitions, styles = false, false, false, false, false + else + report("missing svg root element") + end + else + report("bad svg blob") + end + return mps + end + +end + +-- These helpers might move to their own module .. some day ... also they will become +-- a bit more efficient, because we now go to mp and back which is kind of redundant, +-- but for now it will do. + +do + + local bpfactor = number.dimenfactors.bp + + function metapost.includesvgfile(filename,offset) -- offset in sp + if lfs.isfile(filename) then + context.startMPcode("doublefun") + context('draw lmt_svg [ filename = "%s", offset = %N ] ;',filename,(offset or 0)*bpfactor) + context.stopMPcode() + end + end + + function metapost.includesvgbuffer(name,offset) -- offset in sp + context.startMPcode("doublefun") + context('draw lmt_svg [ buffer = "%s", offset = %N ] ;',name or "",(offset or 0)*bpfactor) + context.stopMPcode() + end + + interfaces.implement { + name = "includesvgfile", + actions = metapost.includesvgfile, + arguments = { "string", "dimension" }, + } + + interfaces.implement { + name = "includesvgbuffer", + actions = metapost.includesvgbuffer, + arguments = { "string", "dimension" }, + } + + function metapost.showsvgpage(data) + local dd = data.data + if not dd then + local fn = data.filename + dd = fn and table.load(fn) + end + if type(dd) == "table" then + local comment = data.comment + local offset = data.pageoffset + local index = data.index + local first = math.max(index or 1,1) + local last = math.min(index or #dd,#dd) + for i=first,last do + local d = setmetatableindex( { + data = dd[i], + comment = comment and i or false, + pageoffset = offset or nil, + }, data) + metapost.showsvgpage(d) + end + elseif data.method == "code" then + context.startMPcode(doublefun) + context(metapost.svgtomp(data)) + context.stopMPcode() + else + context.startMPpage { instance = "doublefun", offset = data.pageoffset or nil } + context(metapost.svgtomp(data)) + local comment = data.comment + if comment then + context("draw boundingbox currentpicture withcolor .6red ;") + context('draw textext.bot("\\strut\\tttf %s") ysized (10pt) shifted center bottomboundary currentpicture ;',comment) + end + context.stopMPpage() + end + end + + function metapost.typesvgpage(data) + local dd = data.data + if not dd then + local fn = data.filename + dd = fn and table.load(fn) + end + if type(dd) == "table" then + local index = data.index + if index and index > 0 and index <= #dd then + data = dd[index] + else + data = nil + end + end + if type(data) == "string" and data ~= "" then + buffers.assign("svgpage",data) + context.typebuffer ({ "svgpage" }, { option = "XML", strip = "yes" }) + end + end + + function metapost.svgtopdf(data,...) + local mps = metapost.svgtomp(data,...) + if mps then + -- todo: special instance, only basics needed + local pdf = metapost.simple("metafun",mps,true,false,"svg") + if pdf then + return pdf + else + -- message + end + else + -- message + end + end + +end + +do + + local runner = sandbox.registerrunner { + name = "otfsvg2pdf", + program = "context", + template = "--batchmode --purgeall --runs=2 %filename%", + reporter = report_svg, + } + + -- By using an independent pdf file instead of pdf streams we can use resources and still + -- cache. This is the old method updated. Maybe a future version will just do this runtime + -- but for now this is the most efficient method. + + local decompress = gzip.decompress + local compress = gzip.compress + + function metapost.svgshapestopdf(svgshapes,pdftarget,report_svg) + local texname = "temp-otf-svg-to-pdf.tex" + local pdfname = "temp-otf-svg-to-pdf.pdf" + local tucname = "temp-otf-svg-to-pdf.tuc" + local nofshapes = #svgshapes + local pdfpages = { filename = pdftarget } + local pdfpage = 0 + local t = { } + local n = 0 + -- + os.remove(texname) + os.remove(pdfname) + os.remove(tucname) + -- + if report_svg then + report_svg("processing %i svg containers",nofshapes) + statistics.starttiming(pdfpages) + end + -- + -- can be option: + -- + -- n = n + 1 ; t[n] = "\\nopdfcompression" + -- + n = n + 1 ; t[n] = "\\starttext" + n = n + 1 ; t[n] = "\\setupMPpage[alternative=offset,instance=doublefun]" + -- + for i=1,nofshapes do + local entry = svgshapes[i] + local data = entry.data + if decompress then + data = decompress(data) or data + end + local specification = { + data = xmlconvert(data), + x = 0, + y = 1000, + width = 1000, + height = 1000, + noclip = true, + } + for index=entry.first,entry.last do + if not pdfpages[index] then + pdfpage = pdfpage + 1 + pdfpages[index] = pdfpage + local pattern = "/svg[@id='glyph" .. index .. "']" + n = n + 1 ; t[n] = "\\startMPpage" + n = n + 1 ; t[n] = metapost.svgtomp(specification,pattern,true,true) or "" + n = n + 1 ; t[n] = "\\stopMPpage" + end + end + end + n = n + 1 ; t[n] = "\\stoptext" + io.savedata(texname,concat(t,"\n")) + runner { filename = texname } + os.remove(pdftarget) + file.copy(pdfname,pdftarget) + if report_svg then + statistics.stoptiming(pdfpages) + report_svg("svg conversion time %s",statistics.elapsedseconds(pdfpages)) + end + os.remove(texname) + os.remove(pdfname) + os.remove(tucname) + return pdfpages + end + + function metapost.svgshapestomp(svgshapes,report_svg) + local nofshapes = #svgshapes + local mpshapes = { } + if report_svg then + report_svg("processing %i svg containers",nofshapes) + statistics.starttiming(mpshapes) + end + for i=1,nofshapes do + local entry = svgshapes[i] + local data = entry.data + if decompress then + data = decompress(data) or data + end + local specification = { + data = xmlconvert(data), + x = 0, + y = 1000, + width = 1000, + height = 1000, + noclip = true, + } + for index=entry.first,entry.last do + if not mpshapes[index] then + local pattern = "/svg[@id='glyph" .. index .. "']" + local mpcode = metapost.svgtomp(specification,pattern,true,true) or "" + if mpcode ~= "" and compress then + mpcode = compress(mpcode) or mpcode + end + mpshapes[index] = mpcode + end + end + end + if report_svg then + statistics.stoptiming(mpshapes) + report_svg("svg conversion time %s",statistics.elapsedseconds(mpshapes)) + end + return mpshapes + end + + function metapost.svgglyphtomp(fontname,unicode) + if fontname and unicode then + local id = fonts.definers.internal { name = fontname } + if id then + local tfmdata = fonts.hashes.identifiers[id] + if tfmdata then + local properties = tfmdata.properties + local svg = properties.svg + local hash = svg and svg.hash + local timestamp = svg and svg.timestamp + if hash then + local svgfile = containers.read(fonts.handlers.otf.svgcache,hash) + local svgshapes = svgfile and svgfile.svgshapes + if svgshapes then + if type(unicode) == "string" then + unicode = utfbyte(unicode) + end + local chardata = tfmdata.characters[unicode] + local index = chardata and chardata.index + if index then + for i=1,#svgshapes do + local entry = svgshapes[i] + if index >= entry.first and index <= entry.last then + local data = entry.data + if data then + local root = xml.convert(gzip.decompress(data) or data) + return metapost.svgtomp ( + { + data = root, + x = 0, + y = 1000, + width = 1000, + height = 1000, + noclip = true, + }, + "/svg[@id='glyph" .. index .. "']", + true, + true + ) + end + end + end + end + end + end + end + end + end + end + +end |