if not modules then modules = { } end modules ['mlib-svg'] = { version = 1.001, optimize = true, 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", } -- todo: svg stripper -- todo: check clip: what if larger than bbox -- todo: when opacity is 1 don't flush it -- 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 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 -- 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. -- -- One would expect official examples to sort of follow the structure guideline, -- like putting gradient definitions in a "defs" element but forget about it ... -- structure seems not to be important (one can even wonder why "defs" is there at -- all). In the end all these systems (tex macro packages included) end up as a -- mess simply because conceptually wrong input gets accepted as normal. A side -- effect is that one starts to get a disliking. Anyway, at sime point I can just -- simplify some code because ugliness is part of the game. 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, abs = math.sqrt, math.abs local concat, setmetatableindex, sortedhash = table.concat, table.setmetatableindex, table.sortedhash local gmatch, gsub, find, match = string.gmatch, string.gsub, string.find, string.match 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) if not c.special then 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 end 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 trace_colors = false trackers.register("metapost.svg.colors", function(v) trace_colors = v end) local trace_fonts = false trackers.register("metapost.svg.fonts", function(v) trace_fonts = v end) -- This is just an experiment. Todo: reset hash etc. Also implement an option handler. local s_draw_image_start = "draw image (" local s_draw_image_stop = ") ;" local ignoredopacity = 1 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, ["%"] = 0.1, ["bp"] = 1, } metapost.svgfactors = factors local percentage_r = 1/100 local percentage_x = percentage_r local percentage_y = percentage_r local asnumber, asnumber_r, asnumber_x, asnumber_y, asnumber_vx, asnumber_vy local asnumber_vx_t, asnumber_vy_t local p_number, p_separator, p_optseparator, p_numbers, p_fournumbers, p_path local p_number_n, p_number_x, p_number_vx, p_number_y, p_number_vy, p_number_r do -- 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")) p_optseparator = S("\t\n\r ,")^0 p_separator = S("\t\n\r ,")^1 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_p (n,u) n = tonumber(n) if u == true then return n / 100 else return n end 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_percent + p_unit)^-1 p_number_n = c_number_n / convert p_number_u = c_number_u / convert p_number_x = c_number_u / convert_x p_number_vx = c_number_u / convert_vx p_number_y = c_number_u / convert_y p_number_vy = c_number_u / convert_vy p_number_r = c_number_u / convert_r p_number_p = c_number_u / convert_p asnumber = function(s) return s and lpegmatch(p_number, s) or 0 end asnumber_r = function(s) return s and lpegmatch(p_number_r, s) or 0 end asnumber_p = function(s) return s and lpegmatch(p_number_p, s) or 0 end asnumber_x = function(s) return s and lpegmatch(p_number_x, s) or 0 end asnumber_y = function(s) return s and lpegmatch(p_number_y, s) or 0 end asnumber_vx = function(s) return s and lpegmatch(p_number_vx,s) or 0 end asnumber_vy = function(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 } asnumber_vx_t = function(s) return s and lpegmatch(p_number_vx_t,s) or zerotable end asnumber_vy_t = function(s) return s and lpegmatch(p_number_vy_t,s) or zerotable end -- local p_numbersep = p_number_n + p_separator local p_numbersep = p_number_u + p_separator p_numbers = p_optseparator * P("(") * p_numbersep^0 * p_optseparator * P(")") p_fournumbers = p_numbersep^4 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 ) end -- 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 colormap = false local function prepared(t) if type(t) == "table" then local mapping = t.mapping or { } local mapper = t.mapper local colormap = setmetatableindex(mapping) if mapper then setmetatableindex(colormap,function(t,k) local v = mapper(k) t[k] = v or k return v end) end return colormap else return false end end local colormaps = setmetatableindex(function(t,k) local v = false if type(k) == "string" then v = prepared(table.load(k)) -- todo: same path as svg file elseif type(k) == "table" then v = prepared(k) k = k.name or k end t[k] = v return v end) function metapost.svgcolorremapper(colormap) return colormaps[colormap] end -- todo: cache colors per image / remapper local colorcomponents, withcolor, thecolor, usedcolors 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_cmyk = formatters[' withcolor svgcmyk(%.3N,%.3N,%.3N,%.3N)'] local f_gray = formatters[' withcolor svggray(%.3N)'] local f_rgba = formatters[' withcolor svgcolor(%.3N,%.3N,%.3N) withopacity %.3N'] local f_graya = formatters[' withcolor svggray(%.3N) withopacity %.3N'] local f_name = formatters[' withcolor "%s"'] local f_svgrgb = formatters['svgcolor(%.3N,%.3N,%.3N)'] local f_svgcmyk = formatters['svgcmyk(%.3N,%.3N,%.3N,%.3N)'] local f_svggray = formatters['svggray(%.3N)'] local f_svgname = formatters['"%s"'] local triplets = setmetatableindex(function(t,k) -- we delay building all these strings local v = svgcolors[k] if v then v = { ((v>>16)&0xFF)/0xFF, ((v>>8)&0xFF)/0xFF, ((v>>0)&0xFF)/0xFF } else v = false end t[k] = v return v end) local p_fraction = C(p_number) * C("%")^-1 / function(a,b) return tonumber(a) / (b and 100 or 255) end local p_angle = C(p_number) * P("deg")^0 / function(a) return tonumber(a) end local p_percent = C(p_number) * P("%") / function(a) return tonumber(a) / 100 end local p_absolute = C(p_number) / tonumber local p_left = P("(") local p_right = P(")") local p_a = P("a")^-1 local p_r_a_color = p_left * (p_fraction * p_separator^-1)^-3 * p_absolute^0 * p_right local p_c_k_color = p_left * (p_absolute + p_separator^-1)^-4 * p_right local p_h_a_color = p_left * p_angle * p_separator * p_percent * p_separator * p_percent * p_separator^0 * p_absolute^0 * p_right local colors = attributes.colors local colorvalues = colors.values local colorindex = attributes.list[attributes.private('color')] local hsvtorgb = colors.hsvtorgb local hwbtorgb = colors.hwbtorgb local forcedmodel = colors.forcedmodel local p_splitcolor = -- offet lowercase ff P("#") * C(p_hexdigit*p_hexdigit)^1 / function(r,g,b) if not r then return "gray", 0 elseif not (g and b) then return "gray", (r == "00" and 0) or (r == "ff" and 1) or (tonumber(r,16)/255) else return "rgb", (r == "00" and 0) or (r == "ff" and 1) or (tonumber(r,16)/255), (g == "00" and 0) or (g == "ff" and 1) or (tonumber(g,16)/255), (b == "00" and 0) or (b == "ff" and 1) or (tonumber(b,16)/255) end end + P("rgb") * p_a * p_r_a_color / function(r,g,b,a) return "rgb", r or 0, g or 0, b or 0, a or false end + P("cmyk") * p_c_k_color / function(c,m,y,k) return "cmyk", c or 0, m or 0, y or 0, k or 0 end + P("hsl") * p_a * p_h_a_color / function(h,s,l,a) local r, g, b = hsvtorgb(h,s,l,a) return "rgb", r or 0, g or 0, b or 0, a or false end + P("hwb") * p_a * p_h_a_color / function(h,w,b,a) local r, g, b = hwbtorgb(h,w,b) return "rgb", r or 0, g or 0, b or 0, a or false end function metapost.svgsplitcolor(color) if type(color) == "string" then local what, s1, s2, s3, s4 = lpegmatch(p_splitcolor,color) if not what then local t = triplets[color] if t then what, s1, s2, s3 = "rgb", t[1], t[2], t[3] end end return what, s1, s2, s3, s4 else return "gray", 0, false end end local function registeredcolor(name) local color = colorindex[name] if color then local v = colorvalues[color] local t = forcedmodel(v[1]) if t == 2 then return "gray", v[2] elseif t == 3 then return "rgb", v[3], v[4], v[5] elseif t == 4 then return "cmyk", v[6], v[7], v[8], v[9] else -- end end end -- we can have a fast check for #000000 local function validcolor(color) if usedcolors then usedcolors[color] = usedcolors[color] + 1 end if colormap then local c = colormap[color] local t = type(c) if t == "table" then local what = t[1] if what == "rgb" then return what, tonumber(t[2]) or 0, tonumber(t[3]) or 0, tonumber(t[4]) or 0, tonumber(t[5]) or false elseif what == "cmyk" then return what, tonumber(t[2]) or 0, tonumber(t[3]) or 0, tonumber(t[4]) or 0, tonumber(t[5]) or 0 elseif what == "gray" then return what, tonumber(t[2]) or 0, tonumber(t[3]) or false end elseif t == "string" then color = c end end if color == "#000000" then return "rgb", 0, 0, 0 elseif color == "#ffffff" then return "rgb", 1, 1, 1 else local what, s1, s2, s3, s4 = registeredcolor(color) if not what then what, s1, s2, s3, s4 = lpegmatch(p_splitcolor,color) -- we could cache if not what then local t = triplets[color] if t then s1, s2, s3 = t[1], t[2], t[3] what = "rgb" end end end return what, s1, s2, s3, s4 end end colorcomponents = function(color) local what, s1, s2, s3, s4 = validcolor(color) return s1, s2, s3, s4 -- so 4 means cmyk end withcolor = function(color) local what, s1, s2, s3, s4 = validcolor(color) if what == "rgb" then if s4 then if s1 == s2 and s1 == s3 then return f_graya(s1,s4) else return f_rgba(s1,s2,s3,s4) end else if s1 == s2 and s1 == s3 then return f_gray(s1) else return f_rgb(s1,s2,s3) end end elseif what == "cmyk" then return f_cmyk(s1,s2,s3,s4) elseif what == "gray" then if s2 then return f_graya(s1,s2) else return f_gray(s1) end end return f_name(color) end thecolor = function(color) local what, s1, s2, s3, s4 = validcolor(color) if what == "rgb" then if s4 then if s1 == s2 and s1 == s3 then return f_svggraya(s1,s4) else return f_svgrgba(s1,s2,s3,s4) end else if s1 == s2 and s1 == s3 then return f_svggray(s1) else return f_svgrgb(s1,s2,s3) end end elseif what == "cmyk" then return f_cmyk(s1,s2,s3,s4) elseif what == "gray" then if s2 then return f_svggraya(s1,s2) else return f_svggray(s1) 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 } -- local t = { } -- no real saving here if we share -- local n = 0 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 -- 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 -- local f_rotatedaround = formatters["svg_p := svg_p rotatedaround((%N,%N),%N) ;"] -- local f_rotated = formatters["svg_p := svg_p rotated(%N) ;"] -- local f_shifted = formatters["svg_p := svg_p shifted(%N,%N) ;"] -- local f_slanted_x = formatters["svg_p := svg_p xslanted(%N) ;"] -- local f_slanted_y = formatters["svg_p := svg_p yslanted(%N) ;"] -- local f_scaled = formatters["svg_p := svg_p scaled(%N) ;"] -- local f_xyscaled = formatters["svg_p := svg_p xyscaled(%N,%N) ;"] -- local f_matrix = formatters["svg_p := svg_p transformed bymatrix(%N,%N,%N,%N,%N,%N) ;"] -- local s_transform_start = "draw image ( begingroup ; save svg_p ; picture svg_p ; svg_p := image ( " -- local f_transform_stop = formatters[" ; ) ; %s ; draw svg_p ; endgroup ; ) ; "] 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 transforms = { } local noftransforms = 0 local function rotate(r,x,y) if r then noftransforms = noftransforms + 1 if x then transforms[noftransforms] = f_rotatedaround(x,-(y or x),-r) else transforms[noftransforms] = f_rotated(-r) end end end local function translate(x,y) if x == 0 then x = false end if y == 0 then y = false end if y then noftransforms = noftransforms + 1 transforms[noftransforms] = f_shifted(x or 0,-y) elseif x then noftransforms = noftransforms + 1 transforms[noftransforms] = f_shifted(x,0) end end local function scale(x,y) if x == 1 then x = false end if y == 1 then y = false end if y then noftransforms = noftransforms + 1 transforms[noftransforms] = f_xyscaled(x or 1,y) elseif x then noftransforms = noftransforms + 1 transforms[noftransforms] = f_scaled(x) end end local function skew(x,y) -- if x = 0 then x = false end -- if y = 0 then y = false end if x then noftransforms = noftransforms + 1 transforms[noftransforms] = f_slanted_x(sind(-x)) end if y then noftransforms = noftransforms + 1 transforms[noftransforms] = f_slanted_y(sind(-y)) end end local function matrix(rx,sx,sy,ry,tx,ty) if not ty then ty = 0 end if not tx then tx = 0 end if not sx then sx = 0 end if not sy then sy = 0 end if not rx then rx = 1 end if not ry then ry = 1 end noftransforms = noftransforms + 1 -- transforms[noftransforms] = f_matrix(rx, sx, sy, ry, tx, -ty) -- https://en.wikipedia.org/wiki/Rotation_matrix : we're counter clockwise transforms[noftransforms] = f_matrix(rx, -sy, -sx, ry, tx, -ty) end local p_transform = ( p_space^0 * ( P("translate") * (p_numbers / translate) -- maybe xy + P("scale") * (p_numbers / scale) + P("rotate") * (p_numbers / rotate) + P("matrix") * (p_numbers / matrix) + P("skew") * (p_numbers / skew) + P("translateX") * (p_numbers / translate) + P("translateY") * (Cc(false) * p_numbers / translate) + P("scaleX") * (p_numbers / translate) + P("scaleY") * (Cc(false) * p_numbers / translate) + P("skewX") * (p_numbers / skew) + P("skewY") * (Cc(false) * p_numbers / skew) ) )^1 -- indeed, we need to reverse the order ... not that pretty and counter intuitive too local function combined() if noftransforms == 1 then return transforms[1] elseif noftransforms == 2 then return transforms[2] .. transforms[1] elseif noftransforms == 3 then return transforms[3] .. transforms[2] .. transforms[1] else -- the rare case (but anything can happen in svg and it gets worse) local m = noftransforms + 1 for i=1,noftransforms//2 do local j = m - i transforms[i], transforms[j] = transforms[j], transforms[i] end return concat(transforms,"",1,noftransforms) end end handletransform = function(at) local t = at.transform if t then noftransforms = 0 lpegmatch(p_transform,t) if noftransforms > 0 then -- currentpicture return s_transform_start, f_transform_stop(combined()), t end end end handletransformstring = function(t) if t then noftransforms = 0 lpegmatch(p_transform,t) return noftransforms > 0 and combined() 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 usetags = { ["circle"] = true, ["ellipse"] = true, ["g"] = true, ["image"] = true, ["line"] = true, ["path"] = true, ["polygon"] = true, ["polyline"] = true, ["rect"] = true, -- ["text"] = true, -- ["tspan"] = true, } local pathtracer = { ["stroke"] = "darkred", ["stroke-opacity"] = ".5", ["stroke-width"] = ".5", ["fill"] = "darkgray", ["fill-opacity"] = ".75", } local skipspace = p_space^0 local colon = P(":") local semicolon = P(";") local eos = P(-1) local someaction = ( skipspace * C((1 - (skipspace * (semicolon + eos + colon)))^1) * colon * skipspace * C((1 - (skipspace * (semicolon + eos)))^0) * Carg(1) / function(k,v,a) a[k] = v end + (p_space + semicolon)^1 )^1 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 -- clear entry and might prevent table growth local estyle = rawget(at,"style") if estyle and estyle ~= "" then lpegmatch(someaction,estyle,1,at) 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,c) if id == none then return end local res = definitions[id] local ref if res then return res end ref = gsub(id,"^url%(#(.-)%)$","%1") ref = gsub(ref,"^#","") -- we can make a fast id lookup res = xmlfirst(root,"**[@id='"..ref.."']") if res then definitions[id] = res return res end -- we expect resource paths to be specified but for now we want -- them on the same path .. we could use the url splitter .. todo ref = url.hashed(id) if not ref.nosheme and ref.scheme == "file" then local filename = ref.filename local fragment = ref.fragment if filename and filename ~= "" then local fullname = resolvers.findbinfile(filename) if lfs.isfile(fullname) then report("loading use file: %s",fullname) local root = xml.load(fullname) res = xmlfirst(root,"**[@id='"..fragment.."']") if res then xmlinheritattributes(res,c) -- tricky setmetatableindex(res.at,c.at) definitions[id] = res return res end end end 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 this 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 = rawget(ca,"d") if d then local p = grabpath(d) p.evenodd = ca["clip-rule"] == "evenodd" p.close = true local transform = rawget(ca,"transform") if transform then transform = handletransformstring(transform) end return p, clippath, transform else return end else -- inherit? end end end -- todo: clip = [ auto | rect(llx,lly,urx,ury) ] 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 s_offset_start = "draw image ( " local f_offset_stop = formatters[") shifted (%N,%N) ;"] local s_size_start = "draw image ( " local f_size_stop = formatters[") xysized (%N,%N) ;"] local handleoffset, handlesize do handleoffset = function(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 handlesize = function(at) local width = asnumber_x(rawget(at,"width")) local height = asnumber_y(rawget(at,"height")) if width == 0 or height == 0 then -- bad scaling elseif width == 1 and height == 1 then -- no need for scaling else return s_size_start, f_size_stop(width,height) end end end function handlers.symbol(c) local at = c.at -- x y refX refY local boffset, eoffset = handleoffset(at) local bsize, esize = handlesize(at) local btransform, etransform, transform = handletransform(at) if boffset then r = r + 1 result[r] = boffset end if btransform then r = r + 1 result[r] = btransform end if bsize then r = r + 1 ; result[r] = bsize end -- local _x = at.x at.x = 0 -- local _y = at.y at.y = 0 -- local _w = at.width at.width = 0 -- local _h = at.height at.height = 0 process(c,"/*") -- at.x = _x -- at.y = _y -- at.width = _w -- at.height = _h if esize then r = r + 1 result[r] = esize end if etransform then r = r + 1 ; result[r] = etransform end if eoffset then r = r + 1 result[r] = eoffset end end -- do local s_shade_linear = ' withshademethod "linear" ' local s_shade_circular = ' withshademethod "circular" ' local f_color = formatters[' withcolor "%s"'] local f_opacity = formatters[' withopacity %N'] local f_pen = formatters[' withpen pencircle scaled %N'] -- todo: gradient unfinished -- todo: opacity but first we need groups in mp -- this is rather hard to deal with because browsers differ (at the time of writing) -- and what they show on screen comes out different (or not at all) in print -- todo: gradientUnits = "userSpaceOnUse" : use units instead of ratios -- spreadMethod = "pad" : default -- spreadMethod = "repeat" : crap -- spreadMethod = "reflect" : crap -- stop-opacity = "0" : strange, just use steps for that -- todo: test for kind independently in caller, make a plug instead local function pattern(id) local c = definitions[id] -- no locate ! if c and c.tg == "pattern" then -- just use result and then prune local _r = r local _result = result r = 0 result = { } -- -- handlers.pattern(spec) -- -- inlined because of width -- local at = c.at local width = asnumber_x(rawget(at,"width")) local height = asnumber_y(rawget(at,"height")) if width == 0 or height == 0 then -- bad scaling width = nil height = nil elseif width == 1 and height == 1 then -- no need for scaling width = nil height = nil else -- for now only relative end local boffset, eoffset = handleoffset(at) -- local bsize, esize = handlesize(at) local btransform, etransform, transform = handletransform(at) if boffset then r = r + 1 result[r] = boffset end if btransform then r = r + 1 result[r] = btransform end -- if bsize then -- r = r + 1 ; result[r] = bsize -- end local _x = at.x at.x = 0 local _y = at.y at.y = 0 local _w = at.width at.width = 0 local _h = at.height at.height = 0 process(c,"/*") at.x = _x at.y = _y at.width = _w at.height = _h -- if esize then -- r = r + 1 result[r] = esize -- end if etransform then r = r + 1 ; result[r] = etransform end if eoffset then r = r + 1 result[r] = eoffset end -- local okay if width and height then okay = formatters[" withpattern image ( % t )\n withpatternscale(%N,%N)"](result,width,height) else okay = formatters[" withpattern image ( % t )"](result) end r = _r result = _result return okay end end local gradient do local f_shade_step = formatters['withshadestep ( withshadefraction %N withshadecolors (%s,%s) )'] local f_shade_step_opacity = formatters['withshadestep ( withshadefraction %N withshadecolors (%s,%s) withshadeopacity %N )'] local f_shade_center = formatters['withshadecenter (%N,%N)'] local f_shade_center_f = formatters['withshadecenterfraction (%N,%N)'] local f_shade_radius = formatters['withshaderadius (%N,%N) '] local f_shade_radius_f = formatters['withshaderadiusfraction %N'] local f_shade_center_one = formatters['withshadecenterone (%N,%N)'] local f_shade_center_two = formatters['withshadecentertwo (%N,%N)'] local f_shade_center_one_f = formatters['withshadecenteronefraction (%N,%N)'] local f_shade_center_two_f = formatters['withshadecentertwofraction (%N,%N)'] gradient = function(id) local spec = definitions[id] -- no locate ! if spec then local kind = spec.tg local shade = nil local n = 1 local a = spec.at -- bah local gu = rawget(a, "gradientUnits") -- userSpaceOnUse local gt = rawget(a, "gradientTransform") local sm = rawget(a, "spreadMethod") -- local userspace = gu == "userSpaceOnUse" -- 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_center_one_f(asnumber_p(x1),1-asnumber_p(y1)) end if x2 and y2 then n = n + 1 ; shade[n] = f_shade_center_two_f(asnumber_p(x2),1-asnumber_p(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 userspace then if cx and cy then n = n + 1 ; shade[n] = f_shade_center(asnumber_p(cx),asnumber_p(cy)) end if fx and fy then n = n + 1 ; shade[n] = f_shade_center_one(asnumber_p(fx),-asnumber_p(fy)) end if r then n = n + 1 ; shade[n] = f_shade_radius(asnumber_p(r)) end if fx and fy then -- todo end else if cx and cy then n = n + 1 ; shade[n] = f_shade_center_f(asnumber_p(cx),1-asnumber_p(cy)) end if fx and fy then n = n + 1 ; shade[n] = f_shade_center_one_f(asnumber_p(fx),1-asnumber_p(fy)) end if r then n = n + 1 ; shade[n] = f_shade_radius_f(asnumber_p(r)) end if fx and fy then -- todo end end else return end 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") -- not in pdf for steps if not colora then colora = colorb end -- what if no percentage -- local fraction = offset and asnumber_r(offset) -- asnumber_p ? local fraction = offset and asnumber_p(offset) if not fraction then -- for now fraction = xmlcount(spec,"/stop")/100 -- asnumber_p ? end if colora and colorb and colora ~= "" and colorb ~= "" then n = n + 1 -- if opacity then -- shade[n] = f_shade_step_opacity(fraction,thecolor(colora),thecolor(colorb),asnumber(o)) -- else if userspace then shade[n] = f_shade_step(fraction,thecolor(colora),thecolor(colorb)) else shade[n] = f_shade_step(fraction,thecolor(colora),thecolor(colorb)) end -- end end colora = colorb end return concat(shade,"\n ") end 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 == "transparent" then o = f_opacity(0) elseif o then o = asnumber_r(o) if o == ignoredopacity then o = nil elseif o 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_content = formatters["setgroup currentpicture to boundingbox currentpicture withopacity %N;"] local s_opacity_stop = ") ;" local function sharedopacity(at) local o = at["opacity"] if o and o ~= "none" then o = asnumber_r(o) if o == ignoredopacity then return end if o then return s_opacity_start, f_opacity_content(o), s_opacity_stop end end end -- it looks like none and transparent are both used (mozilla examples) local function fillproperties(fill,at,opacity) local o = at["fill-opacity"] or (opacity and at["opacity"]) local c = nil if c ~= "none" then c = gradient(fill) if not c then c = pattern(fill) if c then if o and o ~= "none" then o = asnumber_r(o) if o ~= ignoredopacity then return c, f_opacity(o), "pattern" end end return c, false, "pattern" else c = withcolor(fill) end end end if not o and fill == "transparent" then return nil, f_opacity(0), true elseif o and o ~= "none" then o = asnumber_r(o) if o == ignoredopacity then return c end if o then return c, f_opacity(o), (o == 1 and "invisible") end end return c end local viewport do local s_viewport_start = "draw image (" local s_viewport_stop = ") ;" local f_viewport_shift = formatters["currentpicture := currentpicture shifted (%N,%N);"] local f_viewport_scale = formatters["currentpicture := currentpicture xysized (%N,%N);"] local f_viewport_clip = formatters["clip currentpicture to (unitsquare xyscaled (%N,%N));"] viewport = function(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 end -- maybe forget about defs and just always locate (and then backtrack -- over if needed) .. so, only store after locating function handledefinitions(c) for c in xmlcollected(c,"defs/*") 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 for c in xmlcollected(c,"(symbol|radialGradient|linearGradient)") do local id = rawget(c.at,"id") if id then definitions["#" .. id ] = c definitions["url(#" .. id .. ")"] = c end end end -- 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 -- lots of stuff todo: transform 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,c) if res then uselevel = uselevel + 1 local boffset, eoffset = handleoffset(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 setmetatableindex(res.at,at) local tg = res.tg -- if usetags[tg] then process(res,".") -- else -- process(res,"/*") -- end 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 closedcurve(%s)'] local f_do_fill_c = formatters[' fill closedcurve(%s)'] local f_eo_fill_c = formatters[' eofill closedcurve(%s)'] local f_no_fill_l = formatters[' nofill closedlines(%s)'] local f_do_fill_l = formatters[' fill closedlines(%s)'] local f_eo_fill_l = formatters[' eofill closedlines(%s)'] local f_closed_draw = formatters[' draw closedcurve(%s)'] 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 = 'save p ; picture p ; p := image (' local f_clip_stop_c = formatters[') ; clip p to closedcurve(%s) %s ; draw p ;'] local f_clip_stop_l = formatters[') ; clip p to closedlines(%s) %s ; draw p ;'] local f_clip_stop = f_clip_stop_c local f_eoclip_stop_c = formatters[') ; eoclip p to closedcurve(%s) %s ; draw p ;'] local f_eoclip_stop_l = formatters[') ; eoclip p to closedlines(%s) %s ; draw p ;'] 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, _, ctransform = 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],ctransform or "") 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,parentat) 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 units = rawget(at,"markerUnits") -- no parentat["stroke-width"], bad for m4mbo local view = rawget(at,"viewBox") local orient = rawget(at,"orient") -- local ratio = rawget(at,"preserveAspectRatio") local units = units and asnumber(units) or 1 local angx = 0 local angy = 0 local angle = 0 if where == "beg" then if orient == "auto" then -- unchecked -- no angle angx = abs(x2 - x3) angy = abs(y2 - y3) elseif orient == "auto-start-reverse" then -- checked -- points to start angx = -abs(x2 - x3) angy = -abs(y2 - y3) elseif orient then -- unchecked angle = asnumber_r(orient) end elseif where == "end" then -- funny standard .. bug turned feature? if orient == "auto" or orient == "auto-start-reverse" then angx = abs(x1 - x2) angy = abs(y1 - y2) 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) * units height = (height and asnumber_y(height) or 3) * units 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 -- do we need to metatable the attributes here? local function addmarkers(list,begmarker,midmarker,endmarker,at) 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],at) 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],at) 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,at) 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, _, ctransform = 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, copacity, eopacity if has_stroke and has_fill then bopacity, copacity, eopacity = sharedopacity(at) end if copacity then r = r + 1 ; result[r] = bopacity end if has_fill then local color, opacity, option = 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 if option == "pattern" then r = r + 1 result[r] = f_closed_draw(shape) else r = r + 1 result[r] = f_xx_fill(shape) 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 " ;" 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,at) end -- if wrapup then wrapup() end end if copacity then r = r + 1 ; result[r] = copacity 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],ctransform) 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_x(rx) end if ry then ry = asnumber_y(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, option = fillproperties(fill,at) local f_xx_fill = at["fill-rule"] == "evenodd" if option == "pattern" then f_xx_fill = f_closed_draw elseif 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 s_draw_image_start 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,at) end r = r + 1 ; result[r] = etransform or s_draw_image_stop 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 f_image = formatters[ [[svgembeddedfigure(%i) 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 local data = basexx.decode64(data) -- local name = "temp-svg-image-" .. nofimages .. "." .. kind local index = images.storedata("svg", { kind = kind, data = data, info = graphics.identifiers[kind](data,"string"), }) -- io.savedata(name,data) if not w or not h then 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) -- flushobject(f_image(name,w,h,x,y - h),at) flushobject(f_image(index,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, ctransform = 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,"/!(defs|symbol)") -- /* 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],ctransform or "") 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. -- a mix of text and spans and possibly wrap (where xy is to be ignored) ... bah ... -- it's fuzzy when we have a span with positions mixed with text ... basically that -- is a box and we can assume that an editor then has all positioned do local s_start = "\\svgstart " local s_stop = "\\svgstop " local f_set = formatters["\\svgset{%N}{%N}"] -- we need a period local f_color_c = formatters["\\svgcolorc{%.3N}{%.3N}{%.3N}{"] local f_color_o = formatters["\\svgcoloro{%.3N}{"] local f_color_b = formatters["\\svgcolorb{%.3N}{%.3N}{%.3N}{%.3N}{"] local f_poscode = formatters["\\svgpcode{%N}{%N}{%s}"] local f_poschar = formatters["\\svgpchar{%N}{%N}{%s}"] local f_posspace = formatters["\\svgpspace{%N}{%N}"] local f_code = formatters["\\svgcode{%s}"] local f_char = formatters["\\svgchar{%s}"] local s_space = "\\svgspace " local f_size = formatters["\\svgsize{%0.6f}"] -- we need a period local f_font = formatters["\\svgfont{%s}{%s}{%s}"] local f_hashed = formatters["\\svghashed{%s}"] ----- p_texescape = lpegpatterns.texescape local anchors = { ["start"] = "drt", ["end"] = "dlft", ["middle"] = "d", } -- we can now just use the lmt maptext feature local f_text_normal_svg = formatters['(onetimetextext.%s("%s") shifted (%N,%N))'] local f_text_simple_svg = formatters['onetimetextext.%s("%s")'] 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) -- 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 -- An arbitrary mix of text and spans with x/y is asking for troubles. The fact that the -- description in the (proposed) standard is so complex indicates this (its also looks -- like reveng application specs and doesn't aim at simplicity. Basically we have two -- cases: positioned lines and words and such (text & span with xy), or just stripes of -- text and span. Free flow automatically broken into lines text is kind of strange in -- svg and the fact that glyph placement is dropped is both an indication that svg lost -- part of its purpose and probably also that it never really was a standard (although -- maybe today standards are just short term specifications. Who knows. -- text with spans, all with x/y -- text mixed with spans, no xy in inner elements -- -- the spec says that nested x/y are absolute local defaultsize = 10 local sensitive = { -- todo: characters.sensitive ["#"] = true, ["$"] = true, ["%"] = true, ["&"] = true, ["\\"] = true, ["{"] = true, ["|"] = true, ["}"] = true, ["~"] = true, } -- messy: in nested spans (they happen) the x/y are not accumulated local function validdelta(usedscale,d) if d then local value, unit = match(d,"^([%A]-)(%a+)") value = tonumber(value) or 0 if not unit then return value .. "bp" elseif unit == "ex" or unit == "em" then return (usedscale * value) .. unit else return value .. "bp" end else return "0bp" end end local cleanfontname = fonts.names.cleanname local x_family = false local x_weight = false local x_style = false local function collect(parent,t,c,x,y,size,scale,family,tx,ty,tdx,tdy) if c.special then return nil end local dt = c.dt local nt = #dt local at = c.at local tg = c.tg local ax = rawget(at,"x") local ay = rawget(at,"y") local v_opacity = tonumber(at["fill-opacity"]) 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"] local v_lineheight = at["line-height"] -- ax = ax and asnumber_vx(ax) or x ay = ay and asnumber_vy(ay) or y -- 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,size/100) or tonumber(v_size) 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 = cleanfontname(v_family) v_family = checkedfamily(v_family) end -- usedfonts[v_family][v_weight][v_style] = true -- local lh = v_lineheight and asnumber_vx(v_lineheight) or false -- ax = ax - x ay = ay - y -- local usedsize = v_size or defaultsize local usedscale = usedsize / defaultsize -- -- todo: rotate : list of numbers -- todo: lengthAdjust : spacing|spacingAndGlyphs -- todo: textLength : scale to width -- toto: font-size-adjust -- toto: font-stretch -- letter-spacing -- word-spacing -- writing-mode:lr-tb -- local newfont = v_family ~= x_family or v_weight ~= x_weight or v_style ~= x_style if newfont then x_family = v_family x_weight = v_weight x_style = v_style t[#t+1] = f_font(v_family,v_weight,v_style) t[#t+1] = "{" end t[#t+1] = f_size(usedscale) t[#t+1] = "{" -- if trace_fonts then -- we can hash and keep it when no change report("element : %s",c.tg) report(" font family : %s",v_family) report(" font weight : %s",v_weight) report(" font style : %s",v_style) report(" parent size : %s",size) -- report(" parent scale : %s",scale) report(" used size : %s",v_size or defaultsize) end -- local ecolored = v_fill ~= "" and v_fill or false local opacity = v_opacity ~= ignoredopacity and v_opacity or false -- -- todo cmyk -- if ecolored then local r, g, b = colorcomponents(v_fill) if r and g and b then if opacity then t[#t+1] = f_color_b(r,g,b,opacity) else t[#t+1] = f_color_c(r,g,b) end elseif opacity then t[#t+1] = f_color_o(opacity) else ecolored = false end elseif opacity then t[#t+1] = f_color_o(opacity) end -- local hasa = ax ~= 0 or ay ~= 0 if hasa then -- we abuse the fact that flushing layers can be nested t[#t+1] = f_set(ax or 0,ay or 0) t[#t+1] = "{" end for i=1,nt do local di = dt[i] if type(di) == "table" then -- when x or y then absolute else inline if #di.dt > 0 then collect(tg,t,di,x,y,usedsize,usedscale,v_family) end else -- check for preserve 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 -- dx dy di = f_hashed(svghash[di]) else if tx or ty or tdx or tdy then local txi, tyi, tdxi, tdyi for i=1,#chars do txi = tx and (tx [i] or txi ) tyi = ty and (ty [i] or tyi ) tdxi = tdx and (tdx[i] or tdxi) or 0 tdyi = tdy and (tdy[i] or tdyi) or 0 local dx = (txi and (txi - x) or 0) + tdxi local dy = (tyi and (tyi - y) or 0) + tdyi local ci = chars[i] if ci == " " then chars[i] = f_posspace(dx, dy) elseif sensitive[ci] then chars[i] = f_poscode(dx, dy, utfbyte(ci)) else chars[i] = f_poschar(dx, dy, ci) end end di = "{" .. concat(chars) .. "}" t[#t+1] = di 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 local ci = chars[i] if ci == " " then chars[i] = s_space elseif sensitive[ci] then chars[i] = f_code(utfbyte(ci)) else chars[i] = f_char(ci) -- chars[i] = ci end end di = concat(chars) t[#t+1] = di end end end end if hasa then if t[#t] == "{" then t[#t] = nil t[#t] = nil else t[#t+1] = "}" end end -- if opacity or ecolored then t[#t+1] = "}" end -- t[#t+1] = "}" -- if newfont then t[#t+1] = "}" end -- return t end -- case 1: just text, maybe with spans -- case 2: only positioned spans -- case 3: just text, seen as label local textlevel = 0 function handlers.text(c) if textlevel == 0 then x_family = v_family x_weight = v_weight x_style = v_style end -- textlevel = textlevel + 1 -- analyze local only = fullstrip(xmltextonly(c)) local at = c.at local x = rawget(at,"x") local y = rawget(at,"y") local dx = rawget(at,"dx") local dy = rawget(at,"dy") local tx = asnumber_vx_t(x) local ty = asnumber_vy_t(y) local tdx = asnumber_vx_t(dx) local tdy = asnumber_vy_t(dy) x = tx[1] or 0 -- catch bad x/y spec y = ty[1] or 0 -- catch bad x/y spec dx = tdx[1] or 0 -- catch bad x/y spec dy = tdy[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, option = fillproperties(v_fill,at) local anchor = anchors[at["text-anchor"] or "start"] or "drt" local remap = metapost.remappedtext(only) -- x = x + dx -- y = y + dy if remap then if x == 0 and y == 0 then only = f_mapped_simple_svg(remap.index) else only = f_mapped_normal_svg(remap.index,x,y) end flushobject(only,at,color,opacity) if trace_text then report("text: %s",only) end elseif option == "invisible" then if trace_text then report("invisible text: %s",only) end else local scale = 1 local textid = 0 local result = { } local nx = #tx local ny = #ty local ndx = #tdx local ndy = #tdy -- local t = { } t[#t+1] = s_start if nx > 1 or ny > 1 or ndx > 1 or ndy > 1 then collect(tg,t,c,x,y,defaultsize,1,"serif",tx,ty,tdx,tdy) else collect(tg,t,c,x,y,defaultsize,1,"serif") end t[#t+1] = s_stop t = concat(t) if x == 0 and y == 0 then t = f_text_simple_svg(anchor,t) else -- dx dy t = f_text_normal_svg(anchor,t,x,y) end -- flushobject(t,at,color,opacity) -- otherwise mixup with transparency flushobject(t,at,false,false) if trace_text then report("text: %s",result) end end -- textlevel = textlevel - 1 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) 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 = handleoffset(at) if boffset then r = r + 1 result[r] = boffset end at["transform"] = false at["viewBox"] = false process(c,"/!(defs|symbol)") 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 images.resetstore("svg") 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 = { } colormap = specification.colormap usedcolors = trace_colors and setmetatableindex("number") or false 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 handledefinitions(c) handlers.svg ( c, specification.x, specification.y, specification.width, specification.height, specification.noclip, notransform, normalize, specification.remap ) if trace_result == "file" then io.savedata( tex.jobname .. "-svg-to-mp.tex", "\\startMPpage[instance=doublefun]\n" .. concat(result,"\n") .. "\n\\stopMPpage\n" ) elseif trace_result then report("result graphic:\n %\n t",result) end if usedcolors and next(usedcolors) then report("graphic %a uses colors: %s",specification.id or "unknown",table.sequenced(usedcolors)) end mps = concat(result," ") root = false result = false r = false definitions = false tagstyles = false classstyles = false colormap = 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 local fullname = resolvers.findbinfile(filename) if lfs.isfile(fullname) 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 filename = data.filename local fullname = filename and resolvers.findbinfile(filename) dd = fullname and table.load(fullname) 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