https://github.com/diegonehab/stroke-to-fill
Raw File
Tip revision: e54cde26fea5f72645838a48ffcb9fff4c204596 authored by Diego Nehab on 02 October 2020, 02:04:18 UTC
Add Graphics Replicability Stamp requirements
Tip revision: e54cde2
svg.lua
-- Stroke-to-fill conversion program and test harness
-- Copyright (C) 2020 Diego Nehab
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published
-- by the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- Contact information: diego.nehab@gmail.com
--
local facade = require"facade"
local indent = require"indent"
local base64 = require"base64"
local indent = require"indent"

local util = require"util"

local format = string.format

local is_almost_zero = util.is_almost_zero
local is_almost_one = util.is_almost_one
local is_almost_equal = util.is_almost_equal
local sgn = util.sgn
local asvd = util.asvd

local math = require"math"
local image = require"image"

local abs = math.abs
local max = math.max
local min = math.min
local deg = math.deg
local sqrt = math.sqrt
local unpack = table.unpack
local atan2 = math.atan2

local _M = facade.driver()

local function det3(x0, y0, x1, y1, w1, x2, y2)
	return -x1*y0+w1*x2*y0+x0*y1-x2*y1-w1*x0*y2+x1*y2;
end

local function unpack8_helper(rgba, i)
    local v = rgba[i]
    if v then
        return math.floor(v*255. + .5), unpack8_helper(rgba, i+1)
    end
end

local function unpack8(rgba)
    return unpack8_helper(rgba, 1)
end

local function empty(t)
    return #t == 0
end

local function back(t)
    return t[#t]
end

local function push_back(t, v)
    t[#t+1] = v
end

local function pop_back(t)
    local n = #t
    local b = t[n]
    t[n] = nil
    return b
end

local function front(t)
    return t[1]
end

-- define acceleration function
function _M.accelerate(content, window, viewport)
    return content
end

local winding_rule_name = {
    [_M.winding_rule.non_zero] = "nonzero",
    [_M.winding_rule.zero] = "zero",
    [_M.winding_rule.even] = "even",
    [_M.winding_rule.odd] = "evenodd",
}

local spread_name = {
    [_M.spread.clamp] = "pad",
    [_M.spread.mirror] = "reflect",
    [_M.spread.wrap] = "repeat",
    [_M.spread.transparent] = "transparent",
}

local stroke_join_name = {
	[_M.stroke_join.miter_clip] = "miter-clip",
	[_M.stroke_join.round] = "round",
	[_M.stroke_join.bevel] = "bevel",
	[_M.stroke_join.miter_or_bevel] = "miter",
	[_M.stroke_join.arcs] = "arcs"
}

local stroke_cap_name = {
	[_M.stroke_cap.butt] = "butt",
	[_M.stroke_cap.round] = "round",
	[_M.stroke_cap.square] = "square",
	[_M.stroke_cap.triangle] = "triangle",
	[_M.stroke_cap.fletching] = "fletching"
}

local function print_rotation(c, s, title, out)
    if not is_almost_one(c) or not is_almost_zero(s) then
        out:write(title, format("=\"rotate(%g)\"", deg(atan2(s, c))))
    end
end

local function print_scaling(sx, sy, title, out)
    if is_almost_equal(sx, sy) then
        local avg = 0.5*(sx+sy)
        if not is_almost_one(avg) then
            out:write(title, format("=\"scale(%g)\"", avg))
        end
    else
        out:write(title, format("=\"scale(%g,%g)\"", sx, sy))
    end
end

local function print_translation(tx, ty, title, out)
    if not is_almost_zero(tx) or not is_almost_zero(ty) then
        out:write(title, format("=\"translate(%g,%g)\"", tx, ty))
    end
end

local function print_linear(a, b, c, d, title, out)
    if is_almost_zero(a*b+c*d) and is_almost_one(a*d-b*c) and
       is_almost_one(a*a+c*c) and is_almost_one(b*b+d*d) then
        print_rotation(a, c, title, out)
    elseif is_almost_zero(b) and is_almost_zero(c) then
        print_scaling(a, d, title, out)
    else
        out:write(title, format("=\"matrix(%g,%g,%g,%g,0,0)\"", a, c, b ,d))
    end
end

local function print_affinity(a, b, tx, c, d, ty, title, out)
    if is_almost_zero(tx) and is_almost_zero(ty) then
        print_linear(a, b, c, d, title, out)
    elseif is_almost_one(a) and is_almost_zero(b) and
        is_almost_zero(c) and is_almost_one(d) then
        print_translation(tx, ty, title, out)
    else
        out:write(title, format("=\"matrix(%g,%g,%g,%g,%g,%g)\"",
            a, c, b, d, tx, ty))
    end
end

local function print_xform(xf, title, out)
    local a, b, tx, c, d, ty = unpack(xf, 1, 6)
    print_affinity(a, b, tx, c, d, ty, title, out)
end

-- transforms path by xf
function make_path_f_xform(xf, forward)
    local px, py -- previous cursor
    local xformer = {}
    function xformer:begin_contour(x0, y0)
        px, py = xf:apply(x0, y0)
        forward:begin_contour(px, py)
    end
    function xformer:end_closed_contour(x0, y0)
        forward:end_closed_contour(px, py)
    end
    function xformer:end_open_contour(x0, y0)
        forward:end_open_contour(px, py)
    end
    function xformer:degenerate_segment(x0, y0, dx0, dy0, dx1, dy1, x1, y1)
       x1, y1 = xf:apply(x1, y1)
       dx0, dy0 = xf:apply(dx0, dy0, 0)
       dx1, dy1 = xf:apply(dx1, dy1, 0)
       forward:degenerate_segment(px, py, dx0, dy0, dx1, dy1, x1, y1)
       px, py = x1, y1
    end
    function xformer:linear_segment(x0, y0, x1, y1)
       x1, y1 = xf:apply(x1, y1)
       forward:linear_segment(px, py, x1, y1)
       px, py = x1, y1
    end
    function xformer:quadratic_segment(x0, y0, x1, y1, x2, y2)
        x1, y1 = xf:apply(x1, y1)
        x2, y2 = xf:apply(x2, y2)
        forward:quadratic_segment(px, py, x1, y1, x2, y2)
        px, py = x2, y2
    end
    function xformer:rational_quadratic_segment(x0, y0, x1, y1, w1, x2, y2)
        x1, y1, w1 = xf:apply(x1, y1, w1)
        x2, y2 = xf:apply(x2, y2)
        forward:rational_quadratic_segment(px, py, x1, y1, w1, x2, y2)
        px, py = x2, y2
    end
    function xformer:cubic_segment(x0, y0, x1, y1, x2, y2, x3, y3)
        x1, y1 = xf:apply(x1, y1)
        x2, y2 = xf:apply(x2, y2)
        x3, y3 = xf:apply(x3, y3)
        forward:cubic_segment(px, py, x1, y1, x2, y2, x3, y3)
        px, py = x3, y3
    end
    return xformer
end

-- here is yet an example showcasing how you can write
-- much less code if you learn a bit about Lua. you can also
-- write code that is harder to understand, and perhaps slower. :)
local function newcommandprinter(command, start, stop, file)
    -- returns a new function that receives the callback table
    -- and all arguments passed by the iterator (eg, self, len, x0, y0)
    return function(self, ...)
        -- if the previous was different, output current command
        if self.previous ~= command then
            file:write(command, " ")
        end
        -- output arguments start through stop
        for i = start, stop do
            file:write(format("%g ", select(i, ...)))
        end
        -- current is new previous
        self.previous = command
    end
end

local function make_path_f_strip_rational_quadratics(forward)
	return setmetatable({
		rational_quadratic_segment = function(self, x0, y0, x1, y1, w1, x2, y2)
			local s2 = 1-w1*w1
			if is_almost_zero(s2) then
				if is_almost_equal(x0, x1) and is_almost_equal(x1, x2)
				   and is_almost_equal(y0, y1) and is_almost_equal(y1, y2) then
					return forward:linear_segment(x0, y0, x2, y2)
				else
					return forward:quadratic_segment(x0, y0, x1, y1, x2, y2)
				end
			end
			local s = sgn(s2)*sqrt(abs(s2))
			-- we get the SVD of the linear part of the transformation
			local cos, sin, sx, sy = asvd(2*x1-w1*(x0+x2), s*(x2-x0),
				2*y1-w1*(y0+y2), s*(y2-y0))
            if not is_almost_zero(sx) or not is_almost_zero(sy) then
                -- the sign of the middle weight gives the large/small angle flag
                local fa = w1 < 0 and 1 or 0
                -- the sign of the area of the control point triangle gives the orientation
                local d = det3(x0, y0, x1, y1, w1, x2, y2)
                local fs = d > 0 and 1 or 0
                -- the rotation and the scaling parts from SVD give the angle and axes
                return forward:svg_arc(x0, y0, sx/(2*s2), sy/(2*s2),
                    deg(atan2(sin, cos)), fa, fs, x2, y2);
            else
                return forward:linear_segment(x0, y0, x2, y2)
            end
		end
    }, {
		__index = function(self, index)
            local f = function(self, ...)
                return forward[index](forward, ...)
            end
            rawset(self, index, f)
            return f
		end
	})
end

function make_path_f_command_printer(file)
    return {
        -- store previous command
        previous = nil,
        -- example: begin_contour receives self, x0, y0
        -- we should output "M,x0,y0". so we pass "M" as the
        -- command, and select 1 through 2 as the arguments to be output
        begin_contour = newcommandprinter("M", 1, 2, file),
        -- remaining are similar
        linear_segment = newcommandprinter("L", 3, 4, file),
        degenerate_segment = newcommandprinter("L", 7, 8, file),
        quadratic_segment = newcommandprinter("Q", 3, 6, file),
        svg_arc = newcommandprinter("A", 3, 9, file),
        cubic_segment = newcommandprinter("C", 3, 8, file),
        end_closed_contour = newcommandprinter("Z", 2, 1, file),
        end_open_contour = function(self) self.previous = nil end,
    }
end

local function print_xformed_path_data(path_data, pre_xf, out)
    out:write(" d=\"")
    path_data:iterate(
        make_path_f_xform(pre_xf,
			make_path_f_strip_rational_quadratics(
				make_path_f_command_printer(out))))
    out:write("\"")
end

local function print_path_data(path_data, out)
    out:write(" d=\"")
    path_data:iterate(
		make_path_f_strip_rational_quadratics(
			make_path_f_command_printer(out)));
    out:write("\"")
end

local scene_f_print_painted_meta = { __index = { } }

function scene_f_print_painted_meta.__index:
    print_paint(mode, shape, paint)
    self.out:write(' ', mode, "=\"")
    local type = paint:get_type()

    if type == _M.paint_type.solid_color then
        local c = paint:get_solid_color()
        local n = _M.color_name(c)
        if not n then
            local r, g, b = unpack8(c)
            self.out:write("rgb(", r, ',', g, ',', b, ')')
        else
            self.out:write(n)
        end
        local o = paint:get_opacity()
        if c[4] < 1 or paint:get_opacity() < 1 then
            self.out:write("\" ", mode, "-opacity=\"", c[4] * o)
        end
        self.out:write('"')

    elseif type == _M.paint_type.linear_gradient or
        type == _M.paint_type.radial_gradient then
        local key = paint:get_key()
        local found = self.map[key]
        if found then
             self.out:write("url(#gradient", found, ")")
             local o = paint:get_opacity()
             if o < 1 then
                 self.out:write("\" ", mode, "-opacity=\"", o)
             end
        end
        self.out:write('"')

    elseif type == _M.paint_type.texture then
        local key = paint:get_key()
        local found = self.map[key]
        if found then
             self.out:write("url(#texture", found, ")")
             local o = paint:get_opacity()
             if  o < 1 then
                 self.out:write("\" ", mode, "-opacity=\"", o)
             end
         end
         self.out:write('"')
    end
end

function scene_f_print_painted_meta.__index:
    print_stroke_style(width, st)
    self.out:write(" stroke-width=\"", width, '"')
    self.out:write(" stroke-linejoin=\"", stroke_join_name[st:get_join()], '"')
    self.out:write(" stroke-miterlimit=\"", st:get_miter_limit(), '"')
    if st:get_initial_cap() == st:get_terminal_cap() and
       st:get_initial_cap() == st:get_dash_initial_cap() and
       st:get_initial_cap() == st:get_dash_terminal_cap() then
        self.out:write(" stroke-linecap=\"",
            stroke_cap_name[st:get_initial_cap()], '"')
    end
    if not empty(st:get_dashes()) then
        self.out:write(" stroke-dasharray=\"")
        local dashes = st:get_dashes()
        for i, d in ipairs(dashes) do
            self.out:write(d*width, " ")
        end
        self.out:write('"')
    end
    self.out:write(" stroke-dashoffset=\"", st:get_dash_offset()*width, '"')
end

function scene_f_print_painted_meta.__index:
    painted_shape(winding_rule, shape, paint)
    local mode, style, data, pre_xf
    local width = 0
    local post_xf = shape:get_xf()
    if (shape:is_stroke()) then
        mode = "stroke";
        style = shape:get_stroke_data():get_style();
        width = shape:get_stroke_data():get_width();
        pre_xf = shape:get_stroke_data():get_shape():get_xf();
        -- Convert shape to be stroked into a path
        data = shape:get_stroke_data():get_shape():as_path_data(
            shape:get_xf():transformed(self.screen_xf))
    else
        mode = "fill";
        data = shape:as_path_data(self.screen_xf)
    end
    self.out:write(self.nl(), "<path id=\"shape", self.shape_id, '"')
    self.out:write(" fill-rule=\"", winding_rule_name[winding_rule], '"')
    if style then
        self.out:write(" fill=\"none\"")
        self:print_stroke_style(width, style)
    end
    print_xform(post_xf, " transform", self.out)
    self:print_paint(mode, shape, paint);
    if pre_xf then
        print_xformed_path_data(data, pre_xf, self.out)
    else
        print_path_data(data, self.out)
    end
    self.out:write("/>")
    self.shape_id = self.shape_id + 1
end

function scene_f_print_painted_meta.__index:begin_clip(depth)
    push_back(self.not_yet_active_clips, self.clippath_id)
    self.clippath_id = self.clippath_id + 1
end

function scene_f_print_painted_meta.__index:activate_clip(depth)
    push_back(self.active_clips, pop_back(self.not_yet_active_clips))
    if empty(self.not_yet_active_clips) then
        self.out:write(self.nl:inc(), "<g clip-path=\"url(#clip",
            back(self.active_clips), ")\">")
    end
end

function scene_f_print_painted_meta.__index:end_clip(depth)
    pop_back(self.active_clips)
    if empty(self.not_yet_active_clips) then
        self.out:write(self.nl:dec(), "</g>")
    end
end

function scene_f_print_painted_meta.__index:stencil_shape(winding_rule, shape)
    -- All stencil elements are in inside the <defs> section
end

function scene_f_print_painted_meta.__index:begin_fade(depth, opacity)
    if opacity < 1 then
        self.out:write(self.nl:inc(), "<g opacity=\"",
            opacity, "\">")
    end
end

function scene_f_print_painted_meta.__index:end_fade(depth, opacity)
    if opacity < 1 then
        self.out:write(self.nl:dec(), "</g>")
    end
end

function scene_f_print_painted_meta.__index:begin_blur(depth, radius)
    if not is_almost_zero(radius) then
        local key = "blur" .. radius
        local found = assert(self.map[key])
        self.out:write(self.nl:inc(), "<g filter=\"url(#blur",
            tostring(found), ")\">")
    end
end

function scene_f_print_painted_meta.__index:end_blur(depth, radius)
    if not is_almost_zero(radius) then
        self.out:write(self.nl:dec(), "</g>")
    end
end

function scene_f_print_painted_meta.__index:begin_transform(depth, xf)
    if empty(self.not_yet_active_clips) then
        self.out:write(self.nl:inc(), "<g")
        print_xform(xf, " transform", self.out)
        self.out:write('>')
    end
end

function scene_f_print_painted_meta.__index:end_transform(depth, xf)
    if empty(self.not_yet_active_clips) then
        self.out:write(self.nl:dec(), "</g>")
    end
end

local function make_scene_f_print_painted(nl, map, screen_xf, out)
    return setmetatable({
        nl = nl,
        map = map,
        screen_xf = screen_xf,
        out = out,
        shape_id = 0,
        clippath_id = 0,
        active_clips = {},
        not_yet_active_clips = {},
    }, scene_f_print_painted_meta)
end

local scene_f_print_paint_and_stencil_meta = { __index = { } }

function scene_f_print_paint_and_stencil_meta.__index:
    print_ramp(ramp)
    for i,stop in ipairs(ramp:get_color_stops()) do
        self.out:write(self.nl(), "<stop offset=\"", stop:get_offset(),
            "\" stop-color=\"")
        local c = stop:get_color()
        local n = _M.color_name(c)
        if not n then
            local r, g, b = unpack8(c)
            self.out:write("rgb(", r, ',', g, ',', b, ')')
        else
            self.out:write(n)
        end
        if c[4] < 1 then
            self.out:write("\" stop-opacity=\"", c[4])
        end
        self.out:write("\"/>")
    end
end

function scene_f_print_paint_and_stencil_meta.__index:
    print_linear_gradient(shape, paint)
    local key = paint:get_key()
    local found = self.map[key]
    if not found then
        self.map[key] = self.gradient_id
        local linear_gradient = paint:get_linear_gradient_data()
        self.out:write(self.nl(), "<linearGradient id=\"gradient",
            self.gradient_id, "\" gradientUnits=\"userSpaceOnUse\" x1=\"",
            linear_gradient:get_x1(), "\" y1=\"", linear_gradient:get_y1(),
            "\" x2=\"", linear_gradient:get_x2(), "\" y2=\"",
            linear_gradient:get_y2(), "\" spreadMethod=\"",
            spread_name[linear_gradient:get_color_ramp():get_spread()],
            "\"")
        print_xform(paint:get_xf():transformed(shape:get_xf():inverse()),
            " gradientTransform", self.out);
        self.out:write(">")
        self.nl:inc()
        self:print_ramp(linear_gradient:get_color_ramp())
        self.nl:dec()
        self.out:write(self.nl(), "</linearGradient>")
        self.gradient_id = self.gradient_id + 1
    end
end

function scene_f_print_paint_and_stencil_meta.__index:
    print_radial_gradient(shape, paint)
    local key = paint:get_key()
    local found = self.map[key]
    if not found then
        self.map[key] = self.gradient_id
        local radial_gradient = paint:get_radial_gradient_data()
        self.out:write(self.nl(), "<radialGradient id=\"gradient",
            self.gradient_id, "\" gradientUnits=\"userSpaceOnUse\" cx=\"",
            radial_gradient:get_cx(), "\" cy=\"" ,
            radial_gradient:get_cy(), "\" fx=\"" ,
            radial_gradient:get_fx(), "\" fy=\"" ,
            radial_gradient:get_fy(), "\" r=\"" ,
            radial_gradient:get_r(), "\" spreadMethod=\"" ,
            spread_name[radial_gradient:get_color_ramp():get_spread()], "\"")
        print_xform(paint:get_xf():transformed(shape:get_xf():inverse()),
            " gradientTransform", self.out);
        self.out:write(">")
        self.nl:inc()
        self:print_ramp(radial_gradient:get_color_ramp());
        self.nl:dec()
        self.out:write(self.nl(), "</radialGradient>")
        self.gradient_id = self.gradient_id + 1
    end
end

function scene_f_print_paint_and_stencil_meta.__index:
    print_texture(shape, paint)
    local key = paint:get_key()
    local found = self.map[key]
    if not found then
        self.map[key] = self.texture_id
        local texture = paint:get_texture_data();
        self.out:write(self.nl:inc(), "<pattern id=\"texture", self.texture_id,
            "\" patternUnits=\"userSpaceOnUse\" width=\"1\"",
            " height=\"1\" preserveAspectRatio=\"none\" ")
        print_xform(paint:get_xf():transformed(shape:get_xf():inverse()),
            " patternTransform", self.out)
        self.out:write(">")
        self.out:write(self.nl(), "<image id=\"image", self.texture_id,
            "\" width=\"1\" height=\"1\" preserveAspectRatio=\"none\"",
            " transform=\"scale(1,-1) translate(0,-1)\" xlink:href=\"",
            " data:image/png;base64,\n")
        local img = texture:get_image()
        if img:get_channel_type() == "uint8_t" then
            self.out:write(
                base64.encode(
                    image.png.string8(img)))
        else
            self.out:write(
                base64.encode(
                    image.png.string16(img)))
        end
        self.out:write("\"/>", self.nl:dec(), "</pattern>")
        self.texture_id = self.texture_id + 1
    end
end

function scene_f_print_paint_and_stencil_meta.__index:
    painted_shape(winding_rule, shape, paint)
    local type = paint:get_type()
    if type == _M.paint_type.linear_gradient then
        self:print_linear_gradient(shape, paint)
    elseif type == _M.paint_type.radial_gradient then
        self:print_radial_gradient(shape, paint)
    elseif type == _M.paint_type.texture then
        self:print_texture(shape, paint)
    end
end

function scene_f_print_paint_and_stencil_meta.__index:
    stencil_shape(winding_rule, shape)
    self.out:write(self.nl(), "<path id=\"stencil", self.stencil_id, '"',
        " clip-rule=\"", winding_rule_name[winding_rule], '"')
    local path_data = shape:as_path_data(self.screen_xf)
    print_path_data(path_data, self.out);
    print_xform(shape:get_xf():transformed(self.stencil_xf),
        " transform", self.out)
    self.out:write("/>")
    self.stencil_id = self.stencil_id + 1
end

function scene_f_print_paint_and_stencil_meta.__index:begin_clip(depth)
    push_back(self.not_yet_active_clips, self.clippath_id)
    self.clippath_id = self.clippath_id + 1
end

function scene_f_print_paint_and_stencil_meta.__index:activate_clip(depth)
    push_back(self.active_clips, pop_back(self.not_yet_active_clips))
end

function scene_f_print_paint_and_stencil_meta.__index:end_clip(depth)
    pop_back(self.active_clips)
end

function scene_f_print_paint_and_stencil_meta.__index:begin_fade(depth, opacity)
end

function scene_f_print_paint_and_stencil_meta.__index:end_fade(depth, opacity)
end

function scene_f_print_paint_and_stencil_meta.__index:begin_blur(depth, radius)
    if not is_almost_zero(radius) then
        local key = "blur" .. radius
        local found = self.map[key]
        if not found then
            self.map[key] = self.blur_id
            self.out:write(self.nl:inc(), "<filter id=\"blur", self.blur_id,
                "\">")
            self.out:write(self.nl(), "<feGaussianBlur stdDeviation=\"",
                radius, "\"/>")
            self.out:write(self.nl:dec(), "</filter>")
            self.blur_id = self.blur_id + 1
        end
    end
end

function scene_f_print_paint_and_stencil_meta.__index:end_blur(depth, radius)
end

function scene_f_print_paint_and_stencil_meta.__index:begin_transform(depth, xf)
    -- If the transformation happened within a stencil
    -- definition, we accumulate it
    if not empty(self.not_yet_active_clips) then
        push_back(self.stencil_xf_stack, self.stencil_xf)
        self.stencil_xf = xf:transformed(self.stencil_xf)
    end
end

function scene_f_print_paint_and_stencil_meta.__index:end_transform(depth, xf)
    if not empty(self.not_yet_active_clips) then
        self.stencil_xf = pop_back(self.stencil_xf_stack)
    end
end

local function make_scene_f_print_paint_and_stencil(nl, map, screen_xf, out)
    return setmetatable({
        nl = nl,
        map = map,
        screen_xf = screen_xf,
        out = out,
        blur_id = 0,
        gradient_id = 0,
        texture_id = 0,
        stencil_id = 0,
        clippath_id = 0,
        active_clips = {},
        not_yet_active_clips = {},
        stencil_xf_stack = {},
        stencil_xf = _M.identity()
    }, scene_f_print_paint_and_stencil_meta)
end

local scene_f_print_clip_meta = { __index = { } }

function scene_f_print_clip_meta.__index:
    painted_shape(winding_rule, shape, paint)
end

function scene_f_print_clip_meta.__index:
    stencil_shape(winding_rule, shape)
    self.out:write(self.nl(), "<use xlink:href=\"#stencil",
        self.stencil_id, '"')
    if not empty(self.nested_clips) then
        self.out:write(" clip-path=\"url(#clip", back(self.nested_clips), ")\"")
    end
    self.out:write("/>")
    self.stencil_id = self.stencil_id + 1;
end

function scene_f_print_clip_meta.__index:
    begin_clip(depth)
    self.out:write(self.nl:inc(), "<clipPath id=\"clip",
        self.clippath_id, "\">")
    push_back(self.not_yet_active_clips, self.clippath_id)
    self.clippath_id = self.clippath_id + 1
end

function scene_f_print_clip_meta.__index:
    activate_clip(depth)
    self.out:write(self.nl:dec(), "</clipPath>")
    push_back(self.active_clips, pop_back(self.not_yet_active_clips))
    if not empty(self.not_yet_active_clips) then
        push_back(self.nested_clips, back(self.active_clips))
    end
end

function scene_f_print_clip_meta.__index:
    end_clip(depth)
    if (not empty(self.nested_clips) and
        back(self.nested_clips) == back(self.active_clips)) then
        pop_back(self.nested_clips)
    end
    pop_back(self.active_clips)
end

function scene_f_print_clip_meta.__index:begin_fade(depth, opacity)
end

function scene_f_print_clip_meta.__index:end_fade(depth, opacity)
end

function scene_f_print_clip_meta.__index:begin_blur(depth, radius)
end

function scene_f_print_clip_meta.__index:end_blur(depth, radius)
end

function scene_f_print_clip_meta.__index:begin_transform(depth, xf)
end

function scene_f_print_clip_meta.__index:end_transform(depth, xf)
end

local function make_scene_f_print_clip(nl, map, screen_xf, out)
    return setmetatable({
        nl = nl,
        map = map,
        screen_xf = screen_xf,
        out = out,
        stencil_id = 0,
        clippath_id = 0,
        active_clips = {},
        not_yet_active_clips = {},
        nested_clips = {}
    }, scene_f_print_clip_meta)
end

function _M.render(content, window, viewport, out)
    local vxl, vyb, vxr, vyt = unpack(viewport)
    local wxl, wyb, wxr, wyt = unpack(window)
    local map = {}
    local nl = indent.indent(1, "  ")
    local br, bg, bb, ba = unpack8(content:get_background_color())
    ba = ba/255
    out:write("<?xml version=\"1.0\" standalone=\"no\"?>\n",
       "<svg\n",
       "  xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n",
       "  xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n",
       "  xmlns:cc=\"http://creativecommons.org/ns#\"\n",
       "  xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n",
       "  xmlns:svg=\"http://www.w3.org/2000/svg\"\n",
       "  xmlns=\"http://www.w3.org/2000/svg\"\n",
       "  version=\"1.1\"\n",
       "  style=\"background-color: rgba(", br, ",", bg, ",", bb, ",", ba,");\"\n",
       "  preserveAspectRatio=\"none\"\n",
       "  width=\"", abs(vxr-vxl),
         "\" height=\"", abs(vyt-vyb), "\"\n",
           "  viewBox=\"", min(wxl,wxr), ' ', min(wyt,wyb), ' ',
            abs(wxl-wxr), ' ', abs(wyt-wyb), "\"", ">")
    local flip = _M.identity()
    if wxl > wxr then
        flip = flip:translated(0.,-wxl):scaled(-1,1):translated(0,wxr)
    end
    if wyt > wyb then
        flip = flip:translated(0.,-wyb):scaled(1,-1):translated(0,wyt)
    end
    flip = flip:toxform()
    local screen_xf = content:get_xf():
        windowviewport(window, viewport):
            transformed(flip):toxform();
    out:write(nl:inc(), "<defs>")
    -- Write stencil shape, gradient paints, and textures
    content:get_scene_data():iterate(
        make_scene_f_print_paint_and_stencil(nl, map, screen_xf, out))
    -- Write clip-paths
    content:get_scene_data():iterate(
        make_scene_f_print_clip(nl, map, screen_xf, out))
    out:write(nl:dec(), "</defs>")
    out:write(nl:inc(), "<g")
    print_xform(flip, " transform", out);
    out:write("> <!-- invert y -->")
    out:write(nl:inc(), "<g")
    print_xform(content:get_xf(), " transform", out);
    out:write("> <!-- content transformation-->")
    -- Write painted shapes
    content:get_scene_data():iterate(
        make_scene_f_print_painted(nl, map, screen_xf, out))
    out:write(nl:dec(), "</g>")
    out:write(nl:dec(), "</g>\n</svg>\n")
end

return _M
back to top