├── examples ├── favicon.ico ├── app_usahigh.lua ├── app_path.lua ├── app_circle.lua ├── app_lines.lua └── app_arcs02.lua ├── testy ├── images │ ├── charicature.jpg │ ├── charicature_small.jpg │ └── textelem.svg ├── experimental.js ├── pentagram.lua ├── test_table.lua ├── test_radialgradient.lua ├── test_lineargradient.lua ├── test_builder.lua ├── netview ├── translatesvg.lua ├── gensvg ├── test_svgattributes.lua ├── ezsvg_example2.lua ├── test_nameplate.lua ├── test_inherit.lua ├── ezsvg_example1.lua ├── test_elemiterator.lua ├── ezsvg_features.lua ├── test_patterns.lua └── test_literal.lua ├── remotesvg ├── Bezier.lua ├── ctypes.lua ├── SVGStream.lua ├── stream.lua ├── jskeycodes.lua ├── filestream.lua ├── parsesvg.lua ├── maths.lua ├── matrix2D.lua ├── sharesvg.htm ├── viewsvg_htm.lua ├── SVGXmlParser.lua ├── SVGInteractor.lua ├── colors.lua ├── memorystream.lua ├── SVGElements.lua ├── SVGAttributes.lua └── SVGParser.lua ├── LICENSE └── README.md /examples/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wiladams/remotesvg/HEAD/examples/favicon.ico -------------------------------------------------------------------------------- /testy/images/charicature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wiladams/remotesvg/HEAD/testy/images/charicature.jpg -------------------------------------------------------------------------------- /testy/images/charicature_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wiladams/remotesvg/HEAD/testy/images/charicature_small.jpg -------------------------------------------------------------------------------- /testy/images/textelem.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | This is the text 10 | -------------------------------------------------------------------------------- /testy/experimental.js: -------------------------------------------------------------------------------- 1 | // For loading an image 2 | // without flicker 3 | var downloadingImage = new Image(); 4 | downloadingImage.onload = function(){ 5 | document.images['myScreen'].src = this.src; 6 | } 7 | 8 | downloadingImage.src = asrc; 9 | -------------------------------------------------------------------------------- /remotesvg/Bezier.lua: -------------------------------------------------------------------------------- 1 | -- Bezier.lua 2 | -- Evaluate a single dimension along a bezier curve 3 | -- call multiple times with components of each dimension 4 | local function eval(t, p0, p1, p2, p3) 5 | local it = 1.0-t; 6 | return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; 7 | end 8 | 9 | return { 10 | eval = eval; 11 | } -------------------------------------------------------------------------------- /testy/pentagram.lua: -------------------------------------------------------------------------------- 1 | local doc = svg { 2 | width="304", 3 | height="290", 4 | --[[ 5 | style { 6 | fill = "#FB2"; 7 | stroke = "#BBB"; 8 | stroke_width = 15; 9 | stroke_linejoin = "round" 10 | }, 11 | --]] 12 | path { 13 | d="M2,111 h300 l-242.7,176.3 92.7,-285.3 92.7,285.3 z", 14 | 15 | style="fill:#FB2;stroke:#BBB;stroke-width:15;stroke-linejoin:round" 16 | }, 17 | } 18 | 19 | return doc 20 | -------------------------------------------------------------------------------- /remotesvg/ctypes.lua: -------------------------------------------------------------------------------- 1 | local function isspace(c) 2 | return c == string.byte(' ') or 3 | c == string.byte('\t') or 4 | c == string.byte('\n') or 5 | c == string.byte('\v') or 6 | c == string.byte('\f') or 7 | c == string.byte('\r') 8 | end 9 | 10 | 11 | local function isdigit(c) 12 | return c >= string.byte('0') and 13 | c <= string.byte('9') 14 | end 15 | 16 | local function isnum(c) 17 | return isdigit(c) or 18 | c == string.byte('+') or 19 | c == string.byte('-') or 20 | c == string.byte('e') or 21 | c == string.byte('E') 22 | end 23 | 24 | return { 25 | isspace = isspace; 26 | isdigit = isdigit; 27 | isnum = isnum; 28 | } -------------------------------------------------------------------------------- /testy/test_table.lua: -------------------------------------------------------------------------------- 1 | local tbl = { 2 | name1 = "value1"; 3 | name2 = "value2"; 4 | 5 | [[ 6 | some text here 7 | ]]; 8 | 9 | name3 = "value3"; 10 | 11 | [[ 12 | followed by more text 13 | ]]; 14 | 15 | function() 16 | print("this is a test") 17 | end, 18 | 19 | 20 | name4 = "value4"; 21 | } 22 | 23 | -- This one will print all the things in the table 24 | -- with the literals having a key type that is numeric 25 | for name, value in pairs(tbl) do 26 | print(name, value); 27 | end 28 | 29 | -- This one will only print the literals, starting 30 | -- from an index of '1' 31 | for idx, value in ipairs(tbl) do 32 | print(idx, value); 33 | end 34 | 35 | -------------------------------------------------------------------------------- /examples/app_usahigh.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | local SVGInteractor = require("remotesvg.SVGInteractor") 6 | require("remotesvg.SVGElements")() 7 | 8 | 9 | local ImageStream = size() 10 | 11 | --[[ 12 | -- BUGBUG 13 | -- need to set focus on element 14 | function keyDown(activity) 15 | print(activity.action, activity.json); 16 | end 17 | --]] 18 | 19 | function mouseDown(activity) 20 | print("activity: ", activity.action, activity.id, activity.which, activity.x, activity.y) 21 | end 22 | 23 | function mouseMove(activity) 24 | print("activity: ", activity.action, activity.x, activity.y) 25 | end 26 | 27 | local doc = require("usaHigh_svg") 28 | 29 | doc:write(ImageStream); 30 | 31 | run() 32 | -------------------------------------------------------------------------------- /testy/test_radialgradient.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | require("remotesvg.SVGElements")() 6 | 7 | local doc = svg { 8 | ['xmlns:xlink']="http://www.w3.org/1999/xlink", 9 | width="120", 10 | height="120", 11 | viewBox="0 0 120 120", 12 | 13 | defs { 14 | radialGradient {id="MyGradient", 15 | stop {offset="10%", ['stop-color']="gold"}, 16 | stop {offset="95%", ['stop-color']="green"}, 17 | }, 18 | }, 19 | 20 | circle {fill="url(#MyGradient)", 21 | cx="60", cy="60", r="50" 22 | }, 23 | } 24 | 25 | local FileStream = require("remotesvg.filestream") 26 | local SVGStream = require("remotesvg.SVGStream") 27 | local ImageStream = SVGStream(FileStream.open("test_radialgradient.svg")) 28 | 29 | doc:write(ImageStream); 30 | -------------------------------------------------------------------------------- /testy/test_lineargradient.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | require("remotesvg.SVGElements")() 6 | local FileStream = require("remotesvg.filestream") 7 | local SVGStream = require("remotesvg.SVGStream") 8 | 9 | 10 | local ImageStream = SVGStream(FileStream.open("test_lineargradient.svg")) 11 | 12 | local doc = svg { 13 | width = "120", 14 | height = "120", 15 | viewBox = "0 0 120 120", 16 | ['xmlns:xlink'] ="http://www.w3.org/1999/xlink", 17 | 18 | defs{ 19 | linearGradient {id="MyGradient", 20 | stop {offset="5%", ['stop-color']="green"}; 21 | stop {offset="95%", ['stop-color']="gold"}; 22 | } 23 | }, 24 | 25 | rect { 26 | fill="url(#MyGradient)", 27 | x=10, y=10, width=100, height=100, 28 | }, 29 | } 30 | 31 | doc:write(ImageStream); 32 | -------------------------------------------------------------------------------- /examples/app_path.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | local SVGInteractor = require("remotesvg.SVGInteractor") 6 | require("remotesvg.SVGElements")() 7 | 8 | local ImageStream = size() 9 | 10 | 11 | function mouseMove(activity) 12 | print("mouseMove: ", activity.x, activity.y) 13 | end 14 | 15 | --[[ 16 | 17 | --]] 18 | local function draw(ImageStream) 19 | ImageStream:reset(); 20 | 21 | local p1 = path { 22 | fill="red", 23 | stroke="blue", 24 | ["stroke-width"]=3 25 | }; 26 | 27 | p1:moveTo(100, 100); 28 | p1:lineTo(300, 100); 29 | p1:lineTo(200, 300); 30 | p1:close(); 31 | 32 | local doc = svg { 33 | width="4cm", 34 | height="4cm", 35 | viewBox="0 0 400 400", 36 | --onload="alert('svg loaded');"; 37 | 38 | rect { 39 | x="1", y="1", 40 | width="398", height="398", 41 | fill="none", stroke="blue"}; 42 | 43 | p1; 44 | } 45 | 46 | doc:write(ImageStream); 47 | end 48 | 49 | draw(ImageStream); 50 | run(); 51 | 52 | -------------------------------------------------------------------------------- /testy/test_builder.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | local SVGInteractor = require("remotesvg.SVGInteractor") 6 | require("remotesvg.SVGElements")() 7 | 8 | 9 | local ImageStream = size() 10 | 11 | 12 | local function draw(ImageStream) 13 | ImageStream:reset(); 14 | 15 | -- Show outline of canvas using 'rect' element 16 | 17 | -- demonstrate document format API 18 | local doc = svg { 19 | width = "12cm", height= "4cm", 20 | viewBox="0 0 1200 400", 21 | 22 | 23 | circle { 24 | fill="red", 25 | stroke="blue", 26 | ["stroke-width"]=10, 27 | cx =600, 28 | cy = 200, 29 | r = 100 30 | }, 31 | 32 | } 33 | 34 | -- Demonstrate programmatic builder API 35 | doc:append('rect') 36 | :attr('x', 1) 37 | :attr('y', 1) 38 | :attr('width', 1198) 39 | :attr('height', 398) 40 | :attr('fill', "none") 41 | :attr('stroke', "blue") 42 | :attr('stroke-width', 2); 43 | 44 | doc:write(ImageStream); 45 | end 46 | 47 | draw(ImageStream); 48 | 49 | run() 50 | -------------------------------------------------------------------------------- /examples/app_circle.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | local SVGInteractor = require("remotesvg.SVGInteractor") 6 | require("remotesvg.SVGElements")() 7 | 8 | 9 | local keycodes = require("remotesvg.jskeycodes") 10 | 11 | 12 | local ImageStream = size() 13 | 14 | 15 | function mouseMove(activity) 16 | print("mouseMove: ", activity.x, activity.y) 17 | end 18 | 19 | local function drawCircle(filename) 20 | ImageStream:reset(); 21 | 22 | local doc = svg { 23 | width = "12cm", height= "4cm", 24 | viewBox="0 0 1200 400", 25 | 26 | -- Show outline of canvas using 'rect' element 27 | rect {x = 1, y = 1, width = 1198, height = 398, 28 | fill = "none", 29 | stroke = "blue", 30 | ["stroke-width"] = 2 31 | }, 32 | 33 | circle { 34 | fill="red", 35 | stroke="blue", 36 | ["stroke-width"]=10, 37 | cx =600, 38 | cy = 200, 39 | r = 100 40 | }, 41 | 42 | } 43 | 44 | doc:write(ImageStream); 45 | end 46 | 47 | 48 | function frame() 49 | drawCircle(); 50 | end 51 | 52 | run() 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 William Adams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /testy/netview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | --[[ 4 | This little command will read in a .lua file which contains nothing 5 | more than a svg document table, and will generate the .svg file for it. 6 | 7 | The input is the name of the 'document' file, without the lua extension. 8 | So, for example, if you have a file 'mysvg.lua', the command would be: 9 | 10 | $ ./gensvg mysvg 11 | 12 | Which will generate the appropriate svg to stdout. You can then 13 | redirect this to whatever file you actually want it to be stored 14 | in, or redirect to another tool. 15 | 16 | 17 | --]] 18 | package.path = "../?.lua;"..package.path; 19 | require("remotesvg.SVGElements")() 20 | 21 | 22 | -- read the source file 23 | -- pass in the name of your SVG document.lua file, without 24 | -- the '.lua' extension. 25 | local filename = arg[1] 26 | if not filename then return end 27 | 28 | local doc = require(filename) 29 | 30 | -- generate svg and view it as a web page. 31 | -- access as: http://localhost:8080/screen 32 | -- Ctl-C to end 33 | 34 | local SVGInteractor = require("remotesvg.SVGInteractor") 35 | local ImageStream = size(); 36 | doc:write(ImageStream) 37 | run(); -------------------------------------------------------------------------------- /testy/translatesvg.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | --[[ 4 | This little command will read in a .lua file which contains nothing 5 | more than a svg document table, and will generate the .svg file for it. 6 | 7 | The input is the name of the 'document' file, without the lua extension. 8 | So, for example, if you have a file 'mysvg.lua', the command would be: 9 | 10 | $ ./gensvg mysvg 11 | 12 | Which will generate the appropriate svg to stdout. You can then 13 | redirect this to whatever file you actually want it to be stored 14 | in, or redirect to another tool. 15 | 16 | 17 | --]] 18 | package.path = "../?.lua;"..package.path; 19 | local parser = require("remotesvg.parsesvg") 20 | 21 | -- read the source file 22 | -- pass in the name of your SVG document.lua file, without 23 | -- the '.lua' extension. 24 | local filename = arg[1] 25 | if not filename then return end 26 | 27 | local doc = parser:parseFile(filename); 28 | doc:parseAttributes(); 29 | 30 | -- generate the svg and write to stdout 31 | local FileStream = require("remotesvg.filestream") 32 | local SVGStream = require("remotesvg.SVGStream") 33 | local ImageStream = SVGStream(FileStream.new(io.stdout)) 34 | 35 | doc:write(ImageStream) 36 | -------------------------------------------------------------------------------- /testy/gensvg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | --[[ 4 | This little command will read in a .lua file which contains nothing 5 | more than a svg document table, and will generate the .svg file for it. 6 | 7 | The input is the name of the 'document' file, without the lua extension. 8 | So, for example, if you have a file 'mysvg.lua', the command would be: 9 | 10 | $ ./gensvg mysvg 11 | 12 | Which will generate the appropriate svg to stdout. You can then 13 | redirect this to whatever file you actually want it to be stored 14 | in, or redirect to another tool. 15 | 16 | 17 | --]] 18 | package.path = "../?.lua;"..package.path; 19 | require("remotesvg.SVGElements")() 20 | 21 | 22 | -- read the source file 23 | -- pass in the name of your SVG document.lua file, without 24 | -- the '.lua' extension. 25 | local filename = arg[1] 26 | if not filename then return end 27 | 28 | local doc = require(filename) 29 | 30 | 31 | -- generate the svg and write to stdout 32 | local FileStream = require("remotesvg.filestream") 33 | local SVGStream = require("remotesvg.SVGStream") 34 | local ImageStream = SVGStream(FileStream.new(io.stdout)) 35 | 36 | doc:write(ImageStream) 37 | 38 | -- If you want to run it as a web page, use the following 39 | --local SVGInteractor = require("remotesvg.SVGInteractor") 40 | --local ImageStream = size(); 41 | --doc:write(ImageStream) 42 | --run(); -------------------------------------------------------------------------------- /remotesvg/SVGStream.lua: -------------------------------------------------------------------------------- 1 | --SVGStream.lua 2 | 3 | local SVGStream = {} 4 | setmetatable(SVGStream, { 5 | __call = function(self, ...) 6 | return self:new(...) 7 | end, 8 | }) 9 | local SVGStream_mt = { 10 | __index = SVGStream; 11 | } 12 | 13 | function SVGStream.init(self, baseStream) 14 | local obj = { 15 | BaseStream = baseStream; 16 | TopElement = {}; 17 | Elements = {}; 18 | } 19 | setmetatable(obj, SVGStream_mt); 20 | 21 | return obj; 22 | end 23 | 24 | function SVGStream.new(self, baseStream) 25 | return self:init(baseStream); 26 | end 27 | 28 | 29 | function SVGStream.reset(self) 30 | --strm:write('\n'); 31 | 32 | return self.BaseStream:reset(); 33 | end 34 | 35 | 36 | function SVGStream.openElement(self, elName) 37 | self.CurrentOpenTag = elName; 38 | self.BaseStream:writeString("<"..elName) 39 | end 40 | 41 | function SVGStream.closeTag(self) 42 | self.BaseStream:writeString(">\n") 43 | self.CurrentOpenTag = nil; 44 | end 45 | 46 | 47 | function SVGStream.writeAttribute(self, name, value) 48 | self.BaseStream:writeString(" "..name.." = '"..value.."'"); 49 | end 50 | 51 | function SVGStream.closeElement(self, elName) 52 | if elName then 53 | self.BaseStream:writeString("\n"); 54 | else 55 | self.BaseStream:writeString(" />\n"); 56 | end 57 | end 58 | 59 | function SVGStream.write(self, str) 60 | self.BaseStream:writeString(str); 61 | end 62 | 63 | return SVGStream 64 | -------------------------------------------------------------------------------- /examples/app_lines.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | local SVGInteractor = require("remotesvg.SVGInteractor") 6 | require("remotesvg.SVGElements")() 7 | 8 | 9 | local ImageStream = size() 10 | 11 | 12 | function mouseDown(activity) 13 | print("mouseDown: ", activity.which) 14 | end 15 | 16 | function mouseMove(activity) 17 | print("mouseMove: ", activity.x, activity.y) 18 | end 19 | 20 | local function draw(strm) 21 | strm:reset(); 22 | 23 | local doc = svg { 24 | width = "12cm", 25 | height= "4cm", 26 | viewBox="0 0 1200 400", 27 | } 28 | 29 | 30 | doc:append('rect') 31 | :attr("x", 1) 32 | :attr("y", 2) 33 | :attr("width", 1198) 34 | :attr("height", 398) 35 | :attr("fill", "none") 36 | :attr("stroke", "blue") 37 | :attr("stroke-width", 2); 38 | 39 | local l1 = line({x1=100, y1=300, x2=300, y2=100, stroke = "green", ["stroke-width"]=5}); 40 | local l2 = line({x1=300, y1=300, x2=500, y2=100, stroke = "green", ["stroke-width"]=20}); 41 | local l3 = line({x1=500, y1=300, x2=700, y2=100, stroke = "green", ["stroke-width"]=25}); 42 | local l4 = line({x1=700, y1=300, x2=900, y2=100, stroke = "green", ["stroke-width"]=20}); 43 | local l5 = line({x1=900, y1=300, x2=1100, y2=100, stroke = "green", ["stroke-width"]=25}); 44 | 45 | 46 | --doc:append(r1); 47 | doc:append(l1); 48 | doc:append(l2); 49 | doc:append(l3); 50 | doc:append(l4); 51 | doc:append(l5); 52 | 53 | 54 | doc:write(strm); 55 | end 56 | 57 | 58 | draw(ImageStream); 59 | 60 | run() 61 | -------------------------------------------------------------------------------- /testy/test_svgattributes.lua: -------------------------------------------------------------------------------- 1 | package.path = "../?.lua;"..package.path 2 | 3 | local sattr = require("remotesvg.SVGAttributes") 4 | local parse = sattr.parseAttribute; 5 | local matrix2D = require("remotesvg.matrix2D") 6 | 7 | local svg = require("remotesvg.SVGElements") 8 | 9 | local function test_rect() 10 | local r1 = svg.rect { 11 | x = "10", 12 | y = 10, 13 | width = "100", 14 | height = "200" 15 | } 16 | 17 | r1:parseAttributes(); 18 | 19 | for k,v in pairs(r1) do 20 | print(k, v, type(v)) 21 | end 22 | 23 | end 24 | 25 | function test_color() 26 | local function printcolor(value) 27 | local name, num = parse("color", value) 28 | print("color - ", value, num) 29 | end 30 | 31 | printcolor("red") 32 | printcolor("#888888") 33 | printcolor("#CCC") 34 | printcolor("rgb(255, 0, 0)") 35 | printcolor("rgb(50%, 25%, 100%)") 36 | end 37 | 38 | function test_coord() 39 | local function printcoord(str) 40 | local name, v = parse('cx', str) 41 | print(name, str, v.value, v.units, v) 42 | end 43 | 44 | printcoord('100%') 45 | printcoord('75') 46 | printcoord('12cm') 47 | printcoord('-306in') 48 | 49 | end 50 | 51 | function test_style() 52 | local style = 'stop-color:#191b21;stop-opacity:0.34437087;' 53 | local name, value = parse("style", style) 54 | print("STYLE: ", name, value); 55 | end 56 | 57 | function test_viewbox() 58 | local name, vbox = parse("viewBox", "0 50 10 100") 59 | print(vbox) 60 | 61 | local name, vbox = parse("viewBox", "20, 30, 100, 200") 62 | print(vbox) 63 | 64 | end 65 | 66 | function test_matrix() 67 | local m = matrix2D.translation(20,30) 68 | print(m) 69 | end 70 | 71 | --test_color(); 72 | --test_coord() 73 | --test_matrix(); 74 | --test_rect(); 75 | test_style(); 76 | --test_viewbox(); 77 | -------------------------------------------------------------------------------- /testy/ezsvg_example2.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | -- A translation of this 6 | -- https://github.com/cappelnord/EzSVG/blob/master/example2.lua 7 | 8 | local abs = math.abs 9 | local cos = math.cos 10 | local sin = math.sin 11 | 12 | 13 | require("remotesvg.SVGElements")() 14 | 15 | local dim = 500 16 | 17 | local doc = svg { 18 | width = dim, height = dim, 19 | 20 | rect { 21 | x = 0, y = 0, 22 | width = dim, height = dim, 23 | stroke = "none", 24 | fill = "rgbcolor(40,40,40)" 25 | }, 26 | 27 | stroke_linecap= "round", 28 | fill= "white", 29 | stroke= "white", 30 | 31 | } 32 | 33 | 34 | local group = g {} 35 | local circles = g{} 36 | 37 | doc:append(group) 38 | doc:append(circles) 39 | 40 | 41 | local num = 800 42 | for i=0,num do 43 | local norm = i / num 44 | 45 | local length = dim/2.1 * (0.5 + abs(sin(norm * 80)) * 0.5) 46 | local lw = abs(sin(norm*200)) * 1 47 | 48 | local x = dim/2 + sin(norm * 50) * length 49 | local y = dim/2 + cos(norm * 50) * length 50 | 51 | local line = line { 52 | x1 = dim/2, 53 | y1 = dim/2, 54 | x2 = x, y2=y, 55 | 56 | stroke_width= lw + 0.3 57 | } 58 | 59 | group:append(line) 60 | 61 | local circ = circle { 62 | cx = x, cy = y, 63 | r = lw * 15, 64 | 65 | fill= "#000000", 66 | stroke_width = 1 67 | } 68 | 69 | circles:append(circ) 70 | 71 | end 72 | 73 | 74 | doc:append(circle { 75 | cx=dim/2, cy=dim/2, r=60, 76 | fill= "black", 77 | stroke= "white", 78 | stroke_width = 5 79 | }) 80 | 81 | local FileStream = require("remotesvg.filestream") 82 | local SVGStream = require("remotesvg.SVGStream") 83 | local ImageStream = SVGStream(FileStream.open("ezsvg_example2.svg")) 84 | 85 | doc:write(ImageStream) 86 | -------------------------------------------------------------------------------- /testy/test_nameplate.lua: -------------------------------------------------------------------------------- 1 | package.path = "../?.lua;"..package.path; 2 | 3 | require("remotesvg.SVGElements")() 4 | 5 | local doc = svg { 6 | ['xmlns:xlink']="http://www.w3.org/1999/xlink", 7 | width="7.50in", 8 | height="2.00in", 9 | viewBox="0 0 540 144", 10 | --[[ 11 | defs { 12 | radialGradient {id="MyGradient", 13 | stop {offset="10%", ['stop-color']="gold"}, 14 | stop {offset="95%", ['stop-color']="green"}, 15 | }, 16 | }, 17 | 18 | circle {fill="url(#MyGradient)", 19 | cx="60", cy="60", r="50" 20 | }, 21 | --]] 22 | -- The outline to be cut out 23 | rect { 24 | --fill="url(#MyGradient)", 25 | fill="none", 26 | stroke="black", 27 | x=0, y=0, width="7.5in", height="2in", 28 | }, 29 | 30 | image { 31 | stroke = "red", 32 | id = "faceimage", 33 | viewBox = "0 0 240 320", 34 | x = "0.75in", 35 | y = "0", 36 | width = "1.0in", 37 | height = "2.0in", 38 | ["xlink:href"] = "images/charicature_small.jpg", 39 | }, 40 | 41 | -- shaded rectangle left side 42 | --[[ 43 | rect { 44 | fill="gray", 45 | ["fill-opacity"] = "0.4", 46 | stroke="black", 47 | x=0, y=0, width="1.5in", height="2in", 48 | }, 49 | --]] 50 | 51 | ellipse { 52 | fill = "none", 53 | stroke = "black", 54 | cx = "0.5in", 55 | cy = "1.25in", 56 | rx = "0.188in", 57 | ry = "0.188in" 58 | }; 59 | 60 | ellipse { 61 | fill = "none", 62 | stroke = "black", 63 | cx = "7in", 64 | cy = "1.25in", 65 | rx = "0.188in", 66 | ry = "0.188in" 67 | }; 68 | } 69 | 70 | local FileStream = require("remotesvg.filestream") 71 | local SVGStream = require("remotesvg.SVGStream") 72 | local ImageStream = SVGStream(FileStream.open("test_nameplate.svg")) 73 | 74 | doc:write(ImageStream); 75 | -------------------------------------------------------------------------------- /testy/test_inherit.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Create a new class that inherits from a base class 3 | -- 4 | function inheritsFrom( baseClass ) 5 | 6 | -- The following lines are equivalent to the SimpleClass example: 7 | 8 | -- Create the table and metatable representing the class. 9 | local new_class = {} 10 | local class_mt = { __index = new_class } 11 | 12 | -- Note that this function uses class_mt as an upvalue, so every instance 13 | -- of the class will share the same metatable. 14 | -- 15 | function new_class:create() 16 | local newinst = {} 17 | setmetatable( newinst, class_mt ) 18 | return newinst 19 | end 20 | 21 | -- The following is the key to implementing inheritance: 22 | 23 | -- The __index member of the new class's metatable references the 24 | -- base class. This implies that all methods of the base class will 25 | -- be exposed to the sub-class, and that the sub-class can override 26 | -- any of these methods. 27 | -- 28 | if baseClass then 29 | setmetatable( new_class, { __index = baseClass } ) 30 | end 31 | 32 | return new_class 33 | end 34 | 35 | 36 | local function test_animal() 37 | local Animal = {} 38 | local Animal_mt = { 39 | __index = Animal; 40 | } 41 | 42 | function Animal.new(kind) 43 | local obj = { 44 | kind = kind; 45 | } 46 | 47 | setmetatable(obj, Animal_mt) 48 | return obj; 49 | end 50 | 51 | 52 | function Animal.whatKind(self) 53 | print(self.kind) 54 | end 55 | 56 | 57 | 58 | local Dog = inheritsFrom(Animal); 59 | 60 | 61 | function Dog.new(self) 62 | local obj = { 63 | kind = "canine"; 64 | } 65 | setmetatable(obj, Dog_mt) 66 | 67 | return obj; 68 | end 69 | 70 | function Dog.bark(self) 71 | print("I am a dog: woof!") 72 | end 73 | 74 | -- test 75 | local a = Animal.new("ostrich") 76 | a:whatKind(); 77 | 78 | local d = Dog:new() 79 | d:whatKind(); 80 | d:bark(); 81 | end 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | test_animal(); 90 | -------------------------------------------------------------------------------- /remotesvg/stream.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Abstract interface for a stream object 3 | 4 | -- Attributes of the stream 5 | function Stream:GetLength() 6 | end 7 | 8 | function Stream:GetPosition() 9 | end 10 | 11 | function Stream:Seek(offset, origin) 12 | end 13 | 14 | -- Reading interface 15 | function Stream:ReadByte() 16 | return nil; 17 | end 18 | 19 | function Stream:ReadBytes(buffer, len, offset) 20 | return 0 21 | end 22 | 23 | function Stream:ReadString(count) 24 | return nil 25 | end 26 | 27 | 28 | -- Writing interface 29 | function Stream:WriteByte(value) 30 | return 0 31 | end 32 | 33 | function Stream:WriteBytes(buffer, len, offset) 34 | return 0 35 | end 36 | 37 | function Stream:WriteString(str, count, offset) 38 | offset = offset or 0 39 | count = count or #str 40 | 41 | return self:WriteBytes(str, count, offset) 42 | end 43 | --]] 44 | 45 | local STREAM_SEEK_SET = 0 -- from beginning 46 | local STREAM_SEEK_CUR = 1 -- from current position 47 | local STREAM_SEEK_END = 2 -- from end 48 | 49 | -- Read characters from a stream until the specified 50 | -- ending is found, or until the stream runs out of bytes 51 | local function ReadLine(stream, maxbytes) 52 | if not stream then 53 | return nil 54 | end 55 | 56 | maxbytes = maxbytes or 1024 57 | --print(string.format("-- ReadLine(), maxbytes: 0x%x", maxbytes)); 58 | 59 | local chartable = {} 60 | local CR = string.byte("\r") 61 | local LF = string.byte("\n") 62 | 63 | 64 | -- use the stream's byte iterator 65 | local haveCR = false 66 | for abyte in stream:Bytes(maxbytes) do 67 | if abyte == CR then 68 | haveCR = true 69 | elseif abyte == LF then 70 | break 71 | else 72 | table.insert(chartable, string.char(abyte)) 73 | end 74 | end 75 | 76 | local str = table.concat(chartable) 77 | 78 | return str 79 | end 80 | 81 | return { 82 | SEEK_SET = STREAM_SEEK_SET, 83 | SEEK_CUR = STREAM_SEEK_CUR, 84 | SEEK_END = STREAM_SEEK_END, 85 | 86 | ReadLine = ReadLine, 87 | } 88 | -------------------------------------------------------------------------------- /testy/ezsvg_example1.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | 6 | 7 | require("remotesvg.SVGElements")() 8 | local FileStream = require("remotesvg.filestream") 9 | local SVGStream = require("remotesvg.SVGStream") 10 | local ImageStream = SVGStream(FileStream.open("ezsvg_example1.svg")) 11 | 12 | 13 | -- A Translation of this: 14 | -- https://github.com/cappelnord/EzSVG/blob/master/example1.lua 15 | 16 | local width = 350 17 | local height = 370 18 | 19 | local doc = svg { 20 | width = width, height = height, 21 | 22 | rect { 23 | x = 0, y = 0, 24 | width = width, height = height, 25 | stroke = "none", 26 | fill = "rgbcolor(50,50,50)" 27 | } 28 | } 29 | 30 | local lines = g{ 31 | stroke = "white", 32 | stroke_linecap = "round", 33 | } 34 | 35 | 36 | for x=0, width,width/80 do 37 | lines:append (line ({ 38 | stroke_width = x/150, 39 | x1 = x, y1 = 0, 40 | x2 = x, y2 = math.abs(math.sin(x/30) * 250) 41 | })); 42 | end 43 | 44 | 45 | doc:append(lines); 46 | 47 | 48 | for i=0,9 do 49 | local x = i%3 * 70 + 100 50 | local y = math.floor(i/3) * 70 + 100 51 | 52 | local group = g { 53 | transform = string.format("translate(%d, %d)", x, y) 54 | } 55 | 56 | local t1 = nil 57 | for r=-90,0,0.5 do 58 | local rotation = string.format('rotate (%f)', r) 59 | local filler = string.format("rgb(0, %d, %d", 255-(r*-3), r+90) 60 | 61 | t1 = text { 62 | x = 0, 63 | y = 0, 64 | fill = filler, 65 | font_family = "Arial Rounded MT Bold", 66 | font_size = "40px", 67 | transform = rotation, 68 | 69 | -- The actual text 70 | tostring(i); 71 | } 72 | 73 | group:append(t1) 74 | end 75 | 76 | t1:attr("stroke", "#FFFFFF"); 77 | t1:attr("fill", "#AAFF00"); 78 | group:append(t1) 79 | 80 | doc:append(group) 81 | end 82 | 83 | 84 | 85 | doc:write(ImageStream) 86 | -------------------------------------------------------------------------------- /remotesvg/jskeycodes.lua: -------------------------------------------------------------------------------- 1 | return { 2 | [8] = "backspace"; 3 | [9] = "tab"; 4 | 5 | [13] = "enter"; 6 | [16] = "shift"; 7 | [17] = "ctrl"; 8 | [18] = "alt"; 9 | [19] = "break"; 10 | 11 | [20] = "capslock"; 12 | [27] = "esc"; 13 | 14 | [32] = "space"; 15 | [33] = "pageup"; 16 | [34] = "pagedown"; 17 | [35] = "end"; 18 | [36] = "home"; 19 | [37] = "leftarrow"; 20 | [38] = "uparrow"; 21 | [39] = "rightarrow"; 22 | 23 | [40] = "downarrow"; 24 | [45] = "insert"; 25 | [46] = "delete"; 26 | 27 | [48] = "0"; 28 | [49] = "1"; 29 | [50] = "2"; 30 | [51] = "3"; 31 | [52] = "4"; 32 | [53] = "5"; 33 | [54] = "6"; 34 | [55] = "7"; 35 | [56] = "8"; 36 | [57] = "9"; 37 | 38 | 39 | [65] = "a"; 40 | [66] = "b"; 41 | [67] = "c"; 42 | [68] = "d"; 43 | [69] = "e"; 44 | 45 | [70] = "f"; 46 | [71] = "g"; 47 | [72] = "h"; 48 | [73] = "i"; 49 | [74] = "j"; 50 | [75] = "k"; 51 | [76] = "l"; 52 | [77] = "m"; 53 | [78] = "n"; 54 | [79] = "o"; 55 | 56 | [80] = "p"; 57 | [81] = "q"; 58 | [82] = "r"; 59 | [83] = "s"; 60 | [84] = "t"; 61 | [85] = "u"; 62 | [86] = "v"; 63 | [87] = "w"; 64 | [88] = "x"; 65 | [89] = "y"; 66 | [90] = "z"; 67 | 68 | [91] = ""; 69 | [92] = ""; 70 | [93] = ""; 71 | 72 | [96] = "numpad0"; 73 | [97] = "numpad1"; 74 | [98] = "numpad2"; 75 | [99] = "numpad3"; 76 | [100] = "numpad4"; 77 | [101] = "numpad5"; 78 | [102] = "numpad6"; 79 | [103] = "numpad7"; 80 | [104] = "numpad8"; 81 | [105] = "numpad9"; 82 | 83 | [106] = "multiply"; 84 | [107] = "add"; 85 | [109] = "subtract"; 86 | [110] = "decimalpoint"; 87 | [111] = "divide"; 88 | 89 | [112] = "f1"; 90 | [113] = "f2"; 91 | [114] = "f3"; 92 | [115] = "f4"; 93 | [116] = "f5"; 94 | [117] = "f6"; 95 | [118] = "f7"; 96 | [119] = "f8"; 97 | [120] = "f9"; 98 | [121] = "f10"; 99 | [122] = "f11"; 100 | [123] = "f12"; 101 | 102 | 103 | [144] = "numlock"; 104 | [145] = "scrolllock"; 105 | [186] = "semicolon"; 106 | [187] = "equalsign"; 107 | [188] = "comma"; 108 | [189] = "dash"; 109 | [190] = "period"; 110 | [191] = "forwardslash"; 111 | [192] = "graveaccent"; 112 | [219] = "openbracket"; 113 | [220] = "backslash"; 114 | [221] = "closebracket"; 115 | [222] = "singlequote"; 116 | 117 | } -------------------------------------------------------------------------------- /testy/test_elemiterator.lua: -------------------------------------------------------------------------------- 1 | --test_elemiterator.lua 2 | -- load svg 3 | package.path = "../?.lua;"..package.path; 4 | local parser = require("remotesvg.parsesvg") 5 | 6 | -- read the source file 7 | -- pass in the name of your SVG document.lua file, without 8 | -- the '.lua' extension. 9 | local filename = arg[1] 10 | if not filename then return end 11 | 12 | local doc = parser:parseFile(filename); 13 | 14 | 15 | local function printElement(elem) 16 | if type(elem) == "string" then 17 | --print(elem) 18 | return 19 | end 20 | 21 | 22 | print(string.format("==== %s ====", elem._kind)) 23 | 24 | -- print the attributes 25 | for name, value in elem:attributes() do 26 | print(name,value) 27 | end 28 | 29 | -- print the content 30 | end 31 | 32 | local function test_selectAll() 33 | -- iterate through all the nodes in 34 | -- document order, printing something interesting along 35 | -- the way 36 | 37 | for child in doc:selectAll() do 38 | printElement(child) 39 | end 40 | end 41 | 42 | local function test_selectAttribute() 43 | -- select the elements that have an attribute 44 | -- with the name 'sodipodi' in them 45 | local function hasSodipodiAttribute(entry) 46 | if type(entry) ~= "table" then 47 | return false; 48 | end 49 | 50 | for name, value in entry:attributes() do 51 | --print("hasSodipodi: ", entry._kind, name, value, type(name)) 52 | if name:find("sodipodi") then 53 | return true; 54 | end 55 | end 56 | 57 | return false 58 | end 59 | 60 | for child in doc:selectElementMatches(hasSodipodiAttribute) do 61 | if type(child) == "table" then 62 | printElement(child) 63 | end 64 | end 65 | end 66 | 67 | local function test_selectElementMatches() 68 | print("<==== selectElementMatches: entry._kind == 'g' ====>") 69 | for child in doc:selectElementMatches(function(entry) if entry._kind == "g" then return true end end) do 70 | print(child._kind) 71 | end 72 | end 73 | 74 | 75 | 76 | 77 | local function test_getelementbyid() 78 | local elem = doc:getElementById("radialGradient10091") 79 | 80 | printElement(elem) 81 | end 82 | 83 | --test_selectAll(); 84 | test_selectAttribute() 85 | --test_selectElementMatches() 86 | --test_getelementbyid() 87 | -------------------------------------------------------------------------------- /testy/ezsvg_features.lua: -------------------------------------------------------------------------------- 1 | -- A translation of the example embedded here 2 | -- https://github.com/cappelnord/EzSVG 3 | 4 | --require("remotesvg.SVGElements")() 5 | -- Assuming this will be read into a context where SVGElements 6 | -- has already been read, and exposed in global namespace. 7 | 8 | -- create a document 9 | local doc = svg { 10 | ['xmlns:xlink'] = "http://www.w3.org/1999/xlink", 11 | width = 1000, 12 | height = 1000, 13 | 14 | stroke_width = 2, 15 | stroke = "black" 16 | } 17 | 18 | 19 | for d=0,1000,100 do 20 | -- create a circle 21 | local circ = circle { 22 | cx = d, cy = d, r = d/10, 23 | fill = string.format("rgb(%d, 0, 0)", d/4); 24 | } 25 | 26 | -- add the circle to the doc 27 | doc:append(circ) 28 | end 29 | 30 | 31 | -- you can also set a single style 32 | -- create a group (very handy, also stays a group in Inkscape/Illustrator) 33 | local group = g { 34 | stroke = "green" 35 | } 36 | 37 | 38 | for r=0,360, 10 do 39 | -- create a line and add a transform (rotation with r degrees, centered on 500/500) 40 | local l = line{x1 = 100, y1=500, x2=900, y2=500, 41 | transform = string.format("rotate(%d, 500, 500)", r), 42 | } 43 | 44 | -- add it to the group 45 | group:append(l) 46 | end 47 | 48 | -- add the group to the document 49 | doc:append(group) 50 | 51 | 52 | -- create a path object and set its styling 53 | local path1 = path { 54 | id = "TextPath", 55 | } 56 | 57 | path1:moveBy(500, 500) 58 | :sqCurveBy(300,300) 59 | :sqCurveBy(-200, 0) 60 | :sqCurveBy(-200, -400) 61 | :sqCurveTo(500, 500) 62 | 63 | doc:append ( 64 | defs { 65 | radialGradient {id="PathGradient", 66 | stop {offset="0%", ['stop-color']="black"}, 67 | stop {offset="100%", ['stop-color']="yellow"}, 68 | }, 69 | 70 | path1, 71 | }) 72 | 73 | 74 | 75 | -- draw the path 76 | doc:append(use { 77 | ['xlink:href'] = "#TextPath", 78 | stroke = "blue", 79 | ['stroke-width'] = 2, 80 | fill = "url(#PathGradient)", 81 | ['fill-opacity'] = "0.8" 82 | }) 83 | 84 | -- add text to doc, add a path and style it 85 | doc:append(text { 86 | font_size = 60, 87 | font_family = "Arial", 88 | fill = "#CCCCCC", 89 | stroke = "black", 90 | 91 | textPath { 92 | ['xlink:href'] ="#TextPath", 93 | 94 | [[ 95 | It's so ez to draw some lines, it's so easy to draw some lines ... 96 | ]] 97 | } 98 | }) 99 | 100 | return doc 101 | -------------------------------------------------------------------------------- /remotesvg/filestream.lua: -------------------------------------------------------------------------------- 1 | 2 | local ffi = require "ffi" 3 | local stream = require "remotesvg.stream" 4 | 5 | 6 | local FileStream = {} 7 | local FileStream_mt = { 8 | __index = FileStream, 9 | } 10 | 11 | function FileStream.new(handle) 12 | if not handle then return nil end 13 | 14 | local obj = { 15 | FileHandle = handle, 16 | } 17 | 18 | setmetatable(obj, FileStream_mt) 19 | 20 | return obj; 21 | end 22 | 23 | 24 | 25 | function FileStream.open(filename, mode) 26 | if not filename then return nil end 27 | 28 | mode = mode or "wb+" 29 | local handle = io.open(filename, mode) 30 | if not handle then return nil end 31 | 32 | return FileStream.new(handle) 33 | end 34 | 35 | function FileStream:close() 36 | return self.FileHandle:close(); 37 | end 38 | 39 | function FileStream:getLength() 40 | local currpos = self.FileHandle:seek() 41 | local size = self.FileHandle:seek("end") 42 | 43 | self.FileHandle:seek("set",currpos) 44 | 45 | return size; 46 | end 47 | 48 | function FileStream:getPosition() 49 | local currpos = self.FileHandle:seek() 50 | return currpos; 51 | end 52 | 53 | function FileStream:seek(offset, origin) 54 | offset = offset or 0 55 | origin = origin or stream.SEEK_SET 56 | 57 | if origin == stream.SEEK_CUR then 58 | return self.FileHandle:seek("cur", offset) 59 | elseif origin == stream.SEEK_SET then 60 | return self.FileHandle:seek("set", offset) 61 | elseif origin == stream.SEEK_END then 62 | return self.FileHandle:seek("end", offset) 63 | end 64 | 65 | return nil 66 | end 67 | 68 | function FileStream:readByte() 69 | local str = self.FileHandle:read(1) 70 | if not str then return str end 71 | 72 | return string.byte(str); 73 | end 74 | 75 | function FileStream:readBytes(buffer, len, offset) 76 | offset = offset or 0 77 | local str = self.FileHandle:read(len) 78 | local maxbytes = math.min(len, #str) 79 | ffi.copy(buffer+offset, str, maxbytes) 80 | 81 | return maxbytes 82 | end 83 | 84 | function FileStream:readString(count) 85 | local str = self.FileHandle:read(count) 86 | 87 | return str 88 | end 89 | 90 | 91 | 92 | function FileStream:writeByte(value) 93 | self.FileHandle:write(string.char(value)) 94 | return 1 95 | end 96 | 97 | function FileStream:writeBytes(buffer, len, offset) 98 | offset = offset or 0 99 | 100 | if type(buffer) == "string" then 101 | self.FileHandle:write(buffer) 102 | return len 103 | end 104 | 105 | -- assume we have a pointer to a buffer 106 | -- convert to string and write it out 107 | local str = ffi.string(buffer, len) 108 | self.FileHandle:write(str) 109 | 110 | return len 111 | end 112 | 113 | function FileStream:writeString(str, count, offset) 114 | offset = offset or 0 115 | count = count or #str 116 | local strptr = ffi.cast("char *", str); 117 | 118 | return self:writeBytes(strptr, count, offset) 119 | end 120 | 121 | return FileStream; 122 | -------------------------------------------------------------------------------- /remotesvg/parsesvg.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A simple SVG Parser which fills in a lua based object model 3 | for the image specified. 4 | --]] 5 | 6 | local ffi = require("ffi") 7 | local bit = require("bit") 8 | local lshift, rshift, band, bor = bit.lshift, bit.rshift, bit.band, bit.bor 9 | 10 | local XmlParser = require("remotesvg.SVGXmlParser") 11 | local svg = require("remotesvg.SVGElements") 12 | 13 | 14 | local function Stack() 15 | local obj = {} 16 | setmetatable(obj, { 17 | __index = obj; 18 | }) 19 | 20 | function obj.push(self, value) 21 | table.insert(self, value); 22 | 23 | return value; 24 | end 25 | 26 | function obj.pop(self) 27 | return table.remove(self) 28 | end 29 | 30 | function obj.top(self) 31 | if #self < 1 then 32 | return nil; 33 | end 34 | 35 | return self[#self]; 36 | end 37 | 38 | return obj; 39 | end 40 | 41 | 42 | 43 | local SVGParser = {} 44 | setmetatable(SVGParser, { 45 | __call = function(self, ...) 46 | return self:new(...); 47 | end 48 | }) 49 | local SVGParser_mt = { 50 | __index = SVGParser; 51 | } 52 | 53 | function SVGParser.init(self) 54 | local obj = { 55 | ElemStack = Stack(); 56 | } 57 | 58 | setmetatable(obj, SVGParser_mt); 59 | 60 | return obj; 61 | end 62 | 63 | function SVGParser.new(self) 64 | local parser = self:init() 65 | 66 | return parser; 67 | end 68 | 69 | function SVGParser.parse(self, input) 70 | XmlParser.parseXML(input, SVGParser.startElement, SVGParser.endElement, SVGParser.content, self) 71 | 72 | 73 | return self.Top; 74 | end 75 | 76 | function SVGParser.parseFile(self, filename) 77 | local fp = io.open(filename, "rb") 78 | if not fp then 79 | return 80 | end 81 | 82 | local data = fp:read("*a"); 83 | fp:close(); 84 | 85 | local parser = SVGParser(); 86 | local shape = parser:parse(data) 87 | 88 | return shape; 89 | end 90 | 91 | 92 | 93 | function SVGParser.startElement(self, name, attr) 94 | --print("SVGParser.startElement: ", el, attr) 95 | --print("BEGIN: ", name) 96 | 97 | -- create new element 98 | local newElem = svg.BasicElem(name, attr) 99 | 100 | -- if there's already something on the stack, 101 | -- then add the current element as a child 102 | if self.ElemStack:top() then 103 | self.ElemStack:top():append(newElem) 104 | else 105 | self.Top = newElem 106 | end 107 | 108 | -- push the new element onto the stack 109 | self.ElemStack:push(newElem); 110 | end 111 | 112 | -- called whenever there is a closing tag for an element 113 | function SVGParser.endElement(self, name) 114 | 115 | -- pop it off the top of the stack 116 | local oldone = self.ElemStack:pop(); 117 | --print("END: ", name, oldone._kind, #self.ElemStack) 118 | end 119 | 120 | -- Content is called for things like 121 | -- text, comments, titles, descriptions and the like 122 | function SVGParser.content(self, s) 123 | --print("CONTENT: ", s) 124 | self.ElemStack:top():appendLiteral(s) 125 | end 126 | 127 | return SVGParser 128 | -------------------------------------------------------------------------------- /examples/app_arcs02.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | local SVGInteractor = require("remotesvg.SVGInteractor") 6 | require("remotesvg.SVGElements")() 7 | 8 | local group = g; 9 | 10 | 11 | local ImageStream = size() 12 | 13 | 14 | local function draw(ImageStream) 15 | ImageStream:reset(); 16 | 17 | local doc = svg{ 18 | ['xmlns:xlink'] = "http://www.w3.org/1999/xlink", 19 | width="12cm", 20 | height="5.25cm", 21 | viewBox="0 0 1200 525", 22 | 23 | group { 24 | ['font-family'] = "Verdana", 25 | 26 | defs { 27 | group {id="baseEllipses", ["font-size"]=20; 28 | ellipse {cx=125, cy=125, rx=100, ry=50, fill="none", stroke="#888888", ["stroke-width"] = 2}; 29 | ellipse {cx=225, cy=75, rx=100, ry=50, fill="none", stroke="#888888", ["stroke-width"] = 2}; 30 | text ({x=35, y=70}, "Arc start"); 31 | text ({x=225, y=145}, "Arc end"); 32 | }; 33 | }; 34 | 35 | rect {x=1, y=1, width=1198, height=523, fill="none", stroke="blue", ["stroke-width"] = 1}; 36 | 37 | group {['font-size'] = 30, 38 | group {transform="translate(0,0)", 39 | use {["xlink:href"] = "#baseEllipses", ["xmlns:xlink"] ="http://www.w3.org/1999/xlink"}; 40 | }, 41 | 42 | group {transform="translate(400,0)", 43 | text {x=50, y=210, "large-arc-flag=0"}; 44 | text {x=50, y=250, "sweep-flag=0"}; 45 | use {["xlink:href"] = "#baseEllipses", ["xmlns:xlink"] = "http://www.w3.org/1999/xlink"}, 46 | path {d="M 125,75 a100,50 0 0,0 100,50", fill="none", stroke="red", ["stroke-width"] = 6}, 47 | }, 48 | 49 | group {transform="translate(800,0)", 50 | text {x=50, y=210, "large-arc-flag=0"}; 51 | text {x=50, y=250, "sweep-flag=1"}; 52 | use {["xlink:href"]="#baseEllipses", ["xmlns:xlink"] = "http://www.w3.org/1999/xlink"}; 53 | path {d="M 125,75 a100,50 0 0,1 100,50", fill="none", stroke="red", ["stroke-width"] = 6}; 54 | }, 55 | group {transform="translate(400,250)"; 56 | text {x=50, y=210, "large-arc-flag=1"}; 57 | text {x=50, y=250, "sweep-flag=0"}; 58 | use {["xlink:href"] ="#baseEllipses", ["xmlns:xlink"] = "http://www.w3.org/1999/xlink"}; 59 | path {d="M 125,75 a100,50 0 1,0 100,50", fill="none", stroke="red", ["stroke-width"] = 6}; 60 | }; 61 | group {transform="translate(800,250)"; 62 | text {x=50, y=210, "large-arc-flag=1"}; 63 | text {x=50, y=250, "sweep-flag=1"}; 64 | use {["xlink:href"]="#baseEllipses", ["xmlns:xlink"] = "http://www.w3.org/1999/xlink"}; 65 | path {d="M 125,75 a100,50 0 1,1 100,50", fill="none", stroke="red", ["stroke-width"] = 6}; 66 | }; 67 | }; 68 | }; 69 | }; 70 | 71 | 72 | doc:write(ImageStream); 73 | end 74 | 75 | -- This is a one off 76 | -- we won't be refreshing the image, so just draw 77 | -- into the image stream once, and do NOT implement 78 | -- the 'frame()' function. 79 | draw(ImageStream); 80 | 81 | run() 82 | -------------------------------------------------------------------------------- /testy/test_patterns.lua: -------------------------------------------------------------------------------- 1 | --test_patterns.lua 2 | package.path = "../?.lua;"..package.path 3 | 4 | local sattr = require("remotesvg.SVGAttributes") 5 | local parse = sattr.parseAttribute; 6 | 7 | local function test_numbers() 8 | local str = "10, 20.3, 42 375.67" 9 | local patt1 = "(%S+)" 10 | local patt2 = "(%d+%.?%d+)" 11 | 12 | 13 | local function printItems(patt, str) 14 | print("printItems: ", patt, str) 15 | 16 | for item in str:gmatch(patt) do 17 | print(item) 18 | end 19 | end 20 | 21 | printItems(patt1, str) 22 | printItems(patt2, str) 23 | end 24 | 25 | local function test_style() 26 | local str = 'stop-color:#191b21;stop-opacity:0.34437087;' 27 | local patt = "([^;]+)" 28 | print("parseStyle: ", patt, str) 29 | 30 | local tbl = {} 31 | 32 | for item in str:gmatch(patt) do 33 | local name, value = item:match("(%g+):(%g+)") 34 | print("item:match: ", name, value) 35 | name, value = parse(name, value) 36 | print("parse result: ", name, value) 37 | tbl[name] = value; 38 | end 39 | 40 | for k,v in pairs(tbl) do 41 | print(k,v) 42 | end 43 | end 44 | 45 | local function parsetransform(name, value, strict) 46 | 47 | local patt = "(%a+)(%b())" 48 | local numpatt = "([+-]?%d+%.?%d*)" 49 | 50 | local tbl = {} 51 | for kind, value in value:gmatch(patt) do 52 | --print (kind, value) 53 | local nums = {} 54 | -- get the list of numbers 55 | for num in value:gmatch(numpatt) do 56 | table.insert(nums, num) 57 | end 58 | 59 | if #nums > 0 then 60 | if kind == "translate" then 61 | local x = nums[1]; 62 | local y = nums[2] or 0; 63 | local item = {name = 'translate', x = x, y = y} 64 | setmetatable(item, { 65 | __tostring = function(self) 66 | return string.format('translate(%f %f)', self.x, self.y); 67 | end, 68 | }) 69 | table.insert(tbl, item) 70 | elseif kind == "rotate" then 71 | local angle = nums[1]; 72 | local item = {name = "rotate", angle=angle, x = nums[2], y = nums[3]} 73 | setmetatable(item, { 74 | __tostring = function(self) 75 | if self.x and self.y then 76 | return string.format("%s(%f %f %f)", self.name, self.angle, self.x, self.y) 77 | elseif self.angle then 78 | return string.format("%s(%f)", self.name, self.angle); 79 | end 80 | return "" 81 | end, 82 | }) 83 | table.insert(tbl, item) 84 | elseif kind == "scale" then 85 | local x = nums[1] 86 | local y = nums[2] or x 87 | local item = {name="scale", x=x, y=y} 88 | setmetatable(item, { 89 | __tostring = function(self) 90 | if self.x and self.y then 91 | return string.format("%s(%f %f)", self.name, self.x, self.y) 92 | elseif self.x then 93 | return string.format("%s(%f)", self.name, self.x); 94 | else 95 | return "" 96 | end 97 | end 98 | }) 99 | table.insert(tbl, item) 100 | elseif kind == "skewX" then 101 | elseif kind == "skewY" then 102 | -- get the numbers out 103 | elseif kind == "matrix" then 104 | end 105 | end 106 | end 107 | 108 | return name, tbl 109 | end 110 | 111 | local function test_transform() 112 | print("==== test_transform ====") 113 | local subject = "translate(20 10) rotate(30 10 10)" 114 | 115 | name, obj = parsetransform("transform", subject) 116 | 117 | print("== RESULTS ==") 118 | for idx, item in ipairs(obj) do 119 | print(item) 120 | end 121 | end 122 | 123 | --test_numbers(); 124 | --test_style(); 125 | test_transform() 126 | -------------------------------------------------------------------------------- /remotesvg/maths.lua: -------------------------------------------------------------------------------- 1 | local bit = require("bit") 2 | local band, bor, lshift, rshift = bit.band, bit.bor, bit.lshift, bit.rshift 3 | 4 | local acos = math.acos; 5 | local sqrt = math.sqrt; 6 | local floor = math.floor; 7 | local ceil = math.ceil; 8 | 9 | 10 | 11 | local function curveDivs(r, arc, tol) 12 | local da = acos(r / (r + tol)) * 2.0; 13 | local divs = ceil(arc / da); 14 | if (divs < 2) then 15 | divs = 2; 16 | end 17 | 18 | return divs; 19 | end 20 | 21 | -- fairly efficient division by 255 22 | local function div255(x) 23 | return rshift(((x+1) * 257), 16); 24 | end 25 | 26 | local function GetAlignedByteCount(width, bitsPerPixel, byteAlignment) 27 | local nbytes = width * (bitsPerPixel/8); 28 | return nbytes + (byteAlignment - (nbytes % byteAlignment)) % 4 29 | end 30 | 31 | -- determine where a point (p3) intersects the 32 | -- line determined by points p1 and p2 33 | -- Points on line defined by 34 | -- P = P1 + u*(P2 - P1) 35 | local function lineMag(x1, y1, x2, y2) 36 | local x = x2 - x1; 37 | local y = y2 - y1; 38 | 39 | return sqrt(x*x+y*y); 40 | end 41 | 42 | local function vmag(x, y) return sqrt(x*x + y*y); end 43 | 44 | local function normalize(x, y) 45 | local d = sqrt(x*x + y*y); 46 | if (d > 1e-6) then 47 | local id = 1.0 / d; 48 | x = x * id; 49 | y = y * id; 50 | end 51 | 52 | return d, x, y; 53 | end 54 | 55 | local function vecrat(ux, uy, vx, vy) 56 | return (ux*vx + uy*vy) / (vmag(ux,uy) * vmag(vx,vy)); 57 | end 58 | 59 | 60 | 61 | local function vecang(ux, uy, vx, vy) 62 | 63 | local r = vecrat(ux,uy, vx,vy); 64 | if (r < -1.0) then 65 | r = -1.0; 66 | end 67 | 68 | if (r > 1.0) then 69 | r = 1.0; 70 | end 71 | 72 | local acs = acos(r); 73 | if (ux*vy < uy*vx) then 74 | return -1.0 * acs; 75 | end 76 | 77 | return acs; 78 | end 79 | 80 | local function pointLineIntersection(x1, y1, x2, y2, x3, y3) 81 | local mag = lineMag(x1, y1, x2, y2) 82 | local u = ((x3 - x1)*(x2-x1) + (y3-y1)*(y2-y1))/(mag*mag) 83 | local x = x1 + u * (x2 - x1); 84 | local y = y1 + u * (y2 - y1); 85 | 86 | return x, y 87 | end 88 | 89 | local function pointLineDistance(x1, y1, x2, y2, x3, y3) 90 | local x, y = pointLineIntersection(x1, y1, x2, y2, x3, y3) 91 | 92 | return lineMag(x3, y3, x, y) 93 | end 94 | 95 | -- determine if two points are equal, within a specified tolerance 96 | local function pointEquals(x1, y1, x2, y2, tol) 97 | local dx = x2 - x1; 98 | local dy = y2 - y1; 99 | 100 | return dx*dx + dy*dy < tol*tol; 101 | end 102 | 103 | function RANGEMAP(x, a, b, c, d) 104 | return c + ((x-a)/(b-a)*(d-c)) 105 | end 106 | 107 | local function round(n) 108 | if n >= 0 then 109 | return floor(n+0.5) 110 | end 111 | 112 | return ceil(n-0.5) 113 | end 114 | 115 | local function clamp(x, a, b) 116 | if x < a then return a end 117 | if x > b then return b end 118 | 119 | return x; 120 | end 121 | 122 | local function sgn(x) 123 | if x < 0 then return -1 end 124 | if x > 0 then return 1 end 125 | 126 | return 0 127 | end 128 | 129 | local function sqr(x) return x*x; end 130 | 131 | 132 | 133 | 134 | 135 | return { 136 | clamp = clamp; 137 | curveDivs = curveDivs; 138 | div255 = div255; 139 | GetAlignedByteCount = GetAlignedByteCount; 140 | 141 | lineMag = lineMag; 142 | 143 | pointEquals = pointEquals; 144 | pointLineIntersection = pointLineIntersection; 145 | pointLineDistance = pointLineDistance; 146 | 147 | RANGEMAP = RANGEMAP; 148 | round = round; 149 | sgn = sgn; 150 | sqr = sqr; 151 | 152 | vecang = vecang; 153 | vecrat = vecrat; 154 | vmag = vmag; 155 | } -------------------------------------------------------------------------------- /remotesvg/matrix2D.lua: -------------------------------------------------------------------------------- 1 | local ffi = require ("ffi") 2 | 3 | local RAD = math.rad 4 | local mtan = math.tan; 5 | local mcos = math.cos; 6 | local msin = math.sin; 7 | 8 | local tan = function(d) return mtan(RAD(d)) end 9 | local cos = function(d) return mcos(RAD(d)) end 10 | local sin = function(d) return msin(RAD(d)) end 11 | 12 | 13 | local matrix2D = {} 14 | setmetatable(matrix2D, { 15 | __call = function (self, ...) 16 | return self:new(...); 17 | end, 18 | }) 19 | local matrix2D_mt = { 20 | __index = matrix2D; 21 | 22 | __tostring = function(self) 23 | return string.format("matrix (%f %f %f %f %f %f)", 24 | self[1], self[2], self[3], self[4], self[5], self[6]) 25 | end, 26 | } 27 | 28 | function matrix2D.new(self, a, b, c, d, e, f) 29 | a = a or 0 30 | b = b or 0 31 | c = c or 0 32 | d = d or 0 33 | e = e or 0 34 | f = f or 0 35 | 36 | local obj = {a, b, c, d, e, f} 37 | setmetatable(obj, matrix2D_mt) 38 | 39 | return obj; 40 | end 41 | 42 | --[[ 43 | Instance Constructors 44 | --]] 45 | 46 | --[[ 47 | Create a new identity matrix 48 | --]] 49 | function matrix2D.identity() 50 | return matrix2D(1, 0, 0, 1, 0, 0) 51 | end 52 | 53 | --[[ 54 | Create a new matrix which represents the specified 55 | translation 56 | --]] 57 | function matrix2D.translation(tx, ty) 58 | tx = tx or 0 59 | ty = ty or 0 60 | return matrix2D(1, 0, 0, 1, tx, ty) 61 | end 62 | 63 | --[[ 64 | Create a new matrix which represents the specified scaling 65 | --]] 66 | function matrix2D.scale(sx, sy) 67 | sx = sx or 1 68 | sy = sy or sx 69 | 70 | return matrix2D(sx, 0, 0, sy, 0, 0) 71 | end 72 | 73 | function matrix2D.skewX(a) 74 | a = a or 0 75 | return matrix2D(1, 0, tan(a), 1, 0, 0) 76 | end 77 | 78 | function matrix2D.skewY(a) 79 | a = a or 0 80 | return matrix2D(1, tan(a), 0, 1, 0, 0) 81 | end 82 | 83 | --[[ 84 | create a matrix which is a rotation 85 | around the given angle 86 | --]] 87 | function matrix2D.rotation(a, x, y) 88 | local cs = cos(a); 89 | local sn = sin(a); 90 | 91 | return matrix2D(cs, sn, -sn, cs, 0, 0) 92 | end 93 | 94 | -- BUGBUG - need to convert to 1 based index 95 | function matrix2D.xformMultiply(t, s) 96 | 97 | local t0 = t[0] * s[0] + t[1] * s[2]; 98 | local t2 = t[2] * s[0] + t[3] * s[2]; 99 | local t4 = t[4] * s[0] + t[5] * s[2] + s[4]; 100 | t[1] = t[0] * s[1] + t[1] * s[3]; 101 | t[3] = t[2] * s[1] + t[3] * s[3]; 102 | t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; 103 | t[0] = t0; 104 | t[2] = t2; 105 | t[4] = t4; 106 | end 107 | 108 | --[[ 109 | Create a new matrix which is the inverse of the 110 | current matrix. 111 | --]] 112 | function matrix2D.inverse(t) 113 | 114 | local invdet = 0; 115 | 116 | local det = t[1] * t[4] - t[3] * t[2]; 117 | if (det > -1e-6 and det < 1e-6) then 118 | return matrix2D.identity(); 119 | end 120 | 121 | invdet = 1.0 / det; 122 | local inv = {} 123 | inv[1] = (t[4] * invdet); 124 | inv[3] = (-t[3] * invdet); 125 | inv[5] = ((t[3] * t[6] - t[4] * t[5]) * invdet); 126 | inv[2] = (-t[2] * invdet); 127 | inv[4] = (t[1] * invdet); 128 | inv[6] = ((t[2] * t[5] - t[1] * t[6]) * invdet); 129 | end 130 | 131 | function matrix2D.Premultiply(t, s) 132 | 133 | local s2 = ffi.new("double[6]"); 134 | ffi.copy(s2, s, ffi.sizeof("double")*6); 135 | matrix2D.xformMultiply(s2, t); 136 | ffi.copy(t, s2, ffi.sizeof("double")*6); 137 | end 138 | 139 | --[[ 140 | return two coordinates transformed 141 | by the current matrix 142 | --]] 143 | function matrix2D.xformPoint(t, x, y) 144 | local dx = x*t[1] + y*t[3] + t[5]; 145 | local dy = x*t[2] + y*t[4] + t[6]; 146 | 147 | 148 | return dx, dy; 149 | end 150 | 151 | --[[ 152 | Return a transformed vector. 153 | Since vectors don't have position, 154 | leave off the translation portion. 155 | --]] 156 | function matrix2D.xformVec(t, x, y) 157 | 158 | local dx = x*t[1] + y*t[3]; 159 | local dy = x*t[2] + y*t[4]; 160 | 161 | return dx, dy 162 | end 163 | 164 | return matrix2D 165 | -------------------------------------------------------------------------------- /remotesvg/sharesvg.htm: -------------------------------------------------------------------------------- 1 | return [[ 2 | 3 | 4 | 5 | Screen View 6 | 7 | 8 | 75 | 76 | 77 | 78 |
81 | 82 | 83 |
84 | 85 | 86 | 127 | 128 | ]] 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remotesvg 2 | Remote interaction using SVG 3 | 4 | This project presents a fairly straight forward interactive graphics environment 5 | using SVG graphics. The basic premise is that you will use a web browser to view 6 | graphics on a client machine. The rendering on the client is done using SVG 7 | graphics. 8 | 9 | On the server side, the SVG graphics can be built up using any mechanism desired. 10 | Since this is in Lua, you are free to do whatever you want on the lua side, as 11 | long as by the end, you generate an SVG image that can be rendered on the client. 12 | 13 | The SVGGeometry.lua file contains a lot of convenience classes that make building 14 | up an SVG image relatively simple. A typical example might look like this: 15 | 16 | ```lua 17 | 18 | local doc = svg({ 19 | version="1.1", 20 | width = "12cm", height= "4cm", 21 | viewBox="0 0 1200 400", 22 | 23 | -- Show outline of canvas using 'rect' element 24 | rect({x = 1;y = 1;width = 1198; height = 398; 25 | fill = "none"; 26 | stroke = "blue"; 27 | ["stroke-width"] = 2; 28 | }); 29 | 30 | circle({ 31 | fill="red", 32 | stroke="blue", 33 | ["stroke-width"]=10, 34 | cx =600; 35 | cy = 200; 36 | r = 100; 37 | }); 38 | 39 | }) 40 | 41 | ``` 42 | 43 | The style here is to essentially build up a document as a table of parameters. The 'svg' object, for instance, has top level attributes, followed by graphic 44 | elements, such as rect, and circle. The graphic elements in term have the 45 | same hierarchy. 46 | 47 | The entirety of the application may look like this: 48 | 49 | ```lua 50 | #!/usr/bin/env luajit 51 | 52 | package.path = "../?.lua;"..package.path; 53 | 54 | local SVGInteractor = require("remotesvg.SVGInteractor") 55 | local SVGStream = require("remotesvg.SVGStream") 56 | local SVGGeometry = require("remotesvg.SVGGeometry") 57 | local svg = SVGGeometry.Document; 58 | local rect = SVGGeometry.Rect; 59 | local circle = SVGGeometry.Circle; 60 | 61 | local keycodes = require("remotesvg.jskeycodes") 62 | 63 | local width = 640; 64 | local height = 480; 65 | local mstream = size(width, height) 66 | local ImageStream = SVGStream(mstream); 67 | 68 | 69 | 70 | function mouseMove(activity) 71 | print("mouseMove: ", activity.x, activity.y) 72 | end 73 | 74 | local function drawCircle(filename) 75 | ImageStream:reset(); 76 | 77 | local doc = svg({ 78 | version="1.1", 79 | width = "12cm", height= "4cm", 80 | viewBox="0 0 1200 400", 81 | 82 | -- Show outline of canvas using 'rect' element 83 | rect({x = 1;y = 1;width = 1198; height = 398; 84 | fill = "none"; 85 | stroke = "blue"; 86 | ["stroke-width"] = 2; 87 | }); 88 | 89 | circle({ 90 | fill="red", 91 | stroke="blue", 92 | ["stroke-width"]=10, 93 | cx =600; 94 | cy = 200; 95 | r = 100; 96 | }); 97 | 98 | }) 99 | 100 | doc:write(ImageStream); 101 | end 102 | 103 | 104 | function frame() 105 | drawCircle(); 106 | end 107 | 108 | run() 109 | 110 | ``` 111 | 112 | Mouse and keyboard interaction can be achieved by simply implementing the various 113 | mouse and keyboard event routines: 114 | 115 | mouseDown, mouseUp, mouseMove 116 | keyDown, keyUp, keyPress 117 | 118 | These routines each receive an 'activity' argument, which contains the 119 | details of what the event was. 120 | 121 | mouseDown 122 | Mouse Button Pressed, called whenever a button is pressed. 123 | 124 | {action='mouseDown', x, y, which}; 125 | x - horizontal location of mouse 126 | y - vertical location of mouse 127 | which - which mouse button (1: left, 2:right, 3:middle) 128 | 129 | mouseUp 130 | Mouse Button Released, called whenever a pressed button 131 | is released. 132 | 133 | {action='mouseUp', x, y, which}; 134 | x - horizontal location of mouse 135 | y - vertical location of mouse 136 | which - which mouse button (1: left, 2:right, 3:middle) 137 | 138 | mouseMove(activity) 139 | Mouse Movement, called any time the mouse moves 140 | 141 | {action='mouseMove', x, y}; 142 | x - horizontal location of mouse 143 | y - vertical location of mouse 144 | 145 | 146 | Installation 147 | ------------ 148 | 149 | remotesvg relies on turbo lua (https://turbo.readthedocs.org/en/latest/) as a web server. You need to first install that before trying to run 150 | your application: 151 | 152 | luarocks install turbo 153 | 154 | Then, assuming you're sitting in the 'examples' directory of remotesvg, 155 | you can simply execute your file: 156 | 157 | luajit myapp.lua 158 | 159 | -------------------------------------------------------------------------------- /remotesvg/viewsvg_htm.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | This brief html is the most minimal necessary to display 3 | svg graphics oming from the server. There is no interaction 4 | back to the server, other than whatever the svg itself sets up. 5 | --]] 6 | return [[ 7 | 8 | 9 | 10 | SVG View 11 | 12 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ]] 137 | -------------------------------------------------------------------------------- /remotesvg/SVGXmlParser.lua: -------------------------------------------------------------------------------- 1 | -- Simple XML parser 2 | local ffi = require("ffi") 3 | 4 | local ctypes = require("remotesvg.ctypes") 5 | 6 | local isspace = ctypes.isspace; 7 | 8 | local NSVG_XML_TAG = 1; 9 | local NSVG_XML_CONTENT = 2; 10 | local NSVG_XML_MAX_ATTRIBS = 256; 11 | 12 | local function parseContent(s, markoffset, endoffset, contentCb, ud) 13 | -- Trim start white spaces 14 | local soffset = markoffset; 15 | 16 | -- skip leading whitespace 17 | while (soffset < endoffset and isspace(s[soffset])) do 18 | soffset = soffset + 1; 19 | end 20 | 21 | if soffset == endoffset then 22 | return nil; 23 | end 24 | 25 | if (contentCb) then 26 | contentCb(ud, ffi.string(s+soffset, endoffset-soffset)); 27 | end 28 | end 29 | 30 | 31 | -- void (startelCb)(any ud, string name, table attr) 32 | -- void (endelCb)(any ud, string name) 33 | 34 | -- void * ud 35 | -- Some user supplied data that just gets passed along 36 | 37 | local function parseElement(s, startoffset, endoffset, startelCb, endelCb, ud) 38 | local attr = {}; 39 | local start = false; 40 | local ending = false; 41 | local quote = nil; 42 | local soffset = startoffset; 43 | 44 | -- Skip white space after the '<' 45 | while (soffset < endoffset and isspace(s[soffset])) do 46 | soffset = soffset + 1; 47 | end 48 | 49 | -- Check if the tag is end tag 50 | if s[soffset] == string.byte('/') then 51 | soffset = soffset + 1; 52 | ending = true; 53 | else 54 | start = true; 55 | end 56 | 57 | -- Skip comments, data and preprocessor stuff. 58 | if soffset == endoffset or s[soffset] == string.byte('?') or s[soffset] == string.byte('!') then 59 | return; 60 | end 61 | 62 | 63 | -- Get tag name 64 | local nameoffset = soffset; 65 | local nameendoffset = nameoffset; 66 | 67 | while (soffset < endoffset) and (not isspace(s[soffset])) do 68 | soffset = soffset + 1; 69 | end 70 | 71 | nameendoffset = soffset; 72 | if (soffset < endoffset) then 73 | soffset = soffset + 1; 74 | end 75 | 76 | -- turn it into a lua string 77 | local namelen = nameendoffset - nameoffset; 78 | --print("TAG name len: ", nameoffset, nameendoffset, namelen) 79 | local name = ffi.string(s+nameoffset, namelen) 80 | 81 | -- Get attribs 82 | while (not ending and (soffset < endoffset) ) do 83 | -- Skip white space before the attrib name 84 | while (soffset < endoffset and isspace(s[soffset])) do 85 | soffset = soffset + 1; 86 | end 87 | 88 | if (soffset == endoffset) then 89 | break; 90 | end 91 | 92 | if s[soffset] == string.byte('/') then 93 | ending = true; 94 | break; 95 | end 96 | 97 | local attrnamemark = soffset; 98 | local attrnameend = soffset; 99 | 100 | -- Find end of the attrib name. 101 | while soffset < endoffset and not isspace(s[soffset]) and s[soffset] ~= string.byte('=') do 102 | soffset = soffset + 1; 103 | end 104 | 105 | if (soffset < endoffset) then 106 | attrnameend = soffset; 107 | soffset = soffset + 1; 108 | end 109 | 110 | -- Skip until the beginning of the value. 111 | while soffset < endoffset and s[soffset] ~= string.byte('\"') and s[soffset] ~= string.byte('\'') do 112 | soffset = soffset + 1; 113 | end 114 | 115 | if (s[soffset] == 0) then 116 | break; 117 | end 118 | 119 | quote = s[soffset]; 120 | soffset = soffset + 1; 121 | 122 | -- Store value and find the end of it. 123 | local attrvaluemark = soffset; 124 | local attrvaluemarkend = soffset; 125 | 126 | while (soffset < endoffset and s[soffset] ~= quote) do 127 | soffset = soffset + 1; 128 | end 129 | 130 | attrvaluemarkend = soffset; 131 | if (soffset < endoffset) then 132 | soffset = soffset + 1; 133 | end 134 | 135 | -- store attribute as a key value pair 136 | attr[ffi.string(s+attrnamemark, attrnameend-attrnamemark)] = ffi.string(s+attrvaluemark, attrvaluemarkend-attrvaluemark); 137 | end 138 | 139 | -- Call callbacks. 140 | if (start and startelCb) then 141 | startelCb(ud, name, attr); 142 | end 143 | 144 | if (ending and endelCb) then 145 | endelCb(ud, name); 146 | end 147 | end 148 | 149 | 150 | local function parseXML(input, startelCb, endelCb, contentCb, ud) 151 | local s = ffi.cast("const char *", input); 152 | local soffset = 0; 153 | local markoffset = soffset; 154 | local markoffsetend = soffset; 155 | 156 | local state = NSVG_XML_CONTENT; 157 | 158 | while s[soffset] ~= 0 do 159 | if (s[soffset] == string.byte('<')) and (state == NSVG_XML_CONTENT) then 160 | -- Start of a tag 161 | markoffsetend = soffset; 162 | soffset = soffset + 1; 163 | 164 | parseContent(s, markoffset, markoffsetend, contentCb, ud); 165 | markoffset = soffset; 166 | markoffsetend = markoffset; 167 | 168 | state = NSVG_XML_TAG; 169 | elseif (s[soffset] == string.byte('>')) and (state == NSVG_XML_TAG) then 170 | -- Start of a content or new tag. 171 | markoffsetend = soffset; 172 | 173 | soffset = soffset + 1; 174 | 175 | parseElement(s, markoffset, markoffsetend, startelCb, endelCb, ud); 176 | markoffset = soffset; 177 | markoffsetend = markoffset; 178 | 179 | state = NSVG_XML_CONTENT; 180 | else 181 | soffset = soffset + 1; 182 | end 183 | end 184 | 185 | return true; 186 | end 187 | 188 | 189 | return { 190 | parseXML = parseXML; 191 | } -------------------------------------------------------------------------------- /testy/test_literal.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | package.path = "../?.lua;"..package.path; 4 | 5 | --[[ 6 | a literal chunk of text can be injected into the SVG directly. 7 | in the body elements. This is useful in a couple of cases. 8 | 9 | You might want to include an XML comment block. There 10 | is no comment block element, so you enclose it in 11 | a string literal. 12 | 13 | You may have some css style sheet embedded. There 14 | is no CSS specific element type, so you just include 15 | it with a literal. 16 | 17 | You might be converting a large chunk of svg by hand 18 | and you haven't tackled a section yet. you can just 19 | include that section in a literal block, until you 20 | get around to dealing with it. 21 | 22 | The disadvantage of literal blocks is that the content 23 | is not parsed, so it's just an opaque chunk of text. 24 | All the other elements give you access to the various 25 | attributes as lua values, that can be queried and 26 | modified. 27 | --]] 28 | 29 | require("remotesvg.SVGElements")() 30 | local FileStream = require("remotesvg.filestream") 31 | local SVGStream = require("remotesvg.SVGStream") 32 | 33 | 34 | local ImageStream = SVGStream(FileStream.open("test_lineargradient.svg")) 35 | 36 | 37 | local doc = svg{ 38 | version="1.2", 39 | baseProfile="tiny", 40 | ['xml:id']="svg-root", 41 | ['xmlns:xlink']="http://www.w3.org/1999/xlink", 42 | ['xmlns:xe']="http://www.w3.org/2001/xml-events", 43 | width="100%", 44 | height="100%", 45 | viewBox="0 0 480 360", 46 | 47 | [[ 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ]]; 56 | 57 | [[ 58 | 62 | 63 |

Test different ways of defining a motion path.

64 |

65 | An animation moves a triangle along a path. Reference rectangles, lines and text are provided to help show what 66 | the correct behavior is. The red text shows the way that the motion path is specified. 67 |

68 |

This animation uses the 'values' attribute to define the motion path, with a linear calcMode.

69 |
70 |
71 | ]]; 72 | 73 | [[ 74 | $RCSfile: animate-elem-05-t.svg,v $ 75 | ]]; 76 | 77 | defs{ 78 | [[ 79 | 80 | 81 | 82 | 83 | 84 | ]]; 85 | }; 86 | 87 | 88 | g { 89 | ['xml:id']="test-body-content", 90 | ['font-family']="SVGFreeSansASCII,sans-serif", 91 | ['font-size']="18", 92 | 93 | g { 94 | ['font-family']="Arial", 95 | ['font-size']="36", 96 | 97 | [[ 98 | Test a motion path 99 | 'values' attribute. 100 | 101 | 102 | 0 sec. 103 | 104 | 3+ 105 | 106 | 6+ 107 | ]]; 108 | 109 | path { 110 | d="M-30,0 L0,-60 L30,0 z", 111 | fill="blue", 112 | stroke="red", 113 | ['stroke-width']=6, 114 | 115 | animateMotion {values="90,258;240,180;390,180", begin="0s", dur="6s", calcMode="linear", fill="freeze"} 116 | } 117 | } 118 | }; 119 | 120 | 121 | g { 122 | ['font-family']="SVGFreeSansASCII,sans-serif", 123 | ['font-size']="32", 124 | text ({['xml:id']="revision", x="10", y="340", stroke="none", fill="black"}, "$Revision: 1.9 $"), 125 | }; 126 | 127 | [[ 128 | 129 | 130 | 135 | ]] 136 | } 137 | 138 | doc:write(ImageStream) 139 | 140 | -------------------------------------------------------------------------------- /remotesvg/SVGInteractor.lua: -------------------------------------------------------------------------------- 1 | _G.TURBO_SSL = true -- SSL must be enabled for WebSocket support! 2 | 3 | local ffi = require("ffi") 4 | local turbo = require("turbo") 5 | 6 | local MemoryStream = require("remotesvg.memorystream") 7 | local SVGStream = require("remotesvg.SVGStream") 8 | 9 | --[[ 10 | Application Variables 11 | --]] 12 | local serviceport = tonumber(arg[1]) or 8080 13 | local ioinstance = nil; 14 | local FrameInterval = 1000; 15 | local FrameIntervalRef = nil; 16 | 17 | 18 | local PageInterval = 2000; 19 | local ImageBitCount = 32; 20 | local ScreenWidth = nil; 21 | local ScreenHeight = nil; 22 | local captureWidth = nil; 23 | local captureHeight = nil; 24 | 25 | local mstream = nil; 26 | local ImageStream = nil; 27 | 28 | 29 | --[[ 30 | pageInterval 31 | 32 | Set the number of milliseconds between page loads. This 33 | value is embedded in the .htm file that the client downloads 34 | initially, so it should be called before 'run()'. 35 | --]] 36 | function pageInterval(newInterval) 37 | PageInterval = newInterval or 1000; 38 | end 39 | 40 | --[[ 41 | frameInterval 42 | 43 | Determines how frequently your 'frame()' function is called, 44 | if you choose to implement one. 45 | 46 | From within your application, call 'frameInterval(frameTime)' 47 | to specify the interval between frames. You can call this 48 | at any time. 49 | --]] 50 | function frameInterval(newInterval) 51 | 52 | -- cancel the last interval 53 | if ioinstance then 54 | ioinstance:clear_interval(FrameIntervalRef); 55 | end 56 | 57 | FrameInterval = newInterval; 58 | 59 | if ioinstance then 60 | FrameIntervalRef = ioinstance:set_interval(FrameInterval, onInterval, ioinstance) 61 | end 62 | end 63 | 64 | 65 | --[[ 66 | size 67 | 68 | Called initially by your application to get a handle 69 | on the stream your image is written into. 70 | 71 | params can contain 'width' and 'height' as hints as to 72 | how large the image is going to be. This will also be 73 | embedded in the .htm file the client loads. 74 | --]] 75 | function size(params) 76 | params = params or {} 77 | 78 | ScreenWidth = params.width or 0; 79 | ScreenHeight = params.height or 0; 80 | 81 | captureWidth = ScreenWidth; 82 | captureHeight = ScreenHeight; 83 | 84 | ImageWidth = captureWidth * 1.0; 85 | ImageHeight = captureHeight * 1.0; 86 | 87 | 88 | -- 1Mb should be good enough for most 89 | -- images 90 | mstream = MemoryStream:new(1024*1024); 91 | ImageStream = SVGStream(mstream); 92 | 93 | return ImageStream; 94 | end 95 | 96 | 97 | local DefaultHandler = class("DefaultHandler", turbo.web.RequestHandler) 98 | function DefaultHandler:get(...) 99 | self:write("Example Handler: Hello world!") 100 | end 101 | 102 | 103 | -- Request handler for /grab%.svg(.*)$ 104 | local GrabHandler = class("GrabHandler", turbo.web.RequestHandler) 105 | 106 | function GrabHandler:get(...) 107 | --turbo.log.devel("ScreenHandler: "..self.request.host) 108 | local bytesWritten = mstream.BytesWritten; 109 | 110 | self:set_status(200) 111 | self:add_header("Content-Type", "image/svg+xml") 112 | self:add_header("Content-Length", tostring(bytesWritten)) 113 | --self:add_header("Content-Encoding", "gzip") 114 | self:add_header("Connection", "Keep-Alive") 115 | 116 | --print("=== SVG - BEGIN ===") 117 | local str = ffi.string(mstream.Buffer, bytesWritten) 118 | -- print(str); 119 | 120 | self:write(str); 121 | self:flush(); 122 | end 123 | 124 | -- Default request handler 125 | local StartupHandler = class("StartupHandler", turbo.web.RequestHandler) 126 | 127 | local startupContent = nil; 128 | 129 | local function loadStartupContent(self) 130 | 131 | -- load the file into memory 132 | local content = require("remotesvg.viewsvg_htm") 133 | 134 | -- perform the substitution of values 135 | -- assume content looks like this: 136 | -- : 137 | local subs = { 138 | ["authority"] = self.request.host; 139 | --["hostip"] = net:GetLocalAddress(), 140 | --["httpbase"] = self:get_header("x-rmt-http-url-base"), 141 | --["websocketbase"] = self:get_header("x-rmt-ws-url-base"), 142 | ["websocketbase"] = "ws://"..self.request.host..'/', 143 | ["serviceport"] = serviceport, 144 | 145 | ["pageinterval"] = PageInterval, 146 | ["capturewidth"] = captureWidth, 147 | ["captureheight"] = captureHeight, 148 | ["imagewidth"] = ImageWidth, 149 | ["imageheight"] = ImageHeight, 150 | ["screenwidth"] = ScreenWidth, 151 | ["screenheight"] = ScreenHeight, 152 | } 153 | 154 | startupContent = string.gsub(content, "%<%?(%a+)%?%>", subs) 155 | 156 | return startupContent 157 | end 158 | 159 | function StartupHandler:get(...) 160 | 161 | if not startupContent then 162 | loadStartupContent(self) 163 | end 164 | 165 | -- send the content back to the requester 166 | self:set_status(200) 167 | self:add_header("Content-Type", "text/html") 168 | self:add_header("Connection", "Keep-Alive") 169 | self:write(startupContent); 170 | self:flush(); 171 | 172 | return true 173 | end 174 | 175 | local WSExHandler = class("WSExHandler", turbo.websocket.WebSocketHandler) 176 | 177 | function WSExHandler:on_message(msg) 178 | --print("WSExHandler:on_message: ", msg) 179 | -- assume the msg is a lua string, so parse it 180 | -- BUGBUG - This should be changed, as it's an easy injection vector 181 | local f = loadstring("return "..msg) 182 | if not f then return end 183 | 184 | local tbl = f(); 185 | 186 | if type(tbl) ~= "table" then 187 | return; 188 | end 189 | 190 | self:handleIOActivity(tbl) 191 | end 192 | 193 | 194 | function WSExHandler:handleIOActivity(activity) 195 | --print("IO: ", activity.action) 196 | 197 | local handler = _G[activity.action]; 198 | 199 | if not handler then return end 200 | 201 | handler(activity) 202 | end 203 | 204 | --[[ 205 | There are two ways to handle iteration 206 | Either by idle time (every time through event loop) 207 | or by time interval. 208 | 209 | You can change which way is utilized by using the 210 | appropriate mechanism in the run() function. 211 | 212 | You can even setup to use both. 213 | --]] 214 | local function onLoop(ioinstance) 215 | if loop then 216 | loop() 217 | end 218 | ioinstance:add_callback(onLoop, ioinstance) 219 | end 220 | 221 | local function onInterval(ioinstance) 222 | if frame then 223 | frame() 224 | 225 | -- create a fresh new memorystream 226 | -- dump the older one 227 | end 228 | end 229 | 230 | 231 | -- uncomment if you want to suppress success messages 232 | --turbo.log.categories.success = false; 233 | 234 | 235 | local app = nil; 236 | 237 | function run() 238 | app = turbo.web.Application({ 239 | {"/(jquery%.js)", turbo.web.StaticFileHandler, "./jquery.js"}, 240 | {"/(favicon%.ico)", turbo.web.StaticFileHandler, "./favicon.ico"}, 241 | {"/grab%.svg(.*)$", GrabHandler}, 242 | {"/screen", StartupHandler}, 243 | {"/ws/uiosocket", WSExHandler} 244 | }) 245 | 246 | app:listen(serviceport) 247 | ioinstance = turbo.ioloop.instance() 248 | 249 | FrameIntervalRef = ioinstance:set_interval(FrameInterval, onInterval, ioinstance) 250 | --ioinstance:add_callback(onLoop, ioinstance) 251 | 252 | ioinstance:start() 253 | end 254 | -------------------------------------------------------------------------------- /remotesvg/colors.lua: -------------------------------------------------------------------------------- 1 | local bit = require("bit") 2 | local band, bor, lshift, rshift = bit.band, bit.bor, bit.lshift, bit.rshift 3 | local maths = require("remotesvg.maths"); 4 | local clamp = maths.clamp 5 | 6 | -- The actual layout in memory is: 7 | -- Little Endian - BGRA 8 | -- Big Endian - ARGB 9 | -- For things that actually depend on this memory ordering 10 | -- they should take care of this difference 11 | local function RGBA(r, g, b, a) 12 | a = a or 255; 13 | local num = tonumber(bor(lshift(tonumber(a),24), lshift(tonumber(r),16), lshift(tonumber(g),8), tonumber(b))) 14 | 15 | return num; 16 | end 17 | 18 | local function colorComponents(c) 19 | local b = band(c, 0xff); 20 | local g = band(rshift(c,8), 0xff); 21 | local r = band(rshift(c,16), 0xff); 22 | local a = band(rshift(c,24), 0xff); 23 | 24 | return r, g, b, a 25 | end 26 | 27 | --[[ 28 | c - unsigned int 29 | a color value, including r,g,b,a components 30 | u - float 31 | should be a value between 0.0 and 1.0 32 | --]] 33 | local function applyOpacity(c, u) 34 | 35 | local iu = clamp(u, 0.0, 1.0) * 256.0; 36 | local r, g, b, a = colorComponents(c); 37 | a = rshift(a*iu, 8); 38 | 39 | return RGBA(r, g, b, a); 40 | end 41 | 42 | --[[ 43 | linear interpolation of value from color c0 to 44 | color c1 45 | --]] 46 | local function lerpRGBA(c0, c1, u) 47 | 48 | local iu = clamp(u, 0.0, 1.0) * 256.0; 49 | local c0r, c0g, c0b, c0a = colorComponents(c0); 50 | local c1r, c1g, c1b, c1a = colorComponents(c1); 51 | 52 | local r = rshift(c0r*(256-iu) + (c1r*iu), 8); 53 | local g = rshift(c0g*(256-iu) + (c1g*iu), 8); 54 | local b = rshift(c0b*(256-iu) + (c1b*iu), 8); 55 | local a = rshift(c0a*(256-iu) + (c1a*iu), 8); 56 | 57 | return RGBA(r, g, b, a); 58 | end 59 | 60 | --[[ 61 | local function SVG_RGB(r, g, b) 62 | return bor(lshift(b,16), lshift(g,8), r) 63 | end 64 | --]] 65 | 66 | local exports = { 67 | applyOpacity = applyOpacity; 68 | colorComponents = colorComponents; 69 | lerpRGBA = lerpRGBA; 70 | RGBA = RGBA; 71 | 72 | white = RGBA(255, 255, 255); 73 | black = RGBA(0,0,0); 74 | blue = RGBA(0,0,255); 75 | green = RGBA(0,255,0); 76 | red = RGBA(255, 0, 0); 77 | 78 | yellow = RGBA(255, 255, 0); 79 | darkyellow = RGBA(127, 127, 0); 80 | 81 | -- grays 82 | lightgray = RGBA(235, 235, 235); 83 | 84 | -- SVG color values 85 | svg = { 86 | ["red"] = RGBA(255, 0, 0) ; 87 | ["green"] = RGBA( 0, 128, 0) ; 88 | ["blue"] = RGBA( 0, 0, 255) ; 89 | ["yellow"] = RGBA(255, 255, 0) ; 90 | ["cyan"] = RGBA( 0, 255, 255) ; 91 | ["magenta"] = RGBA(255, 0, 255) ; 92 | ["black"] = RGBA( 0, 0, 0) ; 93 | ["grey"] = RGBA(128, 128, 128) ; 94 | ["gray"] = RGBA(128, 128, 128) ; 95 | ["white"] = RGBA(255, 255, 255) ; 96 | 97 | ["aliceblue"] = RGBA(240, 248, 255) ; 98 | ["antiquewhite"] = RGBA(250, 235, 215) ; 99 | ["aqua"] = RGBA( 0, 255, 255) ; 100 | ["aquamarine"] = RGBA(127, 255, 212) ; 101 | ["azure"] = RGBA(240, 255, 255) ; 102 | ["beige"] = RGBA(245, 245, 220) ; 103 | ["bisque"] = RGBA(255, 228, 196) ; 104 | ["blanchedalmond"] = RGBA(255, 235, 205) ; 105 | ["blueviolet"] = RGBA(138, 43, 226) ; 106 | ["brown"] = RGBA(165, 42, 42) ; 107 | ["burlywood"] = RGBA(222, 184, 135) ; 108 | ["cadetblue"] = RGBA( 95, 158, 160) ; 109 | ["chartreuse"] = RGBA(127, 255, 0) ; 110 | ["chocolate"] = RGBA(210, 105, 30) ; 111 | ["coral"] = RGBA(255, 127, 80) ; 112 | ["cornflowerblue"] = RGBA(100, 149, 237) ; 113 | ["cornsilk"] = RGBA(255, 248, 220) ; 114 | ["crimson"] = RGBA(220, 20, 60) ; 115 | ["darkblue"] = RGBA( 0, 0, 139) ; 116 | ["darkcyan"] = RGBA( 0, 139, 139) ; 117 | ["darkgoldenrod"] = RGBA(184, 134, 11) ; 118 | ["darkgray"] = RGBA(169, 169, 169) ; 119 | ["darkgreen"] = RGBA( 0, 100, 0) ; 120 | ["darkgrey"] = RGBA(169, 169, 169) ; 121 | ["darkkhaki"] = RGBA(189, 183, 107) ; 122 | ["darkmagenta"] = RGBA(139, 0, 139) ; 123 | ["darkolivegreen"] = RGBA( 85, 107, 47) ; 124 | ["darkorange"] = RGBA(255, 140, 0) ; 125 | ["darkorchid"] = RGBA(153, 50, 204) ; 126 | ["darkred"] = RGBA(139, 0, 0) ; 127 | ["darksalmon"] = RGBA(233, 150, 122) ; 128 | ["darkseagreen"] = RGBA(143, 188, 143) ; 129 | ["darkslateblue"] = RGBA( 72, 61, 139) ; 130 | ["darkslategray"] = RGBA( 47, 79, 79) ; 131 | ["darkslategrey"] = RGBA( 47, 79, 79) ; 132 | ["darkturquoise"] = RGBA( 0, 206, 209) ; 133 | ["darkviolet"] = RGBA(148, 0, 211) ; 134 | ["deeppink"] = RGBA(255, 20, 147) ; 135 | ["deepskyblue"] = RGBA( 0, 191, 255) ; 136 | ["dimgray"] = RGBA(105, 105, 105) ; 137 | ["dimgrey"] = RGBA(105, 105, 105) ; 138 | ["dodgerblue"] = RGBA( 30, 144, 255) ; 139 | ["firebrick"] = RGBA(178, 34, 34) ; 140 | ["floralwhite"] = RGBA(255, 250, 240) ; 141 | ["forestgreen"] = RGBA( 34, 139, 34) ; 142 | ["fuchsia"] = RGBA(255, 0, 255) ; 143 | ["gainsboro"] = RGBA(220, 220, 220) ; 144 | ["ghostwhite"] = RGBA(248, 248, 255) ; 145 | ["gold"] = RGBA(255, 215, 0) ; 146 | ["goldenrod"] = RGBA(218, 165, 32) ; 147 | ["greenyellow"] = RGBA(173, 255, 47) ; 148 | ["honeydew"] = RGBA(240, 255, 240) ; 149 | ["hotpink"] = RGBA(255, 105, 180) ; 150 | ["indianred"] = RGBA(205, 92, 92) ; 151 | ["indigo"] = RGBA( 75, 0, 130) ; 152 | ["ivory"] = RGBA(255, 255, 240) ; 153 | ["khaki"] = RGBA(240, 230, 140) ; 154 | ["lavender"] = RGBA(230, 230, 250) ; 155 | ["lavenderblush"] = RGBA(255, 240, 245) ; 156 | ["lawngreen"] = RGBA(124, 252, 0) ; 157 | ["lemonchiffon"] = RGBA(255, 250, 205) ; 158 | ["lightblue"] = RGBA(173, 216, 230) ; 159 | ["lightcoral"] = RGBA(240, 128, 128) ; 160 | ["lightcyan"] = RGBA(224, 255, 255) ; 161 | ["lightgoldenrodyellow"] = RGBA(250, 250, 210) ; 162 | ["lightgray"] = RGBA(211, 211, 211) ; 163 | ["lightgreen"] = RGBA(144, 238, 144) ; 164 | ["lightgrey"] = RGBA(211, 211, 211) ; 165 | ["lightpink"] = RGBA(255, 182, 193) ; 166 | ["lightsalmon"] = RGBA(255, 160, 122) ; 167 | ["lightseagreen"] = RGBA( 32, 178, 170) ; 168 | ["lightskyblue"] = RGBA(135, 206, 250) ; 169 | ["lightslategray"] = RGBA(119, 136, 153) ; 170 | ["lightslategrey"] = RGBA(119, 136, 153) ; 171 | ["lightsteelblue"] = RGBA(176, 196, 222) ; 172 | ["lightyellow"] = RGBA(255, 255, 224) ; 173 | ["lime"] = RGBA( 0, 255, 0) ; 174 | ["limegreen"] = RGBA( 50, 205, 50) ; 175 | ["linen"] = RGBA(250, 240, 230) ; 176 | ["maroon"] = RGBA(128, 0, 0) ; 177 | ["mediumaquamarine"] = RGBA(102, 205, 170) ; 178 | ["mediumblue"] = RGBA( 0, 0, 205) ; 179 | ["mediumorchid"] = RGBA(186, 85, 211) ; 180 | ["mediumpurple"] = RGBA(147, 112, 219) ; 181 | ["mediumseagreen"] = RGBA( 60, 179, 113) ; 182 | ["mediumslateblue"] = RGBA(123, 104, 238) ; 183 | ["mediumspringgreen"] = RGBA( 0, 250, 154) ; 184 | ["mediumturquoise"] = RGBA( 72, 209, 204) ; 185 | ["mediumvioletred"] = RGBA(199, 21, 133) ; 186 | ["midnightblue"] = RGBA( 25, 25, 112) ; 187 | ["mintcream"] = RGBA(245, 255, 250) ; 188 | ["mistyrose"] = RGBA(255, 228, 225) ; 189 | ["moccasin"] = RGBA(255, 228, 181) ; 190 | ["navajowhite"] = RGBA(255, 222, 173) ; 191 | ["navy"] = RGBA( 0, 0, 128) ; 192 | ["oldlace"] = RGBA(253, 245, 230) ; 193 | ["olive"] = RGBA(128, 128, 0) ; 194 | ["olivedrab"] = RGBA(107, 142, 35) ; 195 | ["orange"] = RGBA(255, 165, 0) ; 196 | ["orangered"] = RGBA(255, 69, 0) ; 197 | ["orchid"] = RGBA(218, 112, 214) ; 198 | ["palegoldenrod"] = RGBA(238, 232, 170) ; 199 | ["palegreen"] = RGBA(152, 251, 152) ; 200 | ["paleturquoise"] = RGBA(175, 238, 238) ; 201 | ["palevioletred"] = RGBA(219, 112, 147) ; 202 | ["papayawhip"] = RGBA(255, 239, 213) ; 203 | ["peachpuff"] = RGBA(255, 218, 185) ; 204 | ["peru"] = RGBA(205, 133, 63) ; 205 | ["pink"] = RGBA(255, 192, 203) ; 206 | ["plum"] = RGBA(221, 160, 221) ; 207 | ["powderblue"] = RGBA(176, 224, 230) ; 208 | ["purple"] = RGBA(128, 0, 128) ; 209 | ["rosybrown"] = RGBA(188, 143, 143) ; 210 | ["royalblue"] = RGBA( 65, 105, 225) ; 211 | ["saddlebrown"] = RGBA(139, 69, 19) ; 212 | ["salmon"] = RGBA(250, 128, 114) ; 213 | ["sandybrown"] = RGBA(244, 164, 96) ; 214 | ["seagreen"] = RGBA( 46, 139, 87) ; 215 | ["seashell"] = RGBA(255, 245, 238) ; 216 | ["sienna"] = RGBA(160, 82, 45) ; 217 | ["silver"] = RGBA(192, 192, 192) ; 218 | ["skyblue"] = RGBA(135, 206, 235) ; 219 | ["slateblue"] = RGBA(106, 90, 205) ; 220 | ["slategray"] = RGBA(112, 128, 144) ; 221 | ["slategrey"] = RGBA(112, 128, 144) ; 222 | ["snow"] = RGBA(255, 250, 250) ; 223 | ["springgreen"] = RGBA( 0, 255, 127) ; 224 | ["steelblue"] = RGBA( 70, 130, 180) ; 225 | ["tan"] = RGBA(210, 180, 140) ; 226 | ["teal"] = RGBA( 0, 128, 128) ; 227 | ["thistle"] = RGBA(216, 191, 216) ; 228 | ["tomato"] = RGBA(255, 99, 71) ; 229 | ["turquoise"] = RGBA( 64, 224, 208) ; 230 | ["violet"] = RGBA(238, 130, 238) ; 231 | ["wheat"] = RGBA(245, 222, 179) ; 232 | ["whitesmoke"] = RGBA(245, 245, 245) ; 233 | ["yellowgreen"] = RGBA(154, 205, 50) ; 234 | }; -- svg 235 | } 236 | 237 | return exports 238 | -------------------------------------------------------------------------------- /remotesvg/memorystream.lua: -------------------------------------------------------------------------------- 1 | 2 | local ffi = require "ffi" 3 | local stream = require "remotesvg.stream" 4 | 5 | 6 | local MemoryStream = {} 7 | setmetatable(MemoryStream, { 8 | __call = function(self, ...) 9 | return self:new(...) 10 | end, 11 | }) 12 | 13 | local MemoryStream_mt = { 14 | __index = MemoryStream; 15 | } 16 | 17 | function MemoryStream.init(self, buff, bufflen, byteswritten) 18 | if not buff then return nil end 19 | if not bufflen then return nil end 20 | 21 | byteswritten = byteswritten or 0 22 | 23 | local obj = { 24 | Length = bufflen, 25 | Buffer = buff, 26 | Position = 0, 27 | BytesWritten = byteswritten, 28 | } 29 | 30 | setmetatable(obj, MemoryStream_mt) 31 | 32 | return obj 33 | end 34 | 35 | -- MemoryStream constructors 36 | -- 37 | -- MemoryStream:new(8192) -- Create a buffer with 8192 bytes 38 | -- MemoryStream:new("string" [, len]) -- create a buffer atop some lua string 39 | -- MemoryStream:new(cdata, len) -- create a buffer atop some cdata structure with length 40 | 41 | function MemoryStream.new(self, ...) 42 | local buff = nil; 43 | local bufflen = nil; 44 | local byteswritten = 0; 45 | 46 | local nargs = select('#', ...); 47 | 48 | if nargs == 1 then 49 | if type(select(1, ...)=="number") then 50 | bufflen = select(1,...); 51 | buff = ffi.new("uint8_t[?]", bufflen) 52 | byteswritten = 0 53 | elseif type(select(1,...)=="string") then 54 | buff = select(1, ...); 55 | bufflen = #buff; 56 | byteswritten = bufflen; 57 | end 58 | elseif nargs == 2 then 59 | if type(select(1, ...))=="string" then 60 | if type(select(2,...)) ~= "number" then 61 | return nil; 62 | end 63 | 64 | buff = select(1,...); 65 | bufflen = #buff; 66 | byteswritten = bufflen; 67 | elseif type(select(1,...))=="ctype" then 68 | buff = select(1,...); 69 | bufflen = ffi.sizeof(buff); 70 | byteswritten = bufflen; 71 | end 72 | end 73 | 74 | 75 | return self:init(buff, bufflen, byteswritten); 76 | end 77 | 78 | 79 | function MemoryStream:reset() 80 | self.Position = 0 81 | self.BytesWritten = 0 82 | end 83 | 84 | function MemoryStream:length() 85 | return self.Length 86 | end 87 | 88 | function MemoryStream:position(pos, origin) 89 | if pos ~= nil then 90 | return self:seek(pos, origin); 91 | end 92 | 93 | return self.Position 94 | end 95 | 96 | function MemoryStream:remaining() 97 | return self.Length - self.Position 98 | end 99 | 100 | function MemoryStream:bytesReadyToBeRead() 101 | return self.BytesWritten - self.Position 102 | end 103 | 104 | function MemoryStream:canRead() 105 | return self:bytesReadyToBeRead() > 0 106 | end 107 | 108 | function MemoryStream:seek(pos, origin) 109 | origin = origin or stream.SEEK_SET 110 | 111 | if origin == stream.SEEK_CUR then 112 | local newpos = self.Position + pos 113 | if newpos >= 0 and newpos < self.Length then 114 | self.Position = newpos 115 | end 116 | elseif origin == stream.SEEK_SET then 117 | if pos >= 0 and pos < self.Length then 118 | self.Position = pos; 119 | end 120 | elseif origin == stream.SEEK_END then 121 | local newpos = self.Length-1 + pos 122 | if newpos >= 0 and newpos < self.Length then 123 | self.Position = newpos 124 | end 125 | end 126 | 127 | return self.Position 128 | end 129 | 130 | --[[ 131 | Reading interface 132 | --]] 133 | -- The Bytes() function acts as an iterator on bytes 134 | -- from the stream. 135 | function MemoryStream:bytes(maxbytes) 136 | local buffptr = ffi.cast("const uint8_t *", self.Buffer); 137 | local bytesleft = maxbytes or math.huge 138 | local pos = -1 139 | 140 | local function closure() 141 | --print("-- REMAINING: ", bytesleft) 142 | -- if we've read the maximum nuber of bytes 143 | -- then just return nil to indicate finished 144 | if bytesleft == 0 then return end 145 | 146 | pos = pos + 1 147 | 148 | -- We've reached the end of the stream 149 | if pos >= self.Position then 150 | return nil 151 | end 152 | bytesleft = bytesleft - 1 153 | 154 | return buffptr[pos] 155 | end 156 | 157 | return closure 158 | end 159 | 160 | function MemoryStream:readByte() 161 | local buffptr = ffi.cast("const uint8_t *", self.Buffer); 162 | 163 | local pos = self.Position 164 | if pos < self.BytesWritten then 165 | self.Position = pos + 1 166 | return buffptr[pos]; 167 | end 168 | 169 | return nil, "eof" 170 | end 171 | 172 | function MemoryStream:readBytes(buff, count, offset) 173 | offset = offset or 0 174 | 175 | local pos = self.Position 176 | local remaining = self:remaining() 177 | local src = ffi.cast("const uint8_t *", self.Buffer)+pos 178 | local dst = ffi.cast("uint8_t *", buff)+offset 179 | 180 | local maxbytes = math.min(count, remaining) 181 | if maxbytes < 1 then 182 | return nil, "eof" 183 | end 184 | 185 | ffi.copy(dst, src, maxbytes) 186 | 187 | self.Position = pos + maxbytes 188 | 189 | return maxbytes 190 | end 191 | 192 | function MemoryStream:readString(count) 193 | local pos = self.Position 194 | local remaining = self.Length - pos 195 | 196 | local maxbytes = math.min(count, remaining) 197 | if maxbytes < 1 then return nil end 198 | 199 | 200 | local src = ffi.cast("const uint8_t *", self.Buffer)+pos 201 | 202 | -- advance the stream position 203 | self.Position = pos + maxbytes 204 | 205 | return ffi.string(src, maxbytes) 206 | end 207 | 208 | -- Read characters from a stream until the specified 209 | -- ending is found, or until the stream runs out of bytes 210 | local CR = string.byte("\r") 211 | local LF = string.byte("\n") 212 | 213 | function MemoryStream:readLine(maxbytes) 214 | --print("-- MemoryStream:ReadLine()"); 215 | 216 | local readytoberead = self:bytesReadyToBeRead() 217 | 218 | maxbytes = maxbytes or readytoberead 219 | 220 | local maxlen = math.min(maxbytes, readytoberead) 221 | local buffptr = ffi.cast("uint8_t *", self.Buffer); 222 | 223 | local nchars = 0; 224 | local bytesconsumed = 0; 225 | local startptr = buffptr + self.Position 226 | local abyte 227 | local err 228 | 229 | --print("-- MemoryStream:ReadLine(), maxlen: ", maxlen); 230 | 231 | for n=1, maxlen do 232 | abyte, err = self:readByte() 233 | if not abyte then 234 | break 235 | end 236 | 237 | bytesconsumed = bytesconsumed + 1 238 | 239 | if abyte == LF then 240 | break 241 | elseif abyte ~= CR then 242 | nchars = nchars+1 243 | end 244 | end 245 | 246 | -- End of File, nothing consumed 247 | if bytesconsumed == 0 then 248 | return nil, "eof" 249 | end 250 | 251 | -- A blank line 252 | if nchars == 0 then 253 | return '' 254 | end 255 | 256 | -- an actual line of data 257 | return ffi.string(startptr, nchars); 258 | end 259 | 260 | --[[ 261 | Writing interface 262 | --]] 263 | 264 | function MemoryStream:writeByte(byte) 265 | -- don't write a nil value 266 | -- a nil is not the same as a '0' 267 | if not byte then return end 268 | 269 | local pos = self.Position 270 | if pos < self.Length-1 then 271 | (ffi.cast("uint8_t *", self.Buffer)+pos)[0] = byte 272 | 273 | self.Position = pos + 1 274 | if self.Position > self.BytesWritten then 275 | self.BytesWritten = self.Position 276 | end 277 | 278 | return 1 279 | end 280 | 281 | return false 282 | end 283 | 284 | function MemoryStream:writeBytes(buff, count, offset) 285 | offset = offset or 0 286 | local pos = self.Position 287 | local size = self.Length 288 | local remaining = size - pos 289 | local maxbytes = math.min(remaining, count) 290 | 291 | if maxbytes <= 0 292 | then return 0 293 | end 294 | 295 | local dst = ffi.cast("uint8_t *", self.Buffer)+pos 296 | local src = ffi.cast("const uint8_t *", buff)+offset 297 | 298 | ffi.copy(dst, src, maxbytes); 299 | 300 | 301 | self.Position = pos + maxbytes; 302 | if self.Position > self.BytesWritten then 303 | self.BytesWritten = self.Position 304 | end 305 | 306 | return maxbytes; 307 | end 308 | 309 | function MemoryStream:writeString(str, count, offset) 310 | offset = offset or 0 311 | count = count or #str 312 | 313 | --print("-- MemoryStream:WriteString():", str); 314 | 315 | return self:writeBytes(str, count, offset) 316 | end 317 | 318 | --[[ 319 | Write the specified number of bytes from the current 320 | stream into the specified stream. 321 | 322 | Start from the current position in the current stream 323 | --]] 324 | 325 | function MemoryStream:writeStream(stream, size) 326 | local count = 0 327 | local abyte = stream:readByte() 328 | while abyte and count < size do 329 | self:writeByte(abyte) 330 | count = count + 1 331 | abyte = stream:readByte() 332 | end 333 | 334 | return count 335 | end 336 | 337 | function MemoryStream:writeLine(line) 338 | local status, err 339 | 340 | if line then 341 | status, err = self:writeString(line) 342 | if err then 343 | return nil, err 344 | end 345 | end 346 | 347 | -- write the terminator 348 | status, err = self:writeString("\r\n"); 349 | 350 | return status, err 351 | end 352 | 353 | --[[ 354 | Moving big chunks around 355 | --]] 356 | 357 | function MemoryStream:copyTo(stream) 358 | -- copy from the beginning 359 | -- to the current position 360 | local remaining = self.BytesWritten 361 | local byteswritten = 0 362 | 363 | while (byteswritten < remaining) do 364 | byteswritten = byteswritten + stream:writeBytes(self.Buffer, self.Position, byteswritten) 365 | end 366 | end 367 | 368 | 369 | 370 | --[[ 371 | Utility 372 | --]] 373 | function MemoryStream:toString() 374 | local len = self.Position 375 | 376 | if len > 0 then 377 | --print("Buffer: ", self.Buffer, len); 378 | local str = ffi.string(self.Buffer, len) 379 | return str; 380 | end 381 | 382 | return nil 383 | end 384 | 385 | return MemoryStream; 386 | -------------------------------------------------------------------------------- /remotesvg/SVGElements.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Attribute names that have the '-' or ':' characters in them 3 | -- https://www.w3.org/TR/SVG/attindex.html 4 | -- we want to allow the user to specify lua friendly names as 5 | -- table indices, without having to quote them. So, we have 6 | -- this alias table. 7 | 8 | local attrNameAlias = { 9 | accent_height = "accent-height", 10 | arabic_form = "arabic-form", 11 | 12 | cap_height = "cap-height", 13 | 14 | fill_opacity = "fill-opacity", 15 | 16 | font_face = "font-face", 17 | font_face_format = "font-face-format", 18 | font_face_src = "font-face-src", 19 | font_face_name = "font-face-name", 20 | font_family = "font-family", 21 | font_size = "font-size", 22 | font_stretch = "font-stretch", 23 | font_style = "font-style", 24 | font_variant = "font-variant", 25 | font_weight = "font-weight", 26 | 27 | glyph_name = "glyph-name", 28 | 29 | horiz_adv_x = "horiz-adv-x", 30 | horiz_origin_x = "horiz-origin-x", 31 | horiz_origin_y = "horiz-origin-y", 32 | 33 | overline_position = "overline-position", 34 | overline_thickness = "overline-thickness", 35 | 36 | panose_1 = "panose-1", 37 | 38 | rendering_intent = "rendering-intent", 39 | 40 | stop_color = "stop-color", 41 | strikethrough_position = "strikethrough-position", 42 | strikethrough_thickness = "strikethrough-thickness", 43 | stroke_linecap = "stroke-linecap", 44 | stroke_linejoin = "stroke-linejoin", 45 | stroke_width = "stroke-width", 46 | 47 | underline_position = "underline-position", 48 | underline_thickness = "underline-thickness", 49 | unicode_range = "unicode-range", 50 | units_per_em = "units-per-em", 51 | 52 | v_alphabetic = "v-alphabetic", 53 | v_hanging = "v-hanging", 54 | v_ideographic = "v-ideographic", 55 | v_mathematical = "v-mathematical", 56 | vert_adv_y = "vert-adv-y", 57 | vert_origin_x = "vert-origin-x", 58 | vert_origin_y = "vert-origin-y", 59 | 60 | x_height = "x-height", 61 | xlink_actuate = "xlink:actuate", 62 | xlink_arcrole = "xlink:arcrole", 63 | xlink_href = "xlink:href", 64 | xlink_role = "xlink:role", 65 | xlink_show = "xlink:show", 66 | xlink_title = "xlink:title", 67 | xlink_type = "xlink:type", 68 | xml_base = "xml:base", 69 | xml_lang = "xml:lang", 70 | xml_space = "xml:space", 71 | } 72 | 73 | local SVGAttributes = require("remotesvg.SVGAttributes") 74 | 75 | -- Given an attribute name, return the SVG name 76 | -- by looking up in alias table. 77 | local function realAttrName(name) 78 | return attrNameAlias[name] or name; 79 | end 80 | 81 | --[[ 82 | SVGElem 83 | 84 | A base type for all other SVG Elements. 85 | This can do the basic writing 86 | --]] 87 | local BasicElem = {} 88 | setmetatable(BasicElem, { 89 | __call = function(self, ...) 90 | return self:new(...); 91 | end, 92 | }) 93 | local BasicElem_mt = { 94 | __index = BasicElem; 95 | } 96 | 97 | function BasicElem.new(self, kind, params) 98 | local obj = params or {} 99 | obj._kind = kind; 100 | 101 | setmetatable(obj, BasicElem_mt); 102 | 103 | return obj; 104 | end 105 | 106 | -- Add an attribute to ourself 107 | function BasicElem.attr(self, name, value) 108 | self[name] = value; 109 | return self; 110 | end 111 | 112 | -- Add a new child element 113 | function BasicElem.appendLiteral(self, literal) 114 | table.insert(self, literal) 115 | return self; 116 | end 117 | 118 | function BasicElem.append(self, name) 119 | -- based on the obj, find the right object 120 | -- to represent it. 121 | local child = nil; 122 | 123 | if type(name) == "table" then 124 | child = name; 125 | elseif type(name) == "string" then 126 | child = BasicElem(name); 127 | else 128 | return nil; 129 | end 130 | 131 | table.insert(self, child); 132 | 133 | return child; 134 | end 135 | 136 | function BasicElem.attributes(self) 137 | local function yieldAttributes(parent) 138 | for name, value in pairs(parent) do 139 | if type(name) ~= "number" and name ~= "_kind" then 140 | coroutine.yield(name, value) 141 | end 142 | end 143 | end 144 | 145 | return coroutine.wrap(function() yieldAttributes(self) end) 146 | end 147 | 148 | -- go through each attribute parsing it 149 | -- to get a native lua form 150 | function BasicElem.parseAttributes(self, strict) 151 | local newVals = {} 152 | --for name, value in pairs(self) do 153 | for name, value in self:attributes() do 154 | --print("BasicElem.parseAttributes: ", name, type(name), value) 155 | local n, val = SVGAttributes.parseAttribute(name, value, strict) 156 | if n ~= name then 157 | self[name] = nil; 158 | end 159 | 160 | if n then 161 | self[n] = val; 162 | end 163 | end 164 | 165 | -- tell our children to parse their 166 | -- attributes 167 | for idx, value in ipairs(self) do 168 | if type(value) == "table" then 169 | value:parseAttributes(strict); 170 | end 171 | end 172 | end 173 | 174 | 175 | --[[ 176 | Traverse the elements in document order, returning 177 | the ones that match a given predicate. 178 | If no predicate is supplied, then return all the 179 | elements. 180 | --]] 181 | function BasicElem.selectElementMatches(self, pred) 182 | local function yieldMatches(parent, predicate) 183 | for idx, value in ipairs(parent) do 184 | if predicate then 185 | if predicate(value) then 186 | coroutine.yield(value) 187 | end 188 | else 189 | coroutine.yield(value) 190 | end 191 | 192 | if type(value) == "table" then 193 | yieldMatches(value, predicate) 194 | end 195 | end 196 | end 197 | 198 | return coroutine.wrap(function() yieldMatches(self, pred) end) 199 | end 200 | 201 | -- A convenient shorthand for selecting all the elements 202 | -- in the document. No predicate is specified. 203 | function BasicElem.selectAll(self) 204 | return self:selectElementMatches() 205 | end 206 | 207 | function BasicElem.getElementById(self, id) 208 | local function filterById(entry) 209 | print("filterById: ", entry.id, id) 210 | if entry.id == id then 211 | return true; 212 | end 213 | end 214 | 215 | for child in self:selectMatches(filterById) do 216 | return child; 217 | end 218 | end 219 | 220 | function BasicElem.write(self, strm) 221 | strm:openElement(self._kind); 222 | 223 | local childcount = 0; 224 | 225 | for name, value in pairs(self) do 226 | --print("\nBasicElem.write (pairs): ", name, value) 227 | if type(name) == "number" then 228 | childcount = childcount + 1; 229 | else 230 | if name ~= "_kind" then 231 | name = realAttrName(name); 232 | strm:writeAttribute(name, tostring(value)); 233 | end 234 | end 235 | end 236 | 237 | -- if we have some number of child nodes 238 | -- then write them out 239 | if childcount > 0 then 240 | -- first close the starting tag 241 | strm:closeTag(); 242 | 243 | -- write out child nodes 244 | for idx, value in ipairs(self) do 245 | if type(value) == "table" then 246 | value:write(strm); 247 | elseif type(value) == "function" then 248 | else 249 | -- write out pure text nodes 250 | strm:write(tostring(value)); 251 | end 252 | end 253 | 254 | strm:closeElement(self._kind); 255 | else 256 | strm:closeElement(); 257 | end 258 | end 259 | 260 | 261 | --[[ 262 | SVG 263 | 264 | We specialize this one to ensure the xmlns and version 265 | attributes are there. 266 | 267 | Without the xmlns attribute, some renderers will fail. 268 | --]] 269 | local function SVG(params) 270 | local elem = BasicElem('svg', params); 271 | elem.xmlns = elem.xmlns or "http://www.w3.org/2000/svg" 272 | elem.version = params.version or "1.1" 273 | 274 | return elem; 275 | end 276 | 277 | 278 | --[[ 279 | Path 280 | 281 | A specialization so that path construction can happen 'on the fly'. 282 | --]] 283 | local Path = {} 284 | setmetatable(Path, { 285 | __call = function(self, ...) -- functor 286 | return self:new(...); 287 | end, 288 | }) 289 | 290 | function Path.new(self, params) 291 | local obj = params or {} 292 | obj._kind = "path"; 293 | 294 | local meta = { 295 | __index = Path; 296 | Commands = {}; 297 | } 298 | setmetatable(obj, meta); 299 | 300 | return obj; 301 | end 302 | 303 | function Path.addCommand(self, ins, ...) 304 | local tbl = getmetatable(self); 305 | local commands = tbl.Commands; 306 | 307 | -- construct the new instruction 308 | local res = {} 309 | table.insert(res,tostring(ins)) 310 | local coords = {...}; 311 | for _, value in ipairs(coords) do 312 | table.insert(res, string.format("%d",tonumber(value))) 313 | end 314 | 315 | table.insert(commands, table.concat(res, ' ')); 316 | end 317 | 318 | function Path.pathToString(self) 319 | local tbl = getmetatable(self); 320 | local commands = tbl.Commands; 321 | 322 | return table.concat(commands); 323 | end 324 | 325 | -- Path construction commands 326 | -- add a single arc segment 327 | function Path.arcBy(self, rx, ry, rotation, large, sweep, x, y) 328 | self:addCommand('a', rx, ry, rotation, large, sweep, x, y) 329 | return self 330 | end 331 | 332 | function Path.arcTo(self, rx, ry, rotation, large, sweep, x, y) 333 | self:addCommand('A', rx, ry, rotation, large, sweep, x, y) 334 | return self 335 | end 336 | 337 | function Path.close(self) 338 | self:addCommand('z') 339 | return self; 340 | end 341 | 342 | function Path.cubicBezierTo(self, ...) 343 | self:addCommand('C', ...) 344 | return self 345 | end 346 | 347 | function Path.quadraticBezierTo(self, ...) 348 | self:addCommand('Q', ...) 349 | return self 350 | end 351 | 352 | function Path.hLineBy(self, x, y) 353 | self:addCommand('h', x, y) 354 | return self 355 | end 356 | 357 | function Path.hLineTo(self, x, y) 358 | self:addCommand('H', x, y) 359 | return self 360 | end 361 | 362 | -- Line to relative position 363 | function Path.lineBy(self, x,y) 364 | self:addCommand('l', x, y) 365 | return self 366 | end 367 | 368 | -- Line to absolute position 369 | function Path.lineTo(self, x,y) 370 | self:addCommand('L', x, y) 371 | return self 372 | end 373 | 374 | -- Move to position 375 | function Path.moveBy(self, x, y) 376 | self:addCommand('m', x,y) 377 | return self 378 | end 379 | function Path.moveTo(self, x, y) 380 | self:addCommand('M', x,y) 381 | return self 382 | end 383 | 384 | function Path.sqCurveBy(self, ...) 385 | self:addCommand('t', ...) 386 | return self; 387 | end 388 | 389 | function Path.sqCurveTo(self, ...) 390 | self:addCommand('T', ...) 391 | return self; 392 | end 393 | 394 | -- Vertical Line 395 | function Path.vLineBy(self, x, y) 396 | self:addCommand('v', x, y) 397 | return self 398 | end 399 | function Path.vLineTo(self, x, y) 400 | self:addCommand('V', x, y) 401 | return self 402 | end 403 | 404 | -- Write out the thing to a stream 405 | function Path.write(self, strm) 406 | strm:openElement(self._kind); 407 | 408 | local childcount = 0; 409 | 410 | -- write out attributes first 411 | for name, value in pairs(self) do 412 | if type(value) == "table" then 413 | childcount = childcount + 1; 414 | else 415 | if name ~= "_kind" then 416 | strm:writeAttribute(name, tostring(value)); 417 | end 418 | end 419 | end 420 | 421 | -- write out instructions attribute if it hasn't 422 | -- been written as an attribute already 423 | if not self.d then 424 | strm:writeAttribute("d", self:pathToString()) 425 | end 426 | 427 | if childcount > 0 then 428 | strm:closeTag(); 429 | 430 | -- write out child nodes 431 | for _, value in pairs(self) do 432 | if type(value) == "table" then 433 | value:write(strm); 434 | end 435 | end 436 | 437 | strm:closeElement(self._kind); 438 | else 439 | strm:closeElement(); 440 | end 441 | end 442 | 443 | --[[ 444 | Polygon 445 | 446 | points - table of points, each point represented by a table 447 | --]] 448 | local Polygon = {} 449 | setmetatable(Polygon, { 450 | __call = function (self, ...) 451 | return self:new(...); 452 | end, 453 | }) 454 | local Polygon_mt = { 455 | __index = Polygon; 456 | } 457 | 458 | function Polygon.init(self, params) 459 | local obj = params or {} 460 | setmetatable(obj, Polygon_mt); 461 | 462 | obj.points = obj.points or {}; 463 | 464 | return obj; 465 | end 466 | 467 | function Polygon.new(self, params) 468 | return self:init(params); 469 | end 470 | 471 | function Polygon.write(self, strm) 472 | strm:openElement("polygon") 473 | for name, value in pairs(self) do 474 | if type(value) == "string" or 475 | type(value) == "number" then 476 | strm:writeAttribute(name, tostring(value)); 477 | end 478 | end 479 | 480 | -- write out the points 481 | if #self.points > 0 then 482 | local tbl = {}; 483 | for _, pt in ipairs(self.points) do 484 | table.insert(tbl, string.format(" %d,%d", pt[1], pt[2])) 485 | end 486 | local pointsValue = table.concat(tbl, ' '); 487 | --print("pointsValue: ", pointsValue) 488 | strm:writeAttribute("points", pointsValue); 489 | end 490 | 491 | strm:closeElement(); 492 | end 493 | 494 | 495 | --[[ 496 | PolyLine 497 | 498 | points - table of points, each point represented by a table 499 | --]] 500 | local PolyLine = {} 501 | setmetatable(PolyLine, { 502 | __call = function (self, ...) 503 | return self:new(...); 504 | end, 505 | }) 506 | local PolyLine_mt = { 507 | __index = PolyLine; 508 | } 509 | 510 | function PolyLine.init(self, params) 511 | local obj = params or {} 512 | setmetatable(obj, PolyLine_mt); 513 | 514 | obj.points = obj.points or {}; 515 | 516 | return obj; 517 | end 518 | 519 | function PolyLine.new(self, params) 520 | return self:init(params); 521 | end 522 | 523 | function PolyLine.write(self, strm) 524 | strm:openElement("polyline") 525 | for name, value in pairs(self) do 526 | if type(value) == "string" or 527 | type(value) == "number" then 528 | strm:writeAttribute(name, tostring(value)); 529 | end 530 | end 531 | 532 | -- write out the points 533 | if #self.points > 0 then 534 | local tbl = {}; 535 | for _, pt in ipairs(self.points) do 536 | table.insert(tbl, string.format(" %d,%d", pt[1], pt[2])) 537 | end 538 | local pointsValue = table.concat(tbl, ' '); 539 | --print("pointsValue: ", pointsValue) 540 | strm:writeAttribute("points", pointsValue); 541 | end 542 | 543 | strm:closeElement(); 544 | end 545 | 546 | 547 | --[[ 548 | Interface exposed to outside world 549 | 550 | Reference of SVG Elements 551 | https://developer.mozilla.org/en-US/docs/Web/SVG/Element 552 | --]] 553 | local exports = { 554 | BasicElem = BasicElem; 555 | 556 | -- Animation Elements 557 | animate = function(params) return BasicElem('animate', params) end; 558 | animateColor = function(params) return BasicElem('animateColor', params) end; 559 | animateMotion = function(params) return BasicElem('animateMotion', params) end; 560 | animateTransform = function(params) return BasicElem('animateTransform', params) end; 561 | mpath = function(params) return BasicElem('mpath', params) end; 562 | set = function(params) return BasicElem('set', params) end; 563 | 564 | -- Container elements 565 | a = function(params) return BasicElem('a', params) end; 566 | defs = function(params) return BasicElem('defs', params) end; 567 | glyph = function(params) return BasicElem('glyph', params) end; 568 | g = function(params) return BasicElem('g', params) end; 569 | marker = function(params) return BasicElem('marker', params) end; 570 | mask = function(params) return BasicElem('mask', params) end; 571 | missing_glyph = function(params) return BasicElem('missing-glyph', params) end; 572 | pattern = function(params) return BasicElem('pattern', params) end; 573 | svg = SVG; 574 | switch = function(params) return BasicElem('switch', params) end; 575 | symbol = function(params) return BasicElem('symbol', params) end; 576 | use = function(params) return BasicElem('use', params) end; 577 | 578 | -- Descriptive elements 579 | desc = function(params) return BasicElem('desc', params) end; 580 | metadata = function(params) return BasicElem('metadata', params) end; 581 | title = function(params) return BasicElem('title', params) end; 582 | 583 | -- Filter primitive elements 584 | feBlend = function(params) return BasicElem('feBlend', params) end; 585 | feColorMatrix = function(params) return BasicElem('feColorMatrix', params) end; 586 | feComponentTransfer = function(params) return BasicElem('feComponentTransfer', params) end; 587 | feComposite = function(params) return BasicElem('feComposite', params) end; 588 | feConvolveMatrix = function(params) return BasicElem('feConvolveMatrix', params) end; 589 | feDiffuseLighting = function(params) return BasicElem('feDiffuseLighting', params) end; 590 | feDisplacementMap = function(params) return BasicElem('feDisplacementMap', params) end; 591 | feFlood = function(params) return BasicElem('feFlood', params) end; 592 | feFuncA = function(params) return BasicElem('feFuncA', params) end; 593 | feFuncB = function(params) return BasicElem('feFuncB', params) end; 594 | feFuncR = function(params) return BasicElem('feFuncR', params) end; 595 | feGaussianBlur = function(params) return BasicElem('feGaussianBlur', params) end; 596 | feImage = function(params) return BasicElem('feImage', params) end; 597 | feMerge = function(params) return BasicElem('feMerge', params) end; 598 | feMergeNode = function(params) return BasicElem('feMergeNode', params) end; 599 | feOffset = function(params) return BasicElem('feOffset', params) end; 600 | feSpecularLighting = function(params) return BasicElem('feSpecularLighting', params) end; 601 | feTile = function(params) return BasicElem('feTile', params) end; 602 | feTurbulance = function(params) return BasicElem('feTurbulance', params) end; 603 | 604 | -- Font elements 605 | font = function(params) return BasicElem('font', params) end; 606 | font_face = function(params) return BasicElem('font-face', params) end; 607 | font_face_format = function(params) return BasicElem('font-face-format', params) end; 608 | font_face_name = function(params) return BasicElem('font-face-name', params) end; 609 | font_face_src = function(params) return BasicElem('font-face-src', params) end; 610 | font_face_uri = function(params) return BasicElem('font-face-uri', params) end; 611 | hkern = function(params) return BasicElem('hkern', params) end; 612 | vkern = function(params) return BasicElem('vkern', params) end; 613 | 614 | -- Graphics elements 615 | circle = function(params) return BasicElem('circle', params) end; 616 | ellipse = function(params) return BasicElem('ellipse', params) end; 617 | image = function(params) return BasicElem('image', params) end; 618 | line = function(params) return BasicElem('line', params) end; 619 | path = Path; -- check 620 | polygon = Polygon; -- check 621 | polyline = PolyLine; -- check 622 | rect = function(params) return BasicElem('rect', params) end; 623 | 624 | -- Gradient Elements 625 | linearGradient = function(params) return BasicElem('linearGradient', params) end; 626 | radialGradient = function(params) return BasicElem('radialGradient', params) end; 627 | stop = function(params) return BasicElem('stop', params) end; 628 | 629 | -- Light Source elements 630 | feDistantLight = function(params) return BasicElem('feDistantLight', params) end; 631 | fePointLight = function(params) return BasicElem('fePointLight', params) end; 632 | feSpotLight = function(params) return BasicElem('feSpotLight', params) end; 633 | 634 | -- Text Content elements 635 | altGlyph = function(params) return BasicElem('altGlyph', params) end; 636 | altGlyphDef = function(params) return BasicElem('altGlyphDef', params) end; 637 | altGlyphItem = function(params) return BasicElem('altGlyphItem', params) end; 638 | glyph = function(params) return BasicElem('glyph', params) end; 639 | glyphRef = function(params) return BasicElem('glyphRef', params) end; 640 | text = function(params) return BasicElem('text', params) end; 641 | textPath = function(params) return BasicElem('textPath', params) end; 642 | tref = function(params) return BasicElem('tref', params) end; 643 | tspan = function(params) return BasicElem('tspan', params) end; 644 | 645 | -- Uncategorized elements 646 | clipPath = function(params) return BasicElem('clipPath', params) end; 647 | color_profile = function(params) return BasicElem('color-profile', params) end; 648 | cursor = function(params) return BasicElem('cursor', params) end; 649 | filter = function(params) return BasicElem('filter', params) end; 650 | foreignObject = function(params) return BasicElem('foreignObject', params) end; 651 | script = function(params) return BasicElem('script', params) end; 652 | style = function(params) return BasicElem('style', params) end; 653 | view = function(params) return BasicElem('view', params) end; 654 | 655 | } 656 | setmetatable(exports, { 657 | __call = function(self, tbl) 658 | tbl = tbl or _G; 659 | 660 | for k,v in pairs(exports) do 661 | tbl[k] = v; 662 | end 663 | 664 | return self; 665 | end, 666 | }) 667 | 668 | return exports 669 | -------------------------------------------------------------------------------- /remotesvg/SVGAttributes.lua: -------------------------------------------------------------------------------- 1 | 2 | -- References 3 | -- https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute 4 | -- 5 | local colors = require("remotesvg.colors") 6 | local RGB = colors.RGBA; 7 | local maths = require("remotesvg.maths") 8 | local clamp = maths.clamp; 9 | local bit = require("bit") 10 | local lshift, rshift, band, bor = bit.lshift, bit.rshift, bit.band, bit.bor 11 | 12 | local tonumber = tonumber; 13 | local tostring = tostring; 14 | 15 | -- Individual functions which deal with specific 16 | -- attributes 17 | local attrs = {} 18 | 19 | -- This is the function to be used when we don't have 20 | -- any specific specialization for an attribute 21 | function default(name, value, strict) 22 | --print("{name = '', parser = default}: ", name, value); 23 | 24 | return name, value 25 | end 26 | 27 | 28 | -- These functions convert from SVG Attribute string values 29 | -- to lua specific types 30 | local function color(name, value, strict) 31 | local function parseColorName(name) 32 | return colors.svg[name] or RGB(128, 128, 128); 33 | end 34 | 35 | local function parseColorHex(s) 36 | --print(".parseColorHex: ", s) 37 | 38 | local rgb = s:match("#(%x+)") 39 | --print("rgb: ", rgb) 40 | local r,g,b = 0,0,0; 41 | local c = 0; 42 | --rgb = "0x"..rgb; 43 | 44 | if #rgb == 6 then 45 | c = tonumber("0x"..rgb) 46 | elseif #rgb == 3 then 47 | c = tonumber("0x"..rgb); 48 | c = bor(band(c,0xf), lshift(band(c,0xf0), 4), lshift(band(c,0xf00), 8)); 49 | c = bor(c, lshift(c,4)); 50 | end 51 | b = band(rshift(c, 16), 0xff); 52 | g = band(rshift(c, 8), 0xff); 53 | r = band(c, 0xff); 54 | 55 | return RGB(r,g,b); 56 | end 57 | 58 | local function parseColorRGB(str) 59 | -- if numberpatt uses %x instead of %d, then do the following 60 | -- to get past the 'b' in 'rgb(' 61 | --local loc = str:find("%(") 62 | --str = str:sub(loc+1,#str) 63 | local numberpatt = "(%d+)" 64 | local usePercent = str:find("%%") 65 | 66 | --print("parseColorRGB: ", str, usePercent) 67 | 68 | local tbl = {} 69 | for num in str:gmatch(numberpatt) do 70 | if usePercent then 71 | table.insert(tbl, tonumber(num)*255/100) 72 | else 73 | table.insert(tbl, tonumber(num)) 74 | end 75 | end 76 | 77 | return RGB(tbl[1], tbl[2], tbl[3]) 78 | end 79 | 80 | local str = value:match("%s*(.*)") 81 | local len = #str; 82 | 83 | --print("parseColor: ", str) 84 | if len >= 1 and str:sub(1,1) == '#' then 85 | value = parseColorHex(str); 86 | elseif (len >= 4 and str:match("rgb%(")) then 87 | value = parseColorRGB(str); 88 | else 89 | value = parseColorName(str); 90 | end 91 | 92 | local r, g, b, a = colors.colorComponents(value) 93 | --print("COLOR: ", r,g,b,a) 94 | local obj = {r=r, g = g, b = b, a = a}; 95 | setmetatable(obj, { 96 | __tostring = function(self) 97 | return string.format("rgb(%d, %d, %d)", self.r, self.g, self.b) 98 | end 99 | }) 100 | 101 | return name, obj 102 | end 103 | local paint = color; 104 | 105 | 106 | local function coord(name, value, strict) 107 | --print("coord: ", name, value) 108 | 109 | if type(value) ~= "string" then 110 | return name, value; 111 | end 112 | 113 | local num, units = value:match("([+-]?%d*%.?%d*)(%g*)") 114 | 115 | --print("coord: ", name, value, num, units) 116 | 117 | if not num then return nil; end 118 | 119 | local obj = {value = tonumber(num), units = units} 120 | setmetatable(obj,{ 121 | __tostring = function(self) 122 | --print("coord.tostring: ", self.value, self.units) 123 | return string.format("%f%s",self.value, self.units or "") 124 | end, 125 | }) 126 | 127 | return name, obj; 128 | end 129 | -- The specification for length is the same as for 130 | -- coordinates 131 | local length = coord 132 | 133 | local function number(name, value, strict) 134 | if type(value) ~= "string" then 135 | return name, value; 136 | end 137 | 138 | return name, tonumber(value) 139 | end 140 | 141 | -- strictly, the SVG spec say opacity is simply 142 | -- a number between 0..1 143 | -- but, we'll allow specifying a '%' value as well... 144 | 145 | function numberorpercent(name, value, strict) 146 | if type(value) ~= "string" then 147 | return name, value; 148 | end 149 | 150 | local usePercent = value:find("%%") 151 | local num = 1.0 152 | tonumber(value) 153 | if usePercent then 154 | local str = value:match("(%d+)") 155 | num = tonumber(str) / 100; 156 | else 157 | num = tonumber(value); 158 | end 159 | 160 | local val = clamp(tonumber(value), 0, 1); 161 | 162 | return name, val; 163 | end 164 | local offset = opacity; 165 | local opacity = numberorpercent; 166 | 167 | local function parseStyle(name, value, strict) 168 | local tbl = {} 169 | 170 | for item in value:gmatch("([^;]+)") do 171 | local name, value = item:match("(%g+):(%g+)") 172 | --print("item:match: ", name, value) 173 | name, value = attrs.parseAttribute(name, value, strict) 174 | --print("parse result: ", name, value) 175 | tbl[name] = value; 176 | end 177 | 178 | setmetatable(tbl, { 179 | __tostring = function(self) 180 | local res = {} 181 | for name, value in pairs(self) do 182 | table.insert(res, string.format("%s:%s", name, tostring(value))) 183 | end 184 | 185 | return table.concat(res,';') 186 | end, 187 | }) 188 | 189 | return name, tbl 190 | end 191 | 192 | local function parseviewBox(name, value, strict) 193 | if type(value) ~= "string" then 194 | return name, value 195 | end 196 | local numpatt = "(%d*%.?%d*)" 197 | local nums = {} 198 | for num in value:gmatch(numpatt) do 199 | --print("NUM: ", num, type(num)) 200 | if num ~= "" then 201 | table.insert(nums, tonumber(num)) 202 | end 203 | end 204 | 205 | local obj = { 206 | min_x = nums[1], 207 | min_y = nums[2], 208 | width = nums[3], 209 | height= nums[4] 210 | } 211 | 212 | setmetatable(obj, { 213 | __tostring = function(self) 214 | return string.format("%f %f %f %f", 215 | self.min_x, self.min_y, self.width, self.height); 216 | end, 217 | }) 218 | 219 | return name, obj; 220 | end 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | --[[ 230 | Entries for parsers per attribute 231 | --]] 232 | 233 | attrs.accent_height = {name = 'accent-height', parser = number}; 234 | attrs.accumulate = {name = 'accumulate', pardser = default}; -- 'none' | 'sum' 235 | attrs.additive = {name = 'additive', parser = default}; -- 'replace' | 'sum' 236 | attrs.alignment_baseline = {name = 'alignment-baseline', parser = default}; 237 | attrs.allowReorder = {name = 'allowReorder', parser = default}; 238 | attrs.alphabetic = {name = 'alphabetic', parser = number}; 239 | attrs.amplitude = {name = 'amplitude', parser = default}; 240 | attrs.arabic_form = {name = 'arabic-form', parser = default}; 241 | attrs.ascent = {name = 'ascent', parser = number}; 242 | attrs.attributeName = {name = 'attributeName', parser = default}; 243 | attrs.attributeType = {name = 'attributeType', parser = default}; 244 | attrs.autoReverse = {name = 'autoReverse', parser = default}; 245 | attrs.azimuth = {name = 'azimuth', parser = default}; 246 | 247 | attrs.baseFrequency = {name = 'baseFrequency', parser = default}; 248 | attrs.baseline_shift = {name = 'baseline-shift', parser = default}; 249 | attrs.baseProfile = {name = 'baseProfile', parser = default}; 250 | attrs.bbox = {name = 'bbox', parser = default}; 251 | attrs.begin = {name = 'begin', parser = default}; 252 | attrs.bias = {name = 'bias', parser = default}; 253 | attrs.by = {name = 'by', parser = default}; 254 | 255 | attrs.calcMode = {name = 'calcMode', parser = default}; 256 | attrs.cap_height = {name = 'cap-height', parser = number}; 257 | attrs['cap-height'] = attrs.cap_height; 258 | attrs.class = {name = 'class', parser = default}; 259 | attrs.clip = {name = 'clip', parser = default}; 260 | attrs.clipPathUnits = {name = 'clipPathUnits', parser = default}; 261 | attrs.clip_path = {name = 'clip-path', parser = default}; 262 | attrs.clip_rule = {name = 'clip-rule', parser = default}; 263 | attrs.color = {name = 'color', parser = color}; 264 | attrs.color_interpolation = {name = 'color-interpolation', parser = default}; 265 | attrs.color_interpolation_filters = {name = 'color-interpolation-filters', parser = default}; 266 | attrs.color_profile = {name = 'profile', parser = default}; 267 | attrs.color_rendering = {name = 'color-rendering', parser = default}; 268 | attrs.contentScriptType = {name = 'contentScriptType', parser = default}; 269 | attrs.contentStyleType = {name = 'contentStyleType', parser = default}; 270 | attrs.cursor = {name = 'cursor', parser = default}; 271 | attrs.cx = {name='cx', parser = coord}; 272 | attrs.cy = {name = 'cy', parser = coord}; 273 | 274 | attrs.d = {name = 'd', parser = default}; 275 | attrs.decelerate = {name = 'decelerate', parser = default}; 276 | attrs.descent = {name = 'descent', parser = number}; 277 | attrs.diffuseConstant = {name = 'diffuseConstant', parser = default}; 278 | attrs.direction = {name = 'direction', parser = default}; 279 | attrs.display = {name = 'display', parser = default}; 280 | attrs.divisor = {name = 'divisor', parser = default}; 281 | attrs.dominant_baseline = {name = 'dominant-baseline', parser = default}; 282 | attrs.dur = {name = 'dur', parser = default}; 283 | attrs.dx = {name='dx', parser = number}; 284 | attrs.dy = {name = 'dy', parser = number}; 285 | 286 | attrs.edgeMode = {name = 'edgeMode', parser = default}; 287 | attrs.elevation = {name = 'elevation', parser = default}; 288 | attrs.enable_background = {name = 'enable-background', parser = default}; 289 | attrs['enable-background'] = attrs.enable_background; 290 | attrs['end'] = {name = 'end', parser = default}; 291 | attrs.exponent = {name = 'exponent', parser = default}; 292 | attrs.externalResourcesRequired = {name = 'externalResourcesRequired', parser = default}; 293 | 294 | attrs.fill = {name='fill', parser = paint}; 295 | attrs.fill_opacity = {name = 'fill-opacity', parser = opacity}; 296 | attrs['fill-opacity'] = attrs.fill_opacity; 297 | attrs.fill_rule = {name = 'fill-rule', parser = default}; 298 | attrs['fill-rule'] = attrs.fill_rule; 299 | attrs.filter = {name = 'filter', parser = default}; 300 | attrs.filterRes = {name = 'filterRes', parser = default}; 301 | attrs.filterUnits = {name = 'filterUnits', parser = default}; 302 | attrs.flood_color = {name='flood-color', parser =color}; 303 | attrs['flood-color'] = attrs.flood_color; 304 | attrs.flood_opacity = {name = 'flood-opacity', parser = opacity}; 305 | attrs.font_family = {name = 'font-family', parser = default}; 306 | attrs.font_size = {name = 'font-size', parser = default}; 307 | attrs['font-size'] = attrs.font_size; 308 | attrs.font_size_adjust = {name = 'font-size-adjust', parser = default}; 309 | attrs.font_stretch = {name = 'font-stretch', parser = default}; 310 | attrs.font_style = {name = 'font-style', parser = default}; 311 | attrs.font_variant = {name = 'font-variant', parser = default}; 312 | attrs.font_weight = {name = 'font-weight', parser = default}; 313 | attrs.format = {name = 'format', parser = default}; 314 | attrs.from = {name = 'from', parser = default}; 315 | attrs.fx = {name='fx', parser=coord}; 316 | attrs.fy = {name='fy', parser=coord}; 317 | 318 | attrs.g1 = {name = 'g1', parser = default}; 319 | attrs.g2 = {name = 'g2', parser = default}; 320 | attrs.glyph_name = {name = 'glyph-name', parser = default}; 321 | attrs.glyph_orientation_horizontal = {name = 'glyph-orinentation-horizontal', parser = default}; 322 | attrs.glyph_orientation_vertical = {name = 'glyph-orientation-vertical', parser = default}; 323 | attrs.glyphRef = {name = 'glyphRef', parser = default}; 324 | attrs.gradientTransform = {name = 'gradientTransform', parser = default}; 325 | attrs.gradientUnits = {name = 'gradientUnits', parser = default}; 326 | 327 | attrs.hanging = {name ='hanging', parser=number}; 328 | attrs.height = {name = 'height', parser=length}; 329 | attrs.horiz_adv_x = {name = 'horiz-adv-x', parser = default}; 330 | attrs.horiz_origin_x = {name = 'horiz-origin-x', parser = default}; 331 | attrs['horiz-origin-x'] = attrs.horiz_origin_x; 332 | 333 | attrs.id = {name = 'id', parser = default}; 334 | attrs.ideographic = {name = 'ideographic', parser = default}; 335 | attrs.image_rendering = {name = 'image-rendering', parser = default}; 336 | attrs['in'] = {name = 'in', parser = default}; 337 | attrs.in2 = {name = 'in2', parser = default}; 338 | attrs.intercept = {name = 'intercept', parser = default}; 339 | 340 | attrs.k = {name = 'k', parser = number}; 341 | attrs.k1 = {name = 'k1', parser = number}; 342 | attrs.k2 = {name = 'k2', parser = number}; 343 | attrs.k3 = {name = 'k3', parser = number}; 344 | attrs.k4 = {name = 'k4', parser = number}; 345 | attrs.kernelMatrix = {name = 'kernelMatrix', parser = default}; 346 | attrs.kernelUnitLength = {name = 'kernelUnitLength', parser = default}; 347 | attrs.kerning = {name = 'kerning', parser = default}; 348 | attrs.keyPoints = {name = 'keyPoints', parser = default}; 349 | attrs.keySplines = {name = 'keySplines', parser = default}; 350 | attrs.keyTimes = {name = 'keyTimes', parser = default}; 351 | 352 | attrs.lang = {name = 'lang', parser = default}; 353 | attrs.lengthAdjust = {name = 'lengthAdjust', parser = default}; 354 | attrs.letter_spacing = {name = 'letter-spacing', parser = default}; 355 | attrs.lighting_color = {name = 'lighting-color', parser = color}; 356 | attrs['lighting-color'] = attrs.lighting_color; 357 | attrs.limitingConeAngle = {name = 'limitingConeAngle', parser = default}; 358 | attrs['local'] = {name = 'local', parser = default}; 359 | 360 | attrs.marker_end = {name = 'marker-end', parser = default}; 361 | attrs['marker-end'] = attrs.marker_end; 362 | attrs.marker_mid = {name = 'marker_mid', parser = default}; 363 | attrs['marker-mid'] = attrs.marker_mid; 364 | attrs.marker_start = {name = 'marker_start', parser = default}; 365 | attrs['marker-start'] = attrs.marker_start; 366 | attrs.markerHeight = {name = 'markerHeight', parser = default}; 367 | attrs.markerUnits = {name = 'markerUnits', parser = default}; 368 | attrs.markerWidth = {name = 'markerWidth', parser = default}; 369 | attrs.mask = {name = 'mask', parser = default}; 370 | attrs.maskContentUnits = {name = 'maskContentUnits', parser = default}; 371 | attrs.maskUnits = {name = 'maskUnits', parser = default}; 372 | attrs.mathematical = {name='mathematical', parser=number}; 373 | attrs.max = {name = 'max', parser = default}; 374 | attrs.media = {name = 'media', parser = default}; 375 | attrs.method = {name = 'method', parser = default}; 376 | attrs.min = {name = 'min', parser = default}; 377 | attrs.mode = {name = 'mode', parser = default}; 378 | 379 | attrs.name = {name = 'name', parser = default}; 380 | attrs.numOctaves = {name = 'numOctaves', parser = default}; 381 | 382 | attrs.offset = {name = 'offset', parser = numberorpercent}; 383 | attrs.onabort = {name = 'onabort', parser = default}; 384 | attrs.onactivate = {name = 'onactivate', parser = default}; 385 | attrs.onbegin = {name = 'onbegin', parser = default}; 386 | attrs.onclick = {name = 'onclick', parser = default}; 387 | attrs.onend = {name = 'onend', parser = default}; 388 | attrs.onerror = {name = 'onerror', parser = default}; 389 | attrs.onfocusin = {name = 'onfocusin', parser = default}; 390 | attrs.onfocusout = {name = 'onfocusout', parser = default}; 391 | attrs.onload = {name = 'onload', parser = default}; 392 | attrs.onmousedown = {name = 'onmousedown', parser = default}; 393 | attrs.onmousemove = {name = 'onmousemove', parser = default}; 394 | attrs.onmouseout = {name = 'onmouseout', parser = default}; 395 | attrs.onmouseover = {name = 'onmouseover', parser = default}; 396 | attrs.onmouseup = {name = 'onmouseup', parser = default}; 397 | attrs.onrepeat = {name = 'onrepeat', parser = default}; 398 | attrs.onresize = {name = 'onresize', parser = default}; 399 | attrs.onscroll = {name = 'onscroll', parser = default}; 400 | attrs.onunload = {name = 'onunload', parser = default}; 401 | attrs.onzoom = {name = 'onzoom', parser = default}; 402 | attrs.opacity = {name = 'opacity', parser = opacity}; 403 | attrs.operator = {name = 'operator', parser = default}; 404 | attrs.order = {name = 'order', parser = default}; 405 | attrs.orient = {name = 'orient', parser = default}; 406 | attrs.orientation = {name = 'orientation', parser = default}; 407 | attrs.origin = {name = 'origin', parser = default}; 408 | attrs.overflow = {name = 'overflow', parser = default}; 409 | attrs.overline_position = {name = 'overline-position', parser = number}; 410 | attrs['overline-position'] = attrs.overline_position; 411 | attrs.overline_thickness = {name = 'overline-thickness', parser = number}; 412 | attrs['overline-thickness'] = attrs.overline_thickness; 413 | 414 | attrs.panose_1 = {name = 'panose-1', parser = default}; 415 | attrs['panose-1'] = attrs.panose_1; 416 | attrs.paint_order = {name = 'paint-order', parser = default}; 417 | attrs['paint-order'] = attrs.paint_order; 418 | attrs.pathLength = {name = 'pathLength', parser = default}; 419 | attrs.patternContentUnits = {name = 'patternContentUnits', parser = default}; 420 | attrs.patternTransform = {name = 'patternTransform', parser = default}; 421 | attrs.patternUnits = {name = 'patternUnits', parser = default}; 422 | attrs.pointer_events = {name = 'pointer-events', parser = default}; 423 | attrs['pointer-events'] = attrs.pointer_events; 424 | attrs.points = {name = 'points', parser = default}; 425 | attrs.pointsAtX = {name = 'pointsAtX', parser = default}; 426 | attrs.pointsAtY = {name = 'pointsAtY', parser = default}; 427 | attrs.pointsAtZ = {name = 'pointsAtZ', parser = default}; 428 | attrs.preserveAlpha = {name = 'preserveAlpha', parser = default}; 429 | attrs.preserveAspectRatio = {name = 'preserveAspectRatio', parser = default}; 430 | attrs.primitiveUnits = {name = 'primitiveUnits', parser = default}; 431 | 432 | attrs.r = {name = 'r', parser = default}; 433 | attrs.radius = {name = 'radius', parser = default}; 434 | attrs.refX = {name = 'refX', parser = default}; 435 | attrs.refY = {name = 'refY', parser = default}; 436 | attrs.rendering_intent = {name = 'rendering-intent', parser = default}; 437 | attrs['rendering-intent'] = attrs.rendering_intent; 438 | attrs.repeatCount = {name = 'repeatCount', parser = default}; 439 | attrs.repeatDur = {name = 'repeatDur', parser = default}; 440 | attrs.requiredExtensions = {name = 'requiredExtensions', parser = default}; 441 | attrs.requiredFeatures = {name = 'requiredFeatures', parser = default}; 442 | attrs.restart = {name = 'restart', parser = default}; 443 | attrs.result = {name = 'result', parser = default}; 444 | attrs.rotate = {name = 'rotate', parser = default}; 445 | attrs.rx = {name='rx', parser=length}; 446 | attrs.ry = {name='ry', parser=length}; 447 | 448 | attrs.scale = {name = 'scale', parser = number}; 449 | attrs.seed = {name = 'seed', parser = number}; 450 | attrs.shape_rendering = {name = 'shape-rendering', parser = default}; 451 | attrs['shape-rendering'] = attrs.shape_rendering; 452 | attrs.slope = {name='slope', parser=number}; 453 | attrs.spacing = {name = 'spacing', parser = default}; 454 | attrs.specularConstant = {name = 'specularConstant', parser = default}; 455 | attrs.specularExponent = {name = 'specularExponent', parser = default}; 456 | attrs.speed = {name = 'speed', parser = default}; 457 | attrs.spreadMethod = {name = 'spreadMethod', parser = default}; 458 | attrs.startOffset = {name = 'startOffset', parser = default}; 459 | attrs.stdDeviation = {name = 'stdDeviation', parser = default}; 460 | attrs.stemh = {name='stemh', parser = number}; 461 | attrs.stemv = {name = 'stemv', parser=number}; 462 | attrs.stitchTiles = {name = 'stitchTiles', parser = default}; 463 | attrs.stop_color = {name='stop-color', parser=color}; 464 | attrs['stop-color'] = attrs.stop_color; 465 | attrs.stop_opacity = {name = 'stop-opacity', parser = opacity}; 466 | attrs['stop-opacity'] = attrs.stop_opacity; 467 | attrs.strikethrough_position = {name = 'strikethrough-position', parser = number}; 468 | attrs['strikethrough-position'] = attrs.strikethrough_position; 469 | attrs.strikethrough_thickness = {name = 'strikethrough-thickness', parser = number}; 470 | attrs['strikethrough-thickness'] = attrs.strikethrough_thickness; 471 | attrs.string = {name = 'string', parser = default}; 472 | attrs.stroke = {name='stroke', parser=paint}; 473 | attrs.stroke_dasharray = {name = 'stroke-dasharray', parser = default}; 474 | attrs['stroke-dasharray'] = attrs.stroke_dasharray; 475 | attrs.stroke_dashoffset = {name = 'stroke-dashoffset', parser = default}; 476 | attrs['stroke-dashoffset'] = attrs.stroke_dashoffset; 477 | attrs.stroke_linecap = {name = 'stroke-linecap', parser = default}; 478 | attrs['stroke-linecap'] = attrs.stroke_linecap; 479 | attrs.stroke_miterlimit = {name = 'stroke-miterlimit', parser = default}; 480 | attrs['stroke-miterlimit'] = attrs.stroke_miterlimit; 481 | attrs.stroke_opacity = {name = 'stroke-opacity', parser = opacity}; 482 | attrs['stroke-opacity'] = attrs.stroke_opacity; 483 | attrs.stroke_width = {name = 'stroke-width', parser = length}; 484 | attrs['stroke-width'] = attrs.stroke_width; 485 | attrs.style = {name = 'style', parser = parseStyle}; 486 | attrs.surfaceScale = {name = 'surfaceScale', parser = default}; 487 | attrs.systemLanguage = {name = 'systemLanguage', parser = default}; 488 | 489 | attrs.tableValues = {name = 'tableValues', parser = default}; 490 | attrs.target = {name = 'target', parser = default}; 491 | attrs.targetX = {name = 'targetX', parser = default}; 492 | attrs.targetY = {name = 'targetY', parser = default}; 493 | attrs.text_anchor = {name = 'text-anchor', parser = default}; 494 | attrs['text-anchor'] = attrs.text_anchor; 495 | attrs.text_decoration = {name = 'text-decoration', parser = default}; 496 | attrs['text-decoration'] = attrs.text_decoration; 497 | attrs.text_rendering = {name = 'text-rendering', parser = default}; 498 | attrs['text-rendering'] = attrs.text_rendering; 499 | attrs.textLength = {name = 'textLength', parser = default}; 500 | attrs.to = {name = 'to', parser = default}; 501 | attrs.transform = {name = 'transform', parser = default}; 502 | attrs['type'] = {name = 'type', parser = default}; 503 | 504 | attrs.u1 = {name = '', parser = default}; 505 | attrs.u2 = {name = '', parser = default}; 506 | attrs.underline_position = {name = 'underline-position', parser = number}; 507 | attrs['underline-position'] = attrs.underline_position; 508 | attrs.underline_thickness = {name = 'underline-thickness', parser = number}; 509 | attrs['underline-thickness'] = attrs.underline_thickness; 510 | attrs.unicode = {name = 'unicode', parser = default}; 511 | attrs.unicode_bidi = {name = 'unicode-bidi', parser = default}; 512 | attrs['unicode-bidi'] = attrs.unicode_bidi; 513 | attrs.unicode_range = {name = 'unicode-range', parser = default}; 514 | attrs['unicode-range'] = attrs.unicode_range; 515 | attrs.units_per_em = {name = 'units-per-em', parser = number}; 516 | attrs['units-per-em'] = attrs.units_per_em; 517 | 518 | attrs.v_alphabetic = {name = 'v-alphabetic', parser = number}; 519 | attrs['v-alphabetic'] = attrs.v_alphabetic; 520 | attrs.v_hanging = {name = 'v-hanging', parser = number}; 521 | attrs['v-hanging'] = attrs.v_hanging; 522 | attrs.v_ideographic = {name = 'v-ideographic', parser = number}; 523 | attrs['v-ideographic'] = attrs.v_ideographic; 524 | attrs.v_mathematical = {name = 'v-mathematical', parser = default}; 525 | attrs['v-mathematical'] = attrs.v_mathematical; 526 | attrs.values = {name = 'values', parser = default}; 527 | attrs.version = {name = 'version', parser = default}; 528 | attrs.vert_adv_y = {name = 'v-adv-y', parser = default}; 529 | attrs['vert-adv-y'] = attrs.vert_adv_y; 530 | attrs.vert_origin_x = {name = 'v-origin-x', parser = default}; 531 | attrs['vert-origin-x'] = attrs.v_origin_x; 532 | attrs.vert_origin_y = {name = 'vert-origin-y', parser = default}; 533 | attrs['vert-origin-y'] = attrs.vert_origin_y; 534 | attrs.viewBox = {name = 'viewBox', parser = parseviewBox}; 535 | attrs.visibility = {name = 'visibility', parser = default}; 536 | 537 | attrs.width = {name = 'width', parser = length}; 538 | attrs.widths = {name = 'widths', parser = default}; 539 | attrs.word_spacing = {name = 'word-spacing', parser = default}; 540 | attrs['word-spacing'] = attrs.word_spacing; 541 | attrs.writing_mode = {name = 'writing-mode', parser = default}; 542 | attrs['writing-mode'] = attrs.writing_mode; 543 | 544 | attrs.x = {name='x', parser=coord}; 545 | attrs.x_height = {name='x-height', parser=number}; 546 | attrs['x-height'] = attrs.x_height; 547 | attrs.x1 = {name='x1', parser=coord}; 548 | attrs.x2 = {name='x2', parser=coord}; 549 | attrs.xChannelSelector = {name = 'xChannelSelector', parser = default}; 550 | attrs.xlink_actuate = {name = 'xlink:actuate', parser = default}; 551 | attrs['xlink:actuate'] = attrs.xlink_actuate; 552 | attrs.xlink_arcrole = {name = 'xlink:arcrole', parser = default}; 553 | attrs['xlink:arcrole'] = attrs.xlink_arcrole; 554 | attrs.xlink_href = {name='xlink:href', parser=default}; 555 | attrs['xlink:href'] = attrs.xlink_href; 556 | attrs.xlink_role = {name = 'xlink:role', parser = default}; 557 | attrs['xlink:role'] = attrs.xlink_role; 558 | attrs.xlink_show = {name = 'xlink:show', parser = default}; 559 | attrs['xlink:show'] = attrs.xlink_show; 560 | attrs.xlink_title = {name = 'xlink:title', parser = default}; 561 | attrs['xlink:title'] = attrs.xlink_title; 562 | attrs.xlink_type = {name = 'xlink:type', parser = default}; 563 | attrs['xlink:type'] = attrs.xlink_type; 564 | attrs.xml_base = {name = 'xml:base', parser = default}; 565 | attrs['xml:base'] = attrs.xml_base; 566 | attrs.xml_lang = {name = 'xml:lang', parser = default}; 567 | attrs['xml:lang'] = attrs.xml_lang; 568 | attrs.xml_space = {name = 'xml:space', parser = default}; 569 | attrs['xml:space'] = attrs.xml_space; 570 | 571 | attrs.y = {name = 'y', parser = coord}; 572 | attrs.y1 = {name = 'y1', parser = coord}; 573 | attrs.y2 = {name = 'y2', parser = coord}; 574 | attrs.yChannelSelector = {name = 'yChannelSelector', parser = default}; 575 | 576 | attrs.z = {name = 'z', parser = coord}; 577 | attrs.zoomAndPan = {name = 'zoomAndPan', parser = default}; 578 | 579 | 580 | function attrs.parseAttribute(name, value, strict) 581 | print("parseAttribute: ", name, value) 582 | local func = attrs[name]; 583 | 584 | if not func then 585 | if not strict then 586 | -- Be permissive, if the name is not found, 587 | -- just return what was passed in 588 | --print("attrs.parseAttribute, NOFUNC: ", name, value) 589 | return name, value; 590 | else 591 | -- If we're being strict, then we don't 592 | -- return anything if it's not a valid attribute name 593 | return nil 594 | end 595 | end 596 | 597 | --print("parseAttribute (func.name, func.parser): ", func.name, func.parser) 598 | return func.parser(func.name, value, strict) 599 | end 600 | 601 | return attrs; 602 | -------------------------------------------------------------------------------- /remotesvg/SVGParser.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A simple SVG Parser which fills in a lua based object model 3 | for the image specified. 4 | --]] 5 | 6 | local ffi = require("ffi") 7 | local bit = require("bit") 8 | local lshift, rshift, band, bor = bit.lshift, bit.rshift, bit.band, bit.bor 9 | 10 | local XmlParser = require("remotesvg.SVGXmlParser") 11 | local isspace = ctypes.isspace; 12 | local isdigit = ctypes.isdigit; 13 | 14 | local maths = require("remotesvg.maths") 15 | local clamp = maths.clamp; 16 | local sqr = maths.sqr; 17 | local vecrat = maths.vecrat; 18 | local vecang = maths.vecang; 19 | local vmag = maths.vmag; 20 | 21 | 22 | local sqrt = math.sqrt; 23 | local fabs = math.abs; 24 | local abs = math.abs; 25 | local sin = math.sin; 26 | local cos = math.cos; 27 | 28 | local RGB = colors.RGBA; 29 | 30 | local svg = require("remotesvg.SVGElements") 31 | --[[ 32 | local PaintType = SVGTypes.PaintType; 33 | local FillRule = SVGTypes.FillRule; 34 | local Flags = SVGTypes.Flags; 35 | local LineCap = SVGTypes.LineCap; 36 | local LineJoin = SVGTypes.LineJoin; 37 | local SVGGradientStop = SVGTypes.SVGGradientStop; 38 | local SVGGradient = SVGTypes.SVGGradient; 39 | local SVGPaint = SVGTypes.SVGPaint; 40 | --]] 41 | 42 | 43 | local SVG_PI = math.pi; -- (3.14159265358979323846264338327f) 44 | local SVG_KAPPA90 = 0.5522847493 -- Length proportional to radius of a cubic bezier handle for 90-deg arcs. 45 | local SVG_EPSILON = 1e-12; 46 | 47 | local SVG_ALIGN_MIN = 0; 48 | local SVG_ALIGN_MID = 1; 49 | local SVG_ALIGN_MAX = 2; 50 | local SVG_ALIGN_NONE = 0; 51 | local SVG_ALIGN_MEET = 1; 52 | local SVG_ALIGN_SLICE = 2; 53 | 54 | 55 | 56 | local minf = math.min; 57 | local maxf = math.max; 58 | 59 | -- Simple SVG parser. 60 | local SVG_MAX_ATTR = 128; 61 | 62 | local SVGgradientUnits = { 63 | NSVG_USER_SPACE = 0; 64 | NSVG_OBJECT_SPACE = 1; 65 | }; 66 | 67 | 68 | local SVGUnits = { 69 | USER = 0, 70 | PX = 1, 71 | PT = 2, 72 | PC = 3, 73 | MM = 4, 74 | CM = 5, 75 | IN = 6, 76 | PERCENT = 7, 77 | EM = 8, 78 | EX = 9, 79 | }; 80 | 81 | 82 | ffi.cdef[[ 83 | typedef struct pt2D 84 | { 85 | union { 86 | struct { 87 | double x, y; 88 | }; 89 | double v[2]; 90 | }; 91 | } pt2D_t; 92 | ]] 93 | 94 | 95 | 96 | local pt2D = ffi.typeof("struct pt2D"); 97 | 98 | ffi.cdef[[ 99 | typedef struct SVGCoordinate { 100 | float value; 101 | int units; 102 | } SVGCoordinate_t; 103 | ]] 104 | local SVGCoordinate = ffi.typeof("struct SVGCoordinate"); 105 | 106 | ffi.cdef[[ 107 | typedef struct SVGLinearData { 108 | struct SVGCoordinate x1, y1, x2, y2; 109 | } SVGLinearData_t; 110 | ]] 111 | local SVGLinearData = ffi.typeof("struct SVGLinearData") 112 | 113 | 114 | ffi.cdef[[ 115 | typedef struct SVGRadialData { 116 | struct SVGCoordinate cx, cy, r, fx, fy; 117 | } SVGRadialData_t; 118 | ]] 119 | local SVGRadialData = ffi.typeof("struct SVGRadialData") 120 | 121 | 122 | ffi.cdef[[ 123 | typedef struct SVGGradientData 124 | { 125 | char id[64]; 126 | char ref[64]; 127 | char type; 128 | union { 129 | struct SVGLinearData linear; 130 | struct SVGRadialData radial; 131 | }; 132 | char spread; 133 | char units; 134 | double xform[6]; 135 | int nstops; 136 | struct SVGGradientStop* stops; 137 | struct SVGGradientData* next; 138 | } SVGGradientData_t; 139 | ]] 140 | local SVGGradientData = ffi.typeof("struct SVGGradientData") 141 | 142 | ffi.cdef[[ 143 | static const int SVG_MAX_DASHES = 8; 144 | 145 | typedef struct SVGAttrib 146 | { 147 | char id[64]; 148 | double xform[6]; 149 | uint32_t fillColor; 150 | uint32_t strokeColor; 151 | float opacity; 152 | float fillOpacity; 153 | float strokeOpacity; 154 | char fillGradient[64]; 155 | char strokeGradient[64]; 156 | float strokeWidth; 157 | float strokeDashOffset; 158 | float strokeDashArray[SVG_MAX_DASHES]; 159 | int strokeDashCount; 160 | char strokeLineJoin; 161 | char strokeLineCap; 162 | char fillRule; 163 | float fontSize; 164 | unsigned int stopColor; 165 | float stopOpacity; 166 | float stopOffset; 167 | char hasFill; 168 | char hasStroke; 169 | char visible; 170 | } SVGattrib_t; 171 | ]] 172 | local SVGAttrib = ffi.typeof("struct SVGAttrib") 173 | 174 | 175 | local function AttributeStack() 176 | local obj = {} 177 | setmetatable(obj, { 178 | __index = obj; 179 | }) 180 | 181 | function obj.push(self, value) 182 | if not value then 183 | value = SVGAttrib(); 184 | -- clone whatever is on the top of the stack 185 | -- right now 186 | local topper = self:top() 187 | if topper then 188 | ffi.copy(value, topper, ffi.sizeof(SVGAttrib)) 189 | end 190 | end 191 | 192 | table.insert(self, value); 193 | 194 | return value; 195 | end 196 | 197 | function obj.pop(self) 198 | return table.remove(self) 199 | end 200 | 201 | function obj.top(self) 202 | if #self < 1 then 203 | return nil; 204 | end 205 | 206 | return self[#self]; 207 | end 208 | 209 | return obj; 210 | end 211 | 212 | 213 | local function strncpy(dst, value, limit) 214 | local s = ffi.cast("const char *", value) 215 | for i=0,limit-1 do 216 | if value[i] == 0 then 217 | break; 218 | end 219 | 220 | dst[i] = s[i]; 221 | end 222 | 223 | return dst; 224 | end 225 | 226 | local function strncmp(str1, str2, num) 227 | local ptr1 = ffi.cast("const char*", str1) 228 | local ptr2 = ffi.cast("const char*", str2) 229 | 230 | for i=0,num-1 do 231 | if ptr1[i] == 0 or ptr2[i] == 0 then return 0 end 232 | 233 | if ptr1[i] > ptr2[i] then return 1 end 234 | if ptr1[i] < ptr2[i] then return -1 end 235 | end 236 | 237 | return 0 238 | end 239 | 240 | --[[ 241 | bounds[0] == left 242 | bounds[1] == top 243 | bounds[2] == right 244 | bounds[3] == bottom 245 | --]] 246 | local function ptInBounds(pt, bounds) 247 | return pt.x >= bounds[0] and 248 | pt.x <= bounds[2] and 249 | pt.y >= bounds[1] and 250 | pt.y <= bounds[3]; 251 | end 252 | 253 | 254 | local function curveBoundary(bounds, curve, idx) 255 | print("curveBoundary: ", #curve, idx) 256 | local i, j, count = 0,0,0; 257 | local roots = ffi.new("double[2]"); 258 | local a, b, c, b2ac, t, v = 0,0,0,0,0,0; 259 | 260 | 261 | local v0 = curve[idx]; 262 | local v1 = curve[idx+1]; 263 | local v2 = curve[idx+2]; 264 | local v3 = curve[idx+3]; 265 | 266 | -- Start the bounding box by end points 267 | bounds[0] = minf(v0.x, v3.x); 268 | bounds[1] = minf(v0.y, v3.y); 269 | bounds[2] = maxf(v0.x, v3.x); 270 | bounds[3] = maxf(v0.y, v3.y); 271 | 272 | 273 | -- Bezier curve fits inside the convex hull of it's control points. 274 | -- If control points are inside the bounds, we're done. 275 | if (ptInBounds(v1, bounds) and ptInBounds(v2, bounds)) then 276 | return; 277 | end 278 | 279 | -- Add bezier curve inflection points in X and Y. 280 | 281 | for i = 0, 1 do 282 | a = -3.0 * v0.v[i] + 9.0 * v1.v[i] - 9.0 * v2.v[i] + 3.0 * v3.v[i]; 283 | b = 6.0 * v0.v[i] - 12.0 * v1.v[i] + 6.0 * v2.v[i]; 284 | c = 3.0 * v1.v[i] - 3.0 * v0.v[i]; 285 | 286 | local count = 0; 287 | if (abs(a) < SVG_EPSILON) then 288 | if (abs(b) > SVG_EPSILON) then 289 | t = -c / b; 290 | if (t > SVG_EPSILON and t < 1.0-SVG_EPSILON) then 291 | roots[count] = t; 292 | count = count + 1; 293 | end 294 | end 295 | else 296 | b2ac = b*b - 4.0*c*a; 297 | if (b2ac > SVG_EPSILON) then 298 | t = (-b + sqrt(b2ac)) / (2.0 * a); 299 | if (t > SVG_EPSILON and t < 1.0-SVG_EPSILON) then 300 | roots[count] = t; 301 | count = count + 1; 302 | end 303 | 304 | t = (-b - sqrt(b2ac)) / (2.0 * a); 305 | if (t > SVG_EPSILON and t < 1.0-SVG_EPSILON) then 306 | roots[count] = t; 307 | count = count + 1; 308 | end 309 | end 310 | end 311 | --[[ 312 | for (j = 0; j < count; j++) { 313 | v = Bezier.evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); 314 | bounds[0+i] = minf(bounds[0+i], v); 315 | bounds[2+i] = maxf(bounds[2+i], v); 316 | } 317 | --]] 318 | end 319 | 320 | end 321 | 322 | 323 | 324 | 325 | local SVGParser = {} 326 | setmetatable(SVGParser, { 327 | __call = function(self, ...) 328 | return self:new(...); 329 | end 330 | }) 331 | local SVGParser_mt = { 332 | __index = SVGParser; 333 | } 334 | 335 | function SVGParser.init(self) 336 | local obj = { 337 | attr = AttributeStack(); 338 | pts = {}; 339 | plist = {}; 340 | image = SVGImage(); 341 | gradients = {}; 342 | 343 | viewMinx = 0; 344 | viewMiny = 0; 345 | viewWidth = 0; 346 | viewHeight = 0; 347 | 348 | -- alignment 349 | alignX = 0; 350 | alignY = 0; 351 | alignType = 0; 352 | 353 | dpi = 96; 354 | 355 | pathFlag = false; 356 | defsFlag = false; 357 | } 358 | 359 | setmetatable(obj, SVGParser_mt); 360 | 361 | 362 | 363 | 364 | -- Initialize style with first attribute 365 | local attrib = SVGAttrib(); 366 | transform2D.xformIdentity(attrib.xform); 367 | attrib.fillColor = RGB(0,0,0); 368 | attrib.strokeColor = RGB(0,0,0); 369 | attrib.opacity = 1; 370 | attrib.fillOpacity = 1; 371 | attrib.strokeOpacity = 1; 372 | attrib.stopOpacity = 1; 373 | attrib.strokeWidth = 1; 374 | attrib.strokeLineJoin = LineJoin.MITER; 375 | attrib.strokeLineCap = LineCap.BUTT; 376 | attrib.fillRule = FillRule.NONZERO; 377 | attrib.hasFill = 1; 378 | attrib.visible = 1; 379 | obj.attr:push(attrib); 380 | 381 | return obj; 382 | end 383 | 384 | function SVGParser.new(self) 385 | local parser = self:init() 386 | 387 | return parser; 388 | end 389 | 390 | function SVGParser.parse(self, input, units, dpi) 391 | self.dpi = dpi; 392 | 393 | --self:parseXML(input, SVGParser.startElement, SVGParser.endElement, SVGParser.content, self); 394 | XmlParser.parseXML(input, SVGParser.startElement, SVGParser.endElement, SVGParser.content, self) 395 | 396 | -- Scale to viewBox 397 | self:scaleToViewbox(units); 398 | 399 | local ret = self.image; 400 | self.image = NULL; 401 | 402 | return ret; 403 | end 404 | 405 | function SVGParser.parseFromFile(self, filename, units, dpi) 406 | local fp = io.open(filename, "rb") 407 | if not fp then 408 | return 409 | end 410 | 411 | local data = fp:read("*a"); 412 | fp:close(); 413 | 414 | local parser = SVGParser(); 415 | local image = parser:parse(data, units, dpi) 416 | 417 | return image; 418 | end 419 | 420 | 421 | 422 | function SVGParser.resetPath(self) 423 | self.pts = {}; 424 | end 425 | 426 | function SVGParser.addPoint(self, x, y) 427 | --print("addPoint: ", x, type(x), y, type(y)) 428 | table.insert(self.pts, pt2D({tonumber(x),tonumber(y)})); 429 | end 430 | 431 | function SVGParser.moveTo(self, x, y) 432 | self:addPoint(x, y); 433 | end 434 | 435 | -- Add a line segment. The must be at least 436 | -- one starting point already 437 | function SVGParser.lineTo(self, x, y) 438 | if #self.pts > 0 then 439 | local lastpt = self.pts[#self.pts]; 440 | local px = lastpt.x; 441 | local py = lastpt.y; 442 | dx = x - px; 443 | dy = y - py; 444 | self:addPoint(px + dx/3.0, py + dy/3.0); 445 | self:addPoint(x - dx/3.0, y - dy/3.0); 446 | self:addPoint(x, y); 447 | end 448 | end 449 | 450 | function SVGParser.cubicBezTo(self, cpx1, cpy1, cpx2, cpy2, x, y) 451 | self:addPoint(cpx1, cpy1); 452 | self:addPoint(cpx2, cpy2); 453 | self:addPoint(x, y); 454 | end 455 | 456 | function SVGParser.getAttr(self) 457 | return self.attr:top(); 458 | end 459 | 460 | function SVGParser.pushAttr(self) 461 | -- take the attribute currently on top 462 | -- make a copy 463 | -- and push that copy on top of the stack 464 | self.attr:push(); 465 | end 466 | 467 | function SVGParser.popAttr(self) 468 | return self.attr:pop(); 469 | end 470 | 471 | function SVGParser.actualOrigX(self) 472 | return self.viewMinx; 473 | end 474 | 475 | function SVGParser.actualOrigY(self) 476 | return self.viewMiny; 477 | end 478 | 479 | function SVGParser.actualWidth(self) 480 | return self.viewWidth; 481 | end 482 | 483 | function SVGParser.actualHeight(self) 484 | return self.viewHeight; 485 | end 486 | 487 | function SVGParser.actualLength(self) 488 | local w = self:actualWidth(); 489 | local h = self:actualHeight(); 490 | 491 | return sqrt(w*w + h*h) / sqrt(2.0); 492 | end 493 | 494 | 495 | function SVGParser.convertToPixels(self, c, orig, length) 496 | --print("convertToPixels: ", self, c, orig, length) 497 | local attr = self:getAttr(); 498 | 499 | if c.units == SVGUnits.USER then 500 | return c.value; 501 | elseif c.units == SVGUnits.PX then return c.value; 502 | elseif c.units == SVGUnits.PT then return c.value / 72.0 * self.dpi; 503 | elseif c.units == SVGUnits.PC then return c.value / 6.0 * self.dpi; 504 | elseif c.units == SVGUnits.MM then return c.value / 25.4 * self.dpi; 505 | elseif c.units == SVGUnits.CM then return c.value / 2.54 * self.dpi; 506 | elseif c.units == SVGUnits.IN then return c.value * self.dpi; 507 | elseif c.units == SVGUnits.EM then return c.value * attr.fontSize; 508 | elseif c.units == SVGUnits.EX then return c.value * attr.fontSize * 0.52; -- x-height of Helvetica. 509 | elseif c.units == SVGUnits.PERCENT then 510 | return orig + c.value / 100.0 * length; 511 | end 512 | 513 | return c.value; 514 | end 515 | 516 | 517 | function SVGParser.findGradientData(self, id) 518 | return self.gradients[id]; 519 | --[[ 520 | NSVGgradientData* grad = self.gradients; 521 | while (grad) { 522 | if (strcmp(grad.id, id) == 0) 523 | return grad; 524 | grad = grad.next; 525 | } 526 | return NULL; 527 | --]] 528 | end 529 | 530 | 531 | function SVGParser.createGradient(self, id, localBounds, paintType) 532 | 533 | local attr = self:getAttr(); 534 | --NSVGgradientData* data = NULL; 535 | --NSVGgradientData* ref = NULL; 536 | --NSVGgradientStop* stops = NULL; 537 | --NSVGgradient* grad; 538 | local ox, oy, sw, sh, sl= 0,0,0,0,0; 539 | local nstops = 0; 540 | 541 | local data = self:findGradientData(id); 542 | if data == nil then 543 | return nil; 544 | end 545 | 546 | --[[ 547 | -- TODO: use ref to fill in all unset values too. 548 | local ref = data; 549 | while (ref ~= NULL) do 550 | if (stops == NULL and ref.stops != NULL) { 551 | stops = ref.stops; 552 | nstops = ref.nstops; 553 | break; 554 | } 555 | ref = self:findGradientData(p, ref.ref); 556 | end 557 | 558 | if (stops == NULL) then 559 | return NULL; 560 | end 561 | 562 | --grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); 563 | local grad = SVGGradient(); 564 | 565 | --if (grad == NULL) return NULL; 566 | 567 | -- The shape width and height. 568 | if (data.units == NSVG_OBJECT_SPACE) { 569 | ox = localBounds[0]; 570 | oy = localBounds[1]; 571 | sw = localBounds[2] - localBounds[0]; 572 | sh = localBounds[3] - localBounds[1]; 573 | else 574 | ox = self:actualOrigX(p); 575 | oy = self:actualOrigY(p); 576 | sw = self:actualWidth(p); 577 | sh = self:actualHeight(p); 578 | } 579 | sl = sqrtf(sw*sw + sh*sh) / sqrt(2.0); 580 | 581 | if (data.type == SVGTypes.PaintType.LINEAR_GRADIENT) then 582 | --float x1, y1, x2, y2, dx, dy; 583 | local x1 = self:convertToPixels(data.linear.x1, ox, sw); 584 | local y1 = self:convertToPixels(data.linear.y1, oy, sh); 585 | local x2 = self:convertToPixels(data.linear.x2, ox, sw); 586 | local y2 = self:convertToPixels(data.linear.y2, oy, sh); 587 | -- Calculate transform aligned to the line 588 | local dx = x2 - x1; 589 | local dy = y2 - y1; 590 | grad.xform[0] = dy; grad.xform[1] = -dx; 591 | grad.xform[2] = dx; grad.xform[3] = dy; 592 | grad.xform[4] = x1; grad.xform[5] = y1; 593 | else 594 | --float cx, cy, fx, fy, r; 595 | local cx = self:convertToPixels(p, data.radial.cx, ox, sw); 596 | local cy = self:convertToPixels(p, data.radial.cy, oy, sh); 597 | local fx = self:convertToPixels(p, data.radial.fx, ox, sw); 598 | local fy = self:convertToPixels(p, data.radial.fy, oy, sh); 599 | local r = self:convertToPixels(p, data.radial.r, 0, sl); 600 | -- Calculate transform aligned to the circle 601 | grad.xform[0] = r; grad.xform[1] = 0; 602 | grad.xform[2] = 0; grad.xform[3] = r; 603 | grad.xform[4] = cx; grad.xform[5] = cy; 604 | grad.fx = fx / r; 605 | grad.fy = fy / r; 606 | end 607 | 608 | xform.xformMultiply(grad.xform, data.xform); 609 | xform.xformMultiply(grad.xform, attr.xform); 610 | 611 | grad.spread = data.spread; 612 | ffi.copy(grad.stops, stops, nstops*sizeof(NSVGgradientStop)); 613 | grad.nstops = nstops; 614 | 615 | 616 | return grad, data.type; 617 | --]] 618 | end 619 | 620 | 621 | function SVGParser.getAverageScale(self, t) 622 | 623 | local sx = sqrt(t[0]*t[0] + t[2]*t[2]); 624 | local sy = sqrt(t[1]*t[1] + t[3]*t[3]); 625 | 626 | return (sx + sy) * 0.5; 627 | end 628 | 629 | --[[ 630 | function NSVGParser.getLocalBounds(float* bounds, NSVGshape *shape, float* xform) 631 | 632 | NSVGpath* path; 633 | float curve[4*2], curveBounds[4]; 634 | int i, 635 | local first = true; 636 | 637 | for (path = shape.paths; path != NULL; path = path.next) do 638 | curve[0], curve[1] = xform.xformPoint(path.pts[0], path.pts[1], xform); 639 | for (i = 0; i < path.npts-1; i += 3) { 640 | curve[2], curve[3] = xform.xformPoint(path.pts[(i+1)*2], path.pts[(i+1)*2+1], xform); 641 | curve[4], curve[5] = xform.xformPoint(path.pts[(i+2)*2], path.pts[(i+2)*2+1], xform); 642 | curve[6], curve[7] = xform.xformPoint(path.pts[(i+3)*2], path.pts[(i+3)*2+1], xform); 643 | curveBoundary(curveBounds, curve); 644 | if (first) then 645 | bounds[0] = curveBounds[0]; 646 | bounds[1] = curveBounds[1]; 647 | bounds[2] = curveBounds[2]; 648 | bounds[3] = curveBounds[3]; 649 | first = false; 650 | else 651 | bounds[0] = self:minf(bounds[0], curveBounds[0]); 652 | bounds[1] = self:minf(bounds[1], curveBounds[1]); 653 | bounds[2] = self:maxf(bounds[2], curveBounds[2]); 654 | bounds[3] = self:maxf(bounds[3], curveBounds[3]); 655 | end 656 | curve[0] = curve[6]; 657 | curve[1] = curve[7]; 658 | } 659 | end 660 | end 661 | --]] 662 | 663 | 664 | function SVGParser.addShape(self) 665 | 666 | local attr = self:getAttr(); 667 | local scale = 1.0; 668 | 669 | 670 | if (self.plist == NULL) then 671 | return; 672 | end 673 | 674 | local shape = SVGShape(); 675 | 676 | shape.id = ffi.string(attr.id); 677 | --print("addShape(), shape.id: ", shape.id) 678 | 679 | scale = self:getAverageScale(attr.xform); 680 | shape.strokeWidth = attr.strokeWidth * scale; 681 | shape.strokeDashOffset = attr.strokeDashOffset * scale; 682 | shape.strokeDashCount = attr.strokeDashCount; 683 | 684 | 685 | for i = 0, attr.strokeDashCount-1 do 686 | shape.strokeDashArray[i] = attr.strokeDashArray[i] * scale; 687 | end 688 | 689 | shape.strokeLineJoin = attr.strokeLineJoin; 690 | shape.strokeLineCap = attr.strokeLineCap; 691 | shape.fillRule = attr.fillRule; 692 | shape.opacity = attr.opacity; 693 | 694 | shape.paths = self.plist; 695 | self.plist = {}; 696 | 697 | -- Calculate shape bounds 698 | for idx, path in ipairs(shape.paths) do 699 | shape.bounds[0] = minf(shape.bounds[0], path.bounds[0]); 700 | shape.bounds[1] = minf(shape.bounds[1], path.bounds[1]); 701 | shape.bounds[2] = maxf(shape.bounds[2], path.bounds[2]); 702 | shape.bounds[3] = maxf(shape.bounds[3], path.bounds[3]); 703 | end 704 | 705 | 706 | -- Set fill 707 | if (attr.hasFill == 0) then 708 | shape.fill.type = PaintType.NONE; 709 | elseif (attr.hasFill == 1) then 710 | shape.fill.type = PaintType.COLOR; 711 | shape.fill.color = attr.fillColor; 712 | shape.fill.color = bor(shape.fill.color, lshift((attr.fillOpacity*255), 24)); 713 | elseif attr.hasFill == 2 then 714 | local inv = ffi.new("double[6]"); 715 | local localBounds ffi.new("double[4]"); 716 | transform2D.xformInverse(inv, attr.xform); 717 | self:getLocalBounds(localBounds, shape, inv); 718 | shape.fill.gradient, shape.fill.type = self:createGradient(attr.fillGradient, localBounds); 719 | if (shape.fill.gradient == nil) then 720 | shape.fill.type = PaintType.NONE; 721 | end 722 | end 723 | 724 | 725 | 726 | -- Set stroke 727 | if (attr.hasStroke == 0) then 728 | shape.stroke.type = PaintType.NONE; 729 | elseif (attr.hasStroke == 1) then 730 | shape.stroke.type = PaintType.COLOR; 731 | shape.stroke.color = attr.strokeColor; 732 | shape.stroke.color = bor(shape.stroke.color, lshift((attr.strokeOpacity*255), 24)); 733 | elseif (attr.hasStroke == 2) then 734 | local inv = ffi.new("double[6]") 735 | local localBounds = ffi.new("double[4]"); 736 | 737 | transform2D.xformInverse(inv, attr.xform); 738 | self:getLocalBounds(localBounds, shape, inv); 739 | shape.stroke.gradient, shape.stroke.type = self:createGradient(attr.strokeGradient, localBounds); 740 | if (shape.stroke.gradient == NULL) then 741 | shape.stroke.type = PaintType.NONE; 742 | end 743 | end 744 | 745 | 746 | -- Set flags 747 | if attr.visible ~= 0 then 748 | shape.flags = Flags.VISIBLE 749 | else 750 | shape.flags = 0; 751 | end 752 | 753 | -- Add to tail 754 | table.insert(self.image.shapes, shape); 755 | 756 | return; 757 | end 758 | 759 | 760 | 761 | function SVGParser.addPath(self, closed) 762 | local attr = self:getAttr(); 763 | local bounds = ffi.new("double[4]"); 764 | --float* curve; 765 | --int i; 766 | 767 | --print("addPath: ", #self.pts) 768 | 769 | if (#self.pts < 4) then 770 | return; 771 | end 772 | 773 | local path = SVGPath(); 774 | 775 | path.closed = closed; 776 | 777 | -- Transform path. 778 | for i, pt in ipairs(self.pts) do 779 | pt.x, pt.y = transform2D.xformPoint(pt.x, pt.y, attr.xform); 780 | table.insert(path.pts, pt); 781 | end 782 | 783 | -- Find bounds 784 | for i = 1, #path.pts-1, 3 do 785 | --curve = &path.pts[i*2]; 786 | curveBoundary(bounds, path.pts, i); 787 | if (i == 1) then 788 | path.bounds[0] = bounds[0]; 789 | path.bounds[1] = bounds[1]; 790 | path.bounds[2] = bounds[2]; 791 | path.bounds[3] = bounds[3]; 792 | else 793 | path.bounds[0] = minf(path.bounds[0], bounds[0]); 794 | path.bounds[1] = minf(path.bounds[1], bounds[1]); 795 | path.bounds[2] = maxf(path.bounds[2], bounds[2]); 796 | path.bounds[3] = maxf(path.bounds[3], bounds[3]); 797 | end 798 | end 799 | 800 | --path:dump(); 801 | table.insert(self.plist, path); 802 | 803 | return; 804 | end 805 | 806 | 807 | -- parse a numeric value out of a string pointer 808 | -- stuff it into the 'it' buffer, which is of size 'size' 809 | -- and return the pointer one character past the last 810 | -- digit parsed. 811 | function SVGParser.parseNumber(self, s, it, size) 812 | --print(".parseNumber: ", ffi.string(s), size) 813 | 814 | local last = size-1; 815 | local i = 0; 816 | 817 | -- sign 818 | if (s[0] == string.byte('-') or s[0] == string.byte('+')) then 819 | if i < last then 820 | it[i] = s[0]; 821 | i = i + 1; 822 | end 823 | 824 | s = s + 1; 825 | end 826 | 827 | -- integer part 828 | while s[0]~= 0 and isdigit(s[0]) do 829 | if (i < last) then 830 | it[i] = s[0]; 831 | i = i + 1; 832 | end 833 | 834 | s = s + 1; 835 | end 836 | 837 | if s[0] == string.byte('.') then 838 | -- decimal point 839 | if (i < last) then 840 | it[i] = s[0]; 841 | i = i + 1; 842 | end 843 | 844 | s = s + 1; 845 | 846 | -- fraction part 847 | while s[0] ~= 0 and isdigit(s[0]) do 848 | if (i < last) then 849 | it[i] = s[0]; 850 | i = i + 1; 851 | end 852 | s = s + 1; 853 | end 854 | end 855 | 856 | -- exponent 857 | if (s[0] == string.byte('e') or s[0] == string.byte('E')) then 858 | if (i < last) then 859 | it[i] = s[0]; 860 | i = i + 1; 861 | end 862 | s = s + 1; 863 | 864 | if s[0] == string.byte('-') or s[0] == string.byte('+') then 865 | if (i < last) then 866 | it[i] = s[0]; 867 | i = i + 1; 868 | end 869 | s = s + 1; 870 | end 871 | 872 | while s[0] and isdigit(s[0]) do 873 | if (i < last) then 874 | it[i] = s[0]; 875 | i = i + 1; 876 | end 877 | s = s + 1; 878 | end 879 | end 880 | 881 | it[i] = 0; 882 | --print(".parseNumber - END: ", ffi.string(it)) 883 | 884 | return s, tonumber(ffi.string(it)); 885 | end 886 | 887 | 888 | --[[ 889 | static const char* self:getNextPathItem(self, const char* s, char* it) 890 | 891 | it[0] = '\0'; 892 | -- Skip white spaces and commas 893 | while (*s && (self:isspace(*s) || *s == ',')) s++; 894 | if (!*s) return s; 895 | if (*s == '-' || *s == '+' || *s == '.' || self:isdigit(*s)) { 896 | s = self:parseNumber(s, it, 64); 897 | } else { 898 | // Parse command 899 | it[0] = *s++; 900 | it[1] = '\0'; 901 | return s; 902 | } 903 | 904 | return s; 905 | end 906 | --]] 907 | 908 | 909 | function SVGParser.parseColorHex(self, s) 910 | --print(".parseColorHex: ", s) 911 | 912 | local rgb = s:match("#(%g+)") 913 | --print("rgb: ", rgb) 914 | local r,g,b = 0,0,0; 915 | local c = 0; 916 | rgb = "0x"..rgb; 917 | 918 | if #rgb == 6 then 919 | c = tonumber(rgb) 920 | elseif #rgb == 3 then 921 | c = tonumber(rgb); 922 | c = bor(band(c,0xf), lshift(band(c,0xf0), 4), lshift(band(c,0xf00), 8)); 923 | c = bor(c, lshift(c,4)); 924 | end 925 | b = band(rshift(c, 16), 0xff); 926 | g = band(rshift(c, 8), 0xff); 927 | r = band(c, 0xff); 928 | 929 | return RGB(r,g,b); 930 | end 931 | 932 | 933 | 934 | function SVGParser.parseColorRGB(self, str) 935 | local percentvaluepatt = "(%d+%%),(%d+%%),(%d+%%)" 936 | local valuepatt = "(%d+)%s*,%s*(%d+)%s*,%s*(%d+)" 937 | 938 | local r,g,b = str:match(valuepatt) 939 | r,g,b = tonumber(r), tonumber(g), tonumber(b) 940 | 941 | --print("parseColorRGB: ", str, r, g, b) 942 | 943 | --[[ 944 | char s1[32]="", s2[32]=""; 945 | sscanf(str + 4, "%d%[%%, \t]%d%[%%, \t]%d", &r, s1, &g, s2, &b); 946 | if (strchr(s1, '%')) { 947 | return RGB((r*255)/100,(g*255)/100,(b*255)/100); 948 | else 949 | return RGB(r,g,b); 950 | end 951 | --]] 952 | 953 | return RGB(r,g,b) 954 | end 955 | 956 | 957 | function SVGParser.parseColorName(self, name) 958 | return colors.svg[name] or RGB(128, 128, 128); 959 | end 960 | 961 | 962 | function SVGParser.parseColor(self, s) 963 | local str = s:match("%s*(.*)") 964 | local len = #str; 965 | 966 | -- print("SVGParser.parseColor: ", str, len) 967 | 968 | if len >= 1 and str:sub(1,1) == '#' then 969 | return self:parseColorHex(str); 970 | elseif (len >= 4 and str:match("rgb%(")) then 971 | return self:parseColorRGB(str); 972 | end 973 | 974 | return self:parseColorName(str); 975 | end 976 | 977 | 978 | 979 | function SVGParser.parseOpacity(self, str) 980 | local val = clamp(tonumber(str), 0, 1); 981 | 982 | return val; 983 | end 984 | 985 | 986 | function SVGParser.parseUnits(self, units) 987 | --print(".parseUnits: ", units) 988 | 989 | local unitConverter = { 990 | px = SVGUnits.PX; 991 | pt = SVGUnits.PT; 992 | pc = SVGUnits.PC; 993 | mm = SVGUnits.MM; 994 | cm = SVGUnits.CM; 995 | ["in"] = SVGUnits.IN; 996 | ["%"] = SVGUnits.PERCENT; 997 | em = SVGUnits.EM; 998 | ex = SVGUnits.EX; 999 | } 1000 | 1001 | return unitConverter[units] or SVGUnits.USER; 1002 | end 1003 | 1004 | function SVGParser.parseCoordinateRaw(self, str) 1005 | local coord = SVGCoordinate({0, SVGUnits.USER}); 1006 | --print(".parseCoordinateRaw: ", str) 1007 | 1008 | --sscanf(str, "%f%s", &coord.value, units); 1009 | local num, units = str:match("(%d*%.?%d*)(.*)") 1010 | 1011 | coord.value = tonumber(num); 1012 | coord.units = self:parseUnits(units); 1013 | 1014 | return coord; 1015 | end 1016 | 1017 | function SVGParser.coord(self, v, units) 1018 | return SVGCoordinate({v, units}); 1019 | end 1020 | 1021 | 1022 | function SVGParser.parseCoordinate(self, str, orig, length) 1023 | --print(".parseCoordinate: ", str, orig, length) 1024 | local coord = self:parseCoordinateRaw(str); 1025 | return self:convertToPixels(coord, orig, length); 1026 | end 1027 | 1028 | 1029 | function SVGParser.parseTransformArgs(self, str, args, maxNa, na) 1030 | 1031 | local it = ffi.new("char [64]"); 1032 | 1033 | na = 0; 1034 | 1035 | -- advance past the opening '(' 1036 | local ptr = str; 1037 | while (ptr[0] ~= 0 and ptr[0] ~= string.byte('(')) do 1038 | ptr = ptr + 1; 1039 | end 1040 | 1041 | 1042 | if ptr[0] == 0 then 1043 | return 1, na; 1044 | end 1045 | 1046 | -- find the right ')' 1047 | local ending = ptr; 1048 | while ending[0]~=0 and ending[0] ~= string.byte(')') do 1049 | ending = ending + 1; 1050 | end 1051 | 1052 | if ending[0] == 0 then 1053 | return 1, na; 1054 | end 1055 | 1056 | while (ptr < ending) do 1057 | if (ptr[0] == string.byte('-') or 1058 | ptr[0] == string.byte('+') or 1059 | ptr[0] == string.byte('.') or 1060 | isdigit(ptr[0])) then 1061 | 1062 | if na >= maxNa then 1063 | return 0; 1064 | end 1065 | 1066 | ptr, num = self:parseNumber(ptr, it, 64); 1067 | --print('.printTransformArgs: ', na, num) 1068 | args[na] = num; 1069 | 1070 | --print("IT: ", ffi.string(it)); 1071 | 1072 | na = na + 1; 1073 | else 1074 | ptr = ptr + 1; 1075 | end 1076 | end 1077 | 1078 | return ending - str, na; 1079 | end 1080 | 1081 | 1082 | 1083 | function SVGParser.parseMatrix(self, xform, str) 1084 | print(".parseMatrix: ", ffi.string(str)) 1085 | 1086 | local t = ffi.new("double[6]"); 1087 | local na = 0; 1088 | local len, na = self:parseTransformArgs(str, t, 6, na); 1089 | print(".parseMatrix, len, na: ", len, na) 1090 | 1091 | if (na ~= 6) then 1092 | return len; 1093 | end 1094 | 1095 | ffi.copy(xform, t, ffi.sizeof("double")*6); 1096 | 1097 | for i=0,5 do 1098 | print(xform[i]) 1099 | end 1100 | 1101 | print(".parseMatrix - END") 1102 | 1103 | return len; 1104 | end 1105 | 1106 | --[[ 1107 | static int self:parseTranslate(float* xform, const char* str) 1108 | { 1109 | float args[2]; 1110 | float t[6]; 1111 | int na = 0; 1112 | int len = self:parseTransformArgs(str, args, 2, &na); 1113 | if (na == 1) args[1] = 0.0; 1114 | 1115 | xform.xformSetTranslation(t, args[0], args[1]); 1116 | memcpy(xform, t, sizeof(float)*6); 1117 | return len; 1118 | } 1119 | 1120 | static int self:parseScale(float* xform, const char* str) 1121 | { 1122 | float args[2]; 1123 | int na = 0; 1124 | float t[6]; 1125 | int len = self:parseTransformArgs(str, args, 2, &na); 1126 | if (na == 1) args[1] = args[0]; 1127 | xform.xformSetScale(t, args[0], args[1]); 1128 | memcpy(xform, t, sizeof(float)*6); 1129 | return len; 1130 | } 1131 | 1132 | static int self:parseSkewX(float* xform, const char* str) 1133 | { 1134 | float args[1]; 1135 | int na = 0; 1136 | float t[6]; 1137 | int len = self:parseTransformArgs(str, args, 1, &na); 1138 | xform.xformSetSkewX(t, args[0]/180.0f*NSVG_PI); 1139 | memcpy(xform, t, sizeof(float)*6); 1140 | return len; 1141 | } 1142 | 1143 | static int self:parseSkewY(float* xform, const char* str) 1144 | { 1145 | float args[1]; 1146 | int na = 0; 1147 | float t[6]; 1148 | int len = self:parseTransformArgs(str, args, 1, &na); 1149 | xform.xformSetSkewY(t, args[0]/180.0f*NSVG_PI); 1150 | memcpy(xform, t, sizeof(float)*6); 1151 | return len; 1152 | } 1153 | 1154 | static int self:parseRotate(float* xform, const char* str) 1155 | { 1156 | float args[3]; 1157 | int na = 0; 1158 | float m[6]; 1159 | float t[6]; 1160 | int len = self:parseTransformArgs(str, args, 3, &na); 1161 | if (na == 1) 1162 | args[1] = args[2] = 0.0f; 1163 | xform.xformIdentity(m); 1164 | 1165 | if (na > 1) { 1166 | xform.xformSetTranslation(t, -args[1], -args[2]); 1167 | xform.xformMultiply(m, t); 1168 | } 1169 | 1170 | xform.xformSetRotation(t, args[0]/180.0f*NSVG_PI); 1171 | xform.xformMultiply(m, t); 1172 | 1173 | if (na > 1) { 1174 | xform.xformSetTranslation(t, args[1], args[2]); 1175 | xform.xformMultiply(m, t); 1176 | } 1177 | 1178 | memcpy(xform, m, sizeof(float)*6); 1179 | 1180 | return len; 1181 | } 1182 | --]] 1183 | 1184 | function SVGParser.parseTransform(self, xform, s) 1185 | --print(".parseTransform: ", s); 1186 | 1187 | local str = ffi.cast("const char *", s) 1188 | local t=ffi.new("double[6]"); 1189 | transform2D.xformIdentity(xform); 1190 | 1191 | while (str[0] ~= 0) do 1192 | local cont = false; 1193 | 1194 | if strncmp(str, "matrix", 6) == 0 then 1195 | str = str + self:parseMatrix(t, str); 1196 | elseif strncmp(str, "translate", 9) == 0 then 1197 | str = str + self:parseTranslate(t, str); 1198 | elseif strncmp(str, "scale", 5) == 0 then 1199 | str = str + self:parseScale(t, str); 1200 | elseif strncmp(str, "rotate", 6) == 0 then 1201 | str = str + self:parseRotate(t, str); 1202 | elseif strncmp(str, "skewX", 5) == 0 then 1203 | str = str + self:parseSkewX(t, str); 1204 | elseif strncmp(str, "skewY", 5) == 0 then 1205 | str = str + self:parseSkewY(t, str); 1206 | else 1207 | str = str + 1; 1208 | cont = true; 1209 | end 1210 | 1211 | if not cont then 1212 | transform2D.xformPremultiply(xform, t); 1213 | end 1214 | end 1215 | end 1216 | 1217 | 1218 | function SVGParser.parseUrl(self, id, str) 1219 | print("parseUrl - BEGIN: ", id, str) 1220 | --[[ 1221 | int i = 0; 1222 | str += 4; -- "url("; 1223 | if (*str == '#') 1224 | str++; 1225 | while (i < 63 && *str != ')') { 1226 | id[i] = *str++; 1227 | i++; 1228 | } 1229 | id[i] = '\0'; 1230 | --]] 1231 | return id; 1232 | end 1233 | 1234 | function SVGParser.parseLineCap(self, str) 1235 | print(".parseLineCap: ", str) 1236 | --return LineCap[str:upper()] or LineCap.BUTT; 1237 | 1238 | if str == "butt" then 1239 | return LineCap.BUTT; 1240 | elseif str == "round" then 1241 | return LineCap.ROUND; 1242 | elseif str == "square" then 1243 | return LineCap.SQUARE; 1244 | end 1245 | 1246 | -- TODO: handle inherit. 1247 | return LineCap.BUTT; 1248 | end 1249 | 1250 | function SVGParser.parseLineJoin(self, str) 1251 | if str == "miter" then 1252 | return LineJoin.MITER; 1253 | elseif str == "round" then 1254 | return LineJoin.ROUND; 1255 | elseif str == "bevel" then 1256 | return LineJoin.BEVEL; 1257 | end 1258 | 1259 | -- TODO: handle inherit. 1260 | return LineCap.BUTT; 1261 | end 1262 | 1263 | local FillRuleConversion = { 1264 | nonzero = FillRule.NONZERO; 1265 | evenodd = FillRule.EVENODD; 1266 | } 1267 | 1268 | function SVGParser.parseFillRule(self, str) 1269 | return FillRuleConversion[str] or FillRule.NONZERO; 1270 | end 1271 | 1272 | 1273 | --[[ 1274 | static const char* self:getNextDashItem(const char* s, char* it) 1275 | { 1276 | int n = 0; 1277 | it[0] = '\0'; 1278 | // Skip white spaces and commas 1279 | while (*s && (self:isspace(*s) || *s == ',')) s++; 1280 | // Advance until whitespace, comma or end. 1281 | while (*s && (!self:isspace(*s) && *s != ',')) { 1282 | if (n < 63) 1283 | it[n++] = *s; 1284 | s++; 1285 | } 1286 | it[n++] = '\0'; 1287 | return s; 1288 | } 1289 | --]] 1290 | 1291 | --[[ 1292 | static int self:parseStrokeDashArray(self, const char* str, float* strokeDashArray) 1293 | { 1294 | char item[64]; 1295 | int count = 0, i; 1296 | float sum = 0.0f; 1297 | 1298 | // Handle "none" 1299 | if (str[0] == 'n') 1300 | return 0; 1301 | 1302 | // Parse dashes 1303 | while (*str) { 1304 | str = self:getNextDashItem(str, item); 1305 | if (!*item) break; 1306 | if (count < NSVG_MAX_DASHES) 1307 | strokeDashArray[count++] = fabsf(self:parseCoordinate(p, item, 0.0f, self:actualLength(p))); 1308 | } 1309 | 1310 | for (i = 0; i < count; i++) 1311 | sum += strokeDashArray[i]; 1312 | if (sum <= 1e-6f) 1313 | count = 0; 1314 | 1315 | return count; 1316 | } 1317 | --]] 1318 | 1319 | 1320 | function SVGParser.parseAttr(self, name, value) 1321 | --print("parseAttr: ", name, value); 1322 | 1323 | local xform = ffi.new("double[6]"); 1324 | local attr = self:getAttr(p); 1325 | 1326 | assert(attr ~= nil) 1327 | 1328 | if name == "style" then 1329 | self:parseStyle(value); 1330 | elseif name == "display" then 1331 | if value == "none" then 1332 | attr.visible = 0; 1333 | -- Don't reset .visible on display:inline, 1334 | -- one display:none hides the whole subtree 1335 | end 1336 | elseif name == "fill" then 1337 | if value == "none" then 1338 | attr.hasFill = 0; 1339 | elseif value:sub(1,4) == "url(" then 1340 | attr.hasFill = 2; 1341 | self:parseUrl(attr.fillGradient, value); 1342 | else 1343 | attr.hasFill = 1; 1344 | attr.fillColor = self:parseColor(value); 1345 | end 1346 | elseif name == "opacity" then 1347 | attr.opacity = self:parseOpacity(value); 1348 | elseif name == "fill-opacity" then 1349 | attr.fillOpacity = self:parseOpacity(value); 1350 | elseif name == "stroke" then 1351 | if value == "none" then 1352 | attr.hasStroke = 0; 1353 | elseif value:sub(1,4) == "url(" then 1354 | attr.hasStroke = 2; 1355 | self:parseUrl(attr.strokeGradient, value); 1356 | else 1357 | attr.hasStroke = 1; 1358 | attr.strokeColor = self:parseColor(value); 1359 | end 1360 | elseif name == "stroke-width" then 1361 | attr.strokeWidth = self:parseCoordinate(value, 0.0, self:actualLength()); 1362 | elseif name == "stroke-dasharray" then 1363 | attr.strokeDashCount = self:parseStrokeDashArray(value, attr.strokeDashArray); 1364 | elseif name == "stroke-dashoffset" then 1365 | attr.strokeDashOffset = self:parseCoordinate(value, 0.0, self:actualLength()); 1366 | elseif name == "stroke-opacity" then 1367 | attr.strokeOpacity = self:parseOpacity(value); 1368 | elseif name == "stroke-linecap" then 1369 | attr.strokeLineCap = self:parseLineCap(value); 1370 | elseif name == "stroke-linejoin" then 1371 | attr.strokeLineJoin = self:parseLineJoin(value); 1372 | elseif name == "fill-rule" then 1373 | attr.fillRule = self:parseFillRule(value); 1374 | elseif name == "font-size" then 1375 | attr.fontSize = self:parseCoordinate(value, 0.0, self:actualLength()); 1376 | elseif name == "transform" then 1377 | self:parseTransform(xform, value); 1378 | print("transform: ", xform[0], xform[1], xform[2], xform[3], xform[4], xform[5]) 1379 | transform2D.xformPremultiply(attr.xform, xform); 1380 | elseif name == "stop-color" then 1381 | attr.stopColor = self:parseColor(value); 1382 | elseif name == "stop-opacity" then 1383 | attr.stopOpacity = self:parseOpacity(value); 1384 | elseif name == "offset" then 1385 | attr.stopOffset = self:parseCoordinate(value, 0.0, 1.0); 1386 | elseif name == "id" then 1387 | strncpy(attr.id, value, 63); 1388 | attr.id[63] = 0; 1389 | else 1390 | return false; 1391 | end 1392 | 1393 | return true; 1394 | end 1395 | 1396 | function SVGParser.parseStyle(self, s) 1397 | --print("parseStyle: ", s) 1398 | 1399 | -- match everything but the delimeter 1400 | for word in string.gmatch(s, "([^;]+)") do 1401 | name, value = word:match("([^:]+):(.*)") 1402 | self:parseAttr(name, value) 1403 | end 1404 | end 1405 | 1406 | function SVGParser.parseAttribs(self, attr) 1407 | for k,v in pairs(attr) do 1408 | if k == "style" then 1409 | self:parseStyle(v); 1410 | else 1411 | self:parseAttr(k, v); 1412 | end 1413 | end 1414 | end 1415 | 1416 | function SVGParser.pathMoveTo(self, cpx, cpy, args, rel) 1417 | --print("pathMoveTo: ", rel, args[1], args[2]) 1418 | if rel then 1419 | cpx = cpx + args[1]; 1420 | cpy = cpy + args[2]; 1421 | else 1422 | cpx = args[1]; 1423 | cpy = args[2]; 1424 | end 1425 | 1426 | self:moveTo(cpx, cpy); 1427 | 1428 | return cpx, cpy; 1429 | end 1430 | 1431 | function SVGParser.pathLineTo(self, cpx, cpy, args, rel) 1432 | --print(".pathLineTo: ", rel, cpx, cpy, args[1], args[2]) 1433 | if rel then 1434 | cpx = cpx + args[1]; 1435 | cpy = cpy + args[2]; 1436 | else 1437 | cpx = args[1]; 1438 | cpy = args[2]; 1439 | end 1440 | 1441 | self:lineTo(cpx, cpy); 1442 | 1443 | return cpx, cpy; 1444 | end 1445 | 1446 | 1447 | function SVGParser.pathHLineTo(self, cpx, cpy, args, rel) 1448 | if rel then 1449 | cpx = cpx + args[1]; 1450 | else 1451 | cpx = args[1]; 1452 | end 1453 | 1454 | self:lineTo(cpx, cpy); 1455 | 1456 | return cpx, cpy; 1457 | end 1458 | 1459 | function SVGParser.pathVLineTo(self, cpx, cpy, args, rel) 1460 | if rel then 1461 | cpy = cpy + args[1]; 1462 | else 1463 | cpy = args[1]; 1464 | end 1465 | 1466 | self:lineTo(cpx, cpy); 1467 | 1468 | return cpx, cpy; 1469 | end 1470 | 1471 | function SVGParser.pathCubicBezTo(self, cpx, cpy, cpx2, cpy2, args, rel) 1472 | 1473 | local x2, y2, cx1, cy1, cx2, cy2 =0,0,0,0,0,0; 1474 | 1475 | if (rel) then 1476 | cx1 = cpx + args[1]; 1477 | cy1 = cpy + args[2]; 1478 | cx2 = cpx + args[3]; 1479 | cy2 = cpy + args[4]; 1480 | x2 = cpx + args[5]; 1481 | y2 = cpy + args[6]; 1482 | else 1483 | cx1 = args[1]; 1484 | cy1 = args[2]; 1485 | cx2 = args[3]; 1486 | cy2 = args[4]; 1487 | x2 = args[5]; 1488 | y2 = args[6]; 1489 | end 1490 | 1491 | self:cubicBezTo(cx1,cy1, cx2,cy2, x2,y2); 1492 | 1493 | cpx2 = cx2; 1494 | cpy2 = cy2; 1495 | cpx = x2; 1496 | cpy = y2; 1497 | 1498 | return cpx, cpy, cpx2, cpy2; 1499 | end 1500 | 1501 | 1502 | function SVGParser.pathCubicBezShortTo(self, cpx, cpy,cpx2, cpy2, args, rel) 1503 | 1504 | local x1 = cpx; 1505 | local y1 = cpy; 1506 | 1507 | local cx2 = args[1]; 1508 | local cy2 = args[2]; 1509 | local x2 = args[3]; 1510 | local y2 = args[4]; 1511 | 1512 | if rel then 1513 | cx2 = cx2 + cpx; 1514 | cy2 = cy2 + cpy; 1515 | x2 = x2 + cpx; 1516 | y2 = y2 + cpy; 1517 | end 1518 | 1519 | local cx1 = 2*x1 - cpx2; 1520 | local cy1 = 2*y1 - cpy2; 1521 | 1522 | self:cubicBezTo(cx1,cy1, cx2,cy2, x2,y2); 1523 | 1524 | cpx2 = cx2; 1525 | cpy2 = cy2; 1526 | cpx = x2; 1527 | cpy = y2; 1528 | 1529 | return cpx, cpy, cpx2, cpy2; 1530 | end 1531 | 1532 | function SVGParser.pathQuadBezTo(self, cpx, cpy, cpx2, cpy2, args, rel) 1533 | local x1 = cpx; 1534 | local y1 = cpy; 1535 | 1536 | local cx = args[1]; 1537 | local cy = args[2]; 1538 | local x2 = args[3]; 1539 | local y2 = args[4]; 1540 | 1541 | if rel then 1542 | cx = cx + cpx; 1543 | cy = cy + cpy; 1544 | x2 = x2 + cpx; 1545 | y2 = y2 + cpy; 1546 | end 1547 | 1548 | -- Convert to cubic bezier 1549 | local cx1 = x1 + 2.0/3.0*(cx - x1); 1550 | local cy1 = y1 + 2.0/3.0*(cy - y1); 1551 | local cx2 = x2 + 2.0/3.0*(cx - x2); 1552 | local cy2 = y2 + 2.0/3.0*(cy - y2); 1553 | 1554 | self:cubicBezTo(cx1,cy1, cx2,cy2, x2,y2); 1555 | 1556 | cpx2 = cx; 1557 | cpy2 = cy; 1558 | cpx = x2; 1559 | cpy = y2; 1560 | 1561 | return cpx, cpy, cpx2, cpy2; 1562 | end 1563 | 1564 | function SVGParser.pathQuadBezShortTo(self, cpx, cpy, cpx2, cpy2, args, rel) 1565 | 1566 | local x1 = cpx; 1567 | local y1 = cpy; 1568 | local x2 = args[1]; 1569 | local y2 = args[2]; 1570 | 1571 | if rel then 1572 | x2 = x2 + cpx; 1573 | y2 = y2 + cpy; 1574 | end 1575 | 1576 | local cx = 2*x1 - cpx2; 1577 | local cy = 2*y1 - cpy2; 1578 | 1579 | -- Convert to cubix bezier 1580 | local cx1 = x1 + 2.0/3.0*(cx - x1); 1581 | local cy1 = y1 + 2.0/3.0*(cy - y1); 1582 | local cx2 = x2 + 2.0/3.0*(cx - x2); 1583 | local cy2 = y2 + 2.0/3.0*(cy - y2); 1584 | 1585 | self:cubicBezTo(cx1,cy1, cx2,cy2, x2,y2); 1586 | 1587 | cpx2 = cx; 1588 | cpy2 = cy; 1589 | cpx = x2; 1590 | cpy = y2; 1591 | 1592 | return cpx, cpy, cpx2, cpy2; 1593 | end 1594 | 1595 | 1596 | 1597 | 1598 | 1599 | function SVGParser.pathArcTo(self, cpx, cpy, args, rel) 1600 | print(".pathArcTo: ", rel, cpx, cpy, unpack(args)) 1601 | 1602 | --[[ 1603 | -- Ported from canvg (https://code.google.com/p/canvg/) 1604 | float x1p, y1p, 1605 | float x, y 1606 | --]] 1607 | 1608 | local rx = abs(args[1]); -- y radius 1609 | local ry = abs(args[2]); -- x radius 1610 | local rotx = args[3] / 180.0 * SVG_PI; -- x rotation angle 1611 | 1612 | -- Large arc 1613 | local fa = false; 1614 | if abs(args[4]) > 1e-6 then 1615 | fa = true; 1616 | end 1617 | 1618 | -- Sweep direction 1619 | local fs = false; 1620 | if abs(args[5]) > 1e-6 then 1621 | fs = true; 1622 | end 1623 | 1624 | local x1 = cpx; -- start point 1625 | local y1 = cpy; 1626 | local x2 = args[6]; 1627 | local y2 = args[7]; 1628 | 1629 | if rel then -- end point 1630 | x2 = x2 + cpx; 1631 | y2 = y2 + cpy; 1632 | else 1633 | x2 = args[6]; 1634 | y2 = args[7]; 1635 | end 1636 | 1637 | local dx = x1 - x2; 1638 | local dy = y1 - y2; 1639 | local d = sqrt(dx*dx + dy*dy); 1640 | 1641 | 1642 | if (d < 1e-6 or rx < 1e-6 or ry < 1e-6) then 1643 | -- The arc degenerates to a line 1644 | self:lineTo(x2, y2); 1645 | cpx = x2; 1646 | cpy = y2; 1647 | return cpx, cpy; 1648 | end 1649 | 1650 | local sinrx = sin(rotx); 1651 | local cosrx = cos(rotx); 1652 | 1653 | 1654 | -- Convert to center point parameterization. 1655 | -- http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes 1656 | -- 1) Compute x1', y1' 1657 | local x1p = cosrx * dx / 2.0 + sinrx * dy / 2.0; 1658 | local y1p = -sinrx * dx / 2.0 + cosrx * dy / 2.0; 1659 | d = sqr(x1p)/sqr(rx) + sqr(y1p)/sqr(ry); 1660 | if d > 1 then 1661 | d = sqrt(d); 1662 | rx = rx * d; 1663 | ry = ry * d; 1664 | end 1665 | 1666 | -- 2) Compute cx', cy' 1667 | local s = 0.0; 1668 | local sa = sqr(rx)*sqr(ry) - sqr(rx)*sqr(y1p) - sqr(ry)*sqr(x1p); 1669 | local sb = sqr(rx)*sqr(y1p) + sqr(ry)*sqr(x1p); 1670 | if (sa < 0.0) then 1671 | sa = 0.0; 1672 | end 1673 | 1674 | if (sb > 0.0) then 1675 | s = sqrt(sa / sb); 1676 | end 1677 | 1678 | if (fa == fs) then 1679 | s = -s; 1680 | end 1681 | 1682 | local cxp = s * rx * y1p / ry; 1683 | local cyp = s * -ry * x1p / rx; 1684 | 1685 | 1686 | -- 3) Compute cx,cy from cx',cy' 1687 | local cx = (x1 + x2)/2.0 + cosrx*cxp - sinrx*cyp; 1688 | local cy = (y1 + y2)/2.0 + sinrx*cxp + cosrx*cyp; 1689 | 1690 | -- 4) Calculate theta1, and delta theta. 1691 | local ux = (x1p - cxp) / rx; 1692 | local uy = (y1p - cyp) / ry; 1693 | local vx = (-x1p - cxp) / rx; 1694 | local vy = (-y1p - cyp) / ry; 1695 | local a1 = vecang(1.0,0.0, ux,uy); -- Initial angle 1696 | local da = vecang(ux,uy, vx,vy); -- Delta angle 1697 | 1698 | --[[ 1699 | // if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; 1700 | // if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; 1701 | --]] 1702 | 1703 | if (fa) then 1704 | -- Choose large arc 1705 | if (da > 0.0) then 1706 | da = da - 2*SVG_PI; 1707 | else 1708 | da = 2*SVG_PI + da; 1709 | end 1710 | end 1711 | 1712 | 1713 | -- Approximate the arc using cubic spline segments. 1714 | local t = ffi.new("double[6]"); 1715 | t[0] = cosrx; t[1] = sinrx; 1716 | t[2] = -sinrx; t[3] = cosrx; 1717 | t[4] = cx; t[5] = cy; 1718 | 1719 | 1720 | -- Split arc into max 90 degree segments. 1721 | -- The loop assumes an iteration per end point (including start and end), this +1. 1722 | local ndivs = abs(da) / (SVG_PI*0.5) + 1.0; 1723 | local hda = (da / ndivs) / 2.0; 1724 | local kappa = abs(4.0 / 3.0 * (1.0 - cos(hda)) / sin(hda)); 1725 | 1726 | if (da < 0.0) then 1727 | kappa = -kappa; 1728 | end 1729 | 1730 | local px, py, ptanx, ptany = 0,0,0,0; 1731 | 1732 | for i = 0, ndivs do 1733 | local a = a1 + da * (i/ndivs); 1734 | local dx = cos(a); 1735 | local dy = sin(a); 1736 | local x, y = transform2D.xformPoint(dx*rx, dy*ry, t); -- position 1737 | local tanx, tany = transform2D.xformVec(-dy*rx * kappa, dx*ry * kappa, t); -- tangent 1738 | if i > 0 then 1739 | self:cubicBezTo(px+ptanx,py+ptany, x-tanx, y-tany, x, y); 1740 | end 1741 | 1742 | px = x; 1743 | py = y; 1744 | ptanx = tanx; 1745 | ptany = tany; 1746 | end 1747 | 1748 | cpx = x2; 1749 | cpy = y2; 1750 | 1751 | 1752 | return cpx, cpy; 1753 | end 1754 | 1755 | --[[ 1756 | given a 'd' attribute of an svg path element 1757 | turn that into a table with each command and 1758 | its arguments represented by their own table. 1759 | Each command entry has the command itself (a single character) 1760 | followed by the arguments for that command in subsequent positions 1761 | --]] 1762 | function parseSVGPath(input) 1763 | local out = {}; 1764 | 1765 | for instr, vals in input:gmatch("([a-df-zA-DF-Z])([^a-df-zA-DF-Z]*)") do 1766 | local line = { instr }; 1767 | for v in vals:gmatch("([+-]?[%deE.]+)") do 1768 | line[#line+1] = v; 1769 | end 1770 | out[#out+1] = line; 1771 | end 1772 | return out; 1773 | end 1774 | 1775 | function SVGParser.parsePath(self, attr) 1776 | --print("parsePath: ") 1777 | 1778 | local s = nil; 1779 | for name,value in pairs(attr) do 1780 | --print(' ',name, value) 1781 | if name == "d" then 1782 | s = value; 1783 | else 1784 | self:parseAttribs({[name] = value}); 1785 | end 1786 | end 1787 | 1788 | if (s) then 1789 | self:resetPath(); 1790 | local cpx = 0; 1791 | local cpy = 0; 1792 | local cpx2 = 0; 1793 | local cpy2 = 0; 1794 | local closedFlag = false; 1795 | 1796 | local instructions = parseSVGPath(s) 1797 | 1798 | -- what we have in commands is a table of instructions 1799 | -- each line has 1800 | -- instructions[1] == name of instructions 1801 | -- instructions[2..n] == values for instructions 1802 | for _, args in ipairs(instructions) do 1803 | local cmd = args[1]; 1804 | table.remove(args,1); 1805 | --print(" CMD: ", cmd, #args) 1806 | 1807 | -- now, we have the instruction in the 'ins' value 1808 | -- and the arguments in the cmd table 1809 | if cmd == "m" or cmd == "M" then 1810 | --print("MOVETO:", unpack(args)) 1811 | if #args == 0 then 1812 | -- Commit path. 1813 | if (#self.pts > 0) then 1814 | self:addPath(closedFlag); 1815 | end 1816 | 1817 | -- Start new subpath. 1818 | self:resetPath(); 1819 | closedFlag = false; 1820 | else 1821 | -- The first set of arguments determine the new current 1822 | -- location. After we remove them, the remaining args 1823 | -- are treated as lineTo 1824 | cpx, cpy = self:pathMoveTo(cpx, cpy, args, cmd == 'm'); 1825 | table.remove(args) 1826 | table.remove(args) 1827 | 1828 | if #args >=2 then 1829 | 1830 | while #args >= 2 do 1831 | cpx, cpy = self:pathLineTo(cpx, cpy, args, cmd == 'm'); 1832 | cpx2 = cpx; 1833 | cpy2 = cpy; 1834 | table.remove(args) 1835 | table.remove(args) 1836 | end 1837 | end 1838 | end 1839 | elseif cmd == "l" or cmd == "L" then 1840 | --print("LINETO: ", unpack(args)) 1841 | cpx, cpy = self:pathLineTo(cpx, cpy, args, cmd == 'l'); 1842 | cpx2 = cpx; 1843 | cpy2 = cpy; 1844 | elseif cmd == "h" or cmd == "H" then 1845 | --print("HLINETO: ", unpack(args)) 1846 | cpx, cpy = self:pathHLineTo(cpx, cpy, args, cmd == 'h'); 1847 | cpx2 = cpx; 1848 | cpy2 = cpy; 1849 | elseif cmd == "v" or cmd == "V" then 1850 | --print("VLINETO: ", unpack(args)) 1851 | cpx, cpy = self:pathVLineTo(cpx, cpy, args, cmd == 'v'); 1852 | cpx2 = cpx; 1853 | cpy2 = cpy; 1854 | elseif cmd == "c" or cmd == "C" then 1855 | --print("CUBICBEZIERTO: ", unpack(args)) 1856 | cpx, cpy, cpx2, cpy2 = self:pathCubicBezTo(cpx, cpy, cpx2, cpy2, args, cmd == 'c'); 1857 | elseif cmd == "s" or cmd == "S" then 1858 | --print("CUBICBEZIERSHORTTO: ", unpack(args)) 1859 | cpx, cpy, cpx2, cpy2 = self:pathCubicBezShortTo(cpx, cpy, cpx2, cpy2, args, cmd == 's'); 1860 | elseif cmd == "q" or cmd == "Q" then 1861 | --print("QUADBEZIERTO: ", unpack(args)) 1862 | cpx, cpy, cpx2, cpy2 = self:pathQuadBezTo(cpx, cpy, cpx2, cpy2, args, cmd == 'q'); 1863 | elseif cmd == "t" or cmd == "T" then 1864 | --print("QUADBEZIERSHORTTO: ", unpack(args)) 1865 | cpx, cpy, cpx2, cpy2 = self:pathQuadBezShortTo(cpx, cpy, cpx2, cpy2, args, cmd == 't'); 1866 | 1867 | elseif cmd == "a" or cmd == "A" then 1868 | cpx, cpy = self:pathArcTo(cpx, cpy, args, cmd == 'a'); 1869 | cpx2 = cpx; 1870 | cpy2 = cpy; 1871 | elseif cmd == "z" or cmd == "Z" then 1872 | closedFlag = true; 1873 | -- Commit path. 1874 | if (#self.pts > 0) then 1875 | -- Move current point to first point 1876 | cpx = self.pts[1].x; 1877 | cpy = self.pts[1].y; 1878 | cpx2 = cpx; 1879 | cpy2 = cpy; 1880 | self:addPath(closedFlag); 1881 | end 1882 | 1883 | -- Start new subpath. 1884 | self:resetPath(); 1885 | self:moveTo(cpx, cpy); 1886 | closedFlag = false; 1887 | end 1888 | end 1889 | 1890 | -- Commit path. 1891 | --print("COMMIT: ", #self.pts) 1892 | if (#self.pts > 0) then 1893 | self:addPath(closedFlag); 1894 | end 1895 | end 1896 | 1897 | self:addShape(); 1898 | end 1899 | 1900 | function SVGParser.parseRect(self, attr) 1901 | local x = 0.0; 1902 | local y = 0.0; 1903 | local w = 0.0; 1904 | local h = 0.0; 1905 | local rx = -1.0; -- marks not set 1906 | local ry = -1.0; 1907 | 1908 | for k,v in pairs(attr) do 1909 | if not self:parseAttr(k,v) then 1910 | if k == "x" then 1911 | x = self:parseCoordinate(v, self:actualOrigX(), self:actualWidth()); 1912 | elseif k == "y" then 1913 | y = self:parseCoordinate(v, self:actualOrigY(), self:actualHeight()); 1914 | elseif k == "width" then 1915 | w = self:parseCoordinate(v, 0.0, self:actualWidth()); 1916 | elseif k == "height" then 1917 | h = self:parseCoordinate(v, 0.0, self:actualHeight()); 1918 | elseif k == "rx" then 1919 | rx = math.fabs(self:parseCoordinate(v, 0.0, self:actualWidth())); 1920 | elseif k == "ry" then 1921 | ry = math.fabs(self:parseCoordinate(v, 0.0, self:actualHeight())); 1922 | end 1923 | end 1924 | end 1925 | 1926 | 1927 | if (rx < 0.0 and ry > 0.0) then rx = ry; end 1928 | if (ry < 0.0 and rx > 0.0) then ry = rx; end 1929 | if (rx < 0.0) then rx = 0.0; end 1930 | if (ry < 0.0) then ry = 0.0; end 1931 | if (rx > w/2.0) then rx = w/2.0; end 1932 | if (ry > h/2.0) then ry = h/2.0; end 1933 | 1934 | --print("RECT: ", x, y, w, h, rx, ry) 1935 | 1936 | if w ~= 0 and h ~= 0 then 1937 | self:resetPath(); 1938 | 1939 | if (rx < 0.00001 or ry < 0.0001) then 1940 | self:moveTo(x, y); 1941 | self:lineTo(x+w, y); 1942 | self:lineTo(x+w, y+h); 1943 | self:lineTo(x, y+h); 1944 | else 1945 | --print("ROUNDED RECT") 1946 | --[[ 1947 | -- Rounded rectangle 1948 | self:moveTo(x+rx, y); 1949 | self:lineTo(x+w-rx, y); 1950 | self:cubicBezTo(x+w-rx*(1-SVG_KAPPA90), y, x+w, y+ry*(1-SVG_KAPPA90), x+w, y+ry); 1951 | self:lineTo(x+w, y+h-ry); 1952 | self:cubicBezTo(x+w, y+h-ry*(1-SVG_KAPPA90), x+w-rx*(1-SVG_KAPPA90), y+h, x+w-rx, y+h); 1953 | self:lineTo(x+rx, y+h); 1954 | self:cubicBezTo(x+rx*(1-SVG_KAPPA90), y+h, x, y+h-ry*(1-SVG_KAPPA90), x, y+h-ry); 1955 | self:lineTo(x, y+ry); 1956 | self:cubicBezTo(x, y+ry*(1-SVG_KAPPA90), x+rx*(1-SVG_KAPPA90), y, x+rx, y); 1957 | --]] 1958 | end 1959 | 1960 | self:addPath(true); 1961 | self:addShape(); 1962 | end 1963 | end 1964 | 1965 | 1966 | 1967 | function SVGParser.parseCircle(self, attr) 1968 | 1969 | local cx = 0.0; 1970 | local cy = 0.0; 1971 | local r = 0.0; 1972 | 1973 | for name, value in pairs(attr) do 1974 | if (not self:parseAttr(name, value)) then 1975 | if name == "cx" then 1976 | cx = self:parseCoordinate(value, self:actualOrigX(), self:actualWidth()); 1977 | elseif name == "cy" then 1978 | cy = self:parseCoordinate(value, self:actualOrigY(), self:actualHeight()); 1979 | elseif name == "r" then 1980 | r = fabs(self:parseCoordinate(value, 0.0, self:actualLength())); 1981 | end 1982 | end 1983 | end 1984 | 1985 | if (r > 0.0) then 1986 | self:resetPath(); 1987 | 1988 | self:moveTo(cx+r, cy); 1989 | self:cubicBezTo(cx+r, cy+r*SVG_KAPPA90, cx+r*SVG_KAPPA90, cy+r, cx, cy+r); 1990 | self:cubicBezTo(cx-r*SVG_KAPPA90, cy+r, cx-r, cy+r*SVG_KAPPA90, cx-r, cy); 1991 | self:cubicBezTo(cx-r, cy-r*SVG_KAPPA90, cx-r*SVG_KAPPA90, cy-r, cx, cy-r); 1992 | self:cubicBezTo(cx+r*SVG_KAPPA90, cy-r, cx+r, cy-r*SVG_KAPPA90, cx+r, cy); 1993 | 1994 | self:addPath(true); 1995 | 1996 | self:addShape(); 1997 | end 1998 | end 1999 | 2000 | 2001 | 2002 | function SVGParser.parseEllipse(self, attr) 2003 | 2004 | local cx = 0.0; 2005 | local cy = 0.0; 2006 | local rx = 0.0; 2007 | local ry = 0.0; 2008 | 2009 | for name, value in pairs(attr) do 2010 | if (not self:parseAttr(name, value)) then 2011 | if name == "cx" then cx = self:parseCoordinate(value, self:actualOrigX(), self:actualWidth()); 2012 | elseif name == "cy" then cy = self:parseCoordinate(value, self:actualOrigY(), self:actualHeight()); 2013 | elseif name == "rx" then rx = fabs(self:parseCoordinate(value, 0.0, self:actualWidth())); 2014 | elseif name == "ry" then ry = fabs(self:parseCoordinate(value, 0.0, self:actualHeight())); 2015 | end 2016 | end 2017 | end 2018 | 2019 | if (rx > 0.0 and ry > 0.0) then 2020 | self:resetPath(); 2021 | 2022 | self:moveTo(cx+rx, cy); 2023 | self:cubicBezTo(cx+rx, cy+ry*SVG_KAPPA90, cx+rx*SVG_KAPPA90, cy+ry, cx, cy+ry); 2024 | self:cubicBezTo(cx-rx*SVG_KAPPA90, cy+ry, cx-rx, cy+ry*SVG_KAPPA90, cx-rx, cy); 2025 | self:cubicBezTo(cx-rx, cy-ry*SVG_KAPPA90, cx-rx*SVG_KAPPA90, cy-ry, cx, cy-ry); 2026 | self:cubicBezTo(cx+rx*SVG_KAPPA90, cy-ry, cx+rx, cy-ry*SVG_KAPPA90, cx+rx, cy); 2027 | 2028 | self:addPath(true); 2029 | 2030 | self:addShape(); 2031 | end 2032 | end 2033 | 2034 | function SVGParser.parseLine(self, attr) 2035 | 2036 | local x1, y1, x2, y2 = 0, 0, 0, 0; 2037 | 2038 | for name, value in pairs(attr) do 2039 | if (not self:parseAttr(name, value)) then 2040 | if name == "x1" then x1 = self:parseCoordinate(value, self:actualOrigX(), self:actualWidth()); 2041 | elseif name == "y1" then y1 = self:parseCoordinate(value, self:actualOrigY(), self:actualHeight()); 2042 | elseif name == "x2" then x2 = self:parseCoordinate(value, self:actualOrigX(), self:actualWidth()); 2043 | elseif name == "y2" then y2 = self:parseCoordinate(value, self:actualOrigY(), self:actualHeight()); 2044 | end 2045 | end 2046 | end 2047 | 2048 | self:resetPath(); 2049 | 2050 | self:moveTo(x1, y1); 2051 | self:lineTo(x2, y2); 2052 | 2053 | self:addPath(false); 2054 | 2055 | self:addShape(); 2056 | end 2057 | 2058 | 2059 | 2060 | function SVGParser.parsePoly(self, attr, closeFlag) 2061 | --local coordspatt = "([^ ]+)"; 2062 | local coordspatt = "(%g+%s*,%s*%g+)"; 2063 | --local xypatt = "([^,]+),(.*)"; 2064 | local xypatt = "(%g+)%s*,%s*(%g+)"; 2065 | 2066 | self:resetPath(); 2067 | 2068 | for name, value in pairs(attr) do 2069 | if (not self:parseAttr(name, value)) then 2070 | if name == "points" then 2071 | local firstone = true; 2072 | for coords in string.gmatch(value, coordspatt) do 2073 | --print(".parsePoly(): ",coords) 2074 | x, y = coords:match(xypatt) 2075 | if x and y then 2076 | --print(" x,y: ", x, y) 2077 | 2078 | if firstone then 2079 | self:moveTo(tonumber(x), tonumber(y)); 2080 | firstone = not firstone; 2081 | else 2082 | self:lineTo(tonumber(x), tonumber(y)); 2083 | end 2084 | end 2085 | end 2086 | end 2087 | end 2088 | end 2089 | 2090 | self:addPath(closeFlag); 2091 | 2092 | self:addShape(); 2093 | end 2094 | 2095 | function SVGParser.parseSVG(self, attrs) 2096 | --print("SVGParser.parseSVG - BEGIN") 2097 | 2098 | for name, value in pairs(attrs) do 2099 | --print("SVGParser.parseSVG: ", name, value) 2100 | 2101 | if (not self:parseAttr(name, value)) then 2102 | --print(" name, value: ", name, value) 2103 | if name == "width" then 2104 | self.image.width = self:parseCoordinate(value, 0.0, 1.0); 2105 | elseif name == "height" then 2106 | self.image.height = self:parseCoordinate(value, 0.0, 1.0); 2107 | elseif name == "viewBox" then 2108 | local minx, miny, width, height = value:match("(%d+)%s+(%d+)%s+(%d+)%s+(%d+)") 2109 | self.viewMinx = tonumber(minx); 2110 | self.viewMiny = tonumber(miny); 2111 | self.viewWidth = tonumber(width); 2112 | self.viewHeight = tonumber(height); 2113 | --sscanf(value, "%f%*[%%, \t]%f%*[%%, \t]%f%*[%%, \t]%f", &self.viewMinx, &self.viewMiny, &self.viewWidth, &self.viewHeight); 2114 | elseif name == "preserveAspectRatio" then 2115 | if value:find("none") then 2116 | -- No uniform scaling 2117 | self.alignType = SVG_ALIGN_NONE; 2118 | else 2119 | -- Parse X align 2120 | if value:find("xMin") then 2121 | self.alignX = SVG_ALIGN_MIN; 2122 | elseif value:find("xMid") then 2123 | self.alignX = SVG_ALIGN_MID; 2124 | elseif value:find("xMax") then 2125 | self.alignX = SVG_ALIGN_MAX; 2126 | end 2127 | 2128 | -- Parse Y align 2129 | if value:find("yMin") then 2130 | self.alignY = SVG_ALIGN_MIN; 2131 | elseif value:find("yMid") then 2132 | self.alignY = SVG_ALIGN_MID; 2133 | elseif value:find("yMax") then 2134 | self.alignY = SVG_ALIGN_MAX; 2135 | end 2136 | 2137 | -- Parse meet/slice 2138 | self.alignType = SVG_ALIGN_MEET; 2139 | if value:find("slice") then 2140 | self.alignType = SVG_ALIGN_SLICE; 2141 | end 2142 | end 2143 | end 2144 | end 2145 | end 2146 | end 2147 | 2148 | 2149 | --[[ 2150 | function NSVGParser.parseGradient(self, const char** attr, char type) 2151 | { 2152 | int i; 2153 | NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); 2154 | if (grad == NULL) return; 2155 | memset(grad, 0, sizeof(NSVGgradientData)); 2156 | grad.units = NSVG_OBJECT_SPACE; 2157 | grad.type = type; 2158 | if (grad.type == NSVG_PAINT_LINEAR_GRADIENT) { 2159 | grad.linear.x1 = self:coord(0.0f, NSVG_UNITS_PERCENT); 2160 | grad.linear.y1 = self:coord(0.0f, NSVG_UNITS_PERCENT); 2161 | grad.linear.x2 = self:coord(100.0f, NSVG_UNITS_PERCENT); 2162 | grad.linear.y2 = self:coord(0.0f, NSVG_UNITS_PERCENT); 2163 | elseif (grad.type == NSVG_PAINT_RADIAL_GRADIENT) { 2164 | grad.radial.cx = self:coord(50.0f, NSVG_UNITS_PERCENT); 2165 | grad.radial.cy = self:coord(50.0f, NSVG_UNITS_PERCENT); 2166 | grad.radial.r = self:coord(50.0f, NSVG_UNITS_PERCENT); 2167 | } 2168 | 2169 | xform.xformIdentity(grad.xform); 2170 | 2171 | for (i = 0; attr[i]; i += 2) { 2172 | if (strcmp(attr[i], "id") == 0) { 2173 | strncpy(grad.id, attr[i+1], 63); 2174 | grad.id[63] = '\0'; 2175 | elseif (!self:parseAttr(p, attr[i], attr[i + 1])) { 2176 | if (strcmp(attr[i], "gradientUnits") == 0) { 2177 | if (strcmp(attr[i+1], "objectBoundingBox") == 0) 2178 | grad.units = NSVG_OBJECT_SPACE; 2179 | else 2180 | grad.units = NSVG_USER_SPACE; 2181 | elseif (strcmp(attr[i], "gradientTransform") == 0) { 2182 | self:parseTransform(grad.xform, attr[i + 1]); 2183 | elseif (strcmp(attr[i], "cx") == 0) { 2184 | grad.radial.cx = self:parseCoordinateRaw(attr[i + 1]); 2185 | elseif (strcmp(attr[i], "cy") == 0) { 2186 | grad.radial.cy = self:parseCoordinateRaw(attr[i + 1]); 2187 | elseif (strcmp(attr[i], "r") == 0) { 2188 | grad.radial.r = self:parseCoordinateRaw(attr[i + 1]); 2189 | elseif (strcmp(attr[i], "fx") == 0) { 2190 | grad.radial.fx = self:parseCoordinateRaw(attr[i + 1]); 2191 | elseif (strcmp(attr[i], "fy") == 0) { 2192 | grad.radial.fy = self:parseCoordinateRaw(attr[i + 1]); 2193 | elseif (strcmp(attr[i], "x1") == 0) { 2194 | grad.linear.x1 = self:parseCoordinateRaw(attr[i + 1]); 2195 | elseif (strcmp(attr[i], "y1") == 0) { 2196 | grad.linear.y1 = self:parseCoordinateRaw(attr[i + 1]); 2197 | elseif (strcmp(attr[i], "x2") == 0) { 2198 | grad.linear.x2 = self:parseCoordinateRaw(attr[i + 1]); 2199 | elseif (strcmp(attr[i], "y2") == 0) { 2200 | grad.linear.y2 = self:parseCoordinateRaw(attr[i + 1]); 2201 | elseif (strcmp(attr[i], "spreadMethod") == 0) { 2202 | if (strcmp(attr[i+1], "pad") == 0) 2203 | grad.spread = NSVG_SPREAD_PAD; 2204 | else if (strcmp(attr[i+1], "reflect") == 0) 2205 | grad.spread = NSVG_SPREAD_REFLECT; 2206 | else if (strcmp(attr[i+1], "repeat") == 0) 2207 | grad.spread = NSVG_SPREAD_REPEAT; 2208 | elseif (strcmp(attr[i], "xlink:href") == 0) { 2209 | const char *href = attr[i+1]; 2210 | strncpy(grad.ref, href+1, 62); 2211 | grad.ref[62] = '\0'; 2212 | } 2213 | } 2214 | } 2215 | 2216 | grad.next = self.gradients; 2217 | self.gradients = grad; 2218 | } 2219 | --]] 2220 | 2221 | --[[ 2222 | function NSVGParser.parseGradientStop(self, const char** attr) 2223 | { 2224 | NSVGattrib* curAttr = self:getAttr(p); 2225 | NSVGgradientData* grad; 2226 | NSVGgradientStop* stop; 2227 | int i, idx; 2228 | 2229 | curAttr.stopOffset = 0; 2230 | curAttr.stopColor = 0; 2231 | curAttr.stopOpacity = 1.0f; 2232 | 2233 | for (i = 0; attr[i]; i += 2) { 2234 | self:parseAttr(p, attr[i], attr[i + 1]); 2235 | } 2236 | 2237 | // Add stop to the last gradient. 2238 | grad = self.gradients; 2239 | if (grad == NULL) return; 2240 | 2241 | grad.nstops++; 2242 | grad.stops = (NSVGgradientStop*)realloc(grad.stops, sizeof(NSVGgradientStop)*grad.nstops); 2243 | if (grad.stops == NULL) return; 2244 | 2245 | // Insert 2246 | idx = grad.nstops-1; 2247 | for (i = 0; i < grad.nstops-1; i++) { 2248 | if (curAttr.stopOffset < grad.stops[i].offset) { 2249 | idx = i; 2250 | break; 2251 | } 2252 | } 2253 | if (idx != grad.nstops-1) { 2254 | for (i = grad.nstops-1; i > idx; i--) 2255 | grad.stops[i] = grad.stops[i-1]; 2256 | } 2257 | 2258 | stop = &grad.stops[idx]; 2259 | stop.color = curAttr.stopColor; 2260 | stop.color |= (unsigned int)(curAttr.stopOpacity*255) << 24; 2261 | stop.offset = curAttr.stopOffset; 2262 | } 2263 | --]] 2264 | 2265 | 2266 | function SVGParser.startElement(self, el, attr) 2267 | --print("SVGParser.startElement: ", el, attr) 2268 | 2269 | if (self.defsFlag) then 2270 | -- Skip everything but gradients in defs 2271 | if el == "linearGradient" then 2272 | self:parseGradient(attr, SVGTypes.PaintType.LINEAR_GRADIENT); 2273 | elseif el == "radialGradient" then 2274 | self:parseGradient(attr, SVGTypes.PaintType.RADIAL_GRADIENT); 2275 | elseif el == "stop" then 2276 | self:parseGradientStop(attr); 2277 | end 2278 | 2279 | return; 2280 | end 2281 | 2282 | if el == "g" then 2283 | self:pushAttr(); 2284 | self:parseAttribs(attr); 2285 | elseif el == "path" then 2286 | if self.pathFlag then -- Do not allow nested paths. 2287 | print("NESTED PATH") 2288 | return; 2289 | end 2290 | 2291 | self:pushAttr(); 2292 | self:parsePath(attr); 2293 | self:popAttr(); 2294 | elseif el == "rect" then 2295 | self:pushAttr(); 2296 | self:parseRect(attr); 2297 | self:popAttr(); 2298 | elseif el == "circle" then 2299 | self:pushAttr(); 2300 | self:parseCircle(attr); 2301 | self:popAttr(); 2302 | elseif el == "ellipse" then 2303 | self:pushAttr(); 2304 | self:parseEllipse(attr); 2305 | self:popAttr(); 2306 | elseif el == "line" then 2307 | self:pushAttr(); 2308 | self:parseLine(attr); 2309 | self:popAttr(); 2310 | elseif el == "polyline" then 2311 | self:pushAttr(); 2312 | self:parsePoly(attr, false); 2313 | self:popAttr(); 2314 | elseif el == "polygon" then 2315 | self:pushAttr(); 2316 | self:parsePoly(attr, true); 2317 | self:popAttr(); 2318 | elseif el == "linearGradient" then 2319 | self:parseGradient(attr, NSVG_PAINT_LINEAR_GRADIENT); 2320 | elseif el == "radialGradient" then 2321 | self:parseGradient(attr, NSVG_PAINT_RADIAL_GRADIENT); 2322 | elseif el == "stop" then 2323 | self:parseGradientStop(attr); 2324 | elseif el == "defs" then 2325 | self.defsFlag = true; 2326 | elseif el == "svg" then 2327 | self:parseSVG(attr); 2328 | end 2329 | end 2330 | 2331 | 2332 | function SVGParser.endElement(self, el) 2333 | --print("SVGParser.endElement: ", el) 2334 | if el == "g" then 2335 | self:popAttr(); 2336 | elseif el == "path" then 2337 | self.pathFlag = false; 2338 | elseif el == "defs" then 2339 | self.defsFlag = false; 2340 | end 2341 | end 2342 | 2343 | 2344 | function SVGParser.content(self, s) 2345 | -- empty 2346 | end 2347 | 2348 | 2349 | function SVGParser.imageBounds(self, bounds) 2350 | --print(".imageBounds: ", #self.image.shapes) 2351 | 2352 | if #self.image.shapes < 1 then 2353 | bounds[0],bounds[1],bounds[2],bounds[3] = 0,0,0,0; 2354 | return; 2355 | end 2356 | 2357 | for _, shape in ipairs(self.image.shapes) do 2358 | bounds[0] = minf(bounds[0], shape.bounds[0]); 2359 | bounds[1] = minf(bounds[1], shape.bounds[1]); 2360 | bounds[2] = maxf(bounds[2], shape.bounds[2]); 2361 | bounds[3] = maxf(bounds[3], shape.bounds[3]); 2362 | end 2363 | 2364 | print(".imageBounds - END: ", bounds[0], bounds[1], bounds[2], bounds[3]) 2365 | 2366 | return bounds[0], bounds[1], bounds[2], bounds[3]; 2367 | end 2368 | 2369 | local function viewAlign(content, container, kind) 2370 | 2371 | if (kind == SVG_ALIGN_MIN) then 2372 | return 0; 2373 | elseif (kind == SVG_ALIGN_MAX) then 2374 | return container - content; 2375 | end 2376 | 2377 | -- mid 2378 | return (container - content) * 0.5; 2379 | end 2380 | 2381 | local function scaleGradient(grad, tx, ty, sx, sy) 2382 | grad.xform[0] = grad.xform[0] *sx; 2383 | grad.xform[1] = grad.xform[1] *sx; 2384 | grad.xform[2] = grad.xform[2] *sy; 2385 | grad.xform[3] = grad.xform[3] *sy; 2386 | grad.xform[4] = grad.xform[4] + tx*sx; 2387 | grad.xform[5] = grad.xform[2] + ty*sx; 2388 | end 2389 | 2390 | 2391 | function SVGParser.scaleToViewbox(self, units) 2392 | print("SHAPE TO VIEW BOX: ", units) 2393 | local bounds = ffi.new("double[4]"); 2394 | local t = ffi.new("double[6]"); 2395 | 2396 | -- Guess image size if not set completely. 2397 | self:imageBounds(bounds); 2398 | --print("image bounds: ", bounds[0], bounds[1], bounds[2], bounds[3]) 2399 | if (self.viewWidth == 0) then 2400 | if self.image.width > 0 then 2401 | self.viewWidth = self.image.width; 2402 | else 2403 | self.viewMinx = bounds[0]; 2404 | self.viewWidth = bounds[2] - bounds[0]; 2405 | end 2406 | end 2407 | 2408 | if (self.viewHeight == 0) then 2409 | if (self.image.height > 0) then 2410 | self.viewHeight = self.image.height; 2411 | else 2412 | self.viewMiny = bounds[1]; 2413 | self.viewHeight = bounds[3] - bounds[1]; 2414 | end 2415 | end 2416 | 2417 | if (self.image.width == 0) then 2418 | self.image.width = self.viewWidth; 2419 | end 2420 | 2421 | if (self.image.height == 0) then 2422 | self.image.height = self.viewHeight; 2423 | end 2424 | 2425 | local tx = -self.viewMinx; 2426 | local ty = -self.viewMiny; 2427 | local sx = 0; 2428 | if self.viewWidth > 0 then 2429 | sx = self.image.width / self.viewWidth 2430 | end 2431 | 2432 | local sy = 0; 2433 | if self.viewHeight > 0 then 2434 | sy = self.image.height / self.viewHeight; 2435 | end 2436 | 2437 | -- Unit scaling 2438 | local us = 1.0 / self:convertToPixels(self:coord(1.0, self:parseUnits(units)), 0.0, 1.0); 2439 | 2440 | -- Fix aspect ratio 2441 | if (self.alignType == SVG_ALIGN_MEET) then 2442 | -- fit whole image into viewbox 2443 | sx = minf(sx, sy); 2444 | sy = sx; 2445 | tx = tx + viewAlign(self.viewWidth*sx, self.image.width, self.alignX) / sx; 2446 | ty = ty + viewAlign(self.viewHeight*sy, self.image.height, self.alignY) / sy; 2447 | elseif (self.alignType == SVG_ALIGN_SLICE) then 2448 | -- fill whole viewbox with image 2449 | sx = maxf(sx, sy); 2450 | sy = sx; 2451 | tx = tx + viewAlign(self.viewWidth*sx, self.image.width, self.alignX) / sx; 2452 | ty = ty + viewAlign(self.viewHeight*sy, self.image.height, self.alignY) / sy; 2453 | end 2454 | 2455 | -- Transform 2456 | sx = sx*us; 2457 | sy = sy*us; 2458 | local avgs = (sx+sy) / 2.0; 2459 | 2460 | -- BUGBUG Something wrong with scaling 2461 | -- setting values to 0 2462 | 2463 | for _, shape in ipairs(self.image.shapes) do 2464 | shape.bounds[0] = (shape.bounds[0] + tx) * sx; 2465 | shape.bounds[1] = (shape.bounds[1] + ty) * sy; 2466 | shape.bounds[2] = (shape.bounds[2] + tx) * sx; 2467 | shape.bounds[3] = (shape.bounds[3] + ty) * sy; 2468 | 2469 | for _, path in ipairs(shape.paths) do 2470 | --print("PATH, sx, sy: ", sx, sy) 2471 | path.bounds[0] = (path.bounds[0] + tx) * sx; 2472 | path.bounds[1] = (path.bounds[1] + ty) * sy; 2473 | path.bounds[2] = (path.bounds[2] + tx) * sx; 2474 | path.bounds[3] = (path.bounds[3] + ty) * sy; 2475 | 2476 | for _, pt in ipairs(path.pts) do 2477 | pt.x = (pt.x + tx) * sx; 2478 | pt.y = (pt.y + ty) * sy; 2479 | --print(pt.x, pt.y) 2480 | end 2481 | end 2482 | 2483 | if (shape.fill.type == PaintType.LINEAR_GRADIENT or shape.fill.type == PaintType.RADIAL_GRADIENT) then 2484 | scaleGradient(shape.fill.gradient, tx,ty, sx,sy); 2485 | ffi.copy(t, shape.fill.gradient.xform, ffi.sizeof("double")*6); 2486 | xform.xformInverse(shape.fill.gradient.xform, t); 2487 | end 2488 | 2489 | if (shape.stroke.type == PaintType.LINEAR_GRADIENT or shape.stroke.type == PaintType.RADIAL_GRADIENT) then 2490 | scaleGradient(shape.stroke.gradient, tx,ty, sx,sy); 2491 | ffi.copy(t, shape.stroke.gradient.xform, ffi.sizeof("double")*6); 2492 | xform.xformInverse(shape.stroke.gradient.xform, t); 2493 | end 2494 | 2495 | shape.strokeWidth = shape.strokeWidth * avgs; 2496 | shape.strokeDashOffset = shape.strokeDashOffset * avgs; 2497 | for i = 0, shape.strokeDashCount-1 do 2498 | shape.strokeDashArray[i] = shape.strokeDashArray[i] * avgs; 2499 | end 2500 | end 2501 | 2502 | print("SHAPE TO VIEW BOX - END") 2503 | end 2504 | 2505 | 2506 | return SVGParser 2507 | --------------------------------------------------------------------------------