diff options
Diffstat (limited to 'Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua')
-rw-r--r-- | Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua | 3268 |
1 files changed, 0 insertions, 3268 deletions
diff --git a/Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua b/Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua deleted file mode 100644 index 3d15840a7b6..00000000000 --- a/Master/texmf-dist/tex/context/base/mkiv/mlib-svg.lua +++ /dev/null @@ -1,3268 +0,0 @@ -if not modules then modules = { } end modules ['mlib-svg'] = { - version = 1.001, - comment = "companion to mlib-ctx.mkiv", - author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", - copyright = "PRAGMA ADE / ConTeXt Development Team", - license = "see context related readme files", -} - --- Just a few notes: --- --- There is no real need to boost performance here .. we can always make a fast --- variant when really needed. I will also do some of the todo's when I run into --- proper fonts. I need to optimize this a bit but will do that once I'm satisfied --- with the outcome and don't need more hooks and plugs. At some point I will --- optimize the MetaPost part because now we probably have more image wrapping --- than needed. --- --- As usual with these standards, things like a path can be very compact while the --- rest is very verbose which defeats the point. This is a first attempt. There will --- be a converter to MP as well as directly to PDF. This module was made for one of --- the dangerous curves talks at the 2019 CTX meeting. I will do the font when I --- need it (not that hard). --- --- The fact that in the more recent versions of SVG the older text related elements --- are depricated and not even supposed to be supported, combined with the fact that --- the text element assumes css styling, demonstrates that there is not so much as a --- standard. It basically means that whatever technology dominates at some point --- (probably combined with some libraries that at that point exist) determine what --- is standard. Anyway, it probably also means that these formats are not that --- suitable for long term archival purposes. So don't take the next implementation --- too serious. So in the end we now have (1) attributes for properties (which is --- nice and clean and what attributes are for, (2) a style attribute that needs to --- be parsed, (3) classes that map to styles and (4) element related styles, plus a --- kind of inheritance (given the limited number of elements sticking to only <g> as --- wrapper would have made much sense. Anyway, we need to deal with it. With all --- these style things going on, one can wonder where it will end. Basically svg --- became just a html element that way and less clean too. The same is true for --- tspan, which means that text itself is nested xml. --- --- We can do a direct conversion to PDF but then we also loose the abstraction which --- in the future will be used, and for fonts we need to spawn out to TeX anyway, so --- the little overhead of calling MetaPost is okay I guess. Also, we want to --- overload labels, share fonts with the main document, etc. and are not aiming at a --- general purpose SVG converter. For going to PDF one can just use InkScape. --- --- Written with Anne Clark on speakers as distraction. --- --- Todo when I run into an example (but ony when needed and reasonable): --- --- var(color,color) --- --color<decimal> --- currentColor : when i run into an example --- a bit more shading --- clip = [ auto | rect(llx,lly,urx,ury) ] (in svg) --- xlink url ... whatever --- masks --- opacity per group (i need to add that to metafun first, inefficient pdf but --- maybe filldraw can help here) --- --- Maybe in metafun: --- --- penciled n -> withpen pencircle scaled n --- applied (...) -> transformed bymatrix (...) --- withopacity n -> withtransparency (1,n) - --- When testing mbo files: --- --- empty paths --- missing control points --- funny fontnames like abcdefverdana etc --- paths representing glyphs but also with style specs --- all kind of attributes --- very weird and inefficient shading - --- One can run into pretty crazy images, like lines that are fills being clipped --- to some width. That's the danger of hiding yourself behind an interface I guess. - -local rawget, rawset, type, tonumber, tostring, next, setmetatable = rawget, rawset, type, tonumber, tostring, next, setmetatable - -local P, S, R, C, Ct, Cs, Cc, Cp, Cg, Cf, Carg = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Cc, lpeg.Cp, lpeg.Cg, lpeg.Cf, lpeg.Carg - -local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns -local sqrt = math.sqrt -local concat, setmetatableindex, sortedhash = table.concat, table.setmetatableindex, table.sortedhash -local gmatch, gsub, find, match, rep = string.gmatch, string.gsub, string.find, string.match, string.rep -local formatters, fullstrip = string.formatters, string.fullstrip -local utfsplit, utfbyte = utf.split, utf.byte - -local xmlconvert, xmlcollected, xmlcount, xmlfirst, xmlroot = xml.convert, xml.collected, xml.count, xml.first, xml.root -local xmltext, xmltextonly = xml.text, xml.textonly -local css = xml.css or { } -- testing - -local function xmlinheritattributes(c,pa) - local at = c.at - local dt = c.dt - if at and dt then - if pa then - setmetatableindex(at,pa) - end - for i=1,#dt do - local dti = dt[i] - if type(dti) == "table" then - xmlinheritattributes(dti,at) - end - end - else - -- comment of so - end -end - -xml.inheritattributes = xmlinheritattributes - --- Maybe some day helpers will move to the metapost.svg namespace! - -metapost = metapost or { } -local metapost = metapost -local context = context - -local report = logs.reporter("metapost","svg") - -local trace = false trackers.register("metapost.svg", function(v) trace = v end) -local trace_text = false trackers.register("metapost.svg.text", function(v) trace_text = v end) -local trace_path = false trackers.register("metapost.svg.path", function(v) trace_path = v end) -local trace_result = false trackers.register("metapost.svg.result", function(v) trace_result = v end) - -local pathtracer = { - ["stroke"] = "darkred", - ["stroke-opacity"] = ".5", - ["stroke-width"] = ".5", - ["fill"] = "darkgray", - ["fill-opacity"] = ".75", -} - --- This is just an experiment. Todo: reset hash etc. Also implement --- an option handler. - -local svghash = false do - - local svglast = 0 - local svglist = false - - local function checkhash(t,k) - local n = svglast + 1 - svglast = n - svglist[n] = k - t[k] = n - return n - end - - function metapost.startsvghashing() - svglast = 0 - svglist = { } - svghash = setmetatableindex(checkhash) - end - - function metapost.stopsvghashing() - svglast = 0 - svglist = false - svghash = false - end - - interfaces.implement { - name = "svghashed", - arguments = "integer", - actions = function(n) - local t = svglist and svglist[n] - if t then - context(t) - end - end - } - -end - --- We have quite some closures because otherwise we run into the local variable --- limitations. It doesn't always look pretty now, sorry. I'll clean up this mess --- some day (the usual nth iteration of code). --- --- Most of the conversion is rather trivial code till I ran into a file with arcs. A --- bit of searching lead to the a2c javascript function but it has some puzzling --- thingies (like sin and cos definitions that look like leftovers and possible --- division by zero). Anyway, we can if needed optimize it a bit more. Here does it --- come from: - --- http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes --- https://github.com/adobe-webplatform/Snap.svg/blob/b242f49e6798ac297a3dad0dfb03c0893e394464/src/path.js - -local a2c do - - local pi, sin, cos, tan, asin, abs = math.pi, math.sin, math.cos, math.tan, math.asin, math.abs - - local d120 = (pi * 120) / 180 - local pi2 = 2 * pi - - a2c = function(x1, y1, rx, ry, angle, large, sweep, x2, y2, f1, f2, cx, cy) - - if (rx == 0 or ry == 0 ) or (x1 == x2 and y1 == y2) then - return { x1, y1, x2, y2, x2, y2 } - end - - local recursive = f1 - local rad = pi / 180 * angle - local res = nil - local cosrad = cos(-rad) -- local cosrad = cosd(angle) - local sinrad = sin(-rad) -- local sinrad = sind(angle) - - if not recursive then - - x1, y1 = x1 * cosrad - y1 * sinrad, x1 * sinrad + y1 * cosrad - x2, y2 = x2 * cosrad - y2 * sinrad, x2 * sinrad + y2 * cosrad - - local x = (x1 - x2) / 2 - local y = (y1 - y2) / 2 - local xx = x * x - local yy = y * y - local h = xx / (rx * rx) + yy / (ry * ry) - - if h > 1 then - h = sqrt(h) - rx = h * rx - ry = h * ry - end - - local rx2 = rx * rx - local ry2 = ry * ry - local ry2xx = ry2 * xx - local rx2yy = rx2 * yy - local total = rx2yy + ry2xx -- otherwise overflow - - local k = total == 0 and 0 or sqrt(abs((rx2 * ry2 - rx2yy - ry2xx) / total)) - - if large == sweep then - k = -k - end - - cx = k * rx * y / ry + (x1 + x2) / 2 - cy = k * -ry * x / rx + (y1 + y2) / 2 - - f1 = (y1 - cy) / ry -- otherwise crash on a tiny eps - f2 = (y2 - cy) / ry -- otherwise crash on a tiny eps - - f1 = asin((f1 < -1.0 and -1.0) or (f1 > 1.0 and 1.0) or f1) - f2 = asin((f2 < -1.0 and -1.0) or (f2 > 1.0 and 1.0) or f2) - - if x1 < cx then f1 = pi - f1 end - if x2 < cx then f2 = pi - f2 end - - if f1 < 0 then f1 = pi2 + f1 end - if f2 < 0 then f2 = pi2 + f2 end - - if sweep ~= 0 and f1 > f2 then f1 = f1 - pi2 end - if sweep == 0 and f2 > f1 then f2 = f2 - pi2 end - - end - - if abs(f2 - f1) > d120 then - local f2old = f2 - local x2old = x2 - local y2old = y2 - f2 = f1 + d120 * ((sweep ~= 0 and f2 > f1) and 1 or -1) - x2 = cx + rx * cos(f2) - y2 = cy + ry * sin(f2) - res = a2c(x2, y2, rx, ry, angle, 0, sweep, x2old, y2old, f2, f2old, cx, cy) - end - - local c1 = cos(f1) - local s1 = sin(f1) - local c2 = cos(f2) - local s2 = sin(f2) - - local t = tan((f2 - f1) / 4) - local hx = 4 * rx * t / 3 - local hy = 4 * ry * t / 3 - - local r = { x1 - hx * s1, y1 + hy * c1, x2 + hx * s2, y2 - hy * c2, x2, y2, unpack(res or { }) } - - if not recursive then -- we can also check for sin/cos being 0/1 - cosrad = cos(rad) - sinrad = sin(rad) - -- cosrad = cosd(angle) - -- sinrad = sind(angle) - for i0=1,#r,2 do - local i1 = i0 + 1 - local x = r[i0] - local y = r[i1] - r[i0] = x * cosrad - y * sinrad - r[i1] = x * sinrad + y * cosrad - end - end - - return r - end - -end - --- We share some patterns. - -local p_digit = lpegpatterns.digit -local p_hexdigit = lpegpatterns.hexdigit -local p_space = lpegpatterns.whitespace - -local factors = { - ["pt"] = 1.25, - ["mm"] = 3.543307, - ["cm"] = 35.43307, - ["px"] = 1, - ["pc"] = 15, - ["in"] = 90, - ["em"] = 12 * 1.25, - ["ex"] = 8 * 1.25, -} - -local percentage_r = 1/100 -local percentage_x = percentage_r -local percentage_y = percentage_r - --- incredible: we can find .123.456 => 0.123 0.456 ... - -local p_command_x = C(S("Hh")) -local p_command_y = C(S("Vv")) -local p_command_xy = C(S("CcLlMmQqSsTt")) -local p_command_a = C(S("Aa")) -local p_command = C(S("Zz")) - -local p_optseparator = S("\t\n\r ,")^0 -local p_separator = S("\t\n\r ,")^1 -local p_number = (S("+-")^0 * (p_digit^0 * P(".") * p_digit^1 + p_digit^1 * P(".") + p_digit^1)) - * (P("e") * S("+-")^0 * p_digit^1)^-1 - -local function convert (n) n = tonumber(n) return n end -local function convert_r (n,u) n = tonumber(n) if u == true then return percentage_r * n elseif u then return u * n else return n end end -local function convert_x (n,u) n = tonumber(n) if u == true then return percentage_x * n elseif u then return u * n else return n end end -local function convert_y (n,u) n = tonumber(n) if u == true then return percentage_y * n elseif u then return u * n else return n end end -local function convert_vx(n,u) n = tonumber(n) if u == true then return percentage_x * n elseif u then return u * n else return n end end -local function convert_vy(n,u) n = - tonumber(n) if u == true then return percentage_y * n elseif u then return u * n else return n end end - -local p_unit = (P("p") * S("txc") + P("e") * S("xm") + S("mc") * P("m") + P("in")) / factors -local p_percent = P("%") * Cc(true) - -local c_number_n = C(p_number) -local c_number_u = C(p_number) * (p_unit + p_percent)^-1 - -local p_number_n = c_number_n / convert -local p_number_x = c_number_u / convert_x -local p_number_vx = c_number_u / convert_vx -local p_number_y = c_number_u / convert_y -local p_number_vy = c_number_u / convert_vy -local p_number_r = c_number_u / convert_r - -local function asnumber (s) return s and lpegmatch(p_number, s) or 0 end -local function asnumber_r (s) return s and lpegmatch(p_number_r, s) or 0 end -local function asnumber_x (s) return s and lpegmatch(p_number_x, s) or 0 end -local function asnumber_y (s) return s and lpegmatch(p_number_y, s) or 0 end -local function asnumber_vx(s) return s and lpegmatch(p_number_vx,s) or 0 end -local function asnumber_vy(s) return s and lpegmatch(p_number_vy,s) or 0 end - -local p_number_vx_t = Ct { (p_number_vx + p_separator)^1 } -local p_number_vy_t = Ct { (p_number_vy + p_separator)^1 } - -local zerotable = { 0 } - -local function asnumber_vx_t(s) return s and lpegmatch(p_number_vx_t,s) or zerotable end -local function asnumber_vy_t(s) return s and lpegmatch(p_number_vy_t,s) or zerotable end - -local p_numbersep = p_number_n + p_separator -local p_numbers = p_optseparator * P("(") * p_numbersep^0 * p_optseparator * P(")") -local p_fournumbers = p_numbersep^4 -local p_path = Ct ( ( - p_command_xy * (p_optseparator * p_number_vx * - p_optseparator * p_number_vy )^1 - + p_command_x * (p_optseparator * p_number_vx )^1 - + p_command_y * (p_optseparator * p_number_vy )^1 - + p_command_a * (p_optseparator * p_number_vx * - p_optseparator * p_number_vy * - p_optseparator * p_number_r * - p_optseparator * p_number_n * -- flags - p_optseparator * p_number_n * -- flags - p_optseparator * p_number_vx * - p_optseparator * p_number_vy )^1 - + p_command - + p_separator -)^1 ) - --- We can actually use the svg color definitions from the tex end but maybe a user --- doesn't want those replace the normal definitions. --- --- local hexhash = setmetatableindex(function(t,k) local v = lpegmatch(p_hexcolor, k) t[k] = v return v end) -- per file --- local hexhash3 = setmetatableindex(function(t,k) local v = lpegmatch(p_hexcolor3,k) t[k] = v return v end) -- per file --- --- local function hexcolor (c) return hexhash [c] end -- directly do hexhash [c] --- local function hexcolor3(c) return hexhash3[c] end -- directly do hexhash3[c] - -local 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 <g> if needed) - - function handlers.defs(c) - for c in xmlcollected(c,"/*") do - local a = c.at - if a then - local id = rawget(a,"id") - if id then - definitions["#" .. id ] = c - definitions["url(#" .. id .. ")"] = c - end - end - end - end - - function handlers.symbol(c) - if uselevel == 0 then - local id = rawget(c.at,"id") - if id then - definitions["#" .. id ] = c - definitions["url(#" .. id .. ")"] = c - end - else - handlers.g(c) - end - end - - local uselevel = 0 - - function handlers.use(c) - local at = c.at - local id = rawget(at,"href") or rawget(at,"xlink:href") -- better a rawget - local res = locate(id) - if res then - -- width height ? - uselevel = uselevel + 1 - local boffset, eoffset = offset(at) - local btransform, etransform, transform = handletransform(at) - - if boffset then - r = r + 1 result[r] = boffset - end - - -- local clippath = at.clippath - - if btransform then - r = r + 1 result[r] = btransform - end - - local _transform = transform - local _clippath = clippath - at["transform"] = false - -- at["clip-path"] = false - - process(res,"/*") - - at["transform"] = _transform - -- at["clip-path"] = _clippath - - if etransform then - r = r + 1 ; result[r] = etransform - end - - if eoffset then - r = r + 1 result[r] = eoffset - end - - uselevel = uselevel - 1 - else - report("use: unknown definition %a",id) - end - end - - local f_no_draw = formatters['nodraw (%s)'] - local f_do_draw = formatters['draw (%s)'] - local f_no_fill_c = formatters['nofill (%s..cycle)'] - local f_do_fill_c = formatters['fill (%s..cycle)'] - local f_eo_fill_c = formatters['eofill (%s..cycle)'] - local f_no_fill_l = formatters['nofill (%s--cycle)'] - local f_do_fill_l = formatters['fill (%s--cycle)'] - local f_eo_fill_l = formatters['eofill (%s--cycle)'] - local f_do_fill = f_do_fill_c - local f_eo_fill = f_eo_fill_c - local f_no_fill = f_no_fill_c - local s_clip_start = 'draw image (' - local f_clip_stop_c = formatters[') ; clip currentpicture to (%s..cycle) ;'] - local f_clip_stop_l = formatters[') ; clip currentpicture to (%s--cycle) ;'] - local f_clip_stop = f_clip_stop_c - local f_eoclip_stop_c = formatters[') ; eoclip currentpicture to (%s..cycle) ;'] - local f_eoclip_stop_l = formatters[') ; eoclip currentpicture to (%s--cycle) ;'] - local f_eoclip_stop = f_eoclip_stop_c - - -- could be shared and then beginobject | endobject - - local function flushobject(object,at,c,o) - local btransform, etransform = handletransform(at) - local cpath = handleclippath(at) - - if cpath then - r = r + 1 ; result[r] = s_clip_start - end - - if btransform then - r = r + 1 ; result[r] = btransform - end - - r = r + 1 ; result[r] = f_do_draw(object) - - if c then - r = r + 1 ; result[r] = c - end - - if o then - r = r + 1 ; result[r] = o - end - - if etransform then - r = r + 1 ; result[r] = etransform - end - - r = r + 1 ; result[r] = ";" - - if cpath then - local f_done = cpath.evenodd - if cpath.curve then - f_done = f_done and f_eoclip_stop_c or f_clip_stop_c - else - f_done = f_done and f_eoclip_stop_l or f_clip_stop_l - end - r = r + 1 ; result[r] = f_done(cpath[1]) - end - end - - do - - local flush - - local f_linecap = formatters["interim linecap := %s ;"] - local f_linejoin = formatters["interim linejoin := %s ;"] - local f_miterlimit = formatters["interim miterlimit := %s ;"] - - local s_begingroup = "begingroup;" - local s_endgroup = "endgroup;" - - local linecaps = { butt = "butt", square = "squared", round = "rounded" } - local linejoins = { miter = "mitered", bevel = "beveled", round = "rounded" } - - local function startlineproperties(at) - local cap = at["stroke-linecap"] - local join = at["stroke-linejoin"] - local limit = at["stroke-miterlimit"] - cap = cap and linecaps [cap] - join = join and linejoins[join] - limit = limit and asnumber_r(limit) - if cap or join or limit then - r = r + 1 ; result[r] = s_begingroup - if cap then - r = r + 1 ; result[r] = f_linecap(cap) - end - if join then - r = r + 1 ; result[r] = f_linejoin(join) - end - if limit then - r = r + 1 ; result[r] = f_miterlimit(limit) - end - return function() - at["stroke-linecap"] = false - at["stroke-linejoin"] = false - at["stroke-miterlimit"] = false - r = r + 1 ; result[r] = s_endgroup - at["stroke-linecap"] = cap - at["stroke-linejoin"] = join - at["stroke-miterlimit"] = limit - end - end - end - - -- markers are a quite rediculous thing .. let's assume simple usage for now - - function handlers.marker() - -- todo: is just a def too - end - - -- kind of local svg ... so make a generic one - -- - -- todo: combine more (offset+scale+rotation) - - local function makemarker(where,c,x1,y1,x2,y2,x3,y3) - local at = c.at - local refx = rawget(at,"refX") - local refy = rawget(at,"refY") - local width = rawget(at,"markerWidth") - local height = rawget(at,"markerHeight") - local view = rawget(at,"viewBox") - local orient = rawget(at,"orient") - -- local ratio = rawget(at,"preserveAspectRatio") - local units = at["markerUnits"] - local height = at["markerHeight"] - local angx = 0 - local angy = 0 - local angle = 0 - - if where == "beg" then - if orient == "auto" then -- unchecked - -- no angle - angx = x2 - x3 - angy = y2 - y3 - elseif orient == "auto-start-reverse" then -- checked - -- points to start - angx = x3 - x2 - angy = y3 - y2 - elseif orient then -- unchecked - angle = asnumber_r(orient) - end - elseif where == "end" then - if orient == "auto" then -- unchecked - -- no angle ? - angx = x1 - x2 - angy = y1 - y2 - elseif orient == "auto-start-reverse" then -- unchecked - -- points to end - angx = x2 - x1 - angy = y2 - y1 - elseif orient then -- unchecked - angle = asnumber_r(orient) - end - elseif orient then -- unchecked - angle = asnumber_r(orient) - end - - -- what wins: viewbox or w/h - - refx = asnumber_x(refx) - refy = asnumber_y(refy) - - width = width and asnumber_x(width) or 3 -- defaults - height = height and asnumber_y(height) or 3 -- defaults - - local x = 0 - local y = 0 - local w = width - local h = height - - -- kind of like the main svg - - r = r + 1 ; result[r] = s_offset_start - - local wrapupviewport --- todo : better viewbox code - local xpct, ypct, rpct - if view then - x, y, w, h = handleviewbox(view) - end - - if width ~= 0 then - w = width - end - if height ~= 0 then - h = height - end - - if h then - xpct = percentage_x - ypct = percentage_y - rpct = percentage_r - percentage_x = w / 100 - percentage_y = h / 100 - percentage_r = (sqrt(w^2 + h^2) / sqrt(2)) / 100 - wrapupviewport = viewport(x,y,w,h,true,true) -- no clip - end - - -- we can combine a lot here: - - local hasref = refx ~= 0 or refy ~= 0 - local hasrot = angx ~= 0 or angy ~= 0 or angle ~= 0 - - local btransform, etransform, transform = handletransform(at) - - if btransform then - r = r + 1 ; result[r] = btransform - end - - if hasrot then - r = r + 1 ; result[r] = s_rotation_start - end - - if hasref then - r = r + 1 ; result[r] = s_offset_start - end - - local _transform = transform - at["transform"] = false - - handlers.g(c) - - at["transform"] = _transform - - if hasref then - r = r + 1 ; result[r] = f_offset_stop(-refx,refy) - end - - if hasrot then - if angle ~= 0 then - r = r + 1 ; result[r] = f_rotation_angle(angle) - else - r = r + 1 ; result[r] = f_rotation_stop(angx,angy) - end - end - - if etransform then - r = r + 1 ; result[r] = etransform - end - - if h then - percentage_x = xpct - percentage_y = ypct - percentage_r = rpct - if wrapupviewport then - wrapupviewport() - end - end - r = r + 1 ; result[r] = f_offset_stop(x2,y2) - - end - - local function addmarkers(list,begmarker,midmarker,endmarker) - local n = #list - if n > 3 then - if begmarker then - local m = locate(begmarker) - if m then - makemarker("beg",m,false,false,list[1],list[2],list[3],list[4]) - end - end - if midmarker then - local m = locate(midmarker) - if m then - for i=3,n-2,2 do - makemarker("mid",m,list[i-2],list[i-1],list[i],list[i+1],list[i+2],list[i+3]) - end - end - end - if endmarker then - local m = locate(endmarker) - if m then - makemarker("end",m,list[n-3],list[n-2],list[n-1],list[n],false,false) - end - end - else - -- no line - end - end - - local function flush(shape,dofill,at,list,begmarker,midmarker,endmarker) - - local fill = dofill and (at["fill"] or "black") - local stroke = at["stroke"] or "none" - - local btransform, etransform = handletransform(at) - local cpath = handleclippath(at) - - if cpath then - r = r + 1 ; result[r] = s_clip_start - end - - local has_stroke = stroke and stroke ~= "none" - local has_fill = fill and fill ~= "none" - - local bopacity, eopacity - if has_stroke and has_fill then - bopacity, eopacity = sharedopacity(at) - end - - if bopacity then - r = r + 1 ; result[r] = bopacity - end - - if has_fill then - local color, opacity = fillproperties(fill,at,not has_stroke) - local f_xx_fill = at["fill-rule"] == "evenodd" and f_eo_fill or f_do_fill - if btransform then - r = r + 1 ; result[r] = btransform - end - r = r + 1 result[r] = f_xx_fill(shape) - if color then - r = r + 1 ; result[r] = color - end - if opacity then - r = r + 1 ; result[r] = opacity - end - r = r + 1 ; result[r] = etransform or ";" - end - - if has_stroke then - local wrapup = startlineproperties(at) - local pen, dashing, color, opacity = drawproperties(stroke,at,not has_fill) - if btransform then - r = r + 1 ; result[r] = btransform - end - r = r + 1 ; result[r] = f_do_draw(shape) - if pen then - r = r + 1 ; result[r] = pen - end - if dashing then - r = r + 1 ; result[r] = dashing - end - if color then - r = r + 1 ; result[r] = color - end - if opacity then - r = r + 1 ; result[r] = opacity - end - r = r + 1 ; result[r] = etransform or ";" - -- - if list then - addmarkers(list,begmarker,midmarker,endmarker) - end - -- - if wrapup then - wrapup() - end - end - - if eopacity then - r = r + 1 ; result[r] = eopacity - end - - if cpath then - r = r + 1 ; result[r] = (cpath.evenodd and f_eoclip_stop or f_clip_stop)(cpath[1]) - end - - end - - local f_rectangle = formatters['unitsquare xyscaled (%N,%N) shifted (%N,%N)'] - local f_rounded = formatters['roundedsquarexy(%N,%N,%N,%N) shifted (%N,%N)'] - local f_line = formatters['((%N,%N)--(%N,%N))'] - local f_ellipse = formatters['(fullcircle xyscaled (%N,%N) shifted (%N,%N))'] - local f_circle = formatters['(fullcircle scaled %N shifted (%N,%N))'] - - function handlers.line(c) - local at = c.at - local x1 = rawget(at,"x1") - local y1 = rawget(at,"y1") - local x2 = rawget(at,"x2") - local y2 = rawget(at,"y2") - - x1 = x1 and asnumber_vx(x1) or 0 - y1 = y1 and asnumber_vy(y1) or 0 - x2 = x2 and asnumber_vx(x2) or 0 - y2 = y2 and asnumber_vy(y2) or 0 - - flush(f_line(x1,y1,x2,y2),false,at) - end - - function handlers.rect(c) - local at = c.at - local width = rawget(at,"width") - local height = rawget(at,"height") - local x = rawget(at,"x") - local y = rawget(at,"y") - local rx = rawget(at,"rx") - local ry = rawget(at,"ry") - - width = width and asnumber_x(width) or 0 - height = height and asnumber_y(height) or 0 - x = x and asnumber_vx(x) or 0 - y = y and asnumber_vy(y) or 0 - - y = y - height - - if rx then rx = asnumber(rx) end - if ry then ry = asnumber(ry) end - - if rx or ry then - if not rx then rx = ry end - if not ry then ry = rx end - flush(f_rounded(width,height,rx,ry,x,y),true,at) - else - flush(f_rectangle(width,height,x,y),true,at) - end - end - - function handlers.ellipse(c) - local at = c.at - local cx = rawget(at,"cx") - local cy = rawget(at,"cy") - local rx = rawget(at,"rx") - local ry = rawget(at,"ry") - - cx = cx and asnumber_vx(cx) or 0 - cy = cy and asnumber_vy(cy) or 0 - rx = rx and asnumber_r (rx) or 0 - ry = ry and asnumber_r (ry) or 0 - - flush(f_ellipse(2*rx,2*ry,cx,cy),true,at) - end - - function handlers.circle(c) - local at = c.at - local cx = rawget(at,"cx") - local cy = rawget(at,"cy") - local r = rawget(at,"r") - - cx = cx and asnumber_vx(cx) or 0 - cy = cy and asnumber_vy(cy) or 0 - r = r and asnumber_r (r) or 0 - - flush(f_circle(2*r,cx,cy),true,at) - end - - local f_lineto_z = formatters['(%N,%N)'] - local f_lineto_n = formatters['--(%N,%N)'] - - local p_pair = p_optseparator * p_number_vx * p_optseparator * p_number_vy - local p_open = Cc("(") - local p_close = Carg(1) * P(true) / function(s) return s end - local p_polyline = Cs(p_open * (p_pair / f_lineto_z) * (p_pair / f_lineto_n)^0 * p_close) - local p_polypair = Ct(p_pair^0) - - local function poly(c,final) - local at = c.at - local points = rawget(at,"points") - if points then - local path = lpegmatch(p_polyline,points,1,final) - local list = nil - local begmarker = rawget(at,"marker-start") - local midmarker = rawget(at,"marker-mid") - local endmarker = rawget(at,"marker-end") - if begmarker or midmarker or endmarker then - list = lpegmatch(p_polypair,points) - end - flush(path,true,at,list,begmarker,midmarker,endmarker) - end - end - - function handlers.polyline(c) poly(c, ")") end - function handlers.polygon (c) poly(c,"--cycle)") end - - local s_image_start = "draw image (" - local s_image_stop = ") ;" - - function handlers.path(c) - local at = c.at - local d = rawget(at,"d") - if d then - local shape, l = grabpath(d) - local fill = at["fill"] or "black" - local stroke = at["stroke"] or "none" - local n = #shape - - local btransform, etransform = handletransform(at) - local cpath = handleclippath(at) - - if cpath then - r = r + 1 ; result[r] = s_clip_start - end - - -- todo: image (nicer for transform too) - - if fill and fill ~= "none" then - local color, opacity = fillproperties(fill,at) - local f_xx_fill = at["fill-rule"] == "evenodd" - if shape.closed then - f_xx_fill = f_xx_fill and f_eo_fill or f_do_fill - elseif shape.curve then - f_xx_fill = f_xx_fill and f_eo_fill_c or f_do_fill_c - else - f_xx_fill = f_xx_fill and f_eo_fill_l or f_do_fill_l - end - if n == 1 then - if btransform then - r = r + 1 ; result[r] = btransform - end - r = r + 1 result[r] = f_xx_fill(shape[1]) - if color then - r = r + 1 ; result[r] = color - end - if opacity then - r = r + 1 ; result[r] = opacity - end - r = r + 1 ; result[r] = etransform or ";" - else - r = r + 1 ; result[r] = btransform or s_image_start - for i=1,n do - if i == n then - r = r + 1 ; result[r] = f_xx_fill(shape[i]) - if color then - r = r + 1 ; result[r] = color - end - if opacity then - r = r + 1 ; result[r] = opacity - end - else - r = r + 1 ; result[r] = f_no_fill(shape[i]) - end - r = r + 1 ; result[r] = ";" - end - r = r + 1 ; result[r] = etransform or s_image_stop - end - end - - if stroke and stroke ~= "none" then - local begmarker = rawget(at,"marker-start") - local midmarker = rawget(at,"marker-mid") - local endmarker = rawget(at,"marker-end") - if begmarker or midmarker or endmarker then - list = grablist(l) - end - local wrapup = startlineproperties(at) - local pen, dashing, color, opacity = drawproperties(stroke,at) - if n == 1 and not list then - if btransform then - r = r + 1 ; result[r] = btransform - end - r = r + 1 result[r] = f_do_draw(shape[1]) - if pen then - r = r + 1 ; result[r] = pen - end - if dashing then - r = r + 1 ; result[r] = dashing - end - if color then - r = r + 1 ; result[r] = color - end - if opacity then - r = r + 1 ; result[r] = opacity - end - r = r + 1 result[r] = etransform or ";" - else - r = r + 1 result[r] = btransform or "draw image (" - for i=1,n do - r = r + 1 result[r] = f_do_draw(shape[i]) - if pen then - r = r + 1 ; result[r] = pen - end - if dashing then - r = r + 1 ; result[r] = dashing - end - if color then - r = r + 1 ; result[r] = color - end - if opacity then - r = r + 1 ; result[r] = opacity - end - r = r + 1 ; result[r] = ";" - end - if list then - addmarkers(list,begmarker,midmarker,endmarker) - end - r = r + 1 ; result[r] = etransform or ") ;" - end - if wrapup then - wrapup() - end - end - - if cpath then - r = r + 1 ; result[r] = f_clip_stop(cpath[1]) - end - - end - end - - end - - -- kind of special - - do - - -- some day: - -- - -- specification = identifiers.jpg(data."string") - -- specification.data = data - -- inclusion takes from data - -- specification.data = false - - local f_image = formatters[ [[figure("%s") xysized (%N,%N) shifted (%N,%N)]] ] - - local nofimages = 0 - - function handlers.image(c) - local at = c.at - local im = rawget(at,"xlink:href") - if im then - local kind, data = match(im,"^data:image/([a-z]+);base64,(.*)$") - if kind == "png" then - -- ok - elseif kind == "jpeg" then - kind = "jpg" - else - kind = false - end - if kind and data then - local w = rawget(at,"width") - local h = rawget(at,"height") - local x = rawget(at,"x") - local y = rawget(at,"y") - w = w and asnumber_x(w) - h = h and asnumber_y(h) - x = x and asnumber_vx(x) or 0 - y = y and asnumber_vy(y) or 0 - nofimages = nofimages + 1 - local name = "temp-svg-image-" .. nofimages .. "." .. kind - local data = mime.decode("base64")(data) - io.savedata(name,data) - if not w or not h then - local info = graphics.identifiers[kind](data,"string") - if info then - -- todo: keep aspect ratio attribute - local xsize = info.xsize - local ysize = info.ysize - if not w then - if not h then - w = xsize - h = ysize - else - w = (h / ysize) * xsize - end - else - h = (w / xsize) * ysize - end - end - end - -- safeguard: - if not w then w = h or 1 end - if not h then h = w or 1 end - luatex.registertempfile(name) - -- done: - flushobject(f_image(name,w,h,x,y - h),at) - else - -- nothing done - end - end - end - - end - - -- these transform: g a text svg symbol - - do - - function handlers.a(c) - process(c,"/*") - end - - function handlers.g(c) -- much like flushobject so better split and share - local at = c.at - - local btransform, etransform, transform = handletransform(at) - local cpath, clippath = handleclippath(at) - - if cpath then - r = r + 1 ; result[r] = s_clip_start - end - - if btransform then - r= r + 1 result[r] = btransform - end - - local _transform = transform - local _clippath = clippath - at["transform"] = false - at["clip-path"] = false - - process(c,"/*") - - at["transform"] = _transform - at["clip-path"] = _clippath - - if etransform then - r = r + 1 ; result[r] = etransform - end - - if cpath then - local f_done = cpath.evenodd - if cpath.curve then - f_done = f_done and f_eoclip_stop_c or f_clip_stop_c - else - f_done = f_done and f_eoclip_stop_l or f_clip_stop_l - end - r = r + 1 ; result[r] = f_done(cpath[1]) - end - end - - -- this will never really work out - -- - -- todo: register text in lua in mapping with id, then draw mapping unless overloaded - -- using lmt_svglabel with family,style,weight,size,id passed - - -- nested tspans are messy: they can have displacements but in inkscape we also - -- see x and y (inner and outer element) - - -- The size is a bit of an issue. I assume that the specified size relates to the - -- designsize but we want to be able to use other fonts. - - do - - local f_styled = formatters["\\svgstyled{%s}{%s}{%s}{%s}"] - local f_colored = formatters["\\svgcolored{%.3N}{%.3N}{%.3N}{"] - local f_placed = formatters["\\svgplaced{%.3N}{%.3N}{}{"] - local f_poschar = formatters["\\svgposchar{%.3N}{%.3N}{%s}"] - local f_char = formatters["\\svgchar{%s}"] - - local f_scaled = formatters["\\svgscaled{%N}{%s}{%s}{%s}"] - local f_normal = formatters["\\svgnormal{%s}{%s}{%s}"] - local f_hashed = formatters["\\svghashed{%s}"] - - -- We move to the outer (x,y) and when we have an inner offset we - -- (need to) compensate for that outer offset. - - -- local f_text_scaled_svg = formatters['(svgtext("%s") scaled %N shifted (%N,%N))'] - -- local f_text_normal_svg = formatters['(svgtext("%s") shifted (%N,%N))'] - -- local f_text_simple_svg = formatters['svgtext("%s")'] - - local anchors = { - ["start"] = "drt", - ["end"] = "dflt", - ["middle"] = "d", - } - - local f_text_normal_svg = formatters['(textext.%s("%s") shifted (%N,%N))'] - local f_text_simple_svg = formatters['textext.%s("%s")'] - - -- or just maptext - - local f_mapped_normal_svg = formatters['(svgtext("%s") shifted (%N,%N))'] - local f_mapped_simple_svg = formatters['svgtext("%s")'] - - local cssfamily = css.family - local cssstyle = css.style - local cssweight = css.weight - local csssize = css.size - - local usedfonts = setmetatableindex(function(t,k) - local v = setmetatableindex("table") - t[k] = v - return v - end) - - local p_texescape = lpegpatterns.texescape - - -- For now as I need it for my (some 1500) test files. - - local function checkedfamily(name) - if find(name,"^.-verdana.-$") then - name = "verdana" - end - return name - end - - -- todo: only escape some chars and handle space - - local defaultsize = 10 - - local function collect(t,c,x,y,size,scale,family,tx,ty) - local at = c.at - local ax = rawget(at,"x") - local ay = rawget(at,"y") - local dx = rawget(at,"dx") - local dy = rawget(at,"dy") - local v_fill = at["fill"] - local v_family = at["font-family"] - local v_style = at["font-style"] - local v_weight = at["font-weight"] - local v_size = at["font-size"] - -- - ax = ax and asnumber_vx(ax) or x - ay = ay and asnumber_vy(ay) or y - dx = dx and asnumber_vx(dx) or 0 - dy = dy and asnumber_vy(dy) or 0 - -- - if v_family then v_family = cssfamily(v_family) end - if v_style then v_style = cssstyle (v_style) end - if v_weight then v_weight = cssweight(v_weight) end - if v_size then v_size = csssize (v_size,factors) end - -- - ax = ax - x - ay = ay - y - -- - local elayered = ax ~= 0 or ay ~= 0 or false - local eplaced = dx ~= 0 or dy ~= 0 or false - - local usedsize, usedscaled - - if elayered then - -- we're now at the outer level again so we need to scale - -- back to the outer level values - t[#t+1] = formatters["\\svgsetlayer{%0N}{%0N}{"](ax,-ay) - usedsize = v_size or defaultsize - usedscale = usedsize / defaultsize - else - -- we're nested so we can be scaled - usedsize = v_size or size - usedscale = (usedsize / defaultsize) / scale - end - -- - -- print("element ",c.tg) - -- print(" layered ",elayered) - -- print(" font size ",v_size) - -- print(" parent size ",size) - -- print(" parent scale",scale) - -- print(" used size ",usedsize) - -- print(" used scale ",usedscale) - -- - if eplaced then - t[#t+1] = f_placed(dx,dy) - end - -- - if not v_family then v_family = family end - if not v_weight then v_weight = "normal" end - if not v_style then v_style = "normal" end - -- - if v_family then - v_family = fonts.names.cleanname(v_family) - v_family = checkedfamily(v_family) - end - -- - usedfonts[v_family][v_weight][v_style] = true - -- --- if usedscale == 1 then --- t[#t+1] = f_normal( v_family,v_weight,v_style) --- else - t[#t+1] = f_scaled(usedscale,v_family,v_weight,v_style) --- end - t[#t+1] = "{" - -- - local ecolored = v_fill and v_fill ~= "" or false - if ecolored then - -- todo 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 |