Image
-- ----------------------------------------------------
-- Provide Moho with the name of this script object
-- ----------------------------------------------------
ScriptName = "SS_SVGImport"

-- **************************************************
-- SVG Import - Import SVG vector files
-- version:     01.12 MH12+ #521104
-- by:          Sam Cogheil (SimplSam)
-- **************************************************

--[[ ***** Licence & Warranty *****

    Copyright 2022 - Sam Cogheil (SimplSam)

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at:

        http://www.apache.org/licenses/LICENSE-2.0

    Conditions require preservation of copyright and license notices.

    You must retain, in the Source form of any Derivative Works that
    You distribute, all copyright, patent, trademark, and attribution
    notices from the Source form of the Work, excluding those notices
    that do not pertain to any part of the Derivative Works.

    You can:
        Use   - use/reuse freely, even commercially
        Adapt - remix, transform, and build upon for any purpose
        Share - redistribute the material in any medium or format

    Adapt / Share under the following terms:
        Attribution - You must give appropriate credit, provide a link to
        the Apache 2.0 license, and indicate if changes were made. You may
        do so in any reasonable manner, but not in any way that suggests
        the licensor endorses you or your use.

    Licensed works, modifications and larger works may be distributed
    under different License terms and without source code.

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

    The Developer Sam Cogheil / SimplSam will not be liable for any direct,
    indirect or consequential loss of actual or anticipated - data, revenue,
    profits, business, trade or goodwill that is suffered as a result of the
    use of the software provided.

]]

--[[
    ***** SPECIAL THANKS to:
    *    Stan (and team) @ MOHO Scripting -- https://mohoscripting.com
    *    The friendly faces @ Lost Marble Moho forum -- https://www.lostmarble.com/forum
    *    SVG ref docs:  https://developer.mozilla.org/en-US/docs/Web/SVG/ (Team Mozilla)
    *    SVG tech spec: https://www.w3.org/TR/SVG11/ (SVG Working Group / W3C Team)
    *    Lua XML parse: https://github.com/manoelcampos/xml2lua (Manoel Campos da Silva Filho / Paul Chakravarti)
    *    Arc to Path:   https://stackoverflow.com/questions/30277646/svg-convert-arcs-to-cubic-bezier
                        https://github.com/BigBadaboom/androidsvg/blob/master/androidsvg/src/main/java/com/caverock/androidsvg/utils/SVGAndroidRenderer.java (Paul LeBeau)
]]


-- ----------------------------------------------------
-- General information about this script
-- ----------------------------------------------------

SS_SVGImport = {}

local SS_SVGImportDialog = {}
local SS_SVGImportPrefsDialog = {}

local xml2lua
local handler

local ss_tool_icon_bin_found

function SS_SVGImport:Name()
    return "SS SVG Import"
end

function SS_SVGImport:Version()
    return "01.12 #5211"
end

function SS_SVGImport:UILabel()
    return "SVG Import"
end

function SS_SVGImport:Description()
    return "SVG Import Files"
end

function SS_SVGImport:Creator()
    return "Sam Cogheil (SimplSam)"
end

function SS_SVGImport:IsBeginnerScriptmoho()
    return false
end

function SS_SVGImport:IsEnabled(moho)
    return (moho.document:CurrentDocAction() == "")
end

function SS_SVGImport:IsRelevant(moho)
    if (not _did_resources_package_path) then
        package.path = package.path .. ";" .. moho:UserAppDir() .. "/scripts/ScriptResources/?.lua;"
        _did_resources_package_path = true
    end
    xml2lua = xml2lua or require("xml2lua.xml2lua")
    handler = handler or require("xml2lua.dom")
    self._MOHO_Version = moho:AppVersion() and moho:AppVersion():match("(%d+%.?%d*)")+0 or -1
    return true
end

function SS_SVGImport:ColorizeIcon()
    return false
end


-- **************************************************
-- Recurring values
-- **************************************************

SS_SVGImport._MOHO_Version = -1
SS_SVGImport.debugMode = false
SS_SVGImport.laterMode = true

SS_SVGImport.minify = true
SS_SVGImport.minifyLayers = true
SS_SVGImport.minifyLayersUnnamed = false
SS_SVGImport.minifyGroups = true
SS_SVGImport.minifyPostOp = false

SS_SVGImport.recenter = true
SS_SVGImport.expandGroups = false

SS_SVGImport.browseFirst = false
SS_SVGImport.hideDialog  = false

--= highlight missing image content
SS_SVGImport.hilightImg  = false
SS_SVGImport.hilightImgColor = "#00FF00"

SS_SVGImport.CIRCLE_CURV_MAGIC  = 0.391379 -- the magic number for a circle curvature
SS_SVGImport.STROKE_LINE_ENDS   = {butt=0, round=1, square=1} -- mh 0 = !round, 1 round
SS_SVGImport.SVG_COLOR = {['']='0,0,0', none='0,0,0', transparent='99,66,33', aliceblue='240,248,255', antiquewhite='250,235,215', aqua='0,255,255', aquamarine='127,255,212', azure='240,255,255', beige='245,245,220', bisque='255,228,196', black='0,0,0', blanchedalmond='255,235,205', blue='0,0,255', blueviolet='138,43,226', brown='165,42,42', burlywood='222,184,135', cadetblue='95,158,160', chartreuse='127,255,0', chocolate='210,105,30', coral='255,127,80', cornflowerblue='100,149,237', cornsilk='255,248,220', crimson='220,20,60', cyan='0,255,255', darkblue='0,0,139', darkcyan='0,139,139', darkgoldenrod='184,134,11', darkgray='169,169,169', darkgreen='0,100,0', darkgrey='169,169,169', darkkhaki='189,183,107', darkmagenta='139,0,139', darkolivegreen='85,107,47', darkorange='255,140,0', darkorchid='153,50,204', darkred='139,0,0', darksalmon='233,150,122', darkseagreen='143,188,143', darkslateblue='72,61,139', darkslategray='47,79,79', darkslategrey='47,79,79', darkturquoise='0,206,209', darkviolet='148,0,211', deeppink='255,20,147', deepskyblue='0,191,255', dimgray='105,105,105', dimgrey='105,105,105', dodgerblue='30,144,255', firebrick='178,34,34', floralwhite='255,250,240', forestgreen='34,139,34', fuchsia='255,0,255', gainsboro='220,220,220', ghostwhite='248,248,255', gold='255,215,0', goldenrod='218,165,32', gray='128,128,128', grey='128,128,128', green='0,128,0', greenyellow='173,255,47', honeydew='240,255,240', hotpink='255,105,180', indianred='205,92,92', indigo='75,0,130', ivory='255,255,240', khaki='240,230,140', lavender='230,230,250', lavenderblush='255,240,245', lawngreen='124,252,0', lemonchiffon='255,250,205', lightblue='173,216,230', lightcoral='240,128,128', lightcyan='224,255,255', lightgoldenrodyellow='250,250,210', lightgray='211,211,211', lightgreen='144,238,144', lightgrey='211,211,211', lightpink='255,182,193', lightsalmon='255,160,122', lightseagreen='32,178,170', lightskyblue='135,206,250', lightslategray='119,136,153', lightslategrey='119,136,153', lightsteelblue='176,196,222', lightyellow='255,255,224', lime='0,255,0', limegreen='50,205,50', linen='250,240,230', magenta='255,0,255', maroon='128,0,0', mediumaquamarine='102,205,170', mediumblue='0,0,205', mediumorchid='186,85,211', mediumpurple='147,112,219', mediumseagreen='60,179,113', mediumslateblue='123,104,238', mediumspringgreen='0,250,154', mediumturquoise='72,209,204', mediumvioletred='199,21,133', midnightblue='25,25,112', mintcream='245,255,250', mistyrose='255,228,225', moccasin='255,228,181', navajowhite='255,222,173', navy='0,0,128', oldlace='253,245,230', olive='128,128,0', olivedrab='107,142,35', orange='255,165,0', orangered='255,69,0', orchid='218,112,214', palegoldenrod='238,232,170', palegreen='152,251,152', paleturquoise='175,238,238', palevioletred='219,112,147', papayawhip='255,239,213', peachpuff='255,218,185', peru='205,133,63', pink='255,192,203', plum='221,160,221', powderblue='176,224,230', purple='128,0,128', red='255,0,0', rosybrown='188,143,143', royalblue='65,105,225', saddlebrown='139,69,19', salmon='250,128,114', sandybrown='244,164,96', seagreen='46,139,87', seashell='255,245,238', sienna='160,82,45', silver='192,192,192', skyblue='135,206,235', slateblue='106,90,205', slategray='112,128,144', slategrey='112,128,144', snow='255,250,250', springgreen='0,255,127', steelblue='70,130,180', tan='210,180,140', teal='0,128,128', thistle='216,191,216', tomato='255,99,71', turquoise='64,224,208', violet='238,130,238', wheat='245,222,179', white='255,255,255', whitesmoke='245,245,245', yellow='255,255,0', yellowgreen='154,205,50', rebeccapurple='102,51,153'}
SS_SVGImport.fileMru = {}
SS_SVGImport.FILE_MRU_MAX = 20
SS_SVGImport.didCancelCount = 0
SS_SVGImport.layerHasID = {}


-- **************************************************
-- SS_SVGImportDialog
-- **************************************************

SS_SVGImportDialog.RESET          = MOHO.MSG_BASE + 01
SS_SVGImportDialog.SELECT_FILE    = MOHO.MSG_BASE + 02
SS_SVGImportDialog.SELECT_RECENT  = MOHO.MSG_BASE + 03
SS_SVGImportDialog.SELECT_DELETE  = MOHO.MSG_BASE + 04

SS_SVGImportDialog.SCALE_         = MOHO.MSG_BASE + 10

SS_SVGImportDialog.MINIFY_        = MOHO.MSG_BASE + 30
SS_SVGImportDialog.MINIFY_LAYERS  = SS_SVGImportDialog.MINIFY_ + 01
SS_SVGImportDialog.MINIFY_GROUPS  = SS_SVGImportDialog.MINIFY_ + 02
SS_SVGImportDialog.MINIFY_POSTOP  = SS_SVGImportDialog.MINIFY_ + 03

SS_SVGImportPrefsDialog.PREF_              = MOHO.MSG_BASE + 40
SS_SVGImportPrefsDialog.PREF_BROWSE_FIRST  =  SS_SVGImportPrefsDialog.PREF_ + 01
SS_SVGImportPrefsDialog.PREF_HIDE_DIALOG   =  SS_SVGImportPrefsDialog.PREF_ + 02
SS_SVGImportPrefsDialog.PREF_DEBUG_MODE    =  SS_SVGImportPrefsDialog.PREF_ + 03

SS_SVGImport.scaleFactors = {-.5, -1, -2, 0, .5, 1, 2} -- 0 == d#ivider, -ve == canvas, +ve == entity, 1 = 100% ...
SS_SVGImport.scaleDefault = SS_SVGImportDialog.SCALE_ + 1 -- indx
SS_SVGImport.scaleMode = SS_SVGImport.scaleDefault
SS_SVGImport.filename = ""


-- **************************************************
-- Preference functions
-- **************************************************

function SS_SVGImport:LoadPrefs(prefs)
    self.filename = prefs:GetString("SS_SVGImport.filename", "")
    self.scaleMode = prefs:GetInt("SS_SVGImport.scaleMode", SS_SVGImport.scaleDefault)
    if (self.scaleMode < SS_SVGImportDialog.SCALE_ +1) or (self.scaleMode > SS_SVGImportDialog.SCALE_ + #self.scaleFactors) then
        self.scaleMode = self.scaleDefault
    end
    self.expandGroups = prefs:GetBool("SS_SVGImport.expandGroups", false)
    self.minify = prefs:GetBool("SS_SVGImport.minify", true)
    self.minifyLayers = prefs:GetBool("SS_SVGImport.minifyLayers", true)
    self.minifyLayersUnnamed = prefs:GetBool("SS_SVGImport.minifyLayersUnnamed", false)
    self.minifyGroups = prefs:GetBool("SS_SVGImport.minifyGroups", true)
    self.minifyPostOp = prefs:GetBool("SS_SVGImport.minifyPostOp", false)
    self.recenter = prefs:GetBool("SS_SVGImport.recenter", true)
    self.browseFirst = prefs:GetBool("SS_SVGImport.browseFirst", false)
    self.debugMode = prefs:GetBool("SS_SVGImport.debugMode", false)

    self.hideDialog = false
    for i = 1, self.FILE_MRU_MAX do
        self.fileMru[i] = prefs:GetString("SS_SVGImport.fileMru_" .. i, nil)
    end
end

function SS_SVGImport:SavePrefs(prefs)
    prefs:SetString("SS_SVGImport.filename", self.filename)
    prefs:SetInt("SS_SVGImport.scaleMode", self.scaleMode)
    prefs:SetBool("SS_SVGImport.expandGroups", self.expandGroups)
    prefs:SetBool("SS_SVGImport.minify", self.minify)
    prefs:SetBool("SS_SVGImport.minifyLayers", self.minifyLayers)
    prefs:SetBool("SS_SVGImport.minifyLayersUnnamed", self.minifyLayersUnnamed)
    prefs:SetBool("SS_SVGImport.minifyGroups", self.minifyGroups)
    prefs:SetBool("SS_SVGImport.minifyPostOp", self.minifyPostOp)
    prefs:SetBool("SS_SVGImport.recenter", self.recenter)
    prefs:SetBool("SS_SVGImport.browseFirst", self.browseFirst)
    prefs:SetBool("SS_SVGImport.debugMode", self.debugMode)
    for i = 1, self.FILE_MRU_MAX do
        if ((self.fileMru[i] ~= nil) and (self.fileMru[i] ~= "")) then
            prefs:SetString("SS_SVGImport.fileMru_" .. i, self.fileMru[i])
        end
    end
end

function SS_SVGImport:ResetPrefs()
    self.scaleMode = SS_SVGImport.scaleDefault
    self.expandGroups = false
    self.minify = true
    self.minifyLayers = true
    self.minifyLayersUnnamed = false
    self.minifyGroups = true
    self.minifyPostOp = false
    self.recenter = true
    self.browseFirst = false
    self.hideDialog = false
    self.debugMode = false
end


-- **************************************************
-- The guts of this script
-- **************************************************

function SS_SVGImport:Run(moho)
    local mohodoc  = moho.document
    local docHigh  = mohodoc:Height()
    local docWide  = mohodoc:Width()
    local docRatio = docWide / docHigh
    local frame0   = 0
    local wasFrame = moho.frame
    local laterPrint = ""
    local laterPrintCount = 0
    local laterPrintMax   = 20
    local debugPrintCount = 0
    local debugPrintMax   = 20

    self.tgCount = 0 -- totGrps
    self.tlCount = 0 -- totLyrs
    self.layerHasID = {}

    -- store defs, styles, clips, masks & gradients
    local defs   = {}
    local styles = {}
    local clips  = {}
    local masks  = {}
    local grads  = {}
    local filen, _didCancel, _didBrowse

    moho:SetSelLayer(mohodoc:Layer(mohodoc:CountLayers() -1), false, true) -- slct top

    local function ssRound(inNum, f)
        f = 10^(f or 2)
        return (LM.Round(inNum * f)/f)
    end

    local function dprint(...) -- debug print
        if (self.debugMode) then
            local mString = table.concat({...}, " ")
            if (debugPrintCount < debugPrintMax) then
                print(mString)
                debugPrintCount = debugPrintCount +1
            else
                if (debugPrintCount == debugPrintMax) then
                    print(" ... ")
                    debugPrintCount = debugPrintCount +1
                end
            end
        end
    end

    local function lprint(...) -- later print
        if (self.laterMode and self.debugMode) then
            if (laterPrintCount < laterPrintMax) then
                local mString = table.concat({...}, " ")
                laterPrint = laterPrint .. mString .. "\n"
                laterPrintCount = laterPrintCount +1
            else
                if (laterPrintCount == laterPrintMax) then
                    laterPrint = laterPrint .. " ... " .. "\n"
                    laterPrintCount = laterPrintCount +1
                end
            end
        end
    end


    local function xprint(...)
        -- local mString = "x " .. table.concat({...}, " ")
        -- print(mString)
    end


    -- Colors to RGB - Hex/A, Text, RGB/A
    local function Hex2rgb (color)
        if (color) then
            if (color:sub(1,4) == "rgba") then
                -- return color:match("(%d+),(%d+),(%d+),(%d*%.?%d+)")
                return color:match("%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d*%.?%d+)%s*")

            elseif (color:sub(1,3) == "rgb") then
                return color:match("%s*(%d+)%s*,%s*(%d+)%s*,%s*(%d+)%s*") -- #wsbg 1.0

            elseif (color:sub(1, 1) == '#') then -- hex
                -- Hex RGB #000 / #000000; RGBA #0000, #00000000
                local hex = color:sub(2)
                if hex:len() == 6 then
                    return tonumber("0x"..hex:sub(1,2)), tonumber("0x"..hex:sub(3,4)), tonumber("0x"..hex:sub(5,6))

                elseif hex:len() == 3 then -- #000
                    return (tonumber("0x"..hex:sub(1,1))*17), (tonumber("0x"..hex:sub(2,2))*17), (tonumber("0x"..hex:sub(3,3))*17)

                elseif hex:len() == 4 then -- #000f
                    return (tonumber("0x"..hex:sub(1,1))*17), (tonumber("0x"..hex:sub(2,2))*17), (tonumber("0x"..hex:sub(3,3))*17), (tonumber("0x"..hex:sub(4,4))*17)/255

                elseif hex:len() == 8 then
                    return tonumber("0x"..hex:sub(1,2)), tonumber("0x"..hex:sub(3,4)), tonumber("0x"..hex:sub(5,6)), tonumber("0x"..hex:sub(7,8))/255

                else
                    return 0,0,0 -- blk
                end

            else -- textname (assumed)
                if (color:sub(1,3) ~= "url") then -- not filter etc.
                    if (self.SVG_COLOR[color]) then -- color lookup
                        return self.SVG_COLOR[color]:match("(%d+),(%d+),(%d+)")
                    else
                        return 0,0,0 -- blk
                    end
                else
                    -- gradients
                    local gradName
                    _, _, gradName = color:find("url%((#.*)%)")
                    local grad = gradName and grads[gradName]
                    if (not grad) then
                        lprint("No Gradient found for: ", gradName)
                        return 106,108,110 -- mid grey (instead of filter, gradient)
                    else
                        -- cheap Mid color
                        local _opacity, _red, _grn, _blu = 0, 0, 0, 0
                        for _, stop in ipairs(grad._ss_stops) do
                            _opacity = _opacity + (stop._ss_styles["stop-opacity"] or 1)
                            local _r, _g, _b = Hex2rgb(stop._ss_styles["stop-color"])
                            _red = _red + _r
                            _grn = _grn + _g
                            _blu = _blu + _b

                        end
                        _red = LM.Round(_red / #grad._ss_stops)
                        _grn = LM.Round(_grn / #grad._ss_stops)
                        _blu = LM.Round(_blu / #grad._ss_stops)
                        _opacity = _opacity / #grad._ss_stops
                        return _red, _grn, _blu, _opacity
                    end
                end

            end
        else
            -- lprint("NIL Color @ ", self._name) -- debug color was nil
            return 0,0,0 --blk
        end
    end

    -- Tokenize Path e.g. vals="-0.43151.123.0.66 -8-3.4-15.1-8.2-21.1-14.2-6-6-10.8-13-14.2-21.1-3.4-8-5.1-16.6-5.1-25.8 -7.5e-4,0.347-77.8,0.0053 10e-4,.010410.002,0.01567"
    local function ParseSVGPath(_path)
        local out = {}
        for var, vals in _path:gmatch("([a-df-zA-DF-Z])([^a-df-zA-DF-Z]*)") do -- split by Directive (M, L, h)
            local line = { var }

            -- de-dot 1.5.6 > 1.5 .6  or  1.5-.6 > 1.5 -.6
            vals = vals:gsub("(%.%d+)(%.)", "%1 %2")
            vals = vals:gsub("(%.%d+)(%.)", "%1 %2")
            vals = vals:gsub("(%d)(-[.]?%d)", "%1 %2")
            vals = vals:gsub("(%d)(-[.]?%d)", "%1 %2")

            for val in vals:gmatch("([+-]?[%deE.+-]+)") do -- split by Values (-21.1, -6, 7.5e-4)
                line[#line +1] = val
            end
            out[#out +1] = line
        end
        return out
    end

    local function normx(px)
        return (((px/docWide) * 2 * docRatio) -docRatio)
    end
    local function normy(py)
        return (((py/docHigh) * -2) +1)
    end

    local function denormx(x)
        return (docWide/2 + (x * (docHigh/2)))
    end
    local function denormy(y)
        return (docHigh/2 - (y * (docHigh/2)))
    end

    local function deltax(px)
        return ((px/docWide) * 2 * docRatio)
    end
    local function deltay(py)
        return ((py/docHigh) * -2)
    end

    local normX0, normY0 = normx(0), normy(0)



    -- show/hide browse dialog (if opt), allow reshow diag if 3x browse + cancel
    if (self.browseFirst or self.hideDialog) then
        filen = LM.GUI.OpenFile("Get me some SVGs ...")
        _didBrowse = true
        _didCancel = filen == ""
        if _didCancel then
            if (self.hideDialog) then
                self.didCancelCount = self.didCancelCount +1
                if (self.didCancelCount < 3) then
                    return
                else
                    self.didCancelCount = 0
                end
            else
                self.didCancelCount = 0
            end
        end
    end

    SS_SVGImportDialog.filePathInputPre = ""
    if (not self.hideDialog or _didCancel) then
        if _didBrowse then
            SS_SVGImportDialog.filePathInputPre = filen
        end
        local dlog = SS_SVGImportDialog:new(moho)
        if (dlog:DoModal() == LM.GUI.MSG_CANCEL) then
            self.didCancelCount = 0
            return
        end
    else
        if (not _didCancel) then self.filename = filen end
    end
    self.didCancelCount = 0
    self.startTime = os.clock()
    self.filename = self.filename or ""
    -- local _, extName = self.filename:match"^(.*)%.([^%.]*)$"
    local _,filenameSafe, extName = self.filename:match("^(.*)[\\/](.+)%.([^%.]*)$")
    filenameSafe = filenameSafe or ""
    extName = extName or ""
    dprint("\n\n---------= ".. self:Name() .." ".. self:Version() .." =---------- [ " .. filenameSafe .." . ".. extName .." ] -----------------------\n\n")

    if (self.filename ~= "") and (extName == "svg") and os.rename(self.filename, self.filename) then
        if (wasFrame ~= 0) then moho:SetCurFrame(frame0) end


        local svgHandler = handler:new()
        local svgParser = xml2lua.parser(svgHandler)
        svgParser:parse(xml2lua.loadFile(self.filename))

        local mesh = nil
        local rgbColor = LM.rgb_color:new_local()
        local xy1, xy2, vec = LM.Vector2:new_local(), LM.Vector2:new_local(), LM.Vector2:new_local()


        -----------------------
        -- Prefind ID's and store them - with transforms (clip/mask)
        -----------------------
        local function PrepSVG(current, _ref, level, params)
            level = level or 0
            params = params or {}
            local node = current or current.node
            local name = node._name or "unnamed"
            local xtype = node._type or "untyped"
            local attributes = node._attr or {}
            local id = attributes.id or ""
            local inheritStyles, styleAttrib = {}, {}
            local passedStyles = params.styles or {}
            inheritStyles.fill = passedStyles.fill
            inheritStyles.opacity = passedStyles.opacity
            inheritStyles.stroke = passedStyles.stroke

            local function GetStyles(_attr)
                -- Kill redundancy todo
                local _attributes = _attr or attributes
                if (_attributes["class"] or _attributes["style"]) then
                    if (_attributes["class"]) then
                        for _class in _attributes["class"]:gmatch("([%w_-]+)") do -- split by alphanum _ -
                            if (styles[_class]) then
                                for token in styles[_class]:gmatch("([^;]+)") do
                                    local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                                    if (style) then
                                        styleAttrib[style] = value
                                    end
                                end
                            end
                        end
                    end
                    if (_attributes["style"]) then
                        for token in _attributes["style"]:gmatch("([^;]+)") do
                            local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                            if (style) then
                                styleAttrib[style] = value
                            end
                        end
                    end
                    inheritStyles.fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "") or inheritStyles.fill
                    -- inheritStyles.stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") or inheritStyles.stroke
                    if (styleAttrib["opacity"]) then inheritStyles.opacity = (inheritStyles.opacity or 1) * styleAttrib["opacity"] end
                    if (_attr) then
                        local _newStyle = {}
                        for _varname, _varval in pairs(styleAttrib) do
                            _newStyle[_varname] = _varval
                        end
                        return(_newStyle) -- optionally reqd by caller
                    end
                end
                inheritStyles.fill = _attributes.fill or inheritStyles.fill
            end

            if (xtype == "ELEMENT") then

                if (name == "svg") or (name == "g") or (name == "defs") then
                    local children = node._children or {}
                    local _subref = {grCount=0, elCount=0, xlCount=0, gradRefs={}}

                    GetStyles()

                    for _, child in ipairs(children) do
                        PrepSVG(child, _subref, level +1, {styles=inheritStyles})
                    end

                    if ((name == "svg") or (name == "g")) then
                        _subref.xlCount = _subref.xlCount + _subref.elCount
                        node._ss_isgroup = true
                        node._ss_grCount = _subref.grCount or 0 -- # of groups
                        node._ss_elCount = _subref.elCount or 0 -- # of child draw elements
                        node._ss_xlCount = _subref.xlCount or 0
                        _ref.grCount = _ref.grCount + 1
                        _ref.xlCount = _ref.xlCount + _subref.xlCount -- recurse elem count

                        if (attributes["clip-path"] or styleAttrib["clip-path"]) or (attributes["mask"] or styleAttrib["mask"]) then
                            node._ss_is_masked = true
                        end

                        if (name == "g") then
                            defs["#" .. id] = node
                        end

                    elseif ((name == "defs") and (#_subref.gradRefs > 0)) then
                        -- rerun Gradient defs with refs (href's)
                        for _, child in ipairs(_subref.gradRefs) do
                            PrepSVG(child, _subref, level +1, {styles=inheritStyles, doGradRef=true})
                        end
                    end

                elseif  ((name == "path") or (name == "circle") or (name == "ellipse") or (name == "line") or (name == "rect")
                    or (name == "polyline") or (name == "polygon") or (name == "image")) then
                    defs["#" .. id] = node -- raw node ref
                    _ref.elCount = _ref.elCount + 1

                elseif  (name == "use")  then
                    node._ss_useref = attributes["xlink:href"] or attributes["href"]
                    _ref.elCount = _ref.elCount + 1

                elseif (name == "linearGradient") or (name == "radialGradient")  then
                    local _href = (attributes["xlink:href"] or attributes["href"])
                    if (_href and (not node._ss_gradref)) then
                        -- has href. so delay processing
                        node._ss_gradref = _href
                        table.insert(_ref.gradRefs, node)

                    else
                        local children
                        if (node._ss_gradref) then
                            children = grads[node._ss_gradref] and grads[node._ss_gradref]._children or {} -- steal kids from ref
                        else
                            children = node._children or {}
                        end
                        local thisStops = {}
                        for _, child in ipairs(children) do
                            if (child._name == "stop") then
                                if (child._attr["style"]) then
                                    child._ss_styles = GetStyles(child._attr)
                                else
                                    child._ss_styles = {}
                                    child._ss_styles["offset"] = child._attr["offset"]
                                    child._ss_styles["stop-color"] = child._attr["stop-color"]
                                    child._ss_styles["stop-opacity"] = child._attr["stop-opacity"] or 1
                                end
                                table.insert(thisStops, child) --store raw stops w/ style
                            end
                        end
                        node._ss_stops = thisStops
                        grads["#" .. id] = node -- raw ref
                    end

                elseif (name == "clipPath") then
                    if (attributes["transform"])  then
                        local thisTrans = {}
                        for token in attributes["transform"]:gmatch("%S+%([%deE ,.+-]+%)") do
                            table.insert(thisTrans, token) --store raw
                        end
                        node._ss_transforms = thisTrans
                    end
                    clips["#" .. id] = node -- raw ref

                elseif (name == "mask") then
                    if (attributes["transform"])  then
                        local thisTrans = {}
                        for token in attributes["transform"]:gmatch("%S+%([%deE ,.+-]+%)") do
                            table.insert(thisTrans, token) --store raw
                        end
                        node._ss_transforms = thisTrans
                    end
                    GetStyles()
                    node._ss_styles = inheritStyles
                    masks["#" .. id] = node -- raw ref

                elseif (name == "style") then --styles
                    local _text = (node._children[1]._text:gsub("%c", "")):gsub("}", "}\n") -- 1 liner, split by '}'
                    for line in _text:gmatch("([%C]+)") do -- non special
                        if (line:match("^%s*(.-)%s*$") ~= "") then --< trim
                            local _, _, _styles, value = line:find("([%C]+)[{]%s*(.*%S+)%s*[}]") -- .st1, #st3, .st2 { fill-rule: bla; }
                            for style in _styles:gmatch("%.([^%s,]+)") do
                                if (style and value) then
                                    styles[style] = (styles[style] or "") .. value
                                end
                            end
                        end
                    end
                end
            end
        end


        -----------------------
        -- Walk the walk
        -----------------------
        local function WalkSVG(parentLayer, current, _ref, level, params)
            level = level or 0
            params = params or {}
            local spaces = string.rep(" ",level)
            local node = current or current.node
            local name = node._name or "unnamed"
            local xtype = node._type or "untyped"
            local attributes = node._attr or {}
            local id = attributes.id or ""
            local hasFill, hasLine
            local hasZ = false
            local isPathStarted, seenM
            local transforms = {}
            local styleAttrib = {}
            local _label
            local isMasked = params.is_masked
            local useAttributes = {}
            local _fill, _stroke, _opacity, _stroke_opacity, _fill_opacity, _stroke_width, _stroke_end
            local inheritStyles = {}
            local passedStyles = params.styles or {}
            self._name = name
            inheritStyles.opacity = passedStyles.opacity
            inheritStyles.fill = passedStyles.fill
            inheritStyles.stroke = passedStyles.stroke

            for _, val in ipairs(params.transforms or {}) do
                table.insert(transforms, val)
            end

            local function Sign(x)
                return x >= 0 and 1 or -1 -- treat case 0 as 1
            end

            local function InstantiateShape(shapeId, oriX, oriY)

                -- Transform color etc.
                local _alpha, _rgbA
                if (shapeId >= 0) then
                    local shape = mesh:Shape(shapeId)
                    if (id ~= "") then
                        shape:SetName(id)
                    end
                    shape:MakePlain() -- rids Gradients etc inherited from Style panel
                    if (shape.fInheritedStyle) or (shape.fInheritedStyle2) then
                        shape:RemoveStyles()
                    end
                    shape.fHasFill = hasFill
                    shape.fHasOutline = hasLine

                    if (hasLine) then
                        rgbColor.r, rgbColor.g, rgbColor.b, _rgbA = Hex2rgb(_stroke)
                        _alpha = (_stroke_opacity or _opacity or 1) * 255
                        rgbColor.a = _rgbA and _rgbA * _alpha or _alpha
                        shape.fMyStyle.fLineCol:SetValue(0, rgbColor)
                        shape.fMyStyle.fLineWidth = (_stroke_width or 1) / docHigh
                        _stroke_end = _stroke_end or "butt"
                        shape.fMyStyle.fLineCaps = self.STROKE_LINE_ENDS[_stroke_end] and self.STROKE_LINE_ENDS[_stroke_end] or 0
                    end
                    if (hasFill) then
                        rgbColor.r, rgbColor.g, rgbColor.b, _rgbA = Hex2rgb(_fill)
                        _alpha = (_fill_opacity or _opacity or 1) * 255
                        rgbColor.a = _rgbA and _rgbA * _alpha or _alpha
                        shape.fMyStyle.fFillCol:SetValue(0, rgbColor)
                    end
                    if ((not hasFill) and (not hasLine) and (_fill ~= "none") and (_fill ~= "transparent")) then
                        -- No Fill spec nor Line so ... Fill
                        rgbColor.r, rgbColor.g, rgbColor.b = 0, 0, 0
                        rgbColor.a = (_fill_opacity or _opacity or 1) * 255
                        shape.fMyStyle.fFillCol:SetValue(0, rgbColor)
                        shape.fHasFill = true
                    end

                    -- Transform dimensions
                    local vec2 = LM.Vector2:new_local()
                    for iTrans = #transforms, 1, -1 do
                        local transform = transforms[iTrans]
                        local _, _, command, token = transform:find("(%w+)[(](.*)[)]")
                        if (command == "translate") then
                            local _, _, _tx, _ty = token:find("([%deE.-]+)[ ,]*([%deE.-]*)")
                            vec2:Set(deltax(_tx or 0), deltay((_ty and _ty~='') and _ty or 0))
                            mesh:PrepMovePoints()
                            mesh:TranslatePoints(vec2)

                        elseif (command == "scale") then
                            local _, _, _sx, _sy = token:find("([%deE.-]+)[ ,]*([%deE.-]*)")
                            mesh:PrepMovePoints()
                            _sx = _sx or 1
                            _sy = (_sy and _sy~='') and _sy or _sx
                            local flipHandles = Sign(_sx+0) ~= Sign(_sy+0)
                            vec2:Set(normX0, normY0)
                            if (_sx == _sy) then -- (~ 70-100x faster)!
                                mesh:ScalePoints(_sx, _sy, vec2, flipHandles)
                            else -- dist?
                                local handles = {}
                                for i =0, shape:CountPoints() -1 do
                                    local point = mesh:Point(shape:GetPoint(i))
                                    local curve, _ptID = point:Curve(0) --, _ptID)
                                    local preVec = curve:GetControlHandle(_ptID, frame0, true)
                                    local pstVec = curve:GetControlHandle(_ptID, frame0, false)
                                    if (preVec ~= point.fPos) or (pstVec ~= point.fPos) then
                                        local handle = {cu=curve, pid=_ptID, preX=preVec.x, preY=preVec.y, pstX = pstVec.x, pstY = pstVec.y}
                                        table.insert(handles, handle)
                                    end
                                end

                                mesh:ScalePoints(_sx, _sy, vec2) -- 70% of time (28/40)
                                for _, handle in ipairs(handles) do  -- todo optimise - ignore same as point / use matrix?
                                    vec:Set((_sx * (handle.preX - normX0)) + normX0, (_sy * (handle.preY - normY0)) + normY0)
                                    handle.cu:SetControlHandle(handle.pid, vec, frame0, true)
                                    vec:Set((_sx * (handle.pstX - normX0)) + normX0, (_sy * (handle.pstY - normY0)) + normY0)
                                    handle.cu:SetControlHandle(handle.pid, vec, frame0, false)
                                end
                            end

                        elseif (command == "rotate") then
                            local _, _, _ra, _rx, _ry = token:find("([%deE.-]+)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)")
                            _ra = math.rad(-_ra or 0)
                            _rx = (_rx and _rx~='') and normx(_rx) or normX0
                            _ry = (_ry and _ry~='') and normy(_ry) or normY0
                            vec2:Set(_rx, _ry)
                            mesh:PrepMovePoints()
                            mesh:RotatePoints(_ra, vec2) -- todo origin

                        elseif (command == "matrix") then
                            -- X [a c e]   [Sx*Cosθ Sy*-Sinθ Tx]
                            -- Y [b d f]   [Sx*Sinθ Sy*Cosθ Ty]
                            -- Z [0 0 1]
                            local _x, _y
                            local _, _, _a, _b, _c, _d, _e, _f = token:find("([%deE.-]+)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)[ ,]*([%deE.-]*)") -- todo check got 6 or ('' or 0)
                            _e, _f = deltax(_e), deltay(_f)
                            local flipHandles = Sign(_a+0) ~= Sign(_d+0)
                            mesh:PrepMovePoints()
                            for i =0, shape:CountPoints() -1 do
                                local point = mesh:Point(shape:GetPoint(i))
                                _x = (_a * (point.fPos.x - normX0)) + (-_c * (point.fPos.y - normY0)) + _e
                                _y = (-_b * (point.fPos.x - normX0)) + (_d * (point.fPos.y - normY0)) + _f
                                point.fPos:Set(normX0 + _x, normY0 + _y)
                                if (flipHandles) then point:FlipControlHandles(frame0) end
                            end


                            local _sx = math.sqrt(_a^2 + _b^2)
                            local _sy = math.sqrt(_c^2 + _d^2)
                            local _scaleFactor = ssRound(math.sqrt(_sx * _sy),4)
                            -- Scale matrix LineWidth #wsbg 1.0 521030
                            if (shape.fHasOutline and (_scaleFactor ~= 1)) then
                                local style = shape.fMyStyle
                                style.fLineWidth = style.fLineWidth * _scaleFactor
                            end

                        elseif (command == "skewX") then
                            local _, _, _ra = token:find("([%deE.-]+)")
                            mesh:PrepMovePoints()
                            local _tan = math.tan(math.rad(_ra))
                            for i =0, shape:CountPoints() -1 do
                                local point = mesh:Point(shape:GetPoint(i))
                                point.fPos.x = point.fPos.x - (_tan * (point.fPos.y - normY0))
                            end

                        elseif (command == "skewY") then
                            local _, _, _ra = token:find("([%deE.-]+)")
                            mesh:PrepMovePoints()
                            local _tan = math.tan(math.rad(_ra))
                            for i =0, shape:CountPoints() -1 do
                                local point = mesh:Point(shape:GetPoint(i))
                                point.fPos.y = point.fPos.y - (_tan * (point.fPos.x - normX0))
                            end

                        end
                    end
                    moho:AddPointKeyframe(moho.drawingFrame, moho.drawingLayer, true)
                end
            end

            if (xtype == "ELEMENT") then
                local _subref = {grCount=0, elCount=0, hasLayer=false, hasMaskLayer=false}
                if  (name == "svg") or (name == "g")  or ((name == "use") and defs[node._ss_useref] and defs[node._ss_useref]._ss_isgroup) then --or (name == "svg") then

                    if (name == "use") then
                        node = defs[node._ss_useref]
                    end
                    local children = node._children or {}
                    local group = parentLayer
                    local didMakeGroup
                    local _minifyGroups = self.minifyGroups and self.minify
                    local _minifyLayers = self.minifyLayers and self.minify
                    local _minifyUnnamed = ((not self.minifyLayersUnnamed) or id == "")

                    --= Skip Create Group logic (pre-op consolidation)
                    if (level == 0) or (node._ss_xlCount == 0) or
                    _minifyGroups and _minifyUnnamed and (((not node._ss_is_masked) and (not isMasked) and
                            ((node._ss_elCount + node._ss_grCount == 1) and -- #wsbg 1.0 only elCount
                            ((not self.minifyLayersUnnamed) or (id == ""))) )  or
                        ((not node._ss_is_masked) and _minifyLayers and (node._ss_grCount == 0)))  then

                        node._ss_group_layer = parentLayer
                        -- print("GSkipped: ", name, " ", id)

                    else
                        group = moho:CreateNewLayer(MOHO.LT_GROUP)
                        group = moho:LayerAsGroup(group)
                        if (id ~= "") then
                            group:SetName(id)
                            self.layerHasID[group:UUID()] = true
                        end
                        node._ss_group_layer = group
                        moho:PlaceLayerInGroup(group, parentLayer, true)
                        group:Expand(self.expandGroups)
                        if (params.mask_mode) then
                            group:SetMaskingMode(MOHO.MM_ADD_MASK_INVIS)
                        end
                        _ref.grCount = _ref.grCount +1
                        self.tgCount = self.tgCount +1 -- totl groups
                        didMakeGroup = true
                    end

                    --= Group Style #511220
                    if (attributes["class"] or attributes["style"]) then
                        if (attributes["class"]) then
                            for _class in attributes["class"]:gmatch("([%w_-]+)") do -- split by alphanum _ -
                                if (styles[_class]) then
                                    for token in styles[_class]:gmatch("([^;]+)") do
                                        local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                                        if (style) then
                                            styleAttrib[style] = value
                                        end
                                    end
                                else
                                    -- Class/Style not found
                                    lprint("Class/GStyle not found: ", _class)
                                end
                            end
                        end
                        if (attributes["style"]) then -- can override
                            for token in attributes["style"]:gmatch("([^;]+)") do
                                local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                                if (style) then
                                    styleAttrib[style] = value
                                end
                            end
                        end

                        inheritStyles.fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "") or inheritStyles.fill
                        inheritStyles.stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") or inheritStyles.stroke
                        if (styleAttrib["opacity"]) then inheritStyles.opacity = (inheritStyles.opacity or 1) * styleAttrib["opacity"] end
                        -- inheritStyles.stroke_opacity = styleAttrib["stroke-opacity"]
                        -- inheritStyles.fill_opacity = styleAttrib["fill-opacity"]
                        -- inheritStyles.stroke_width = styleAttrib["stroke-width"] and styleAttrib["stroke-width"]:match("[%d|%.]+")
                    end

                    inheritStyles.fill = attributes.fill or inheritStyles.fill
                    inheritStyles.stroke = attributes.stroke or inheritStyles.stroke
                    if (attributes["opacity"]) then inheritStyles.opacity = (inheritStyles.opacity or 1) * attributes["opacity"] end
                    -- inheritStyles.stroke_opacity = attributes["stroke-opacity"]
                    -- inheritStyles.fill_opacity = attributes["fill-opacity"]
                    -- inheritStyles.stroke_width = attributes["stroke-width"] and attributes["stroke-width"]:match("[%d|%.]+") -- Todo?

                    if (attributes["clip-path"] or styleAttrib["clip-path"]) then
                        local clipName
                        if (attributes["clip-path"]) then
                            _, _, clipName = attributes["clip-path"]:find("%((#.*)%)")
                        else
                            _, _, clipName = styleAttrib["clip-path"]:find("%((#.*)%)")
                        end
                        local clip = clipName and clips[clipName]
                        if (not clip) then
                            lprint("NoGClip for: ", clipName, " @ ", name)
                        else
                            clipName = clipName:sub(2)
                            local _children = clip._children or {}
                            if (#_children > 0) then
                                if (self.debugMode) then group:SetUserTags("gcmask " .. clipName) end
                                group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL)
                                for _, child in ipairs(_children) do  -- add mask layer(s)
                                    WalkSVG(group, child, _subref, level + 1, {transforms=transforms, mask_type="clip", mask_mode=MOHO.MM_ADD_MASK_INVIS, name_prefix="mask " .. clipName .. " ", is_mask=true})
                                end
                                isMasked = true
                            end
                        end
                    end

                    if (attributes["mask"] or styleAttrib["mask"]) then
                        local maskName
                        if (attributes["mask"]) then
                            _, _, maskName = attributes["mask"]:find("%((#.*)%)")
                        else
                            _, _, maskName = styleAttrib["mask"]:find("%((#.*)%)")
                        end
                        local mask = maskName and masks[maskName]
                        if (not mask) then
                            lprint("NoGMask for: ", maskName, " @ ", name)
                        else
                            maskName = maskName:sub(2)
                            local _children = mask._children or {}
                            if (#_children > 0) then
                                if (self.debugMode) then group:SetUserTags("gmmask " .. maskName) end
                                group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL)
                                for _, child in ipairs(_children) do
                                    WalkSVG(group, child, _subref, level + 1, {styles=mask._ss_styles, transforms=transforms, mask_type="mask", mask_mode="SS_MASK_AUTO", name_prefix="mask " .. maskName .. " ", is_mask=true})
                                end
                                isMasked = true
                            end
                        end
                    end


                    --= Group Transforms #511221
                    if (attributes["transform"])  then
                        for token in attributes["transform"]:gmatch("%S+%([%deE ,.+-]+%)") do
                            table.insert(transforms, token) --store raw #511223
                        end
                    end

                    for _, child in ipairs(children) do
                        WalkSVG(node._ss_group_layer, child, _subref, level + 1, {parent_id=id, styles=inheritStyles, transforms=transforms, is_masked=isMasked, is_mask=params.is_mask}) -- ***
                    end

                    if (didMakeGroup) then
                        _ref.hasLayer = false
                    end

                elseif  ((name == "circle") or (name == "ellipse") or (name == "line") or (name == "rect") or (name == "path")
                      or (name == "polyline") or (name == "polygon") or (name == "use")  or (name == "image")) then

                    local def
                    if (name == "use") then
                        local ref = attributes["xlink:href"] or attributes["href"] -- href svg2.0
                        if (ref) then
                            def = defs[ref]
                        else
                            lprint("NoRef for: ", name)
                        end

                        if (not def) then
                            lprint("NoDef for: ", ref)
                        else
                            name = def._name or "unnamed"
                            useAttributes = attributes -- swap to def attributes
                            attributes    = def._attr or {}
                            id = useAttributes.id or attributes.id or ""

                            if  ((name == "circle") or (name == "ellipse") or (name == "line") or (name == "rect") or (name == "path")
                            or (name == "polyline") or (name == "polygon") or (name == "image")) then
                                -- nop
                            else
                                lprint(spaces .. "  def Can't Handle: ", name)
                                return
                            end
                        end
                    end


                    if (attributes["class"] or attributes["style"]) then
                        if (attributes["class"]) then
                            for _class in attributes["class"]:gmatch("([%w_-]+)") do -- split by alphanum _ -
                                if (styles[_class]) then
                                    for token in styles[_class]:gmatch("([^;]+)") do
                                        local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                                        if (style and value) then styleAttrib[style] = value end
                                    end
                                else

                                    lprint("Class/Style not found: ", _class)-- Class/Style not found
                                end
                            end
                        end
                        if (attributes["style"]) then -- can override
                            for token in attributes["style"]:gmatch("([^;]+)") do
                                local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                                if (style and value) then styleAttrib[style] = value end
                            end
                        end

                        _fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "")
                        _stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "")
                        _opacity = styleAttrib["opacity"]
                        _stroke_opacity = styleAttrib["stroke-opacity"]
                        _fill_opacity = styleAttrib["fill-opacity"]
                        _stroke_width = styleAttrib["stroke-width"] and styleAttrib["stroke-width"]:match("[%d|%.]+") -- TODo px em etc.
                        _stroke_end   = styleAttrib["stroke-linecap"] and styleAttrib["stroke-linecap"]:gsub("%s+", "")
                    end

                    _fill = attributes.fill or _fill
                    _stroke = attributes.stroke or _stroke
                    _opacity = attributes["opacity"] or _opacity
                    _stroke_opacity = attributes["stroke-opacity"] or _stroke_opacity
                    _fill_opacity = attributes["fill-opacity"] or _fill_opacity
                    _stroke_width = attributes["stroke-width"] and attributes["stroke-width"]:match("[%d|%.]+") or _stroke_width -- TODo
                    _stroke_end   = attributes["stroke-linecap"] and attributes["stroke-linecap"]:gsub("%s+", "") or _stroke_end

                    if (not _fill) or (not _stroke) then
                        if (useAttributes["style"]) then
                            for token in useAttributes["style"]:gmatch("([^;]+)") do
                              local _, _, style, value = token:find("([%S]+)%s*[:]%s*([%S ]+)")
                              styleAttrib[style] = value
                            end
                            if (not _fill) then _fill = styleAttrib["fill"] and styleAttrib["fill"]:gsub("%s+", "")  or "#000000" end
                            if (not _stroke) then _stroke = styleAttrib["stroke"] and styleAttrib["stroke"]:gsub("%s+", "") or "#000000" end
                            if (not _opacity) then _opacity = styleAttrib["opacity"] end
                            if (not _stroke_opacity) then _stroke_opacity = styleAttrib["stroke-opacity"] end
                            if (not _fill_opacity) then _fill_opacity = styleAttrib["fill-opacity"] end
                            if (not _stroke_width) then _stroke_width = styleAttrib["stroke-width"] and styleAttrib["stroke-width"]:match("[%d|%.]+") end
                            if (not _stroke_end) then _stroke_end = styleAttrib["stroke-linecap"] and styleAttrib["stroke-linecap"]:gsub("%s+", "") end

                        else
                            if (not _fill) then _fill = useAttributes.fill or _fill end
                            if (not _stroke) then _stroke = useAttributes.stroke or _stroke end
                            if (not _opacity) then _opacity = useAttributes["opacity"] end
                            if (not _stroke_opacity) then _stroke_opacity = useAttributes["stroke-opacity"] end
                            if (not _fill_opacity) then _fill_opacity = useAttributes["fill-opacity"] end
                            if (not _stroke_width) then _stroke_width = useAttributes["stroke-width"] and useAttributes["stroke-width"]:match("[%d|%.]+") end
                            if (not _stroke_end) then _stroke_end = useAttributes["stroke-linecap"] and useAttributes["stroke-linecap"]:gsub("%s+", "") end

                        end
                    end

                    _fill = _fill or inheritStyles.fill or "#000000"
                    _stroke = _stroke or inheritStyles.stroke
                    if (inheritStyles.opacity) then _opacity = (_opacity or 1) * inheritStyles.opacity end

                    hasFill = _fill or "none"
                    hasFill = (_fill ~= "none") and (_fill ~= "transparent")
                    hasLine = (_stroke or "none") ~= "none"

                    if ((attributes["clip-path"] or styleAttrib["clip-path"])) then
                        local clipName
                        if (attributes["clip-path"]) then
                            _, _, clipName = attributes["clip-path"]:find("%((#.*)%)")
                        else
                            _, _, clipName = styleAttrib["clip-path"]:find("%((#.*)%)")
                        end
                        local clip = clipName and clips[clipName]
                        if (not clip) then
                            lprint("NoClip for: ", clipName, " @ ", name)
                        else
                            clipName = clipName:sub(2)
                            local children = clip._children or {}
                            if (#children > 0) then
                                local group
                                if (clipName ~= _ref.lastMask) or (not _ref.lastGroup) then
                                    group = moho:CreateNewLayer(MOHO.LT_GROUP)
                                    group = moho:LayerAsGroup(group)
                                    group:SetName(clipName)
                                    if (self.debugMode) then group:SetUserTags("cmask") end
                                    group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL)
                                    _ref.lastMask = clipName
                                    _ref.lastGroup = group
                                    moho:PlaceLayerInGroup(group, parentLayer, true)
                                    group:Expand(SS_SVGImport.expandGroups)
                                    for _, child in ipairs(children) do -- add mask layer(s)
                                        WalkSVG(group, child, _subref, level +1, {transforms=transforms, mask_type="clip", mask_mode=MOHO.MM_ADD_MASK_INVIS, name_prefix="mask ", is_mask=true})
                                    end
                                    _ref.hasLayer = false
                                else
                                    group = _ref.lastGroup  --= reuse last mask group (avoid loads of single masked layers)
                                end
                                parentLayer = group
                                isMasked = true
                                _ref.grCount = _ref.grCount +1
                                self.tgCount = self.tgCount +1
                            end
                        end
                    end

                    if ((attributes["mask"] or styleAttrib["mask"])) then
                        local maskName
                        if (attributes["mask"]) then
                            _, _, maskName = attributes["mask"]:find("%((#.*)%)")
                        else
                            _, _, maskName = styleAttrib["mask"]:find("%((#.*)%)")
                        end
                        local mask = maskName and masks[maskName]
                        if (not mask) then
                            lprint("NoMask for: ", maskName, " @ ", name)
                        else
                            maskName = maskName:sub(2)
                            local children = mask._children or {}
                            if (#children > 0) then
                                local group
                                if (maskName ~= _ref.lastMask) or (not _ref.lastGroup) then
                                    group = moho:CreateNewLayer(MOHO.LT_GROUP)
                                    group = moho:LayerAsGroup(group)
                                    group:SetName(maskName)
                                    if (self.debugMode) then group:SetUserTags("mmask") end
                                    group:SetGroupMask(MOHO.GROUP_MASK_HIDE_ALL)
                                    _ref.lastMask = maskName
                                    _ref.lastGroup = group
                                    moho:PlaceLayerInGroup(group, parentLayer, true)
                                    group:Expand(SS_SVGImport.expandGroups)
                                    for _, child in ipairs(children) do -- add masking layer(s)
                                        WalkSVG(group, child, _subref, level + 1, {styles=mask._ss_styles, transforms=transforms, mask_type="mask", mask_mode="SS_MASK_AUTO", name_prefix="mask ", is_mask=true})
                                    end
                                    _ref.hasLayer = false
                                else
                                    group = _ref.lastGroup  --= reuse last mask group (avoid loads of single masked layers)
                                end
                                parentLayer = group
                                isMasked = true
                                _ref.grCount = _ref.grCount +1
                                self.tgCount = self.tgCount +1
                            end
                        end
                    end

                    if (not params.is_mask) then
                        -- lprint("not is_mask: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) )
                        if (isMasked and _ref.lastMask) then
                            -- lprint(" last mask: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) )
                            self.lastWasMasked = true

                        elseif self.lastWasMasked then
                            -- lprint(" not but lastWasMasked: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) )
                            _ref.hasLayer = false
                            _ref.lastMask = nil
                            self.lastWasMasked = false

                        else
                            -- lprint(" not isMasked: ", id, " - ", (string.sub(attributes.d or "-noid-", 1,20)) )
                            _ref.lastMask = nil

                        end
                    end

                    --= Create/Omit Vector Layer logic _minify
                    local _dontMinifyUnnamed = ((self.minifyLayersUnnamed and self.minifyLayers and self.minify) and id ~= "")
                    if (params.is_mask and ((not _ref.hasMaskLayer) or (params.mask_type == "mask" and _ref.maskFill ~= _fill))) or -- todo mask outline
                        (not params.is_mask and not _ref.hasLayer) or
                        (not self.minifyLayers) or (not self.minify) or
                        (not params.is_mask and _dontMinifyUnnamed)  then

                        local layer = moho:CreateNewLayer(MOHO.LT_VECTOR)
                        layer = moho:LayerAsVector(layer)
                        moho:PlaceLayerInGroup(layer, parentLayer, true)
                        local _parentPriority = (params.parent_id) and (params.parent_id ~= "") and (params.parent_id ~= parentLayer:Name()) and
                        (not parentLayer:LayerByName(params.parent_id))

                        if (id ~= "") and not (_parentPriority) then -- use my id unless I alone & parentName priority
                            layer:SetName((params.name_prefix or "") .. id )
                            self.layerHasID[layer:UUID()] = true
                        else
                            if _parentPriority then
                                layer:SetName(params.parent_id) -- #wsbg 1.0
                                self.layerHasID[layer:UUID()] = true
                            else -- force layer1 name if 1st
                                if (parentLayer:CountLayers() == 1) then
                                    layer:SetName("Layer 1")
                                end
                            end
                            if (params.name_prefix) then
                                layer:SetName(params.name_prefix  .. " " ..  layer:Name())
                            end
                        end
                        mesh = layer:Mesh()

                        -- do mask
                        if (params.mask_mode) then
                            if (params.mask_mode == "SS_MASK_AUTO") then
                                rgbColor.r, rgbColor.g, rgbColor.b = Hex2rgb(_fill)
                                if (rgbColor.r == 0) and (rgbColor.g == 0) and (rgbColor.b == 0) then
                                    layer:SetMaskingMode(MOHO.MM_SUB_MASK_INVIS)
                                else                                --todo Stroke
                                    _opacity = (((rgbColor.r + rgbColor.g + rgbColor.b) / 3) * (_fill_opacity or _opacity or 1)) / 255
                                    layer:SetMaskingMode(MOHO.MM_ADD_MASK_INVIS)
                                end
                            else
                                layer:SetMaskingMode(params.mask_mode)
                            end
                        end

                        if (params.is_mask) and (self._MOHO_Version >= 13.5) then
                            layer:SetIgnoredByLayerPicker(true)  -- mask layers unselectable
                        end

                        if (not params.is_mask) then  -- prevent mask layers merging with draw layers
                            _ref.hasLayer = true and (not _dontMinifyUnnamed)
                            _ref.hasMaskLayer = false
                            _ref.maskFill = nil
                        else
                            _ref.hasLayer = false
                            _ref.hasMaskLayer = true
                            _ref.maskFill = _fill
                        end
                        _ref.elCount = _ref.elCount +1
                        self.tlCount = self.tlCount +1  -- totl layrs
                    end

                    -- extract transforms for later use
                    local _attributes = attributes["transform"] or useAttributes["transform"]
                    if (_attributes) then  -- #wsbg 1.0 #521030
                        for token in _attributes:gmatch("%S+%([%deE ,.+-]+%)") do
                            table.insert(transforms, token)
                        end
                    end


                    --======== Create Shapes ========--

                    if (name == "circle") then
                        local cx = (useAttributes.x or 0) + (attributes.cx or 0)  -- TODO - Can be px, pt etc.
                        local cy = (useAttributes.y or 0) + (attributes.cy or 0)
                        local r = attributes.r or 0

                        -- bounding box
                        local x1 = cx -r
                        local y1 = cy -r
                        local x2 = cx +r
                        local y2 = cy +r

                        -- normalise
                        x1 = ((x1/docWide) * 2 * docRatio) -docRatio
                        x2 = ((x2/docWide) * 2 * docRatio) -docRatio
                        cx = ((cx/docWide) * 2 * docRatio) -docRatio
                        y1 = ((y1/docHigh) * -2) +1
                        y2 = ((y2/docHigh) * -2) +1
                        cy = ((cy/docHigh) * -2) +1

                        local pointsOffset = mesh:CountPoints()
                        mesh:SelectNone()
                        vec:Set(cx, y1)
                        mesh:AddLonePoint(vec, frame0) -- top
                        vec:Set(x2, cy)
                        mesh:AppendPoint(vec, frame0)  -- rgt
                        vec:Set(cx, y2)
                        mesh:AppendPoint(vec, frame0)  -- bot
                        vec:Set(x1, cy)
                        mesh:AppendPoint(vec, frame0)  -- lft
                        vec:Set(cx, y1)
                        mesh:AppendPoint(vec, frame0)  -- top agin
                        mesh:WeldPoints(pointsOffset + 4, pointsOffset, frame0)

                        mesh:Point(pointsOffset + 0):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)
                        mesh:Point(pointsOffset + 1):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)
                        mesh:Point(pointsOffset + 2):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)
                        mesh:Point(pointsOffset + 3):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)

                        mesh:SelectConnected()

                        local shapeID = moho:CreateShape(hasFill)
                        InstantiateShape(shapeID, normx(0), normy(0))


                    elseif (name == "ellipse") then
                        local cx = (useAttributes.x or 0) + (attributes.cx or 0)  -- TODO - Can be px, pt etc.
                        local cy = (useAttributes.y or 0) + (attributes.cy or 0)
                        local rx = attributes.rx or 0
                        local ry = attributes.ry or 0

                        -- bounding box
                        local x1 = cx -rx
                        local y1 = cy -ry
                        local x2 = cx +rx
                        local y2 = cy +ry

                        -- normalise
                        x1 = ((x1/docWide) * 2 * docRatio) -docRatio
                        x2 = ((x2/docWide) * 2 * docRatio) -docRatio
                        cx = ((cx/docWide) * 2 * docRatio) -docRatio
                        y1 = ((y1/docHigh) * -2) +1
                        y2 = ((y2/docHigh) * -2) +1
                        cy = ((cy/docHigh) * -2) +1

                        local pointsOffset = mesh:CountPoints()
                        mesh:SelectNone()
                        vec:Set(cx, y1)
                        mesh:AddLonePoint(vec, frame0) -- top
                        vec:Set(x2, cy)
                        mesh:AppendPoint(vec, frame0)  -- rgt
                        vec:Set(cx, y2)
                        mesh:AppendPoint(vec, frame0)  -- bot
                        vec:Set(x1, cy)
                        mesh:AppendPoint(vec, frame0)  -- lft
                        vec:Set(cx, y1)
                        mesh:AppendPoint(vec, frame0)  -- top agin
                        mesh:WeldPoints(pointsOffset + 4, pointsOffset, frame0)

                        local f = math.abs(x2 - x1) - math.abs(y2 - y1)
                        local eliFactor = .354  --< trial and err

                        if (math.abs(f) > 1e-6) then
                            if (math.abs(y2 - y1) > 1e-6) then
                                f = (x2 - x1) / (y2 - y1)
                            else
                                f = 1000
                            end
                            f = math.abs(f)
                            f = LM.Clamp(f, 0, 0.66667 / eliFactor) * eliFactor
                            mesh:Point(pointsOffset + 0):SetCurvature(f, frame0)
                            mesh:Point(pointsOffset + 2):SetCurvature(f, frame0)

                            if (math.abs(x2 - x1) > 1e-6) then
                                f = (y2 - y1) / (x2 - x1)
                            else
                                f = 1000
                            end
                            f = math.abs(f)
                            f = LM.Clamp(f, 0, 0.66667 / eliFactor) * eliFactor
                            mesh:Point(pointsOffset + 1):SetCurvature(f, frame0)
                            mesh:Point(pointsOffset + 3):SetCurvature(f, frame0)

                        else
                            mesh:Point(pointsOffset + 0):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)
                            mesh:Point(pointsOffset + 1):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)
                            mesh:Point(pointsOffset + 2):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)
                            mesh:Point(pointsOffset + 3):SetCurvature(self.CIRCLE_CURV_MAGIC, frame0)

                        end

                        mesh:SelectConnected()
                        local shapeID = moho:CreateShape(hasFill)
                        InstantiateShape(shapeID, cx, cy)


                    elseif (name == "line") then
                        local x1 = (useAttributes.x or 0) + (attributes.x1 or 0)
                        local y1 = (useAttributes.y or 0) + (attributes.y1 or 0)
                        local x2 = (useAttributes.x or 0) + (attributes.x2 or 0)
                        local y2 = (useAttributes.y or 0) + (attributes.y2 or 0)

                        -- normalise
                        x1 = ((x1/docWide) * 2 * docRatio) -docRatio
                        x2 = ((x2/docWide) * 2 * docRatio) -docRatio
                        y1 = ((y1/docHigh) * -2) +1
                        y2 = ((y2/docHigh) * -2) +1

                        xy1:Set(x1,y1)
                        xy2:Set(x2,y2)

                        local pointsOffset = mesh:CountPoints()
                        mesh:SelectNone()
                        mesh:AddLonePoint(xy1, 0) -- frame0
                        mesh:AppendPoint(xy2, 0)
                        mesh:Point(pointsOffset + 0):SetCurvature(MOHO.PEAKED, 0)
                        mesh:Point(pointsOffset + 1):SetCurvature(MOHO.PEAKED, 0)
                        mesh:SelectConnected()
                        local shapeID = moho:CreateShape(false)
                        hasFill = false
                        InstantiateShape(shapeID, x1, y1)


                    elseif (name == "polyline") or (name == "polygon") then
                        local x1, y1
                        local points_data = (attributes.points or "")
                        local pointsOffset = mesh:CountPoints()
                        isPathStarted = false
                        mesh:SelectNone()
                        for point_AX, point_AY in points_data:gmatch("([%d%.-]+)[, ]+([%d%.-]+)") do
                            -- normalise
                            x1 = ((point_AX/docWide) * 2 * docRatio) -docRatio
                            y1 = ((point_AY/docHigh) * -2) +1
                            xy1:Set(x1,y1)
                            if (not isPathStarted) then
                                mesh:AddLonePoint(xy1, 0) -- frame0
                                isPathStarted = true
                                xy2:Set(xy1)
                            else
                                mesh:AppendPoint(xy1, 0)
                            end
                        end

                        local numPoints = mesh:CountPoints() -pointsOffset
                        if (name == "polygon") or hasFill then
                            mesh:AppendPoint(xy2, 0) -- back to start
                            mesh:WeldPoints(pointsOffset + numPoints, pointsOffset, 0)
                            if (name == "polyline") then
                                --#5211 create as filled, but hide last edge #wsbg 1.10
                                local point = mesh:Point(pointsOffset + numPoints -1)
                                local curve = point:Curve(0, frame0)
                                curve:SetSegmentOn(numPoints -1, false)
                            end
                        else
                            -- hasFill = false (already false)
                        end
                        for iOffset = pointsOffset, pointsOffset + numPoints -1 do
                            mesh:Point(iOffset):SetCurvature(MOHO.PEAKED, 0)
                        end

                        mesh:SelectConnected()
                        local shapeID = moho:CreateShape(hasFill)
                        InstantiateShape(shapeID, xy2.x, xy2.y)


                    -- #521030 faux img #wsbg 1.0 width has px (image) .. todo store useAttrib pre de-marked
                    elseif (name == "rect") or (name == "image"  and  self.hilightImg) then
                        local x1 = (useAttributes.x or "0"):match("[%d%.-]+") + (attributes.x or "0"):match("[%d%.-]+")  -- Can be px, pt etc.
                        local y1 = (useAttributes.y or "0"):match("[%d%.-]+") + (attributes.y or "0"):match("[%d%.-]+")
                        local x2 = x1 + (useAttributes.width or "0"):match("[%d%.-]+") + (attributes.width or "0"):match("[%d%.-]+") -- todo 0 illegal
                        local y2 = y1 + (useAttributes.height or "0"):match("[%d%.-]+") + (attributes.height or "0"):match("[%d%.-]+")
                        -- if (x2 == x1) or (y2 == y1) then
                        --     -- todo end this
                        -- end

                        --#511227
                        local rx = attributes.rx or "auto"
                        local ry = attributes.ry or "auto"
                        rx = (rx == "auto") and ry or rx
                        ry = (ry == "auto") and rx or ry

                        --- rx,ry valid == val, %, auto,
                        local isRound
                        if (rx ~= "auto") or (ry ~= "auto") then
                            rx = (rx == "auto") and ry or rx
                            ry = (ry == "auto") and rx or ry
                            rx = LM.Clamp(rx, 1, attributes.width * .49) -- 49% (spec: 50%)
                            ry = LM.Clamp(ry, 1, attributes.height * .49)
                            rx, ry = deltax(rx), deltay(ry)
                            isRound = true
                        end

                        -- normalise
                        x1 = normx(x1)
                        x2 = normx(x2)
                        y1 = normy(y1)
                        y2 = normy(y2)
                        xy1:Set(x1,y1)
                        xy2:Set(x2,y2)

                        local pointsOffset = mesh:CountPoints()
                        mesh:SelectNone()
                        if (isRound) then
                            -- todo: cheap approximation
                            vec:Set(x1 +rx, y1) mesh:AddLonePoint(vec, 0)
                            vec:Set(x2 -rx, y1) mesh:AppendPoint(vec, 0)
                            vec:Set(x2, y1 +ry) mesh:AppendPoint(vec, 0)
                            vec:Set(x2, y2 -ry) mesh:AppendPoint(vec, 0)
                            vec:Set(x2 -rx, y2) mesh:AppendPoint(vec, 0)
                            vec:Set(x1 +rx, y2) mesh:AppendPoint(vec, 0)
                            vec:Set(x1, y2 -ry) mesh:AppendPoint(vec, 0)
                            vec:Set(x1, y1 +ry) mesh:AppendPoint(vec, 0)
                            vec:Set(x1 +rx, y1) mesh:AppendPoint(vec, 0)
                            local numPoints = mesh:CountPoints() -pointsOffset -1
                            mesh:WeldPoints(pointsOffset + numPoints, pointsOffset, 0)
                            local curve = mesh:Point(pointsOffset):Curve(0, frame0)

                            local curvx, curvy = rx/3, ry/3
                            vec:Set(x1 +curvx, y1) curve:SetControlHandle(0, vec, 0, true) -- pre
                            vec:Set(x2 -curvx, y1) curve:SetControlHandle(1, vec, 0, false, false) -- pst
                            vec:Set(x2, y1 +curvy) curve:SetControlHandle(2, vec, 0, true, false) -- pre
                            vec:Set(x2, y2 -curvy) curve:SetControlHandle(3, vec, 0, false, false) -- pst
                            vec:Set(x2 -curvx, y2) curve:SetControlHandle(4, vec, 0, true, false) -- pre
                            vec:Set(x1 +curvx, y2) curve:SetControlHandle(5, vec, 0, false, false) -- pst
                            vec:Set(x1, y2 -curvy) curve:SetControlHandle(6, vec, 0, true, false) -- pst
                            vec:Set(x1, y1 +curvy) curve:SetControlHandle(7, vec, 0, false, false) -- pre

                            for iOffset = 0, numPoints -1 do
                                if ((iOffset % 2) == 0) then
                                    curve:AimControlHandleAtNeighbor(iOffset, 0, false) -- pst
                                else
                                    curve:AimControlHandleAtNeighbor(iOffset, 0, true) -- pre
                                end
                            end

                        else
                            vec:Set(x1, y1) mesh:AddLonePoint(vec, 0)
                            vec:Set(x2, y1) mesh:AppendPoint(vec, 0)
                            vec:Set(x2, y2) mesh:AppendPoint(vec, 0)
                            vec:Set(x1, y2) mesh:AppendPoint(vec, 0)
                            vec:Set(x1, y1) mesh:AppendPoint(vec, 0)
                            local numPoints = mesh:CountPoints() -pointsOffset -1
                            mesh:WeldPoints(pointsOffset + numPoints, pointsOffset, 0)
                            for iOffset = pointsOffset, pointsOffset + numPoints -1 do
                                mesh:Point(iOffset):SetCurvature(MOHO.PEAKED, 0)
                            end
                        end

                        if (name == "image") then
                            hasFill = true
                            _fill = self.hilightImgColor
                        end

                        mesh:SelectConnected()
                        local shapeID = moho:CreateShape(hasFill)
                        InstantiateShape(shapeID, x1, y1)


                    elseif (name == "path") then

                        local pointX, pointY, pointAx, pointAy, bezx1, bezy1, bezx2, bezy2, bezx3, bezy3
                        local pathData = ParseSVGPath(attributes.d or "")
                        local pointsOffset = mesh:CountPoints()
                        local lastOffset = pointsOffset
                        local isLastCurved = false
                        local isFirstCurved = false
                        local lastAction = ""
                        local thisPeaked = {[true] = MOHO.SMOOTH, [false] = MOHO.PEAKED}
                        local numPoints = 0
                        local dataSize, dataCount, dataOffset
                        local pointsCurveData, pointsLineData = {}, {}

                        isPathStarted, seenM = false, false
                        mesh:SelectNone()

                        local function ArcTo(_lastX, _lastY, rx, ry, angle, largeFlag, sweepFlag, x, y)

                            local function LineTo(_px, _py)
                                vec:Set(_px, _py)
                                mesh:AppendPoint(vec, frame0)
                                local point = mesh:Point(pointsOffset + numPoints -1)
                                local curve = point:Curve(0, frame0)
                                curve:AimControlHandleAtNeighbor(numPoints -1, 0, false) -- pst (prev point)
                                point = mesh:Point(pointsOffset + numPoints)
                                local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                table.insert(pointsLineData, pointData)
                                isLastCurved = false
                                numPoints = numPoints +1
                            end

                            local function ArcToBeziers(angleStart, angleExtent)
                               local numSegments = math.ceil(math.abs(angleExtent) * 2 / math.pi)  -- (angleExtent / 90deg)
                               local angleIncrement = angleExtent / numSegments

                               -- The length of each control point vector is given by the following formula.
                               local controlLength = (4 / 3) * math.sin(angleIncrement / 2) / (1 + math.cos(angleIncrement / 2))
                               local coords = {} --[numSegments * 6]
                               for i=0, numSegments -1 do
                                  local _angle = angleStart + (i * angleIncrement)
                                  -- Calculate the control vector at this angle
                                  local dx = math.cos(_angle)
                                  local dy = math.sin(_angle)
                                  local offset = i * 6

                                  -- First control point
                                  coords[offset +1] = (dx - controlLength * dy)
                                  coords[offset +2] = (dy + controlLength * dx)

                                  -- Second control point
                                  _angle = _angle + angleIncrement
                                  dx = math.cos(_angle)
                                  dy = math.sin(_angle)
                                  coords[offset +3] = (dx + controlLength * dy)
                                  coords[offset +4] = (dy - controlLength * dx)

                                  -- Endpoint of bezier
                                  coords[offset +5] = dx
                                  coords[offset +6] = dy
                               end
                               return coords
                            end

                            if (_lastX == x and _lastY == y) then
                                -- If endpoints are same, exit (as per spec)
                                return
                            end

                            -- Handle degenerate case (behaviour specified by the spec)
                            if (rx == 0 or ry == 0) then
                                LineTo(x, y)
                                return
                            end

                            -- Sign of the radii is ignored (behaviour specified by the spec)
                            rx = math.abs(rx)
                            ry = math.abs(ry)
                            local TWO_PI = math.pi * 2
                            -- Convert angle from degrees to radians
                            local angleRad = math.rad(angle % 360)
                            angleRad = (angleRad > math.pi) and (-TWO_PI + angleRad) or angleRad
                            local cosAngle = math.cos(angleRad)
                            local sinAngle = math.sin(angleRad)

                            -- We simplify the calculations by transforming the arc so that the origin is at the
                            -- midpoint calculated above followed by a rotation to line up the coordinate axes
                            -- with the axes of the ellipse.
                            -- Compute the midpoint of the line between the current and the end point
                            local dx2 = (_lastX - x) / 2
                            local dy2 = (_lastY - y) / 2

                            -- Step 1 : Compute (_x1', _y1')
                            -- _x1,_y1 is the midpoint vector rotated to take the arc's angle out of consideration (sc: anti-rotated)
                            local _x1 = (cosAngle * dx2 + sinAngle * dy2)
                            local _y1 = (-sinAngle * dx2 + cosAngle * dy2)

                            local rx_sq = rx^2
                            local ry_sq = ry^2
                            local x1_sq = _x1^2
                            local y1_sq = _y1^2

                            -- Check that radii are large enough.
                            -- If they are not, the spec says to scale them up so they are.
                            -- This is to compensate for potential rounding errors/differences between SVG implementations.
                            local radiiCheck = x1_sq / rx_sq + y1_sq / ry_sq
                            if (radiiCheck > 0.99999) then
                                local radiiScale = math.sqrt(radiiCheck) * 1.00001
                                rx = radiiScale * rx
                                ry = radiiScale * ry
                                rx_sq = rx^2
                                ry_sq = ry^2
                            end

                            -- Step 2 : Compute (cx1, cy1) - the transformed centre point
                            local sign = (largeFlag == sweepFlag) and -1 or 1  --(largeFlag == sweepFlag) and -1 : 1
                            local sq = ((rx_sq * ry_sq) - (rx_sq * y1_sq) - (ry_sq * x1_sq)) / ((rx_sq * y1_sq) + (ry_sq * x1_sq))
                            sq = (sq < 0) and 0 or sq --and 0 : sq
                            local coef = sign * math.sqrt(sq)
                            local cx1 = coef * ((rx * _y1) / ry)
                            local cy1 = coef * -((ry * _x1) / rx)

                            -- Step 3 : Compute (cx, cy) from (cx1, cy1)
                            local sx2 = (_lastX + x) / 2.0
                            local sy2 = (_lastY + y) / 2.0
                            local cx = sx2 + (cosAngle * cx1 - sinAngle * cy1)
                            local cy = sy2 + (sinAngle * cx1 + cosAngle * cy1)

                            -- Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
                            local ux = (_x1 - cx1) / rx
                            local uy = (_y1 - cy1) / ry
                            local vx = (-_x1 - cx1) / rx
                            local vy = (-_y1 - cy1) / ry

                            -- Angle betwen two vectors is +/- acos( u.v / len(u) * len(v))
                            -- Where '.' is the dot product. And +/- is calculated from the sign of the cross product (u x v)
                            local p, n, v
                            -- Compute the start angle
                            -- The angle between (ux,uy) and the 0deg angle (1,0)
                            n = math.sqrt((ux^2) + (uy^2))
                            p = ux
                            sign = (uy < 0) and -1.0 or 1.0
                            local angleStart = sign * math.acos(p / n);  -- No need for checkedArcCos() here. (p >= n) should always be true.

                            -- Compute the angle extent
                            n = math.sqrt(ux^2 + uy^2) * math.sqrt(vx^2 + vy^2)
                            p = ux * vx + uy * vy
                            sign = ((ux * vy - uy * vx) < 0) and -1.0 or 1.0
                            v = p / n
                            local angleExtent = sign * ((v < -1.0) and math.pi or ((v > 1.0) and 0 or math.acos(v)))  --  sign * checkedArcCos(p / n)

                            -- Catch angleExtent of 0, which will cause problems later in arc ToBeziers
                            if (angleExtent == 0) then
                                LineTo(x, y)
                                return
                            end

                            if (not sweepFlag and angleExtent > 0) then
                                angleExtent = angleExtent - TWO_PI
                            elseif (sweepFlag and angleExtent < 0) then
                                angleExtent = angleExtent + TWO_PI
                            end

                            -- Many elliptical arc implementations including the Java2D and Android ones, only
                            -- support arcs that are axis aligned.  Therefore we need to substitute the arc
                            -- with bezier curves.  The following method call will generate the beziers for
                            -- a unit circle that covers the arc angles we want.
                            local bezierPoints = ArcToBeziers(angleStart, angleExtent)

                            -- Calculate a transformation matrix that will move and scale these bezier points to the correct location.

                            -- Final step is to add the bezier curves to the path
                            for i=1, #bezierPoints, 6 do

                                --== PrePend (reverse Postpend)
                                local mtx = LM.Matrix:new_local()
                                local vec2 = LM.Vector2:new_local()
                                mtx:Translate(cx, cy, 0)
                                mtx:Rotate(LM.Z_AXIS, angleRad)
                                mtx:Scale(rx, ry, 1)
                                for ii = i, i+4, 2 do
                                    vec2:Set(bezierPoints[ii], bezierPoints[ii+1])
                                    mtx:Transform(vec2)
                                    bezierPoints[ii], bezierPoints[ii+1] = vec2.x, vec2.y
                                end

                                local bezAx1 = bezierPoints[i+0]
                                local bezAy1 = bezierPoints[i+1]
                                local bezAx2 = bezierPoints[i+2]
                                local bezAy2 = bezierPoints[i+3]

                                local _pointAX = bezierPoints[i+4]
                                local _pointAY = bezierPoints[i+5]

                                bezx1  = normx(bezAx1)
                                bezy1  = normy(bezAy1)
                                local _pointX = normx(_pointAX) --normx(x
                                local _pointY = normy(_pointAY) --normy(y)
                                bezx2  = normx(bezAx2)
                                bezy2  = normy(bezAy2)

                                if (lastAction == "M") then
                                    isFirstCurved = true
                                end
                                vec:Set(_pointX, _pointY)
                                mesh:AppendPoint(vec, frame0)
                                local point = mesh:Point(pointsOffset + numPoints -1)
                                local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?)
                                xy2:Set(bezx1, bezy1)
                                curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst

                                point = mesh:Point(pointsOffset + numPoints)
                                local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve),  pID = curve:PointID(point)}
                                table.insert(pointsCurveData, pointData)
                                isLastCurved = true
                                numPoints = numPoints +1

                            end
                        end

                        for iAction, action in ipairs(pathData) do
                            local pathAction = action[1]
                            if ((pathAction == "M") or (pathAction == "m")) then -- Moveto
                                dataSize = 2
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize
                                    if  (iAction < #pathData) or (dataCount > 1) then -- avoid lonely M. dupe M -- fix my bad export
                                        if (pathAction == "M") or (not seenM) then  -- ABS (Force ABS at very start (only))
                                            pointAx = action[2 + dataOffset]
                                            pointAy = action[3 + dataOffset]
                                            pointX = normx(pointAx)
                                            pointY = normy(pointAy)
                                            _label = "MOVE"
                                            seenM = true
                                        else -- Rel
                                            pointX = pointX + deltax(action[2 + dataOffset])
                                            pointY = pointY + deltay(action[3 + dataOffset])
                                            pointAx = denormx(pointX) -- Todo why Ax wrong ???
                                            pointAy = denormy(pointY)
                                            _label = "move"
                                        end
                                        vec:Set(pointX, pointY)

                                        if ((iAction < #pathData) and (pathData[iAction +1][1]:upper() ~= 'M')) or (dataCount > 1) then
                                            if (dataOffset == 0) then -- [Mm] command
                                                if (isPathStarted) then -- M .... m ..... m .... M
                                                    --= 2nd+ visit to Move in a Path
                                                    mesh:SelectConnected()
                                                    if (not hasZ) and ((hasFill) or ((vec.x == xy1.x) and (vec.y == xy1.y))) then -- Force a shape weld on prev closed shapelet
                                                        -- TODO: Hide Edge? i.e. contnued path?? think dot dash
                                                        mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, 0)
                                                        mesh:SelectConnected() --?
                                                    end
                                                end

                                                xy1:Set(pointX, pointY) -- first XY of path seg / subpath
                                                isPathStarted = true
                                                hasZ = false -- reset
                                                pointsOffset = mesh:CountPoints() --preempt new point
                                                lastOffset = pointsOffset
                                                mesh:AddLonePoint(vec, frame0) -- frame0
                                                numPoints = 1
                                                isLastCurved = false
                                                isFirstCurved = false

                                            else
                                                -- additional M data (treat as L/l)
                                                mesh:AppendPoint(vec, frame0)
                                                local point = mesh:Point(pointsOffset + numPoints -1)
                                                local curve = point:Curve(0, frame0)
                                                curve:AimControlHandleAtNeighbor(numPoints -1, frame0, false) -- pst (prev point)
                                                point = mesh:Point(pointsOffset + numPoints)
                                                local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                                table.insert(pointsLineData, pointData)
                                                isLastCurved = false
                                                numPoints = numPoints +1

                                            end
                                        end
                                    else
                                        -- if (little M m) -move current
                                    end
                                end

                            elseif (pathAction == "L") or (pathAction == "l") then -- Lineto
                                dataSize = 2
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize
                                    if (pathAction == "L") then
                                        pointAx = action[2 + dataOffset]
                                        pointAy = action[3 + dataOffset]
                                        pointX = normx(pointAx)
                                        pointY = normy(pointAy)
                                        _label = "LINE"
                                    else
                                        pointAx = pointAx + action[2 + dataOffset]
                                        pointAy = pointAy + action[3 + dataOffset]
                                        pointX = pointX + deltax(action[2 + dataOffset])
                                        pointY = pointY + deltay(action[3 + dataOffset])
                                        _label = "line"
                                    end
                                    vec:Set(pointX, pointY)
                                    mesh:AppendPoint(vec, frame0)

                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0)
                                    curve:AimControlHandleAtNeighbor(numPoints -1, frame0, false) -- pst (prev point)
                                    point = mesh:Point(pointsOffset + numPoints)
                                    local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                    table.insert(pointsLineData, pointData)
                                    isLastCurved = false
                                    numPoints = numPoints +1
                                end

                            elseif (pathAction == "H") or (pathAction == "h") then -- Horiz line
                                dataSize = 1
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize
                                    if (pathAction == "H") then
                                        pointAx = action[2 + dataOffset]
                                        pointX = normx(pointAx)
                                        _label = "HORI"
                                    else
                                        pointAx = pointAx + action[2 + dataOffset]
                                        pointX = pointX + deltax(action[2 + dataOffset])
                                        _label = "hori"
                                    end
                                    vec:Set(pointX, pointY)
                                    mesh:AppendPoint(vec, frame0)

                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0)
                                    curve:AimControlHandleAtNeighbor(curve:PointID(point), frame0, false)

                                    isLastCurved = false
                                    point = mesh:Point(pointsOffset + numPoints)
                                    local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                    table.insert(pointsLineData, pointData)
                                    numPoints = numPoints +1
                                end

                            elseif (pathAction == "V") or (pathAction == "v") then -- Vert line
                                dataSize = 1
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize
                                    if (pathAction == "V") then
                                        pointAy = action[2 + dataOffset]
                                        pointY = normy(pointAy)
                                        _label = "VERT"
                                    else
                                        pointAy = pointAy + action[2 + dataOffset]
                                        pointY = pointY + deltay(action[2 + dataOffset])
                                        _label = "vert"
                                    end
                                    vec:Set(pointX, pointY)
                                    mesh:AppendPoint(vec, frame0)

                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0)
                                    curve:AimControlHandleAtNeighbor(curve:PointID(point), frame0, false)

                                    isLastCurved = false
                                    point = mesh:Point(pointsOffset + numPoints)
                                    local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                    table.insert(pointsLineData, pointData)
                                    numPoints = numPoints +1
                                end

                            elseif (pathAction == "C") or (pathAction == "c") then -- Cubic Bezier
                                dataSize = 6
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize
                                    if (pathAction == "C") then  -- ABS
                                        bezx1 = normx(action[2 + dataOffset])
                                        bezy1 = normy(action[3 + dataOffset])
                                        bezx2 = normx(action[4 + dataOffset])
                                        bezy2 = normy(action[5 + dataOffset])
                                        pointAx = action[6 + dataOffset]
                                        pointAy = action[7 + dataOffset]
                                        pointX = normx(pointAx)
                                        pointY = normy(pointAy)
                                        _label = "CURV"
                                    else -- Rel
                                        bezx1 = pointX + deltax(action[2 + dataOffset])
                                        bezy1 = pointY + deltay(action[3 + dataOffset])
                                        bezx2 = pointX + deltax(action[4 + dataOffset])
                                        bezy2 = pointY + deltay(action[5 + dataOffset])
                                        pointAx = pointAx + action[6 + dataOffset]
                                        pointAy = pointAy + action[7 + dataOffset]
                                        pointX = pointX + deltax(action[6 + dataOffset])
                                        pointY = pointY + deltay(action[7 + dataOffset])
                                        _label = "curv"
                                    end

                                    if (lastAction == "M") then
                                        isFirstCurved = true
                                    end

                                    vec:Set(pointX, pointY)
                                    mesh:AppendPoint(vec, frame0)
                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?)
                                    xy2:Set(bezx1, bezy1)
                                    curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst
                                    point = mesh:Point(pointsOffset + numPoints)
                                    local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve),  pID = curve:PointID(point)}
                                    table.insert(pointsCurveData, pointData)
                                    isLastCurved = true
                                    numPoints = numPoints +1
                                end

                            elseif (pathAction == "S") or (pathAction == "s") then -- Smooth Cubic Bezier
                                dataSize = 4
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize

                                    if (lastAction == "C" or lastAction == "S") then  -- C, S
                                        bezx1 = (pointX - bezx2) + pointX
                                        bezy1 = (pointY - bezy2) + pointY
                                    else
                                        bezx1, bezy1 = pointX, pointY
                                    end

                                    if (pathAction == "S") then
                                        bezx2 = normx(action[2 + dataOffset])
                                        bezy2 = normy(action[3 + dataOffset])
                                        pointAx = action[4 + dataOffset]
                                        pointAy = action[5 + dataOffset]
                                        pointX = normx(pointAx)
                                        pointY = normy(pointAy)
                                        _label = "SBEZ"
                                    else
                                        bezx2 = deltax(action[2 + dataOffset]) + pointX
                                        bezy2 = deltay(action[3 + dataOffset]) + pointY
                                        -- pointAx = -1
                                        -- pointAy = -1
                                        pointAx = pointAx + action[4 + dataOffset]
                                        pointAy = pointAy + action[5 + dataOffset]
                                        pointX = deltax(action[4 + dataOffset]) + pointX
                                        pointY = deltay(action[5 + dataOffset]) + pointY
                                        _label = "sbez"
                                    end
                                    if (lastAction == "M") then
                                        isFirstCurved = true
                                    end

                                    -- todo dedupe
                                    vec:Set(pointX, pointY)
                                    mesh:AppendPoint(vec, frame0)
                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?)
                                    xy2:Set(bezx1, bezy1)
                                    curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst
                                    point = mesh:Point(pointsOffset + numPoints)
                                    local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve),  pID = curve:PointID(point)}
                                    table.insert(pointsCurveData, pointData)
                                    isLastCurved = true
                                    numPoints = numPoints +1
                                end

                            elseif (pathAction == "Q") or (pathAction == "q") or (pathAction == "T") or (pathAction == "t") then  -- Quadratic Cubic Bezier
                                if (pathAction == "Q") or (pathAction == "q") then
                                    dataSize = 4
                                else
                                    dataSize = 2 -- T, t
                                end
                                dataCount = (#action -1) / dataSize
                                for iData = 0, dataCount -1 do
                                    dataOffset = iData * dataSize

                                    if (pathAction == "Q") then
                                        bezx3 = normx(action[2 + dataOffset])
                                        bezy3 = normy(action[3 + dataOffset])
                                        bezx1 = (pointX + 2 * bezx3) / 3
                                        bezy1 = (pointY + 2 * bezy3) / 3
                                        pointAx = action[4 + dataOffset]
                                        pointAy = action[5 + dataOffset]
                                        pointX = normx(pointAx)
                                        pointY = normy(pointAy)
                                        bezx2 = (pointX + 2 * bezx3) / 3
                                        bezy2 = (pointY + 2 * bezy3) / 3
                                        _label = "QBEZ"

                                    elseif (pathAction == "q") then
                                        bezx3 = deltax(action[2 + dataOffset]) + pointX
                                        bezy3 = deltay(action[3 + dataOffset]) + pointY
                                        bezx1 = (pointX + 2 * bezx3) / 3
                                        bezy1 = (pointY + 2 * bezy3) / 3
                                        pointAx = action[4 + dataOffset] + pointAx
                                        pointAy = action[5 + dataOffset] + pointAy
                                        pointX = deltax(action[4 + dataOffset]) + pointX
                                        pointY = deltay(action[5 + dataOffset]) + pointY
                                        bezx2 = (pointX + 2 * bezx3) / 3
                                        bezy2 = (pointY + 2 * bezy3) / 3
                                        _label = "qbez"

                                    elseif (pathAction == "T") or (pathAction == "t") then
                                        if (lastAction == "Q") or (lastAction == "T") then
                                            bezx3 = (pointX - bezx3) + pointX
                                            bezy3 = (pointY - bezy3) + pointY
                                        else
                                            bezx3 = pointX
                                            bezy3 = pointY
                                        end

                                        if (pathAction == "T") then
                                            bezx1 = (pointX + 2 * bezx3) / 3
                                            bezy1 = (pointY + 2 * bezy3) / 3
                                            pointAx = action[2 + dataOffset]
                                            pointAy = action[3 + dataOffset]
                                            pointX = normx(pointAx)
                                            pointY = normy(pointAy)
                                            _label = "TBEZ"

                                        else
                                            bezx1 = (pointX + 2 * bezx3) / 3
                                            bezy1 = (pointY + 2 * bezy3) / 3
                                            pointAx = pointAx + action[2 + dataOffset]
                                            pointAy = pointAy + action[3 + dataOffset]
                                            pointX = pointX + deltax(action[2 + dataOffset])
                                            pointY = pointY + deltay(action[3 + dataOffset])
                                            _label = "TBEZ"
                                        end

                                        bezx2 = (pointX + 2 * bezx3) / 3
                                        bezy2 = (pointY + 2 * bezy3) / 3
                                    end

                                    if (lastAction == "M") then
                                        isFirstCurved = true
                                    end

                                    -- todo dedupe
                                    vec:Set(pointX, pointY)
                                    mesh:AppendPoint(vec, frame0)
                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0) --todo expensive + (multi curve point?)
                                    xy2:Set(bezx1, bezy1)
                                    curve:SetControlHandle(curve:PointID(point), xy2, frame0, false, false) --pst
                                    point = mesh:Point(pointsOffset + numPoints)
                                    local pointData = {x2 = bezx2, y2 = bezy2, cID = mesh:CurveID(curve),  pID = curve:PointID(point)}
                                    table.insert(pointsCurveData, pointData)
                                    isLastCurved = true
                                    numPoints = numPoints +1
                                end

                            elseif (pathAction == "A") or (pathAction == "a") then -- Elliptical Arc
                                dataSize = 7
                                dataCount = (#action -1) / dataSize
                                local radiX, radiY, rAngle, bLarge, bSweep
                                for iData = 0, dataCount -1 do
                                    local lastX, lastY = pointAx, pointAy
                                    dataOffset = iData * dataSize
                                    radiX = action[2 + dataOffset]
                                    radiY = action[3 + dataOffset]
                                    rAngle = action[4 + dataOffset] -- ra
                                    bLarge = action[5 + dataOffset]+0 ~= 0
                                    bSweep = action[6 + dataOffset]+0 ~= 0
                                    if (pathAction == "A") then  -- ABS
                                        pointAx = action[7 + dataOffset]
                                        pointAy = action[8 + dataOffset]
                                        pointX = normx(pointAx)
                                        pointY = normy(pointAy)
                                        _label = "ARC"

                                    else -- Rel
                                        pointAx = pointAx + action[7 + dataOffset]
                                        pointAy = pointAy + action[8 + dataOffset]
                                        pointX = pointX + deltax(action[7 + dataOffset])
                                        pointY = pointY + deltay(action[8 + dataOffset])
                                        _label = "arc"
                                    end
                                    if (lastAction == "M") then
                                        isFirstCurved = true
                                    end

                                    ArcTo(lastX, lastY, radiX, radiY, rAngle, bLarge, bSweep, pointAx, pointAy)
                                end

                            elseif (pathAction == "Z") or (pathAction == "z") then -- Closepath
                                -- rules: Close path if last cmd or followed by [Mm].
                                hasZ = true
                                -- if 1st & last not same(ish)
                                if ((ssRound(vec.x,4) ~= ssRound(xy1.x,4)) or (ssRound(vec.y,4) ~= ssRound(xy1.y,4))) then
                                    vec:Set(xy1.x, xy1.y)
                                    mesh:AppendPoint(vec, frame0)
                                    local point = mesh:Point(pointsOffset + numPoints -1)
                                    local curve = point:Curve(0, frame0)
                                    curve:AimControlHandleAtNeighbor(numPoints -1, frame0, false) -- pst (prev point)
                                    point = mesh:Point(pointsOffset) --1st point
                                    local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                    table.insert(pointsLineData, pointData)
                                    isLastCurved = false
                                    numPoints = numPoints +1
                                end

                                if ((iAction < #pathData) and (pathData[iAction +1][1]:upper() == 'M')) then
                                    pointX, pointY = vec.x, vec.y --?? set AX ???
                                else
                                end

                                local _result = mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, frame0)
                                if (not _result) then
                                    dprint("weldfail: @1578")
                                end

                                mesh:SelectConnected()
                                if ((iAction < #pathData) and (pathData[iAction +1][1]:upper() ~= 'M')) then -- start new subpath - bit like m (actuall or 0,0)
                                    pointX, pointY = xy1.x, xy1.y   -- first XY of path seg
                                    vec:Set(xy1)
                                    isPathStarted = true
                                    hasZ = false -- reset
                                    pointsOffset = mesh:CountPoints() --preempt new point
                                    lastOffset = pointsOffset
                                    mesh:AddLonePoint(vec, frame0) -- frame0
                                    numPoints = 1
                                    isLastCurved = false
                                    isFirstCurved = false
                                end

                            else
                                dprint ((spaces .. "  ** UNKNOWN Path Action: " .. pathAction))
                            end
                            lastAction = pathAction:upper()
                        end


                        if (isPathStarted and numPoints > 1) then
                            --= After Path
                            if (not hasZ) and (((ssRound(vec.x,4) == ssRound(xy1.x,4)) and (ssRound(vec.y,4) == ssRound(xy1.y,4)))) then
                                -- Force Close on last shapelet if no Z and lastpoint==firstpoint
                                if (mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, 0)) then
                                    mesh:Point(pointsOffset + 0):SetCurvature(thisPeaked[(isLastCurved) or (isFirstCurved)], frame0) -- ???
                                end

                            elseif (not hasZ) and hasFill then
                                -- Force Close on last shapelet if no Z but filled
                                vec:Set(xy1.x, xy1.y)
                                mesh:AppendPoint(vec, frame0)
                                local point = mesh:Point(pointsOffset + numPoints -1)
                                local curve = point:Curve(0, frame0)
                                curve:AimControlHandleAtNeighbor(curve:PointID(point), frame0, false)
                                point = mesh:Point(pointsOffset)
                                local pointData = {cID = mesh:CurveID(curve), pID = curve:PointID(point)}
                                table.insert(pointsLineData, pointData)
                                local tmpPt = curve:GetControlHandle(pointData.pID, frame0, false)
                                mesh:WeldPoints(mesh:CountPoints() -1, lastOffset +0, 0)
                                curve:SetControlHandle(pointData.pID, tmpPt, frame0, false, false)
                                curve:SetSegmentOn(numPoints -1, false) -- hide edge
                                numPoints = numPoints +1

                            else
                                if (not hasZ) then
                                    mesh:Point(mesh:CountPoints() -1):SetCurvature(thisPeaked[isLastCurved], frame0) -- endpoint  ???
                                end

                            end
                            mesh:SelectConnected()

                            -- limited Loops for Curve Pre handles
                            for _, pointData in ipairs(pointsCurveData) do
                                local curve = mesh:Curve(pointData.cID) --todo efficy
                                xy2:Set(pointData.x2, pointData.y2)
                                local targetPID = (pointData.pID < curve:CountPoints()) and pointData.pID or 0
                                if (targetPID ~= 0) then
                                    curve:SetControlHandle(targetPID, xy2, frame0, true, false)-- todo Same curve ???
                                else
                                    curve:SetControlHandle(0, xy2, frame0, true, false)  -- todo Same curve ???
                                end
                            end

                            for _, pointData in ipairs(pointsLineData) do
                                local curve = mesh:Curve(pointData.cID) --todo efficy
                                local targetPID = (pointData.pID < curve:CountPoints()) and pointData.pID or 0
                                curve:AimControlHandleAtNeighbor(targetPID, frame0, true)
                                if (targetPID == 1) then curve:AimControlHandleAtNeighbor(0, frame0, false) end --fixup welds
                            end

                            local shapeID = moho:CreateShape(false)
                            InstantiateShape(shapeID, 0, 0)
                        end
                    end

                elseif (name == "defs") or (name == "style") or (name == "clipPath") or (name == "mask") or (name == "title") or (name == "desc") then
                    -- NOP processed elsewhere (or silently ignored)
                else
                    -- dprint(spaces .. "  x2 Can't Handle: ", name)  -- todo @ linenum
                end
            end
            if (mesh) then mesh:SelectNone() end
        end

        -----------------------
        -- Resize realign etc (post-op)
        -----------------------
        local function SizeSVG(groupLayer)
            groupLayer = moho:LayerAsGroup(groupLayer)

            for iSubLayer = 0, groupLayer:CountLayers() - 1 do
                local subLayer = groupLayer:Layer(iSubLayer)
                if (subLayer:IsGroupType()) then
                    SizeSVG(subLayer)
                else
                    if (subLayer:LayerType() == MOHO.LT_VECTOR) then
                        subLayer = moho:LayerAsVector(subLayer)
                        local _mesh = subLayer:Mesh()
                        if (_mesh) then
                            _mesh:SelectAll()
                            _mesh:PrepMovePoints()
                            _mesh:TransformPoints(self.transVec, self.scaleFactor, self.scaleFactor, 0, self.orientVec)
                            moho:AddPointKeyframe(0, subLayer, true)
                            _mesh:SelectNone()

                            for iShape = 0, _mesh:CountShapes() -1 do
                                local shape = _mesh:Shape(iShape)
                                if (shape) and (shape.fHasOutline) then
                                    local style = shape.fMyStyle
                                    style.fLineWidth = style.fLineWidth * self.scaleFactor
                                end
                            end
                        end
                    end
                end
            end
        end


        -----------------------
        -- MashUp Group + Vector neighbors (post-op == slow & brutal)- todo Unnamed?
        -----------------------
        local function MashSVG(parentLayer)
            local mergeLayer, mergeMesh, maskMode
            local _mashCount = 0
            local mashed = {}
            parentLayer = moho:LayerAsGroup(parentLayer)
            for iSubLayer = 0, parentLayer:CountLayers() - 1 do
                -- dangling vectors (1 grp -> 1 vec)
                local subLayer = parentLayer:Layer(iSubLayer)
                if (subLayer:IsGroupType()) then
                    _mashCount = _mashCount + MashSVG(subLayer)
                    if (subLayer:CountLayers() == 1) then
                        -- move veclayer to parent, remove group, inherit name .. masking
                        local subSubLayer = subLayer:Layer(0)
                        -- subSubLayer:SetUserTags(subSubLayer:UserTags() .. " " .. subSubLayer:Name())
                        subSubLayer:SetName(subLayer:Name())
                        self.layerHasID[subSubLayer:UUID()] = self.layerHasID[subLayer:UUID()]
                        subSubLayer:SetMaskingMode(subLayer:MaskingMode())
                        moho:PlaceLayerBehindAnother(subSubLayer, subLayer)
                        moho:DeleteLayer(subLayer)
                        _mashCount = _mashCount +1
                    end
                    mergeLayer = nil
                    mergeMesh  = nil
                    maskMode   = nil

                else
                    if (subLayer:LayerType() == MOHO.LT_VECTOR) and
                        ((subLayer:MaskingMode() == MOHO.MM_NOTMASKED) or (subLayer:MaskingMode() == MOHO.MM_MASKED)) then --todo lastMaskMode match

                        subLayer = moho:LayerAsVector(subLayer)
                        if (not mergeLayer) or (subLayer:MaskingMode() ~= maskMode) then --reset
                            mergeLayer = subLayer
                            mergeMesh  = subLayer:Mesh()
                            maskMode   = subLayer:MaskingMode()
                            moho:SetSelLayer(mergeLayer)

                        else -- merge vects ??
                            -- if (not self.minifyLayersUnnamed) or (mergeLayer:Name():sub(1,6) == "Layer " and subLayer:Name():sub(1,6) == "Layer ") then
                            if (not self.minifyLayersUnnamed) or (not self.layerHasID[mergeLayer:UUID()] and not self.layerHasID[subLayer:UUID()]) then
                                local _mesh = subLayer:Mesh()
                                _mesh:SelectAll()
                                moho:Copy(_mesh) -- #wsbg 1.11 kills BitMap bufrer
                                moho:CopyAlternate(_mesh)
                                moho:PasteAlternate() --> mergeLayer
                                mergeMesh:DeselectPoints()
                                table.insert(mashed, subLayer)
                                subLayer:SetVisible(false) -- will del
                                -- subLayer:SetName("_2BD_" .. subLayer:Name())
                                _mashCount = _mashCount +1
                            else
                                mergeLayer = nil
                                mergeMesh  = nil
                            end
                        end

                    else
                        mergeLayer = nil
                        mergeMesh  = nil
                        maskMode   = nil
                    end
                end
            end

            for _, _mash in ipairs(mashed) do
                moho:DeleteLayer(_mash)
            end

            return _mashCount
        end


        -----------------------
        -- SVG Processing starts here ...
        -----------------------
        local svgRoot = svgHandler.root
        if (svgRoot._name == "svg") then

            mohodoc:PrepMultiUndo()
            mohodoc:SetDirty()
            local rootLayer = moho:CreateNewLayer(MOHO.LT_GROUP)
            self.tgCount = self.tgCount +1
            rootLayer:SetName(filenameSafe)
            rootLayer = moho:LayerAsGroup(rootLayer)
            rootLayer:SetVisible(false)
            rootLayer:Expand(false)
            svgRoot._attr.id = filenameSafe
            local lStats = {grCount=0, elCount=0, xlCount=0}

            --==# Prep for import (back refs etc.) :recursive
            PrepSVG(svgRoot, lStats)
            dprint("------ Prep DONE ------ @ ", os.date(), " - ", ssRound(os.clock() - self.startTime))
            self.lastTime = os.clock()

            --==# Do the Import (The Big One) :recursive
            WalkSVG(rootLayer, svgRoot, {grCount=0, elCount=0, hasLayer=false, hasMaskLayer=false}, 0, {})
            self.endTime = os.clock()
            dprint(("------ Walk DONE ------ @ %s - %s / %s --- Gr: %d, la: %d, el: %d"):format(os.date(), ssRound(self.endTime - self.lastTime), ssRound(os.clock() - self.startTime), self.tgCount, self.tlCount, lStats.xlCount))
            self.lastTime2 = self.endTime

            --> at this point SVG is imported <--


            --# now resize / relocate post-op
            self.scaleFactor = 1
            self.transVec, self.scaleVec, self.orientVec = LM.Vector2:new_local(), LM.Vector2:new_local(), LM.Vector2:new_local()
            local bbox = rootLayer:Bounds(frame0)
            local bbWide, bbHigh = bbox.fMax.x - bbox.fMin.x, bbox.fMax.y - bbox.fMin.y

            -- set centered?
            if (self.recenter) then
                self.transVec:Set(-bbox.fMin.x -bbWide/2, -bbox.fMin.y -bbHigh/2)
            end

            -- origin
            self.orientVec:Set(bbox.fMin.x +bbWide/2, bbox.fMin.y +bbHigh/2) --- retain ratio
            local docRatioW, docRatioH = docRatio*2, 1*2
            local scaleIndex = self.scaleMode - SS_SVGImportDialog.SCALE_
            local scaleValue = self.scaleFactors[scaleIndex]
            self.scaleFactor = scaleValue

            if (scaleValue < 0) then  --< Scale to CANV
                if (bbWide > docRatioW) or (bbHigh > docRatioH) then -- shrink
                    if (bbWide/docRatioW) >= (bbHigh/docRatioH) then
                        self.scaleFactor = (docRatioW/bbWide)
                    else
                        self.scaleFactor = (docRatioH/bbHigh)
                    end
                elseif (bbWide < docRatioW) or (bbHigh < docRatioH) then -- grow
                    if (docRatioW/bbWide) >= (docRatioH/bbHigh) then
                        self.scaleFactor = (docRatioH/bbHigh)
                    else
                        self.scaleFactor = (docRatioW/bbWide)
                    end
                end
                self.scaleFactor = self.scaleFactor * math.abs(scaleValue)

            else  --< Scale asset
                self.scaleFactor = scaleValue
            end

            --==# Resize/Scale :recursive
            if ((self.scaleFactor ~= 1) or (self.recenter)) then
                SizeSVG(rootLayer) --< uses self.scaleFactor
            end
            self.endTime = os.clock()
            dprint(("------ Size DONE ------ @ %s - %s / %s"):format(os.date(), ssRound(self.lastTime - self.lastTime), ssRound(os.clock() - self.startTime)))
            self.lastTime3 = self.endTime


            --==# MashUp/Down Vectors neighbors & single layer groups - post-op consolidation :recursive
            local mashLoops, mashCount, wasMashCount = 0, 0
            repeat
                wasMashCount = mashCount
                if (self.minify and self.minifyPostOp) then
                    mashLoops = mashLoops + 1
                    mashCount = mashCount + MashSVG(rootLayer)
                end
            until (mashCount == wasMashCount) or (mashLoops > 10)  -- 10 = sanity chk


            moho:SetSelLayer(mohodoc:Layer(mohodoc:CountLayers() -1), false, true) -- go top layer
            rootLayer:Expand(SS_SVGImport.expandGroups)
            rootLayer:SetVisible(true)  -- was hidden during import

            self.lastTime = os.clock()
            dprint(("-----= Mash DONE =----- @ %s - %s / %s --- Loops: %d, Mashed: %d"):format(os.date(), ssRound(self.lastTime - self.lastTime3), ssRound(os.clock() - self.startTime), mashLoops, mashCount))

            if (laterPrint ~= "") then print(laterPrint) end

        else
            dprint("NOT an SVG Doc") -- todo

        end
        if (moho.frame ~= wasFrame) then moho:SetCurFrame(wasFrame) end
    else

        if (self.filename ~= "") then
            dprint("SVG File not Found: ", self.filename)
        else
            dprint("File not specified !")
        end
    end
end


-- -------------
-- Main Dialog
-- -------------

function SS_SVGImportDialog:new(moho)

    local d = LM.GUI.SimpleDialog("SimplSam - Import SVG files", SS_SVGImportDialog)
    local l = d:GetLayout()
    local padi = 20

    d.prefsDlog  = SS_SVGImportPrefsDialog:new()
    d.prefsPopup = LM.GUI.PopupDialog("☰") -- more settings ...
    d.prefsPopup:SetDialog(d.prefsDlog)
    d.prefsPopup:SetToolTip('Preference settings ...')

    l:PushH(LM.GUI.ALIGN_RIGHT)
        l:AddChild(d.prefsPopup, LM.GUI.ALIGN_RIGHT)
    l:Pop()
    l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL)

    l:PushH()
        l:AddChild(LM.GUI.StaticText("SVG Filename: "), LM.GUI.ALIGN_LEFT)
        d.filePathInput = LM.GUI.TextControl(360, ".", 0, LM.GUI.FIELD_TEXT)
        l:AddChild(d.filePathInput, LM.GUI.ALIGN_LEFT)
        l:AddChild(LM.GUI.Button("Browse ...", self.SELECT_FILE), LM.GUI.ALIGN_LEFT)
    l:Pop()
    l:PushH()
        l:AddChild(LM.GUI.StaticText("Recently used: "), LM.GUI.ALIGN_LEFT)
        d.recentUsedList = LM.GUI.TextList(520, 120, self.SELECT_RECENT)
        l:AddChild(d.recentUsedList, LM.GUI.ALIGN_LEFT)
        local iconFile = moho:UserAppDir() .. "/scripts/scriptresources/ss_tools/ss_bin.png"
        if (ss_tool_icon_bin_found or os.rename(iconFile, iconFile)) then  --< sess
            d.deleteButton = LM.GUI.ImageButton("ScriptResources/ss_tools/ss_bin", "Remove selected entry", false, self.SELECT_DELETE)
            ss_tool_icon_bin_found = ss_tool_icon_bin_found or true
        else
            if (SS_SVGImport._MOHO_Version >= 13.5) then
                d.deleteButton = LM.GUI.ShortButton("x", self.SELECT_DELETE)
            else
                d.deleteButton = LM.GUI.Button("X", self.SELECT_DELETE)
            end
            d.deleteButton:SetToolTip('Remove selected entry')
        end
        l:AddChild(d.deleteButton, LM.GUI.ALIGN_CENTER)
    l:Pop()
    l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) -- --

    l:PushH()
        l:PushV()
            ---------------
            l:AddChild(LM.GUI.StaticText("Scaling:"), LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.StaticText(""))

            ---------------
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            l:AddChild(LM.GUI.StaticText("Consolidation:"), LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.StaticText(""))
            l:AddChild(LM.GUI.StaticText(""))
            l:AddChild(LM.GUI.StaticText(""))
            l:AddChild(LM.GUI.StaticText(""))

            ---------------
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            l:AddChild(LM.GUI.StaticText("General:"), LM.GUI.ALIGN_LEFT)
        l:Pop()
        l:PushV()
            d.scaleMenu = LM.GUI.Menu('Scaling')
            for i, scale in ipairs(SS_SVGImport.scaleFactors) do
                if (scale == 1) then
                    d.scaleMenu:AddItem("100% (no scale)", 0, self.SCALE_ + i)
                elseif (scale < 0) then
                    d.scaleMenu:AddItem(("Fit to %s%% of Screen"):format(math.abs(scale) * 100), 0, self.SCALE_ + i)
                elseif (scale > 0) then
                    d.scaleMenu:AddItem(("%s%%"):format(scale * 100), 0, self.SCALE_ + i)
                else
                    d.scaleMenu:AddItem("", 0, self.SCALE_) ---
                end
            end

            d.scaleMenuPopup = LM.GUI.PopupMenu(140, true)
            d.scaleMenuPopup:SetMenu(d.scaleMenu)
            l:AddChild(d.scaleMenuPopup, LM.GUI.ALIGN_LEFT, 0)
            d.recenterCheck = LM.GUI.CheckBox('Center')
            l:AddChild(d.recenterCheck, LM.GUI.ALIGN_LEFT, 0)

            ---------------
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            d.minifyCheck = LM.GUI.CheckBox('Consolidate layers', d.MINIFY_)
            l:AddChild(d.minifyCheck, LM.GUI.ALIGN_LEFT)
            d.minifyLayersCheck = LM.GUI.CheckBox('Vector layers', d.MINIFY_LAYERS)
            l:AddChild(d.minifyLayersCheck, LM.GUI.ALIGN_LEFT, padi)
            d.minifyGroupsCheck = LM.GUI.CheckBox('Group layers', d.MINIFY_GROUPS)
            l:AddChild(d.minifyGroupsCheck, LM.GUI.ALIGN_LEFT, padi)
            d.minifyLayersUnnamedCheck = LM.GUI.CheckBox('Only Unnamed')
            l:AddChild(d.minifyLayersUnnamedCheck, LM.GUI.ALIGN_LEFT, padi)
            d.minifyPostOpCheck = LM.GUI.CheckBox('Post-Op reduce', d.MINIFY_POSTOP)
            l:AddChild(d.minifyPostOpCheck, LM.GUI.ALIGN_LEFT, padi)

            ---------------
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            d.expandGroupsCheck = LM.GUI.CheckBox('Expand Group tree')
            l:AddChild(d.expandGroupsCheck, LM.GUI.ALIGN_LEFT, 0)
        l:Pop()
        l:PushV()
            l:AddChild(LM.GUI.StaticText(" - Scale the Image relative to the Canvas or to itself"), LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.StaticText(" - Center the imported Image in the Canvas"), LM.GUI.ALIGN_LEFT)

            ---------------
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            l:AddChild(LM.GUI.StaticText(" - Enable / Disable Moho Layer consolidation (as below):"), LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.StaticText("- Combine adjacent Vector layers into one"), LM.GUI.ALIGN_LEFT, padi)
            l:AddChild(LM.GUI.StaticText("- Ungroup Groups which have only one child Vector layer"), LM.GUI.ALIGN_LEFT, padi)
            l:AddChild(LM.GUI.StaticText("- Do Not process 'significant' / named SVG layers"), LM.GUI.ALIGN_LEFT, padi)
            l:AddChild(LM.GUI.StaticText("- Maximize consolidation post-operation (** slow **)"), LM.GUI.ALIGN_LEFT, padi)

            ---------------
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            l:AddChild(LM.GUI.StaticText(" - Show expanded group / layer structure of import"), LM.GUI.ALIGN_LEFT)
        l:Pop()
    l:Pop()

    l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
    if (SS_SVGImport._MOHO_Version >= 13.5) then
        l:AddChild(LM.GUI.ShortButton("Reset", self.RESET), LM.GUI.ALIGN_RIGHT)
    else
        l:AddChild(LM.GUI.Button("Reset", self.RESET), LM.GUI.ALIGN_RIGHT)
    end

    return d
end

function SS_SVGImportDialog:UpdateWidgets()
    self.filePathInput:SetValue(self.filePathInputPre ~= "" and self.filePathInputPre or SS_SVGImport.filename)
    self.filePathInputPre = ""
    self.expandGroupsCheck:SetValue(SS_SVGImport.expandGroups)
    self.minifyCheck:SetValue(SS_SVGImport.minify)
    self.minifyLayersCheck:SetValue(SS_SVGImport.minifyLayers)
    self.minifyLayersUnnamedCheck:SetValue(SS_SVGImport.minifyLayersUnnamed)
    self.minifyGroupsCheck:SetValue(SS_SVGImport.minifyGroups)
    self.minifyPostOpCheck:SetValue(SS_SVGImport.minifyPostOp)
    self.minifyLayersCheck:Enable(SS_SVGImport.minify)
    self.minifyLayersUnnamedCheck:Enable(SS_SVGImport.minify)
    self.minifyGroupsCheck:Enable(SS_SVGImport.minify)
    self.minifyPostOpCheck:Enable(SS_SVGImport.minify)
    self.recenterCheck:SetValue(SS_SVGImport.recenter)
    self.scaleMenu:UncheckAll() -- reqd 4 reset
    self.scaleMenu:SetChecked(SS_SVGImport.scaleMode, true)
    for _, mru in pairs(SS_SVGImport.fileMru) do
        if ((mru ~= nil) and (mru ~= "")) then
            self.recentUsedList:AddItem(mru, false)
        end
    end
    self.scaleMenuPopup:Redraw() -- reqd Mac
end

function SS_SVGImportDialog:OnValidate()
end

function SS_SVGImportDialog:HandleMessage(msg)
    if (msg == self.SELECT_FILE) then
        local filen = LM.GUI.OpenFile("Get me some SVGs ...")
        if (filen ~= nil) and (filen ~= "")  then
            self.filePathInput:SetValue(filen)
        end

    elseif (msg == self.SELECT_RECENT) then
        self.filePathInput:SetValue(self.recentUsedList:SelItemLabel())

    elseif (msg == self.SELECT_DELETE) then
        local count = self.recentUsedList:CountItems()
        self.recentUsedList:RemoveItem(self.recentUsedList:SelItem(), false)
        if (self.recentUsedList:CountItems() ~= count) then
            SS_SVGImport.fileMru = {}
            for i = 1, self.recentUsedList:CountItems() do
                SS_SVGImport.fileMru[i] = self.recentUsedList:GetItem(i -1)
            end
        end

    elseif (msg == self.MINIFY_) then
        SS_SVGImport.minify = self.minifyCheck:Value()
        self.minifyLayersCheck:Enable(SS_SVGImport.minify)
        self.minifyGroupsCheck:Enable(SS_SVGImport.minify)
        self.minifyPostOpCheck:Enable(SS_SVGImport.minify)

    elseif (msg == self.MINIFY_LAYERS) then
        SS_SVGImport.minifyLayers = self.minifyLayersCheck:Value()

    elseif (msg == self.MINIFY_GROUPS) then
        SS_SVGImport.minifyGroups = self.minifyGroupsCheck:Value()

    elseif (msg == self.MINIFY_POSTOP) then
        SS_SVGImport.minifyPostOp = self.minifyPostOpCheck:Value()


    elseif (msg == self.RESET) then
        SS_SVGImport:ResetPrefs()
        -- for MH12 empty the TxtList after Reset
        for _ = 1, self.recentUsedList:CountItems() do
            self.recentUsedList:RemoveItem(0, false)
        end
        self:UpdateWidgets()
        self.scaleMenuPopup:Redraw()
    end
    self.minifyLayersUnnamedCheck:Enable(SS_SVGImport.minify and (SS_SVGImport.minifyLayers or SS_SVGImport.minifyGroups or SS_SVGImport.minifyPostOp))
end

function SS_SVGImportDialog:OnOK()
    SS_SVGImport.filename     = self.filePathInput:Value() or ""
    SS_SVGImport.recenter     = self.recenterCheck:Value()
    SS_SVGImport.scaleMode    = self.scaleMenu:FirstCheckedMsg()
    SS_SVGImport.expandGroups = self.expandGroupsCheck:Value()
    SS_SVGImport.minifyLayers = self.minifyLayersCheck:Value()
    SS_SVGImport.minifyLayersUnnamed = self.minifyLayersUnnamedCheck:Value()
    SS_SVGImport.minifyGroups = self.minifyGroupsCheck:Value()
    SS_SVGImport.minifyPostOp = self.minifyPostOpCheck:Value()

    local _, extName = SS_SVGImport.filename:match"^(.*)%.([^%.]*)$"
    if (SS_SVGImport.filename ~= "") and (extName == "svg") and os.rename(SS_SVGImport.filename, SS_SVGImport.filename) then
        if (SS_SVGImport.filename ~= SS_SVGImport.fileMru[1]) then
            local j = SS_SVGImport.FILE_MRU_MAX
            for i = 2, SS_SVGImport.FILE_MRU_MAX do
                if (SS_SVGImport.filename == SS_SVGImport.fileMru[i]) then
                    j = i
                    break
                end
            end
            for i = j, 2, -1 do
                SS_SVGImport.fileMru[i] = SS_SVGImport.fileMru[i -1]
            end
            SS_SVGImport.fileMru[1] = SS_SVGImport.filename
        end
    end
    -- Save Prefs iWant
end


-- -------------
-- Prefs Dialog
-- -------------

function SS_SVGImportPrefsDialog:new(moho)
    local d = LM.GUI.SimpleDialog("Panel Options", SS_SVGImportPrefsDialog)
    local l = d:GetLayout()

    l:PushH()
        l:PushV()
            d.browseFirstCheck = LM.GUI.CheckBox("Browse First",  self.PREF_BROWSE_FIRST)
            l:AddChild(d.browseFirstCheck, LM.GUI.ALIGN_LEFT)
            d.debugModeCheck = LM.GUI.CheckBox("Debug info", self.PREF_DEBUG_MODE)
            l:AddChild(d.debugModeCheck, LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            d.hideDialogCheck = LM.GUI.CheckBox("Hide me *", self.PREF_HIDE_DIALOG)
            l:AddChild(d.hideDialogCheck, LM.GUI.ALIGN_LEFT)
        l:Pop()
        l:PushV()
            l:AddChild(LM.GUI.StaticText(" - Browse for files, before showing dialog"), LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.StaticText(" - Show SVG import Info / Error output"), LM.GUI.ALIGN_LEFT)
            l:AddChild(LM.GUI.Divider(false), LM.GUI.ALIGN_FILL) ----
            l:AddChild(LM.GUI.StaticText(" - Dont show dialog! Restart/Reload Moho to reset"), LM.GUI.ALIGN_LEFT)
        l:Pop()

    l:Pop()

    return d
end

function SS_SVGImportPrefsDialog:UpdateWidgets()
    self.browseFirstCheck:SetValue(SS_SVGImport.browseFirst)
    self.browseFirstCheck:Enable(not SS_SVGImport.hideDialog)
    self.hideDialogCheck:SetValue(SS_SVGImport.hideDialog)
    self.debugModeCheck:SetValue(SS_SVGImport.debugMode)
end

function SS_SVGImportPrefsDialog:HandleMessage(msg)
    if (msg == self.PREF_BROWSE_FIRST) then
        SS_SVGImport.browseFirst = self.browseFirstCheck:Value()
    elseif (msg == self.PREF_DEBUG_MODE) then
        SS_SVGImport.debugMode = self.debugModeCheck:Value()
    elseif (msg == self.PREF_HIDE_DIALOG) then
        SS_SVGImport.hideDialog = self.hideDialogCheck:Value()
        self.browseFirstCheck:Enable(not SS_SVGImport.hideDialog)
    end
end

Icon
SS - SVG Import
Listed

Script type: Button/Menu

Uploaded: Oct 26 2022, 17:08

Last modified: Nov 14 2022, 09:20

Script Version: 01.12 #5211

Improved SVG vector import -- Layer Grouping, Masking, Colors, Adobe Illustrator & Affinity Designer friendly !! (ver 1.12 for Moho 12.5 & 13.5 only)
Note: Use with Moho 12.5 and 13.5 Only, as Moho 14 has this tool already built-in (File > Import > SVG File...)

The SVG Import tool is a Moho addon that allows you to reliably and faithfully import Scalable Vector Graphics (SVG) assets into Moho - ready for animation and presentation.


   Image

The tool was developed as an upgrade/replacement to the existing built-in SVG import process, and provides a range of enhanced features to improve 3rd party SVG application compatibility and end-user productivity:


Enhanced Features:

  - SVG Grouping & Elements translate to Moho Groups, Layers & Shapes - allowing you to retain naming & folder structures
  - Vastly improved compatibility with Adobe Illustrator exported SVGs
  - Support for Element & Group Transforms (translate, rotate, scale, matrix, skewX/Y)
  - Handles CSS Class, Inline Style and Presentation attribute styling - with Hex #000000 / #000, Named and RGB/RGBA colors
  - Full support for Masking with both ClipPath and Mask masks
  - Provides support for Arcs, Rounded Rect’s, Use (referencing) and partial support for inline embedded SVGs
  - Like-for-Like point count & positioning (no extraneous points)
  - Multiple Image Sizing & Layer consolidation options during import
  - Retain SVG element ID’s in Moho groups, layers and/or shapes
  - Partial support for Gradients (Gradients are averaged to a single color)
  - Respect/Ignore comments / comment blocks in the SVG files
  - Provides a handy MRU feature for re-loading Most Recent Used imported files
  - Dialog Options which determine the import workflow: Dialog > Browse > Load  -or-  Browse > Dialog > Load  -or-  Browse > Load


Video Demo




So … Why do you need the SVG Import tool?

The tool allows for a much greater range of vector based Characters, Props, Scenery and Artwork to be imported directly into Moho - allowing you to faithfully reproduce, manipulate and incorporate artwork from Illustrator, Inkscape, Affinity Designer, CorelDraw, Clip Studio Paint, Photopea + many other graphics/designer tools and SVG image repositories.

Some of the shortcomings of the built-in tool were its inability to fully support Grouping, Masking, CSS Styles, Short hex colors (#000), Use referencing and Arcs. Many of these features are used frequently in SVG imagery - which previously could lead to disfigured or discoloured artwork - or imported assets which failed to retain any grouping / layer structure or element naming.

----------------------------------------------------------------------------------------------------------------------------------------------------------------


    Image

Mashed Up example: car.svg image file imported with the default tool (Picasso would be proud). When the asset was imported with new SVG Import tool, the SVG image was more faithfully reproduced.

----------------------------------------------------------------------------------------------------------------------------------------------------------------


    Image

Layers & Naming example: monkey.svg image file imported with the default tool - which looks OK, but has no layers. When the asset was imported with the new SVG Import tool - the layer structure was retained.

----------------------------------------------------------------------------------------------------------------------------------------------------------------

Tool Options


    Image

  - Specify the SVG Filename to be imported, or browse for a SVG file
  - Use the Recently Used (MRU) feature to quickly select a previously imported file name. The delete button can be used to remove selected entries from the list
  - Set Scaling to scale the size of the imported image asset relative to the Canvas screen size or relative to the original size of the asset (default: 50% Screen)
  - Use Center to align the imported image to the middle-center location of the Canvas
  
  - Use Consolidation to minify or reduce the number of Groups & Layers created in Moho (default)
      - Select Vector Layers to combine adjacent vector layers into one (default)
      - Select Group Layers to fold/ungroup Group Layers than only have one Vector layer in it - resulting in just a Vector Layer (default)
      - Use Only Unnamed will avoid consolidating named Elements (i.e. Elements with SVG IDs)
      - Post-operation reduction will aggressively reduce Groups and Layers after initial import. Good for getting absolute minimum number of layers (whilst respecting masking), but generally bad if you are planning character-type animation. The process can also be Slow
  
  - Expand Group Tree will expand the imported layer group tree to show all groups & layers
  
  - Use Reset to restore default settings. OK to load & process the specified SVG file. Cancel to Cancel


Tool Preferences

There is an additional Preference settings menu (top right) which allows you to:

    Image

  - Browse for file First when the tool button is clicked. More like a traditional File-Open experience. The main Dialog will be shown after the File Browse
  - Use Debug Info to show some limited process and error information during the import process
  
  - Use Hide Me ** to completely hide the main dialog (on subsequent imports). The importer will behave even-more like a traditional File-Open-process experience. 

** This would typically be used if your settings remain the same during the majority of your imports. You can cancel out of Hide Me mode by Restarting/Reloading Moho


Version:

  - version: 01.12 MH12.5+ #521104
  - release: 1.12
  - by Sam Cogheil (SimplSam)


Notes & Limitations

The tool was designed to enhance the Moho SVG import capabilities whilst supporting most common SVG 1.1 features and respecting SVG grouping. The tool is not and never will be 100% SVG 1.1 compliant, and as such has some limitations:


Known Limitations:

  - Filenames (and paths) with non-english characters may not be loadable. Rename or relocate the files to fix.
  - SVG Document and Viewport dimensions are ignored
  - Measurement Units are ignored (i.e. em, px, pt, cm, mm, in, %). All treated as pixels (px)
  - Gradients are averaged to a single color (scripting limitation in MH13.5). Full Gradient color supported in Moho 14.
  - No support for SVG animate or Filter effects
  - Limited support for ‘stroke-linecap’. No support for ‘stroke-linejoin’ (platform limitation)
  - Image, Text, Marker, Symbol, Pattern and ‘Hidden/Display/Hide’ keywords are not supported
  - Limited SVG Syntax checking. SVG files should be valid before attempting import


Additional Notes:

  - The tool has an external dependency on the XML 2 Lua parser (pure Lua)
  - Compatible with MH12.5 - MH13.5
  - Optimized for MH13.5


Changelog:

  - 1.12 - Fix: Clipboard copy buffer
  - 1.11 - Fix: Filled Polyline’s
  - 1.10 - Add: Faux Gradient. Fix: line-width during Matrix scale. respect Unnamed during group & post-op reduce. un-default Unnamed option. +minor bugs.


Special Thanks to:

  - Stan (and the team): MOHO Scripting – https://mohoscripting.com
  - The friendly faces @ Lost Marble Moho forum – https://www.lostmarble.com/forum/
  - XML 2 Lua parser – https://github.com/manoelcampos/xml2lua (Manoel Campos da Silva Filho / Paul Chakravarti)
  - SVG documentation – https://developer.mozilla.org/en-US/docs/Web/SVG/ (Team Mozilla)
  - SVG technical ref – https://www.w3.org/TR/SVG11/ (SVG Working Group / W3C Team)
  - Arc to Path – https://github.com/BigBadaboom/androidsvg/…/utils/SVGAndroidRenderer.java (Paul LeBeau)

Installation Options:

This script, and all other scripts on this site are distributed as free software under the GNU General Public License 3.0 or later.
Downloads count: 3536