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 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. 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 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 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) 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_svgcmyk = formatters['svgcmyk(%.3N,%.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) 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_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 = P("#") * C(p_hexdigit*p_hexdigit)^1 / function(r,g,b) return "rgb", tonumber(r or 0, 16) / 255 or 0, tonumber(g or 0, 16) / 255 or 0, tonumber(b or 0, 16) / 255 or 0 end + P("rgb") * p_a * p_left * (p_fraction + p_separator)^-3 * (p_absolute + p_separator)^0 * p_right / function(r,g,b,a) return "rgb", r or 0, g or 0, b or 0, a or false end + P("cmyk") * p_left * (p_absolute + p_separator)^0 * p_right / 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 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[4]) 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 local what, s1, s2, s3, s4 = registeredcolor(color) if what then return what, s1, s2, s3, s4 end what, s1, s2, s3, s4 = lpegmatch(p_splitcolor,color) if not what then local t = triplets[color] if t then s1, s3, s3 = t[1], t[2], t[3] what = "rgb" end end return what, s1, s2, s3, s4 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) -- print(color,what, s1, s2, s3, s4) 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 } 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 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 cmyk local r, g, b = colorcomponents(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) 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 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 = { } colormap = specification.colormap 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 = 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 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