Question

I have path definition in this form (example):

<path d="M 20 30 L 20 20 20 40 40 40"/>

Which, in Lua, becomes:

"M 20 30 L 20 20 20 40 40 40"

How could I parse it in pure Lua to get something like:

{'M', 20, 30, 'L', 20, 20, 20, 40, 40, 40 }

Or, perfectly:

{{'M', 20, 30}, {'L', 20, 20}, {'L', 20, 40}, {'L', 40, 40}}

Does Lua patterns have such capabilities?

EDIT: I want to cover all valid SVG paths, or at least that Inkscape-generated. specification inkscape-generated path

Was it helpful?

Solution

Not directly, you would need a simplified parser around that of course.

Curiosity got the better of me, though usually I dislike "Do this work for me" posts

--- Parse svg `path` attribute into 2-D array
function parsePath(input)
    local output, line = {}, {};
    output[#output+1] = line;

    input = input:gsub("([^%s,;])([%a])", "%1 %2"); -- Convert "100D" to "100 D"
    input = input:gsub("([%a])([^%s,;])", "%1 %2"); -- Convert "D100" to "D 100"
    for v in input:gmatch("([^%s,;]+)") do
        if not tonumber(v) and #line > 0 then
            line = {};
            output[#output+1] = line;
        end
        line[#line+1] = v;
    end
    return output;
end

-- Test output
local input = 'M20 30L20 20,20 40;40 40 X1 2 3 12.8z';
local r = parsePath(input);
for i=1, #r do
    print("{ "..table.concat(r[i], ", ").." }");
end

Since Inkscape always seems to put a space between instructions and numbers, you could leave out the two gsub lines if you only parse files generated by Inkscape.

The function also throws away most random characters Inkscape likes to put into a path definition, however there may be some details for you to resolve if you really want to read all path definitions that conform to the standard.

Update (after skimming over the SVG BNF definition)

The SVG Standard states Superfluous white space and separators such as commas can be eliminated, however looking at the BNF notation I could not find any other separator than whitespace and comma.

So you could probably change the 2nd regular expression to "([^%a%d%.eE-]+)". But I figured that the following function would fit a lot better:

function parsePath(input)
    local out = {};

    for instr, vals in input:gmatch("([a-df-zA-DF-Z])([^a-df-zA-DF-Z]*)") do
        local line = { instr };
        for v in vals:gmatch("([+-]?[%deE.]+)") do
            line[#line+1] = v;
        end
        out[#out+1] = line;
    end
    return out;
end

-- Test output
local input = 'M20-30L20,20,20X40,40-40H1,2E1.7 1.8e22,3,12.8z';
local r = parsePath(input);
for i=1, #r do
    print("{ "..table.concat(r[i], ", ").." }");
end

This function is quite lenient in that it allows any unnecessary white space to be left out and does not validate any semantics other than that it will discard any data before the first letter that is not e or E.

It will also silently ignore any non-matching data.

If you want to only match existing instructions, you can replace the pattern ([a-df-zA-DF-Z])([^a-df-zA-DF-Z]*) with ([MmZzLlHhVvCcSsQqTtAa])([^MmZzLlHhVvCcSsQqTtAa]*). However this will cause all values of a non-existing instruction to be added to the previous instruction, so I do not think this is a good idea, better to parse a superset and throw errors on semantics later.

OTHER TIPS

local path = 'M 20 30 L 20 20 20 40 40 40'

local s, t = '', {}
for c, x, y in path:gmatch'(%a?)%s*(%d+)%s*(%d+)' do
   s = (s..c):sub(-1)
   t[#t+1] = {s, tonumber(x), tonumber(y)}
end
-- Now t == {{'M', 20, 30}, {'L', 20, 20}, {'L', 20, 40}, {'L', 40, 40}}

I like the solution of Egor, but it doesn't work with decimals and letters V and H, so:

local function parsePath (input)
    input = input:gsub("([^%s,;])([%a])", "%1 %2") -- Convert "100D" to "100 D"
    input = input:gsub("([%a])([^%s,;])", "%1 %2") -- Convert "D100" to "D 100"
    local output, line = {}
    for v in input:gmatch("([^%s,;]+)") do
        if tonumber(v) then
            line[#line+1] = math.floor(tonumber(v)+0.5)
        else
            line = {v}
            output[#output+1] = line
        end
    end
    return output
end

Run it:

local ds = {
    "M40,360H-40", -- line
    "M -40,480 H 40", -- line
    "M 840,1000 V 920 M 720,920 V 1000", -- two lines
    "M 1280.1,-39.9 1200.1,39.9", -- line with decimals
    "M 1320,40 1400,-40",
    "M 40,480 H 360",
    "M 760,360 400,360",
    "M 1120,240 1320,40",
    "M 400,360 40,360",
    
    "M 840,920 C 840,680 1040,320 1120,240", -- cubic bezier
    "M 1200,40 C 1160,80 1120,120 1080,160",
    "M 1080,160 C 920,320 520,360 400,360",
    "M 360,480 C 520,480 720,760 720,920",
    "M 760,360 C 640,360 560,520 640,600",
    "M 640,600 C 720,680 840,640 880,560",
    "M 880,560 C 920,480 880,360 760,360",
    "M 1080,160 C 1040,200 920,360 760,360",
    "M 360,480 C 480,480 560,520 640,600",
    "M 640,600 C 720,680 720,840 720,920",
    "M 840,920 C 840,800 840,640 880,560",
    "M 880,560 C 920,480 1120,240 1120,240",
    
    "M 0,600 H 360 L 600,840 V 960 H 0",
    "M 1440,0 H 1920 V 960 H 960 V 720",
    "M 0,0 H 1080 L 840,240 H 0"
}

for i, d in ipairs (ds) do
    local parsedPath = parsePath (d)
    local str = '{'
    for i, component in ipairs (list) do 
        str = str .. '{'.. table.concat(component, ',') ..'},'
    end
    str = str:sub(1, -2) -- remove last comma
    str = str .. '}'
    print (str)
end

Result:

{{M,40,360},{H,-40}}
{{M,-40,480},{H,40}}
{{M,840,1000},{V,920}}
{{M,720,920},{V,1000}}
{{M,1200,-40},{V,40}}
{{M,1320,40},{V,-40}}
{{M,840,920},{C,840,800,1000,560,1080,480},{M,1080,480},{C,1160,400,1320,240,1320,40}}
{{M,1200,40},{C,1200,120,1160,160,1080,200},{M,1080,200},{C,920,280,720,360,480,360},{M,480,360},{C,3360,360,120,360,40,360}}
{{M,40,480},{C,40,480,240,480,360,480,520,480,720,760,720,920}}
{{M,760,400},{C,640,400,560,520,640,600}}
{{M,640,600},{C,720,680,800,640,840,600}}
{{M,840,600},{C,880,560,880,400,760,400}}
{{M,1080,200},{C,920,280,920,400,760,400}}
{{M,760,400},{C,640,400,600,360,480,360}}
{{M,360,480},{C,480,480,560,520,640,600}}
{{M,640,600},{C,720,680,720,840,720,920}}
{{M,840,920},{C,840,800,760,680,840,600}}
{{M,840,600},{C,880,560,1040,520,1080,480}}
{{M,0,0},{V,240},{H,720},{C,720,240,1080,120,1080,120},{V,0}}
{{M,0,0},{H,1080},{V,120},{L,840,240},{H,0}}
{{M,0,600},{H,360},{L,600,840},{V,960},{H,0}}
{{M,1440,0},{H,1920},{V,960},{H,960},{V,840},{L,1200,800,1400,600,1440,360}}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top