diff options
Diffstat (limited to 'tex/context/base/mkiv/mlib-svg.lua')
-rw-r--r-- | tex/context/base/mkiv/mlib-svg.lua | 1635 |
1 files changed, 1635 insertions, 0 deletions
diff --git a/tex/context/base/mkiv/mlib-svg.lua b/tex/context/base/mkiv/mlib-svg.lua new file mode 100644 index 000000000..c3635480d --- /dev/null +++ b/tex/context/base/mkiv/mlib-svg.lua @@ -0,0 +1,1635 @@ +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", +} + +-- 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). +-- +-- 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. + +-- Written with Anne Clark on speakers as distraction. + +-- TODO: + +-- optimize +-- test for gzip header 0x1F 0x8B 0x08 +-- var() +-- color hash +-- currentColor +-- instances +-- --color<decimal> +-- glyph<id> +-- shading +-- "none" -> false +-- clip = [ auto | rect(llx,lly,urx,ury) ] (in svg) +-- xlink url ... whatever +-- mp svg module + shortcuts +-- withpen -> pickup + +-- 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. + +-- We can do a direct conversion to PDF but then we also loose the abstraction which +-- in the future will be used. + +local type, tonumber = type, tonumber + +local P, S, R, C, Ct, Cs, Cc, Cp, Carg = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Ct, lpeg.Cs, lpeg.Cc, lpeg.Cp, lpeg.Carg + +local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns +local pi, sin, cos, asin, sind, cosd, tan, abs, sqrt = math.pi, math.sin, math.cos, math.asin, math.sind, math.cosd, math.tan, math.abs, math.sqrt +local concat, setmetatableindex = table.concat, table.setmetatableindex +local gmatch, gsub, find, match, rep = string.gmatch, string.gsub, string.find, string.match, string.rep +local formatters = string.formatters + +local xmlconvert, xmlcollected, xmlcount, xmlfirst, xmlroot, xmltext = xml.convert, xml.collected, xml.count, xml.first, xml.root, xml.text + +metapost = metapost or { } +local metapost = metapost +local context = context + +local report = logs.reporter("metapost","svg") + +local trace = false +-- local trace = true + +-- todo: also a high res mode +-- todo: optimize (no hurry) + +local f_rectangle = formatters['unitsquare xyscaled (%.3N,%.3N) shifted (%.3N,%.3N)'] +local f_rounded = formatters['roundedsquarexy(%.3N,%.3N,%.3N,%.3N) shifted (%.3N,%.3N)'] +local f_ellipse = formatters['(fullcircle xyscaled (%.3N,%.3N) shifted (%.3N,%.3N))'] +local f_circle = formatters['(fullcircle scaled %.3N shifted (%.3N,%.3N))'] +local f_line = formatters['((%.3N,%.3N)--(%.3N,%.3N))'] +local f_fill = formatters['fill %s(%s--cycle)%s%s%s ;'] -- play safe +local f_fill_cycle_c = formatters['fill %s(%s..cycle)%s%s%s ;'] +local f_fill_cycle_l = formatters['fill %s(%s--cycle)%s%s%s ;'] +local f_eofill = formatters['eofill %s(%s--cycle)%s%s%s ;'] -- play safe +local f_eofill_cycle_c = formatters['eofill %s(%s..cycle)%s%s%s ;'] +local f_eofill_cycle_l = formatters['eofill %s(%s--cycle)%s%s%s ;'] +local f_nofill = formatters['nofill %s(%s--cycle)%s ;'] -- play safe +local f_nofill_cycle_c = formatters['nofill %s(%s..cycle)%s ;'] +local f_nofill_cycle_l = formatters['nofill %s(%s--cycle)%s ;'] +local f_draw = formatters['draw %s(%s)%s%s%s%s%s ;'] +local f_nodraw = formatters['nodraw %s(%s)%s ;'] + +-- local f_fill = formatters['F %s(%s--C)%s%s%s ;'] -- play safe +-- local f_fill_cycle_c = formatters['F %s(%s..C)%s%s%s ;'] +-- local f_fill_cycle_l = formatters['F %s(%s--C)%s%s%s ;'] +-- local f_eofill = formatters['E %s(%s--C)%s%s%s ;'] -- play safe +-- local f_eofill_cycle_c = formatters['E %s(%s..C)%s%s%s ;'] +-- local f_eofill_cycle_l = formatters['E %s(%s--C)%s%s%s ;'] +-- local f_nofill = formatters['f %s(%s--C)%s ;'] -- play safe +-- local f_nofill_cycle_c = formatters['f %s(%s..C)%s ;'] +-- local f_nofill_cycle_l = formatters['f %s(%s--C)%s ;'] +-- local f_draw = formatters['D %s(%s)%s%s%s%s%s ;'] +-- local f_nodraw = formatters['d %s(%s)%s ;'] + +local f_color = formatters[' withcolor "%s"'] +local f_rgb = formatters[' withcolor (%.3N,%.3N,%.3N)'] +local f_rgba = formatters[' withcolor (%.3N,%.3N,%.3N) withtransparency (1,%3N)'] +local f_triplet = formatters['(%.3N,%.3N,%.3N)'] +local f_gray = formatters[' withcolor %.3N'] +local f_opacity = formatters[' withtransparency (1,%.3N)'] +local f_pen = formatters[' withpen pencircle scaled %.3N'] + +local f_dashed_n = formatters[" dashed dashpattern (%s ) "] +local f_dashed_y = formatters[" dashed dashpattern (%s ) shifted (%.3N,0) "] + +local f_moveto = formatters['(%N,%n)'] +local f_curveto_z = formatters[' controls (%.3N,%.3N) and (%.3N,%.3N) .. (%.3N,%.3N)'] +local f_curveto_n = formatters['.. controls (%.3N,%.3N) and (%.3N,%.3N) .. (%.3N,%.3N)'] +local f_lineto_z = formatters['(%.3N,%.3N)'] +local f_lineto_n = formatters['-- (%.3N,%.3N)'] + +local f_rotatedaround = formatters[" ) rotatedaround((%.3N,%.3N),%.3N)"] +local f_rotated = formatters[" ) rotated(%.3N)"] +local f_shifted = formatters[" ) shifted(%.3N,%.3N)"] +local f_slanted_x = formatters[" ) xslanted(%.3N)"] +local f_slanted_y = formatters[" ) yslanted(%.3N)"] +local f_scaled = formatters[" ) scaled(%.3N)"] +local f_xyscaled = formatters[" ) xyscaled(%.3N,%.3N)"] +local f_matrix = formatters[" ) transformed bymatrix(%.3N,%.3N,%.3N,%.3N,%.3N,%.3N)"] + +-- penciled n -> withpen pencircle scaled n +-- applied (...) -> transformed bymatrix (...) +-- withopacity n -> withtransparency (1,n) + +local s_clip_start = 'draw image (' +local f_clip_stop = formatters[') ; clip currentpicture to (%s) ;'] +local f_eoclip_stop = formatters[') ; eoclip currentpicture to (%s) ;'] + +local f_transform_start = formatters["draw %s image ( "] +local f_transform_stop = formatters[") %s ;"] + +local s_offset_start = "draw image ( " +local f_offset_stop = formatters[") shifted (%.3N,%.3N) ;"] + +local f_viewport_start = "draw image (" +local f_viewport_stop = ") ;" +local f_viewport_shift = formatters["currentpicture := currentpicture shifted (%03N,%03N);"] +local f_viewport_clip = formatters["clip currentpicture to (unitsquare xyscaled (%03N,%03N));"] + +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;" + +-- make dedicated macro + +local s_shade_linear = ' withshademethod "linear" ' +local s_shade_circular = ' withshademethod "circular" ' +local f_shade_step = formatters['withshadestep ( withshadefraction %.3N withshadecolors(%s,%s) )'] +local f_shade_one = formatters['withprescript "sh_center_a=%.3N %.3N"'] +local f_shade_two = formatters['withprescript "sh_center_b=%.3N %.3N"'] + +local f_text_scaled = formatters['(textext.drt("%s") scaled %.3N shifted (%.3N,%.3N))'] +local f_text_normal = formatters['(textext.drt("%s") shifted (%.3N,%.3N))'] + +local p_digit = lpegpatterns.digit +local p_hexdigit = lpegpatterns.hexdigit +local p_space = lpegpatterns.whitespace + +local p_hexcolor = P("#") * C(p_hexdigit*p_hexdigit)^1 / function(r,g,b) + r = tonumber(r,16)/255 + if g then + g = tonumber(g,16)/255 + end + if b then + b = tonumber(b,16)/255 + end + if b then + return f_rgb(r,g,b) + else + return f_gray(r) + end +end + +local p_hexcolor3 = P("#") * C(p_hexdigit*p_hexdigit)^1 / function(r,g,b) + r = tonumber(r,16)/255 + g = g and tonumber(g,16)/255 or r + b = b and tonumber(b,16)/255 or g + return f_triplet(r,g,b) +end + +local function hexcolor(c) + return lpegmatch(p_hexcolor,c) +end + +local function hexcolor3(c) + return lpegmatch(p_hexcolor3,c) +end + +-- gains a little: + +-- 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] + +-- 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). 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 d120 = (pi * 120) / 180 +local pi2 = 2 * pi + +local function a2c(x1, y1, rx, ry, angle, large, sweep, x2, y2, f1, f2, cx, cy) + + local recursive = f1 + + if rx == 0 or ry == 0 then + return { x1, y1, x2, y2, x2, y2 } + end + + if x1 == x2 and y1 == y2 then + return { x1, y1, x2, y2, x2, y2 } + end + + 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 + +-- incredible: we can find .123.456 => 0.123 0.456 ... + +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 + +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_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) ) + +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 p_number = p_number_r -- maybe no percent here + +local function asnumber (s) return lpegmatch(p_number, s) end +local function asnumber_r (s) return lpegmatch(p_number_r, s) end +local function asnumber_x (s) return lpegmatch(p_number_x, s) end +local function asnumber_y (s) return lpegmatch(p_number_y, s) end +local function asnumber_vx(s) return lpegmatch(p_number_vx,s) end +local function asnumber_vy(s) return lpegmatch(p_number_vy,s) end + +local p_numbersep = p_number_n + p_separator +local p_numbers = p_separator^0 * P("(") * p_numbersep^0 * p_separator^0 * P(")") +local p_four = p_numbersep^4 + +-- local p_path = Ct((p_command + (p_number_x * p_separator^0 * p_number_y * p_separator^0) + p_separator)^1) + +local p_path = Ct ( ( + p_command_xy * (p_separator^0 * p_number_vx * + p_separator^0 * p_number_vy )^1 + + p_command_x * (p_separator^0 * p_number_vx )^1 + + p_command_y * (p_separator^0 * p_number_vy )^1 + + p_command_a * (p_separator^0 * p_number_vx * + p_separator^0 * p_number_vy * + p_separator^0 * p_number_r * + p_separator^0 * p_number_n * -- flags + p_separator^0 * p_number_n * -- flags + p_separator^0 * p_number_vx * + p_separator^0 * p_number_vy )^1 + + p_command + + p_separator +)^1 ) + +local p_rgbacolor = P("rgba(") * (C(p_number) + p_separator)^1 * P(")") + +local function rgbacolor(s) + local r, g, b, a = lpegmatch(p_rgbacolor,s) + if a then + return f_rgba(r/255,g/255,b/255,a) + end +end + +local function viewbox(v) + local x, y, w, h = lpegmatch(p_four,v) + if h then + return x, y, w, h + end +end + +-- actually we can loop faster because we can go to the last one + +local function grabpath(str) + local p = lpegmatch(p_path,str) + local np = #p + local t = { } -- no real saving here if we share + local n = 0 + local all = { entries = np, closed = false, curve = false } + local a = 0 + local i = 0 + local last = "M" + local prev = last + local kind = "L" + local x = 0 + local y = 0 + local x1 = 0 + local y1 = 0 + local x2 = 0 + local y2 = 0 + local rx = 0 + local ry = 0 + local ar = 0 + local al = 0 + local as = 0 + local ac = nil + 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 + ::restart:: + 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" + 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" + if n > 0 then + a = a + 1 ; all[a] = concat(t,"",1,n) ; n = 0 + end + if i == n then + break + else + i = i - 1 + end + kind = prev + prev = "Z" + ::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 +end + +local transform do + + --todo: better lpeg + + local n = 0 + + local function rotate(r,x,y) + if x then + n = n + 1 + return r and f_rotatedaround(x,-(y or x),-r) + elseif r then + n = n + 1 + return f_rotated(-r) + else + return "" + end + end + + local function translate(x,y) + if y then + n = n + 1 + return f_shifted(x,-y) + elseif x then + n = n + 1 + return f_shifted(x,0) + else + return "" + end + end + + local function scale(x,y) + if y then + n = n + 1 + return f_xyscaled(x,y) + elseif x then + n = n + 1 + return f_scaled(x) + else + return "" + end + end + + local function skewx(x) + if x then + n = n + 1 + return f_slanted_x(math.sind(-x)) + else + return "" + end + end + + local function skewy(y) + if y then + n = n + 1 + return f_slanted_y(math.sind(-y)) + else + return "" + end + end + + local function matrix(rx,sx,sy,ry,tx,ty) + n = n + 1 + 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? + + local p_transform = Cs ( ( + P("translate") * p_numbers / translate -- maybe xy + + P("scale") * p_numbers / scale + + P("rotate") * p_numbers / rotate + + P("matrix") * p_numbers / matrix + + P("skewX") * p_numbers / skewx + + P("skewY") * p_numbers / skewy + -- + p_separator + + P(1)/"" + )^1) + + transform = function(t,image) + n = 0 + local e = lpegmatch(p_transform,t) + local b = rep("( ",n) + if image then + return f_transform_start(b), f_transform_stop(e) + else + return b, e + end + end + +end + +local dashed do + + -- actually commas are mandate but we're tolerant + + local p_number = p_separator^0/"" * 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 defaults = { + x = 0, x1 = 0, x2 = 0, cx = 0, rx = 0, + y = 0, y1 = 0, y2 = 0, cy = 0, ry = 0, + r = 0, + + width = 0, + height = 0, + + stroke = "none", + fill = "black", + opacity = "none", + + ["stroke-width"] = 1, + ["stroke-linecap"] = "none", + ["stroke-linejoin"] = "none", + ["stroke-dasharray"] = "none", + ["stroke-dashoffset"] = "none", + ["stroke-miterlimit"] = "none", + ["stroke-opacity"] = "none", + + ["fill-opacity"] = "none", + -- ["fill-rule"] = "nonzero", + -- ["clip-rule"] = "nonzero", + } + + local handlers = { } + local process = false + local root = false + local result = false + local r = false + local definitions = false + local styles = false + local bodyfont = false + + -- todo: check use ... and make definitions[id] self resolving + + local function locate(id) + local ref = gsub(id,"^url%(#(.-)%)$","%1") + local ref = gsub(ref,"^#","") + local res = xmlfirst(root,"**[@id='"..ref.."']") + if res then + definitions[id] = res + return res + else + -- warning + end + end + + local function clippath(id,a) + local spec = definitions[id] or locate(id) + if spec then + local kind = spec.tg + if kind == "clipPath" then + ::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" or a["clip-rule"] == "evenodd" + return p + else + goto done + 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" or a["clip-rule"] == "evenodd" + return p + else + goto done + end + else + -- inherit? + end + end + ::done:: + else + report("unknown clip %a",id) + end + end + end + + local function gradient(id) + local spec = definitions[id] + 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 = a.x1 + local y1 = a.y1 + local x2 = a.x2 + local y2 = 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 = a.cx -- x center + local cy = a.cy -- y center + local r = a.r -- radius + local fx = a.fx -- focal points + local fy = 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 + for c in xmlcollected(spec,"/stop") do + local a = c.at + local offset = a.offset + local colorb = a["stop-color"] + local opacity = a["stop-opacity"] + + colorb = colorb and hexcolor3(colorb) or colorb + colora = colora or colorb + + -- 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 + + n = n + 1 shade[n] = f_shade_step(fraction,colora,colorb) + + prevcolor = color + end + return concat(shade) + end + end + + local function drawproperties(stroke,a) + local w = a["stroke-width"] + if w then + w = f_pen(asnumber_r(w)) + else + w = "" + end + local d = a["stroke-dasharray"] + if d ~= "none" then + local o = a["stroke-dashoffset"] + if o ~= "none" then + o = asnumber_r(o) + else + o = false + end + d = dashed(d,o) + else + d = "" + end + local c = hexcolor(stroke) + if not c then + c = rgbacolor(stroke) + if not c then + c = f_color(stroke) + end + end + local o = a["stroke-opacity"] or a.opacity + if o ~= "none" then + o = asnumber_r(o) + if o and o ~= 1 then + o = f_opacity(o) + else + o = "" + end + else + o = "" + end + return w, d, c, o + end + + local function fillproperties(fill,a) + local c = gradient(fill) + if not c then + c = hexcolor(fill) + if not c then + c = f_color(fill) + end + end + local o = a["fill-opacity"] or a.opacity + if o == "none" then + o = asnumber_r(o) + if o and o ~= 1 then + o = f_opacity(asnumber_r(o)) + else + o = "" + end + else + o = "" + end + return c, o + end + + -- todo: clip = [ auto | rect(llx,lly,urx,ury) ] + + local function offset(a) + local x = a.x + local y = a.y + if x then x = asnumber_vx(x) end + if y then y = asnumber_vy(y) end + if not x then x = 0 end + if not y then y = 0 end + if x ~= 0 or y ~= 0 then + r = r + 1 ; result[r] = s_offset_start + return function() + r = r + 1 ; result[r] = f_offset_stop(x,-y) + end + end + end + + local function viewport(x,y,w,h,noclip) + r = r + 1 ; result[r] = f_viewport_start + return function() + r = r + 1 ; result[r] = f_viewport_shift(-x,y) + if not noclip then + r = r + 1 ; result[r] = f_viewport_clip(w,-h) + end + r = r + 1 ; result[r] = f_viewport_stop + end + end + + function handlers.defs(c,top) + for c in xmlcollected(c,"/*") do + local a = c.at + local id = a.id + -- setmetatableindex(a,top) + if id then + definitions["#" .. id ] = c + definitions["url(#" .. id .. ")"] = c + end + end + end + + function handlers.use(c,top) + local a = setmetatableindex(c.at,top) + local id = a["xlink:href"] -- better a rawget + if id then + local d = definitions[id] + if d then + local h = handlers[d.tg] + if h then + h(d,a) + end + else + local res = locate(id) + if res then + local wrapup = offset(a) + process(res,"/*",top) + if wrapup then + wrapup() + end + else + report("unknown definition %a",id) + end + end + end + end + + local linecaps = { butt = "butt", square = "squared", round = "rounded" } + local linejoins = { miter = "mitered", bevel = "beveled", round = "rounded" } + + local function stoplineproperties() + r = r + 1 result[r] = s_endgroup + end + + local function startlineproperties(a) + local cap = a["stroke-linecap"] + local join = a["stroke-linejoin"] + local limit = a["stroke-miterlimit"] + cap = cap ~= "none" and linecaps [cap] or false + join = join ~= "none" and linejoins[join] or false + limit = limit ~= "none" and asnumber_r(limit) or false + 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 stoplineproperties + end + end + + local function flush(a,shape,nofill) + local fill = not nofill and a.fill + local stroke = a.stroke + local cpath = a["clip-path"] + local trans = a.transform + local b, e = "", "" + if cpath then + cpath = clippath(cpath,a) + end + if cpath then + r = r + 1 result[r] = s_clip_start + end + if trans then + b, e = transform(trans) + end + if fill and fill ~= "none" then + r = r + 1 result[r] = (a["fill-rule"] == "evenodd" and f_eofill or f_fill)(b,shape,e,fillproperties(fill,a)) + end + if stroke and stroke ~= "none" and stroke ~= 0 then + local wrapup = startlineproperties(a) + r = r + 1 result[r] = f_draw(b,shape,e,drawproperties(stroke,a)) + if wrapup then + wrapup() + end + end + if cpath then + r = r + 1 result[r] = (cpath.evenodd and f_eoclip_stop or f_clip_stop)(cpath[1]) + end + end + + -- todo: strokes in: + + local function flushpath(a,shape) + local fill = a.fill + local stroke = a.stroke + local cpath = a["clip-path"] + local trans = a.transform + local b, e = "", "" + if cpath then + cpath = definitions[cpath] + end + if cpath then + r = r + 1 result[r] = s_clip_start + end + -- todo: image (nicer for transform too) + if trans then + b, e = transform(trans) + end + if fill and fill ~= "none" then + local n = #shape + for i=1,n do + r = r + 1 + if i == n then + local f = a["fill-rule"] == "evenodd" + if shape.closed then + f = f and f_eofill or f_fill + elseif shape.curve then + f = f and f_eofill_cycle_c or f_fill_cycle_c + else + f = f and f_eofill_cycle_l or f_fill_cycle_l + end + result[r] = f(b,shape[i],e,fillproperties(fill,a)) + else + result[r] = f_nofill(b,shape[i],e) + end + end + end + if stroke and stroke ~= "none" and stroke ~= 0 then + local wrapup = startlineproperties(a) + local n = #shape + for i=1,n do + r = r + 1 + if i == n then + result[r] = f_draw(b,shape[i],e,drawproperties(stroke,a)) + else + result[r] = f_nodraw(b,shape[i],e) + end + end + if wrapup then + wrapup() + end + end + if cpath then + r = r + 1 result[r] = f_clip_stop(cpath[1]) + end + end + + function handlers.line(c,top) + local a = setmetatableindex(c.at,top) + local x1 = asnumber_vx(a.x1) + local y1 = asnumber_vy(a.y1) + local x2 = asnumber_vx(a.x2) + local y2 = asnumber_vy(a.y2) + if trace then + report("line: x1 %.3N, y1 %.3N, x2 %.3N, x3 %.3N",x1,y1,x2,y2) + end + flush(a,f_line(x1,y1,x2,y2),true) + end + + function handlers.rect(c,top) + local a = setmetatableindex(c.at,top) + local w = asnumber_x(a.width) + local h = asnumber_y(a.height) + local x = asnumber_vx(a.x) + local y = asnumber_vy(a.y) - h + local rx = a.rx + local ry = a.ry + if rx == 0 then rx = false end -- maybe no default 0 + if ry == 0 then ry = false end -- maybe no default 0 + if rx or ry then + rx = asnumber_x(rx or ry) + ry = asnumber_y(ry or rx) + if trace then + report("rect: x %.3N, y %.3N, w %.3N, h %.3N, rx %.3N, ry %.3N",x,y,w,h,rx,ry) + end + flush(a,f_rounded(w,h,rx,ry,x,y)) + else + if trace then + report("rect: x %.3N, y %.3N, w %.3N, h %.3N",x,y,w,h) + end + flush(a,f_rectangle(w,h,x,y)) + end + end + + function handlers.ellipse(c,top) + local a = setmetatableindex(c.at,top) + local x = asnumber_vx(a.cx) + local y = asnumber_vy(a.cy) + local rx = asnumber_x (a.rx) + local ry = asnumber_y (a.ry) + if trace then + report("ellipse: x %.3N, y %.3N, rx %.3N, ry %.3N",x,y,rx,ry) + end + flush(a,f_ellipse(2*rx,2*ry,x,y)) + end + + function handlers.circle(c,top) + local a = setmetatableindex(c.at,top) + local x = asnumber_vx(a.cx) + local y = asnumber_vy(a.cy) + local r = asnumber_r (a.r) + if trace then + report("circle: x %.3N, y %.3N, r %.3N",x,y,r) + end + flush(a,f_circle(2*r,x,y)) + end + + function handlers.path(c,top) + local a = setmetatableindex(c.at,top) + local d = a.d + if d then + local p = grabpath(d) + if trace then + report("path: %i entries, %sclosed",p.entries,p.closed and "" or "not ") + end + flushpath(a,p) + end + end + + do + + local p_pair = p_separator^0 * p_number_vx * p_separator^0 * p_number_vy + local p_pair_z = p_pair / f_lineto_z + local p_pair_n = p_pair / f_lineto_n + local p_open = Cc("(") + local p_close = Carg(1) * P(true) / function(s) return s end + local p_polyline = Cs(p_open * p_pair_z * p_pair_n^0 * p_close) + + function handlers.polyline(c,top) + local a = setmetatableindex(c.at,top) + local p = a.points + if p then + flush(a,lpegmatch(p_polyline,p,1,")"),true) + end + end + + function handlers.polygon(c,top) + local a = setmetatableindex(c.at,top) + local p = a.points + if p then + flush(a,lpegmatch(p_polyline,p,1,"--cycle)")) + end + end + + end + + function handlers.g(c,top) + local a = c.at + setmetatableindex(a,top) + process(c,"/*",a) + end + + function handlers.symbol(c,top) + report("todo: %s","symbol") + end + + -- We only need to filter classes .. I will complete this when I really need + -- it. Not hard to do but kind of boring. + + local p_class = P(".") * C((R("az","AZ","09","__","--")^1)) + local p_spec = P("{") * C((1-P("}"))^1) * P("}") + + local p_grab = ((Carg(1) * p_class * p_space^0 * p_spec / function(t,k,v) t[k] = v end) + p_space^1 + P(1))^1 + + function handlers.style(c,top) + local s = xmltext(c) + lpegmatch(p_grab,s,1,styles) + for k, v in next, styles do + -- todo: lpeg + local t = { } + for k, v in gmatch(v,"%s*([^:]+):%s*([^;]+);") do + if k == "font" then + v = xml.css.fontspecification(v) + end + t[k] = v + end + styles[k] = t + end + bodyfont = tex.getdimen("bodyfontsize") / 65536 + end + + -- this will never really work out + + function handlers.text(c,top) + local a = c.at + local x = asnumber_vx(a.x) + local y = asnumber_vy(a.y) + local text = xmltext(c) + local size = false + local fill = a.fill + -- escape text or keep it at the tex end + local class = a.class + if class then + local style = styles[class] + if style and next(style) then + local font = style.font + local s_family = font and font.family + local s_style = font and font.style + local s_weight = font and font.weight + local s_size = font and font.size + if s_size then + local f = factors[s_size[2]] + if f then + size = f * s_size[1] + end + end + -- todo: family: serif | sans-serif | monospace : use mapping + if s_family then + if s_family == "serif" then + text = "\\rm " .. text + elseif s_family == "sans-serif" then + text = "\\ss " .. text + else + text = "\\tt " .. text + end + end + if s_style or s_weight then + text = formatters['\\style[%s%s]{%s}'](s_weight or "",s_style or "",text) + end + fill = style.fill or fill + end + end + if size then + -- bodyfontsize + size = size/bodyfont + text = f_text_scaled(text,size,x,y) + else + text = f_text_normal(text,x,y) + end + if fill and fill ~= "none" then + text = f_draw("",text,"",fillproperties(fill,a)) + else + text = f_draw("",text,"","","","","") + end + if trace then + report("text: x %.3N, y %.3N, text %s",x,y,text) + end + r = r + 1 ; result[r] = text + end + + function handlers.svg(c,top,x,y,w,h,noclip) + local a = setmetatableindex(c.at,top) + local v = a.viewBox + local t = a.transform + local wrapupoffset + local wrapupviewport + -- local ex, em + local xpct, ypct, rpct + if trace then + report("view: %s, xpct %.3N, ypct %.3N","before",percentage_x,percentage_y) + end + if v then + x, y, w, h = viewbox(v) + if trace then + report("viewbox: x %.3N, y %.3N, width %.3N, height %.3N",x,y,w,h) + end + 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 %.3N, ypct %.3N","inside",percentage_x,percentage_y) + end + wrapupviewport = viewport(x,y,w,h,noclip) + end + -- todo: combine transform and offset here + if t then + btransform, etransform = transform(t,true) + end + if btransform then + r = r + 1 ; result[r] = btransform + end + wrapupoffset = offset(a) + process(c,"/*",top or defaults) + if wrapupoffset then + wrapupoffset() + 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 %.3N, ypct %.3N","after",percentage_x,percentage_y) + end + end + + process = function(x,p,top) + for c in xmlcollected(x,p) do + local h = handlers[c.tg] + if h then + h(c,top) + end + end + end + + function metapost.svgtomp(specification,pattern) + 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, result, r, definitions, styles, bodyfont = svg, { }, 0, { }, { }, 12 + -- print("default",x,y,w,h) + handlers.svg ( + c, + nil, + specification.x, + specification.y, + specification.w, + specification.h, + specification.noclip + ) + mps = concat(result," ") + root, result, r, definitions, styles, bodyfont = false, false, false, false, false, false + else + report("missing svg root element") + end + else + report("bad svg blob") + end + return mps + end + +end + +function metapost.showsvgpage(data) + local dd = data.data + if type(dd) == "table" then + local comment = dd.comment + local offset = dd.pageoffset + for i=1,#dd do + local d = setmetatableindex( { + data = dd[i], + comment = comment and i or false, + pageoffset = offset or nil, + }, data) + metapost.showsvgpage(d) + end + 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.svgtopdf(data,...) + local mps = metapost.svgtomp(data,...) + if mps then + local pdf = metapost.simple("metafun",mps,true) -- todo: special instance, only basics needed + if pdf then + return pdf + else + -- message + end + else + -- message + 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. + + 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] = "\\enabledirectives[pdf.stripzeros,metapost.stripzeros]" + -- + -- 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 + 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) 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 + 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 .. "']" + mpshapes[index] = metapost.svgtomp(specification,pattern) or "" + end + end + end + if report_svg then + statistics.stoptiming(mpshapes) + report_svg("svg conversion time %s",statistics.elapsedseconds(mpshapes)) + end + return mpshapes + end + +end |