├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── format ├── SVG.hx ├── gfx │ ├── Gfx.hx │ ├── Gfx2Haxe.hx │ ├── GfxBytes.hx │ ├── GfxExtent.hx │ ├── GfxGraphics.hx │ ├── GfxTextFinder.hx │ ├── Gradient.hx │ └── LineStyle.hx └── svg │ ├── FillType.hx │ ├── Grad.hx │ ├── Gradient.hx │ ├── Group.hx │ ├── Path.hx │ ├── PathParser.hx │ ├── PathSegment.hx │ ├── RenderContext.hx │ ├── SVG2Gfx.hx │ ├── SVGData.hx │ ├── SVGRenderer.hx │ └── Text.hx ├── haxelib.json ├── scripts └── docs.hxml └── test ├── README.md ├── images ├── all_rights_reserved_white-256x256.png ├── all_rights_reserved_white.svg ├── alphachannel-100x100.png ├── alphachannel.svg ├── arc-radius-too-small-256x256.png ├── arc-radius-too-small.svg ├── arc-rotate-256x256.png ├── arc-rotate.svg ├── disabled_test1.svg ├── disabled_test2.svg ├── fancy-sun-256x255.png ├── fancy-sun.svg ├── fill_rgb-100x100.png ├── fill_rgb.svg ├── group-inherited-256x256.png ├── group-inherited.svg ├── layer_test1.svg ├── layer_test2.svg ├── matrix-rotated-square-100x100.png ├── matrix-rotated-square.svg ├── nested_layer.svg ├── path-two-float-decimals-256x256.png ├── path-two-float-decimals.svg ├── rotated-square-100x100.png ├── rotated-square.svg ├── scale_rect-256x256.png ├── scale_rect.svg ├── ubuntu-logo-orange-256x256.png └── ubuntu-logo-orange.svg ├── project.xml └── src ├── MacroTest.hx ├── SvgGenerationTest.hx └── TestMain.hx /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | 6 | package-haxelib: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/upload-artifact@v4 11 | with: 12 | name: svg-haxelib 13 | path: | 14 | ./ 15 | !test/bin/ 16 | !scripts/ 17 | !haxe-*-*/ 18 | !neko-*-*/ 19 | !.git/ 20 | if-no-files-found: error 21 | 22 | docs: 23 | runs-on: ubuntu-latest 24 | steps: 25 | 26 | - uses: actions/checkout@v4 27 | 28 | - uses: krdlab/setup-haxe@v1 29 | with: 30 | haxe-version: 4.2.5 31 | 32 | - name: Set HAXEPATH 33 | run: | 34 | echo "HAXEPATH=$HAXE_STD_PATH/.." >> $GITHUB_ENV 35 | 36 | - name: Install Haxe dependencies 37 | run: | 38 | haxelib install lime --quiet 39 | haxelib install openfl --quiet 40 | haxelib install dox --quiet 41 | haxelib dev svg ${{ github.workspace }} 42 | 43 | - name: Build docs 44 | working-directory: scripts 45 | run: | 46 | haxe docs.hxml 47 | 48 | - uses: actions/upload-artifact@v4 49 | with: 50 | name: svg-docs 51 | path: docs 52 | if-no-files-found: error 53 | 54 | unit-test-neko: 55 | runs-on: ubuntu-latest 56 | env: 57 | SDL_VIDEODRIVER: "dummy" 58 | SDL_AUDIODRIVER: "disk" 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - uses: krdlab/setup-haxe@v1 63 | with: 64 | haxe-version: 4.3.3 65 | 66 | - name: Set HAXEPATH 67 | run: | 68 | echo "HAXEPATH=$HAXE_STD_PATH/.." >> $GITHUB_ENV 69 | 70 | - name: Install Haxe dependencies 71 | run: | 72 | haxelib install lime --quiet 73 | haxelib install openfl --quiet 74 | haxelib install utest --quiet 75 | 76 | - name: Setup environment 77 | run: | 78 | haxelib dev svg ${{ github.workspace }} 79 | 80 | - name: Run tests on Neko 81 | working-directory: test 82 | run: | 83 | haxelib run openfl test neko 84 | 85 | unit-test-hashlink: 86 | # AL init fails on both windows and ubuntu 87 | # macos-14 is arm64, which setup-haxe doesn't support yet 88 | runs-on: macos-13 89 | steps: 90 | - uses: actions/checkout@v4 91 | 92 | - uses: krdlab/setup-haxe@v1 93 | with: 94 | haxe-version: 4.3.3 95 | 96 | - name: Set HAXEPATH 97 | run: | 98 | echo "HAXEPATH=$HAXE_STD_PATH/.." >> $GITHUB_ENV 99 | 100 | - name: Install Haxe dependencies 101 | run: | 102 | haxelib install lime --quiet 103 | haxelib install openfl --quiet 104 | haxelib install utest --quiet 105 | 106 | - name: Setup environment 107 | run: | 108 | haxelib dev svg ${{ github.workspace }} 109 | 110 | - name: Run tests on HashLink 111 | working-directory: test 112 | run: | 113 | haxelib run openfl test hl 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | build 4 | report 5 | *.html 6 | generated 7 | test/bin/ 8 | docs/ 9 | scripts/xml/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2013-2025 Joshua Granick and other SVG contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](LICENSE.md) [](http://lib.haxe.org/p/svg) [](https://github.com/openfl/svg/actions) 2 | 3 | SVG 4 | === 5 | 6 | Provides SVG parsing and rendering 7 | 8 | 9 | Installation 10 | ============ 11 | 12 | You can easily install SVG using haxelib: 13 | 14 | haxelib install svg 15 | 16 | To add it to a Lime or OpenFL project, add this to your project file: 17 | 18 | ```xml 19 | 20 | ``` 21 | 22 | Usage 23 | ===== 24 | 25 | ```haxe 26 | package; 27 | 28 | 29 | import format.SVG; 30 | import openfl.display.Sprite; 31 | import openfl.Assets; 32 | 33 | 34 | class Main extends Sprite { 35 | 36 | 37 | public function new () { 38 | 39 | super (); 40 | 41 | var svg = new SVG (Assets.getText ("assets/icon.svg")); 42 | svg.render (graphics); 43 | 44 | } 45 | 46 | 47 | } 48 | ``` 49 | 50 | 51 | Development Builds 52 | ================== 53 | 54 | Install the haxelib from GitHub: 55 | 56 | haxelib git svg https://github.com/openfl/svg 57 | 58 | To return to release builds: 59 | 60 | haxelib dev svg 61 | 62 | 63 | Running SVG's Tests 64 | =================== 65 | 66 | `svg` includes some tests that render SVGs and make sure they look the way they're supposed to. These tests run automatically with each build/commit. For more information about running them manually, see [test/README.md](test/README.md). 67 | -------------------------------------------------------------------------------- /format/SVG.hx: -------------------------------------------------------------------------------- 1 | package format; 2 | 3 | 4 | import openfl.display.Graphics; 5 | import openfl.geom.Matrix; 6 | import openfl.geom.Rectangle; 7 | import format.svg.SVGData; 8 | import format.svg.SVGRenderer; 9 | 10 | 11 | class SVG { 12 | 13 | 14 | public var data:SVGData; 15 | 16 | 17 | public function new (content:String) { 18 | 19 | if (content != null) { 20 | 21 | data = new SVGData (Xml.parse (content)); 22 | 23 | } 24 | 25 | } 26 | 27 | 28 | public function render (graphics:Graphics, x:Float = 0, y:Float = 0, width:Int = -1, height:Int = -1, ?inLayer:String = null) { 29 | 30 | if (data == null) return; 31 | 32 | var matrix = new Matrix (); 33 | matrix.identity (); 34 | 35 | if (width > -1 && height > -1) { 36 | 37 | matrix.scale (width / data.width, height / data.height); 38 | 39 | } 40 | 41 | matrix.translate (x, y); 42 | 43 | var renderer = new SVGRenderer (data, inLayer); 44 | 45 | renderer.render (graphics, matrix); 46 | 47 | } 48 | 49 | 50 | } -------------------------------------------------------------------------------- /format/gfx/Gfx.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.display.GradientType; 4 | import openfl.display.SpreadMethod; 5 | import openfl.display.InterpolationMethod; 6 | import openfl.display.CapsStyle; 7 | import openfl.display.JointStyle; 8 | import openfl.display.LineScaleMode; 9 | import format.svg.Text; 10 | 11 | import openfl.geom.Matrix; 12 | 13 | class Gfx 14 | { 15 | public function new() { } 16 | public function geometryOnly() { return false; } 17 | public function size(inWidth:Float,inHeight:Float) { } 18 | public function beginGradientFill(grad:Gradient) { } 19 | 20 | public function beginFill(color:Int, alpha:Float) { } 21 | public function endFill() { } 22 | 23 | public function lineStyle(style:LineStyle) { } 24 | public function endLineStyle() { } 25 | 26 | public function moveTo(inX:Float, inY:Float) { } 27 | public function lineTo(inX:Float, inY:Float) { } 28 | public function curveTo(inCX:Float, inCY:Float,inX:Float,inY:Float) { } 29 | 30 | public function renderText(text:Text) { } 31 | 32 | public function eof() {} 33 | } 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /format/gfx/Gfx2Haxe.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.display.GradientType; 4 | import openfl.display.SpreadMethod; 5 | import openfl.display.InterpolationMethod; 6 | import openfl.display.CapsStyle; 7 | import openfl.display.JointStyle; 8 | import openfl.display.LineScaleMode; 9 | import openfl.geom.Matrix; 10 | 11 | 12 | class Gfx2Haxe extends Gfx 13 | { 14 | public var commands : Array; 15 | 16 | public function new( ) 17 | { 18 | super(); 19 | commands = []; 20 | } 21 | 22 | function f2a(f:Float):String 23 | { 24 | if (Math.abs(f)<0.000001) return "0"; 25 | if (Math.abs(1-f)<0.000001) return "1"; 26 | return f+""; 27 | } 28 | 29 | 30 | function newMatrix(m:Matrix) 31 | { 32 | return "new Matrix(" + f2a(m.a) + "," + f2a(m.b) + "," + f2a(m.c) + "," + f2a(m.d) + "," + f2a(m.tx) + "," + f2a(m.ty) + ")"; 33 | } 34 | 35 | 36 | override public function beginGradientFill(grad:Gradient) 37 | { 38 | commands.push("g.beginGradientFill(" + grad.type + ","+ grad.colors + "," + grad.alphas + "," + 39 | grad.ratios + "," + newMatrix(grad.matrix) + "," + grad.spread + "," + 40 | grad.interp+ "," + grad.focus + ");" ); 41 | } 42 | 43 | override public function beginFill(color:Int, alpha:Float) 44 | { 45 | commands.push("g.beginFill(" + color + "," + f2a(alpha) + ");"); 46 | } 47 | override public function endFill() { commands.push("g.endFill();"); } 48 | 49 | 50 | override public function lineStyle(style:LineStyle) 51 | { 52 | commands.push("g.lineStyle("+f2a(style.thickness)+","+style.color+","+f2a(style.alpha)+"," + style.pixelHinting + "," + 53 | style.scaleMode + "," + style.capsStyle + "," + style.jointStyle + "," + f2a(style.miterLimit)+ ");" ); 54 | } 55 | 56 | 57 | override public function endLineStyle() { commands.push("g.lineStyle();"); } 58 | 59 | override public function moveTo(inX:Float, inY:Float) 60 | { commands.push("g.moveTo(" + inX + "," + inY + ");"); } 61 | override public function lineTo(inX:Float, inY:Float) 62 | { commands.push("g.lineTo(" + inX + "," + inY + ");"); } 63 | override public function curveTo(inCX:Float, inCY:Float,inX:Float,inY:Float) 64 | { commands.push("g.curveTo(" + inCX + "," + inCY + "," + inX + "," + inY + ");"); } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /format/gfx/GfxBytes.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.display.GradientType; 4 | import openfl.display.SpreadMethod; 5 | import openfl.display.InterpolationMethod; 6 | import openfl.display.CapsStyle; 7 | import openfl.display.JointStyle; 8 | import openfl.display.LineScaleMode; 9 | import openfl.geom.Matrix; 10 | 11 | import openfl.utils.ByteArray; 12 | 13 | import haxe.io.Bytes; 14 | 15 | #if (haxe_211 || haxe3) 16 | import haxe.crypto.BaseCode; 17 | #else 18 | import haxe.BaseCode; 19 | #end 20 | 21 | 22 | class GfxBytes extends Gfx 23 | { 24 | static inline var EOF = 0; 25 | static inline var SIZE = 1; 26 | 27 | static inline var BEGIN_FILL = 10; 28 | static inline var GRADIENT_FILL = 11; 29 | static inline var END_FILL = 12; 30 | 31 | static inline var LINE_STYLE = 20; 32 | static inline var END_LINE_STYLE = 21; 33 | 34 | static inline var MOVE = 30; 35 | static inline var LINE = 31; 36 | static inline var CURVE = 32; 37 | 38 | 39 | public var buffer:ByteArray; 40 | 41 | private static var base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 42 | private static var baseCoder:BaseCode; 43 | 44 | public function new(?inBuffer:ByteArray,inFlags:Int = 0) 45 | { 46 | super(); 47 | buffer = inBuffer==null ? new ByteArray() : inBuffer; 48 | } 49 | 50 | public function toString() : String 51 | { 52 | #if js 53 | return ""; 54 | #else 55 | #if flash 56 | var buf = new ByteArray(); 57 | buf.length = buffer.length; 58 | #else 59 | var buf = new ByteArray(buffer.length); 60 | #end 61 | buffer.position=0; 62 | buffer.readBytes(buf); 63 | buf.compress(); 64 | if (baseCoder==null) 65 | baseCoder = new BaseCode(haxe.io.Bytes.ofString(base64)); 66 | #if flash 67 | return baseCoder.encodeBytes(haxe.io.Bytes.ofData(buf)).toString(); 68 | #else 69 | return baseCoder.encodeBytes(buf).toString(); 70 | #end 71 | #end 72 | } 73 | public static function fromString(inString:String) : GfxBytes 74 | { 75 | if (baseCoder==null) 76 | baseCoder = new BaseCode(haxe.io.Bytes.ofString(base64)); 77 | #if flash 78 | var bytes:ByteArray = baseCoder.decodeBytes(haxe.io.Bytes.ofString(inString)).getData(); 79 | #elseif js 80 | var bytes:ByteArray = new ByteArray(); 81 | bytes.writeUTF( inString ); 82 | #else 83 | var bytes = ByteArray.fromBytes( baseCoder.decodeBytes(haxe.io.Bytes.ofString(inString)) ); 84 | #end 85 | #if !js 86 | bytes.uncompress(); 87 | #end 88 | return new GfxBytes(bytes); 89 | } 90 | 91 | override public function eof() { buffer.writeByte(EOF); } 92 | 93 | static var scaleModes = [ LineScaleMode.NORMAL, LineScaleMode.NONE, LineScaleMode.VERTICAL, LineScaleMode.HORIZONTAL ]; 94 | static var capsStyles = [ CapsStyle.ROUND, CapsStyle.NONE, CapsStyle.SQUARE ]; 95 | static var jointStyles = [ JointStyle.ROUND, JointStyle.MITER, JointStyle.BEVEL ]; 96 | static var spreadMethods = [ SpreadMethod.PAD, SpreadMethod.REPEAT, SpreadMethod.REFLECT ]; 97 | static var interpolationMethods = [ InterpolationMethod.RGB, InterpolationMethod.LINEAR_RGB ]; 98 | 99 | public function iterate(inGfx:Gfx) 100 | { 101 | buffer.position = 0; 102 | while(true) 103 | { 104 | switch(buffer.readByte()) 105 | { 106 | case EOF: 107 | return; 108 | 109 | case SIZE: 110 | var w = buffer.readFloat(); 111 | var h = buffer.readFloat(); 112 | inGfx.size(w,h); 113 | 114 | case BEGIN_FILL: 115 | var col = readRGB(); 116 | var alpha = buffer.readFloat(); 117 | inGfx.beginFill(col,alpha); 118 | 119 | case GRADIENT_FILL: 120 | var grad = new Gradient(); 121 | #if (openfl_legacy || openfl < "3.6.0") 122 | grad.type = Type.createEnumIndex(GradientType,buffer.readByte()); 123 | #else 124 | grad.type = cast buffer.readByte(); 125 | #end 126 | var len = buffer.readByte(); 127 | for(i in 0...len) 128 | { 129 | grad.colors.push(readRGB()); 130 | grad.alphas.push(buffer.readByte()/255.0); 131 | grad.ratios.push(buffer.readByte()); 132 | } 133 | grad.matrix.a = buffer.readFloat(); 134 | grad.matrix.b = buffer.readFloat(); 135 | grad.matrix.c = buffer.readFloat(); 136 | grad.matrix.d = buffer.readFloat(); 137 | grad.matrix.tx = buffer.readFloat(); 138 | grad.matrix.ty = buffer.readFloat(); 139 | #if (openfl_legacy || openfl < "3.6.0") 140 | grad.spread = spreadMethods[buffer.readByte()]; 141 | grad.interp = interpolationMethods[buffer.readByte()]; 142 | #else 143 | grad.spread = cast buffer.readByte(); 144 | grad.interp = cast buffer.readByte(); 145 | #end 146 | grad.focus = buffer.readFloat(); 147 | inGfx.beginGradientFill(grad); 148 | 149 | case END_FILL: 150 | inGfx.endFill(); 151 | 152 | case LINE_STYLE: 153 | var style = new LineStyle(); 154 | style.thickness = buffer.readFloat(); 155 | style.color = readRGB(); 156 | style.alpha = buffer.readFloat(); 157 | style.pixelHinting = buffer.readByte() > 0; 158 | #if (openfl_legacy || openfl < "3.6.0") 159 | style.scaleMode = scaleModes[buffer.readByte()]; 160 | style.capsStyle = capsStyles[buffer.readByte()]; 161 | style.jointStyle = jointStyles[buffer.readByte()]; 162 | #else 163 | style.scaleMode = cast buffer.readByte(); 164 | style.capsStyle = cast buffer.readByte(); 165 | style.jointStyle = cast buffer.readByte(); 166 | #end 167 | style.miterLimit = buffer.readFloat(); 168 | inGfx.lineStyle(style); 169 | 170 | case END_LINE_STYLE: 171 | inGfx.endLineStyle(); 172 | 173 | case MOVE: 174 | var x = buffer.readFloat(); 175 | var y = buffer.readFloat(); 176 | inGfx.moveTo(x,y); 177 | 178 | case LINE: 179 | var x = buffer.readFloat(); 180 | var y = buffer.readFloat(); 181 | inGfx.lineTo(x,y); 182 | 183 | case CURVE: 184 | var cx = buffer.readFloat(); 185 | var cy = buffer.readFloat(); 186 | var x = buffer.readFloat(); 187 | var y = buffer.readFloat(); 188 | inGfx.curveTo(cx,cy,x,y); 189 | default: 190 | throw "Unknown gfx buffer format."; 191 | } 192 | } 193 | } 194 | 195 | 196 | override public function size(inWidth:Float,inHeight:Float) 197 | { 198 | buffer.writeByte(SIZE); 199 | buffer.writeFloat(inWidth); 200 | buffer.writeFloat(inHeight); 201 | } 202 | 203 | inline function pushClipped(inVal:Float) 204 | { 205 | buffer.writeByte(inVal<0 ? 0 : inVal>255.0 ? 255 : Std.int(inVal) ); 206 | } 207 | function writeRGB(inVal:Int) 208 | { 209 | buffer.writeByte((inVal>>16) & 0xff); 210 | buffer.writeByte((inVal>>8) & 0xff); 211 | buffer.writeByte((inVal) & 0xff); 212 | } 213 | function readRGB() 214 | { 215 | var r = buffer.readByte(); 216 | var g = buffer.readByte(); 217 | var b = buffer.readByte(); 218 | return (r<<16) | (g<<8) | b; 219 | } 220 | 221 | 222 | 223 | override public function beginGradientFill(grad:Gradient) 224 | { 225 | buffer.writeByte(GRADIENT_FILL); 226 | #if (openfl_legacy || openfl < "3.6.0") 227 | buffer.writeByte(Type.enumIndex(grad.type)); 228 | #else 229 | buffer.writeByte(cast grad.type); 230 | #end 231 | buffer.writeByte(grad.colors.length); 232 | for(i in 0...grad.colors.length) 233 | { 234 | writeRGB(#if neko grad.colors[i]==null ? 0 : #end Std.int(grad.colors[i])); 235 | pushClipped(grad.alphas[i]*255.0); 236 | pushClipped(grad.ratios[i]); 237 | } 238 | buffer.writeFloat(grad.matrix.a); 239 | buffer.writeFloat(grad.matrix.b); 240 | buffer.writeFloat(grad.matrix.c); 241 | buffer.writeFloat(grad.matrix.d); 242 | buffer.writeFloat(grad.matrix.tx); 243 | buffer.writeFloat(grad.matrix.ty); 244 | #if (openfl_legacy || openfl < "3.6.0") 245 | buffer.writeByte(Type.enumIndex(grad.spread)); 246 | buffer.writeByte(Type.enumIndex(grad.interp)); 247 | #else 248 | buffer.writeByte(cast grad.spread); 249 | buffer.writeByte(cast grad.interp); 250 | #end 251 | buffer.writeFloat(grad.focus); 252 | } 253 | 254 | override public function beginFill(color:Int, alpha:Float) 255 | { 256 | buffer.writeByte(BEGIN_FILL); 257 | writeRGB(color); 258 | buffer.writeFloat(alpha); 259 | } 260 | override public function endFill() 261 | { 262 | buffer.writeByte(END_FILL); 263 | } 264 | 265 | override public function lineStyle(style:LineStyle) 266 | { 267 | buffer.writeByte(LINE_STYLE); 268 | buffer.writeFloat(style.thickness); 269 | writeRGB(style.color); 270 | buffer.writeFloat(style.alpha); 271 | buffer.writeByte(style.pixelHinting?1:0); 272 | #if (openfl_legacy || openfl < "3.6.0") 273 | buffer.writeByte(Type.enumIndex(style.scaleMode)); 274 | buffer.writeByte(Type.enumIndex(style.capsStyle)); 275 | buffer.writeByte(Type.enumIndex(style.jointStyle)); 276 | #else 277 | buffer.writeByte(cast style.scaleMode); 278 | buffer.writeByte(cast style.capsStyle); 279 | buffer.writeByte(cast style.jointStyle); 280 | #end 281 | buffer.writeFloat(style.miterLimit); 282 | } 283 | 284 | override public function endLineStyle() 285 | { 286 | buffer.writeByte(END_LINE_STYLE); 287 | } 288 | 289 | override public function moveTo(inX:Float, inY:Float) 290 | { 291 | buffer.writeByte(MOVE); 292 | buffer.writeFloat(inX); 293 | buffer.writeFloat(inY); 294 | } 295 | 296 | override public function lineTo(inX:Float, inY:Float) 297 | { 298 | buffer.writeByte(LINE); 299 | buffer.writeFloat(inX); 300 | buffer.writeFloat(inY); 301 | } 302 | 303 | override public function curveTo(inCX:Float, inCY:Float,inX:Float,inY:Float) 304 | { 305 | buffer.writeByte(CURVE); 306 | buffer.writeFloat(inCX); 307 | buffer.writeFloat(inCY); 308 | buffer.writeFloat(inX); 309 | buffer.writeFloat(inY); 310 | } 311 | } 312 | 313 | 314 | -------------------------------------------------------------------------------- /format/gfx/GfxExtent.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.geom.Rectangle; 5 | 6 | class GfxExtent extends Gfx 7 | { 8 | public var extent : Rectangle; 9 | 10 | public function new() 11 | { 12 | super(); 13 | extent = null; 14 | } 15 | 16 | function addExtent(inX:Float, inY:Float) 17 | { 18 | if (extent==null) 19 | { 20 | extent = new Rectangle(inX,inY,0,0); 21 | return; 22 | } 23 | if (inXextent.right) extent.right = inX; 25 | if (inYextent.bottom) extent.bottom = inY; 27 | } 28 | 29 | 30 | override public function geometryOnly() { return true; } 31 | override public function moveTo(inX:Float, inY:Float) 32 | { 33 | addExtent(inX,inY); 34 | } 35 | override public function lineTo(inX:Float, inY:Float) 36 | { 37 | addExtent(inX,inY); 38 | } 39 | override public function curveTo(inCX:Float, inCY:Float,inX:Float,inY:Float) 40 | { 41 | addExtent(inCX,inCY); 42 | addExtent(inX,inY); 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /format/gfx/GfxGraphics.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.display.GradientType; 4 | import openfl.display.SpreadMethod; 5 | import openfl.display.InterpolationMethod; 6 | import openfl.display.CapsStyle; 7 | import openfl.display.JointStyle; 8 | import openfl.display.LineScaleMode; 9 | import openfl.display.Graphics; 10 | 11 | import openfl.geom.Matrix; 12 | 13 | class GfxGraphics extends Gfx 14 | { 15 | var graphics : Graphics; 16 | 17 | public function new(inGraphics:Graphics) 18 | { 19 | super(); 20 | graphics = inGraphics; 21 | } 22 | 23 | override public function beginGradientFill(grad:Gradient) 24 | { 25 | graphics.beginGradientFill(grad.type,grad.colors,grad.alphas,grad.ratios,grad.matrix,grad.spread,grad.interp,grad.focus); 26 | } 27 | 28 | override public function beginFill(color:Int, alpha:Float) { graphics.beginFill(color,alpha); } 29 | override public function endFill() { graphics.endFill(); } 30 | 31 | override public function lineStyle(style:LineStyle) 32 | { 33 | graphics.lineStyle(style.thickness,style.color,style.alpha,style.pixelHinting,style.scaleMode,style.capsStyle,style.jointStyle,style.miterLimit); 34 | } 35 | override public function endLineStyle() { graphics.lineStyle(); } 36 | 37 | override public function moveTo(inX:Float, inY:Float) { graphics.moveTo(inX,inY); } 38 | override public function lineTo(inX:Float, inY:Float) { graphics.lineTo(inX,inY); } 39 | override public function curveTo(inCX:Float, inCY:Float,inX:Float,inY:Float) 40 | { graphics.curveTo(inCX,inCY,inX,inY); } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /format/gfx/GfxTextFinder.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import format.svg.Text; 4 | 5 | class GfxTextFinder extends Gfx 6 | { 7 | public var text : Text; 8 | 9 | public function new() { super(); } 10 | 11 | override public function geometryOnly() { return true; } 12 | override public function renderText(inText:Text) { if (text==null) text = inText; } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /format/gfx/Gradient.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.display.GradientType; 5 | import openfl.display.SpreadMethod; 6 | import openfl.display.InterpolationMethod; 7 | import openfl.display.CapsStyle; 8 | import openfl.display.JointStyle; 9 | import openfl.display.LineScaleMode; 10 | 11 | class Gradient 12 | { 13 | public function new() 14 | { 15 | type = GradientType.LINEAR; 16 | colors = []; 17 | alphas = []; 18 | ratios = []; 19 | matrix = new Matrix(); 20 | spread = SpreadMethod.PAD; 21 | interp = InterpolationMethod.RGB; 22 | focus = 0.0; 23 | } 24 | 25 | public var type:GradientType; 26 | public var colors:Array; 27 | public var alphas:Array; 28 | public var ratios:Array; 29 | public var matrix: Matrix; 30 | public var spread: SpreadMethod; 31 | public var interp:InterpolationMethod; 32 | public var focus:Float; 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /format/gfx/LineStyle.hx: -------------------------------------------------------------------------------- 1 | package format.gfx; 2 | 3 | import openfl.display.LineScaleMode; 4 | import openfl.display.CapsStyle; 5 | import openfl.display.JointStyle; 6 | 7 | class LineStyle 8 | { 9 | public var thickness:Float; 10 | public var color:Int; 11 | public var alpha:Float; 12 | public var pixelHinting:Bool; 13 | public var scaleMode:LineScaleMode; 14 | public var capsStyle:CapsStyle; 15 | public var jointStyle:JointStyle; 16 | public var miterLimit:Float; 17 | 18 | public function new() 19 | { 20 | thickness = 1.0; 21 | color = 0x000000; 22 | alpha = 1.0; 23 | pixelHinting = false; 24 | scaleMode = LineScaleMode.NORMAL; 25 | capsStyle = CapsStyle.ROUND; 26 | jointStyle = JointStyle.ROUND; 27 | miterLimit = 3.0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /format/svg/FillType.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | enum FillType 4 | { 5 | FillGrad(grad:Grad); 6 | FillSolid(colour:Int); 7 | FillNone; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /format/svg/Grad.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.geom.Rectangle; 5 | 6 | import openfl.display.GradientType; 7 | import openfl.display.SpreadMethod; 8 | import openfl.display.InterpolationMethod; 9 | import openfl.display.CapsStyle; 10 | import openfl.display.JointStyle; 11 | import openfl.display.LineScaleMode; 12 | 13 | class Grad extends /*gm2d.gfx.Gradient*/format.gfx.Gradient 14 | { 15 | public var gradMatrix:Matrix; 16 | public var radius:Float; 17 | public var x1:Float; 18 | public var y1:Float; 19 | public var x2:Float; 20 | public var y2:Float; 21 | 22 | public function new(inType:GradientType) 23 | { 24 | super(); 25 | type = inType; 26 | radius = 0.0; 27 | gradMatrix = new Matrix(); 28 | x1 = 0.0; 29 | y1 = 0.0; 30 | x2 = 0.0; 31 | y2 = 0.0; 32 | } 33 | 34 | public function updateMatrix(inMatrix:Matrix) 35 | { 36 | var dx = x2 - x1; 37 | var dy = y2 - y1; 38 | var theta = Math.atan2(dy,dx); 39 | var len = Math.sqrt(dx*dx+dy*dy); 40 | 41 | var mtx = new Matrix(); 42 | 43 | if (type==GradientType.LINEAR) 44 | { 45 | mtx.createGradientBox(1.0,1.0); 46 | mtx.scale(len,len); 47 | } 48 | else 49 | { 50 | if (radius!=0.0) 51 | focus = len/radius; 52 | 53 | mtx.createGradientBox(1.0,1.0); 54 | mtx.translate(-0.5,-0.5); 55 | mtx.scale(radius*2,radius*2); 56 | } 57 | 58 | mtx.rotate(theta); 59 | mtx.translate(x1,y1); 60 | mtx.concat(gradMatrix); 61 | mtx.concat(inMatrix); 62 | matrix = mtx; 63 | } 64 | 65 | 66 | } 67 | 68 | #if haxe3 69 | typedef GradHash = haxe.ds.StringMap; 70 | #else 71 | typedef GradHash = Hash; 72 | #end -------------------------------------------------------------------------------- /format/svg/Gradient.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.display.GradientType; 5 | import openfl.display.SpreadMethod; 6 | import openfl.display.InterpolationMethod; 7 | import openfl.display.CapsStyle; 8 | import openfl.display.JointStyle; 9 | import openfl.display.LineScaleMode; 10 | 11 | class Gradient 12 | { 13 | public function new() 14 | { 15 | type = GradientType.LINEAR; 16 | colors = []; 17 | alphas = []; 18 | ratios = []; 19 | matrix = new Matrix(); 20 | spread = SpreadMethod.PAD; 21 | interp = InterpolationMethod.RGB; 22 | focus = 0.0; 23 | } 24 | 25 | public var type:GradientType; 26 | public var colors:Array; 27 | public var alphas:Array; 28 | public var ratios:Array; 29 | public var matrix: Matrix; 30 | public var spread: SpreadMethod; 31 | public var interp:InterpolationMethod; 32 | public var focus:Float; 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /format/svg/Group.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | 4 | class Group 5 | { 6 | public function new() 7 | { 8 | name = ""; 9 | children = []; 10 | } 11 | 12 | public function hasGroup(inName:String) { return findGroup(inName)!=null; } 13 | public function findGroup(inName:String) : Group 14 | { 15 | for(child in children) 16 | switch(child) 17 | { 18 | case DisplayGroup(group): 19 | if (group.name==inName) { 20 | return group; 21 | } 22 | var inGroup:Group = group.findGroup(inName); 23 | if(inGroup != null) return inGroup; 24 | default: 25 | } 26 | return null; 27 | } 28 | 29 | 30 | public var name:String; 31 | public var children:Array; 32 | } 33 | 34 | enum DisplayElement 35 | { 36 | DisplayPath(path:Path); 37 | DisplayGroup(group:Group); 38 | DisplayText(text:Text); 39 | } 40 | 41 | typedef DisplayElements = Array; 42 | -------------------------------------------------------------------------------- /format/svg/Path.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.display.GradientType; 5 | import openfl.display.SpreadMethod; 6 | import openfl.display.InterpolationMethod; 7 | import openfl.display.CapsStyle; 8 | import openfl.display.JointStyle; 9 | import openfl.display.LineScaleMode; 10 | 11 | typedef PathSegments = Array; 12 | 13 | class Path 14 | { 15 | public function new() { } 16 | 17 | public var matrix:Matrix; 18 | public var name:String; 19 | public var font_size:Float; 20 | public var fill:FillType; 21 | public var alpha:Float; 22 | public var fill_alpha:Float; 23 | public var stroke_alpha:Float; 24 | public var stroke_colour:Null; 25 | public var stroke_width:Float; 26 | public var stroke_caps:CapsStyle; 27 | public var joint_style:JointStyle; 28 | public var miter_limit:Float; 29 | 30 | public var segments:PathSegments; 31 | } 32 | -------------------------------------------------------------------------------- /format/svg/PathParser.hx: -------------------------------------------------------------------------------- 1 | // This code borrowed from "Xinf", http://xinf.org 2 | // Copyright of original author. 3 | //package xinf.ony; 4 | //import xinf.ony.PathSegment; 5 | 6 | package format.svg; 7 | 8 | import format.svg.PathSegment; 9 | 10 | class PathParser { 11 | var lastMoveX:Float; 12 | var lastMoveY:Float; 13 | var prev:PathSegment; 14 | 15 | 16 | static var sCommandArgs:Array; 17 | 18 | static inline var MOVE = "M".code; 19 | static inline var MOVER = "m".code; 20 | static inline var LINE = "L".code; 21 | static inline var LINER = "l".code; 22 | static inline var HLINE = "H".code; 23 | static inline var HLINER = "h".code; 24 | static inline var VLINE = "V".code; 25 | static inline var VLINER = "v".code; 26 | static inline var CUBIC = "C".code; 27 | static inline var CUBICR = "c".code; 28 | static inline var SCUBIC = "S".code; 29 | static inline var SCUBICR = "s".code; 30 | static inline var QUAD = "Q".code; 31 | static inline var QUADR = "q".code; 32 | static inline var SQUAD = "T".code; 33 | static inline var SQUADR = "t".code; 34 | static inline var ARC = "A".code; 35 | static inline var ARCR = "a".code; 36 | static inline var CLOSE = "Z".code; 37 | static inline var CLOSER = "z".code; 38 | 39 | static inline var UNKNOWN = -1; 40 | static inline var SEPARATOR = -2; 41 | static inline var FLOAT = -3; 42 | static inline var FLOAT_SIGN = -4; 43 | static inline var FLOAT_DOT = -5; 44 | static inline var FLOAT_EXP = -6; 45 | 46 | 47 | 48 | public function new() { 49 | if (sCommandArgs==null) 50 | { 51 | sCommandArgs = []; 52 | for(i in 0...128) 53 | sCommandArgs[i] = commandArgs(i); 54 | } 55 | } 56 | 57 | public function parse( pathToParse:String, inConvertCubics:Bool ) :Array { 58 | lastMoveX = lastMoveY = 0; 59 | var pos=0; 60 | var args = new Array(); 61 | var segments = new Array(); 62 | var current_command_pos = 0; 63 | var current_command = -1; 64 | var current_args = -1; 65 | 66 | prev = null; 67 | 68 | var len = pathToParse.length; 69 | var finished = false; 70 | while( pos<=len ) 71 | { 72 | var code = pos==len ? 32 : pathToParse.charCodeAt(pos); 73 | var command = (code>0 && code<128) ? sCommandArgs[code] : UNKNOWN; 74 | 75 | if (command==UNKNOWN) 76 | throw("failed parsing path near '"+pathToParse.substr(pos)+"'"); 77 | 78 | if (command==SEPARATOR) 79 | { 80 | pos++; 81 | } 82 | else if( command<=FLOAT ) 83 | { 84 | var end = pos+1; 85 | var e_pos = -1; 86 | var seen_dot = command == FLOAT_DOT; 87 | if (command==FLOAT_EXP) 88 | { 89 | e_pos = 0; 90 | seen_dot = true; 91 | } 92 | while(end127 ? UNKNOWN :sCommandArgs[ch]; 96 | if (code>FLOAT ) 97 | break; 98 | if (code==FLOAT_DOT) 99 | { 100 | if (seen_dot) { 101 | // a second dot indicates the start of a new float 102 | break; 103 | } 104 | seen_dot = true; 105 | } 106 | if (e_pos>=0) 107 | { 108 | if (code==FLOAT_SIGN) 109 | { 110 | if (e_pos!=0) 111 | break; 112 | } 113 | else if (code!=FLOAT) 114 | break; 115 | e_pos++; 116 | } 117 | else if (code==FLOAT_EXP) 118 | { 119 | if (e_pos>=0) 120 | break; 121 | e_pos = 0; 122 | seen_dot = true; 123 | } 124 | else if (code==FLOAT_SIGN) 125 | break; 126 | end++; 127 | } 128 | if (current_command<0) 129 | { 130 | //throw "Too many numbers near '" + 131 | //pathToParse.substr(current_command_pos) + "'"; 132 | } 133 | else 134 | { 135 | var f = Std.parseFloat(pathToParse.substr(pos,end-pos)); 136 | args.push(f); 137 | } 138 | pos = end; 139 | } 140 | else 141 | { 142 | current_command = code; 143 | current_args = command; 144 | finished = false; 145 | current_command_pos = pos; 146 | args = []; 147 | pos++; 148 | } 149 | 150 | var px:Float = 0.0; 151 | var py:Float = 0.0; 152 | if (current_command>=0) 153 | { 154 | if (current_args==args.length) 155 | { 156 | if (inConvertCubics && prev!=null) 157 | { 158 | px = prev.prevX(); 159 | py = prev.prevY(); 160 | } 161 | prev = createCommand( current_command, args ); 162 | if (prev==null) 163 | throw "Unknown command " + String.fromCharCode(current_command) + 164 | " near '" + pathToParse.substr(current_command_pos) + "'"; 165 | if (inConvertCubics && prev.getType()==PathSegment.CUBIC) 166 | { 167 | var cubic:CubicSegment = cast prev; 168 | var quads = cubic.toQuadratics(px,py); 169 | for(q in quads) 170 | segments.push(q); 171 | } 172 | else 173 | segments.push(prev); 174 | 175 | finished = true; 176 | if (current_args==0) 177 | { 178 | current_args = -1; 179 | current_command = -1; 180 | } 181 | else if (current_command==MOVE) 182 | current_command = LINE; 183 | else if (current_command==MOVER) 184 | current_command = LINER; 185 | 186 | current_command_pos = pos; 187 | args = []; 188 | } 189 | } 190 | } 191 | 192 | if (current_command>=0 && !finished) 193 | { 194 | throw "Unfinished command (" + args.length + "/" + current_args + 195 | ") near '" + pathToParse.substr(current_command_pos) + "'"; 196 | } 197 | 198 | return segments; 199 | } 200 | 201 | function commandArgs( inCode:Int ) : Int 202 | { 203 | if (inCode==10) return SEPARATOR; 204 | 205 | var str = String.fromCharCode(inCode).toUpperCase(); 206 | if (str>="0" && str<="9") 207 | return FLOAT; 208 | 209 | switch(str) 210 | { 211 | case "Z": return 0; 212 | case "H","V": return 1; 213 | case "M","L","T": return 2; 214 | case "S","Q": return 4; 215 | case "C": return 6; 216 | case "A": return 7; 217 | case "\t","\n"," ","\r","," : return SEPARATOR; 218 | case "-" : return FLOAT_SIGN; 219 | case "+" : return FLOAT_SIGN; 220 | case "E","e" : return FLOAT_EXP; 221 | case "." : return FLOAT_DOT; 222 | } 223 | 224 | return UNKNOWN; 225 | } 226 | 227 | function prevX():Float { return (prev!=null) ? prev.prevX() : 0; } 228 | function prevY():Float { return (prev!=null) ? prev.prevY() : 0; } 229 | function prevCX():Float { return (prev!=null) ? prev.prevCX() : 0; } 230 | function prevCY():Float { return (prev!=null) ? prev.prevCY() : 0; } 231 | 232 | function createCommand( code:Int , a:Array ) : PathSegment 233 | { 234 | switch(code) 235 | { 236 | case MOVE: 237 | lastMoveX = a[0]; 238 | lastMoveY = a[1]; 239 | return new MoveSegment( lastMoveX, lastMoveY); 240 | case MOVER: 241 | lastMoveX = a[0]+prevX(); 242 | lastMoveY = a[1]+prevY(); 243 | return new MoveSegment(lastMoveX, lastMoveY); 244 | case LINE: return new DrawSegment( a[0], a[1] ); 245 | case LINER: return new DrawSegment( a[0]+prevX(), a[1]+prevY() ); 246 | case HLINE: return new DrawSegment( a[0], prevY() ); 247 | case HLINER: return new DrawSegment( a[0]+prevX(), prevY()); 248 | case VLINE: return new DrawSegment( prevX(), a[0] ); 249 | case VLINER: return new DrawSegment( prevX(), a[0]+prevY()); 250 | case CUBIC: 251 | return new CubicSegment( a[0], a[1], a[2], a[3], a[4], a[5] ); 252 | case CUBICR: 253 | var rx = prevX(); 254 | var ry = prevY(); 255 | return new CubicSegment( a[0]+rx, a[1]+ry, a[2]+rx, a[3]+ry, a[4]+rx, a[5]+ry ); 256 | case SCUBIC: 257 | var rx = prevX(); 258 | var ry = prevY(); 259 | return new CubicSegment( rx*2-prevCX(), ry*2-prevCY(),a[0], a[1], a[2], a[3] ); 260 | case SCUBICR: 261 | var rx = prevX(); 262 | var ry = prevY(); 263 | return new CubicSegment( rx*2-prevCX(), ry*2-prevCY(),a[0]+rx, a[1]+ry, a[2]+rx, a[3]+ry ); 264 | case QUAD: return new QuadraticSegment( a[0], a[1], a[2], a[3] ); 265 | case QUADR: 266 | var rx = prevX(); 267 | var ry = prevY(); 268 | return new QuadraticSegment( a[0]+rx, a[1]+ry, a[2]+rx, a[3]+ry ); 269 | case SQUAD: 270 | var rx = prevX(); 271 | var ry = prevY(); 272 | return new QuadraticSegment( rx*2-prevCX(), rx*2-prevCY(),a[0], a[1] ); 273 | case SQUADR: 274 | var rx = prevX(); 275 | var ry = prevY(); 276 | return new QuadraticSegment( rx*2-prevCX(), ry*2-prevCY(),a[0]+rx, a[1]+ry ); 277 | case ARC: 278 | return new ArcSegment(prevX(), prevY(), a[0], a[1], a[2], a[3]!=0., a[4]!=0., a[5], a[6] ); 279 | case ARCR: 280 | var rx = prevX(); 281 | var ry = prevY(); 282 | return new ArcSegment( rx,ry, a[0], a[1], a[2], a[3]!=0., a[4]!=0., a[5]+rx, a[6]+ry ); 283 | case CLOSE: 284 | return new DrawSegment(lastMoveX, lastMoveY); 285 | 286 | case CLOSER: 287 | return new DrawSegment(lastMoveX, lastMoveY); 288 | } 289 | 290 | return null; 291 | } 292 | } 293 | 294 | -------------------------------------------------------------------------------- /format/svg/PathSegment.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | import openfl.geom.Rectangle; 3 | import openfl.geom.Matrix; 4 | import openfl.geom.Point; 5 | import openfl.display.Graphics; 6 | import format.gfx.Gfx; 7 | 8 | class PathSegment 9 | { 10 | public static inline var MOVE = 1; 11 | public static inline var DRAW = 2; 12 | public static inline var CURVE = 3; 13 | public static inline var CUBIC = 4; 14 | public static inline var ARC = 5; 15 | 16 | public var x:Float; 17 | public var y:Float; 18 | 19 | public function new(inX:Float,inY:Float) 20 | { 21 | x = inX; 22 | y = inY; 23 | } 24 | public function getType() : Int { return 0; } 25 | 26 | public function prevX() { return x; } 27 | public function prevY() { return y; } 28 | public function prevCX() { return x; } 29 | public function prevCY() { return y; } 30 | 31 | public function toGfx(inGfx:Gfx,ioContext:RenderContext) 32 | { 33 | ioContext.setLast(x,y); 34 | ioContext.firstX = ioContext.lastX; 35 | ioContext.firstY = ioContext.lastY; 36 | inGfx.moveTo(ioContext.lastX, ioContext.lastY); 37 | } 38 | 39 | } 40 | 41 | class MoveSegment extends PathSegment 42 | { 43 | public function new(inX:Float,inY:Float) { super(inX,inY); } 44 | override public function getType() : Int { return PathSegment.MOVE; } 45 | } 46 | 47 | 48 | class DrawSegment extends PathSegment 49 | { 50 | public function new(inX:Float, inY:Float) { super(inX,inY); } 51 | override public function toGfx(inGfx:Gfx,ioContext:RenderContext) 52 | { 53 | ioContext.setLast(x,y); 54 | inGfx.lineTo(ioContext.lastX,ioContext.lastY); 55 | } 56 | 57 | override public function getType() : Int { return PathSegment.DRAW; } 58 | } 59 | 60 | class QuadraticSegment extends PathSegment 61 | { 62 | public var cx:Float; 63 | public var cy:Float; 64 | 65 | public function new(inCX:Float, inCY:Float, inX:Float, inY:Float) 66 | { 67 | super(inX,inY); 68 | cx = inCX; 69 | cy = inCY; 70 | } 71 | 72 | override public function prevCX() { return cx; } 73 | override public function prevCY() { return cy; } 74 | 75 | override public function toGfx(inGfx:Gfx,ioContext:RenderContext) 76 | { 77 | ioContext.setLast(x,y); 78 | inGfx.curveTo(ioContext.transX(cx,cy) , ioContext.transY(cx,cy), 79 | ioContext.lastX , ioContext.lastY ); 80 | } 81 | 82 | override public function getType() : Int { return PathSegment.CURVE; } 83 | } 84 | 85 | class CubicSegment extends PathSegment 86 | { 87 | public var cx1:Float; 88 | public var cy1:Float; 89 | public var cx2:Float; 90 | public var cy2:Float; 91 | 92 | public function new(inCX1:Float, inCY1:Float, inCX2:Float, inCY2:Float, inX:Float, inY:Float ) 93 | { 94 | super(inX,inY); 95 | cx1 = inCX1; 96 | cy1 = inCY1; 97 | cx2 = inCX2; 98 | cy2 = inCY2; 99 | } 100 | 101 | override public function prevCX() { return cx2; } 102 | override public function prevCY() { return cy2; } 103 | 104 | function Interp(a:Float, b:Float, frac:Float) 105 | { 106 | return a + (b-a)*frac; 107 | } 108 | 109 | override public function toGfx(inGfx:Gfx,ioContext:RenderContext) 110 | { 111 | // Transformed endpoints/controlpoints 112 | var tx0 = ioContext.lastX; 113 | var ty0 = ioContext.lastY; 114 | 115 | var tx1 = ioContext.transX(cx1,cy1); 116 | var ty1 = ioContext.transY(cx1,cy1); 117 | var tx2 = ioContext.transX(cx2,cy2); 118 | var ty2 = ioContext.transY(cx2,cy2); 119 | 120 | ioContext.setLast(x,y); 121 | var tx3 = ioContext.lastX; 122 | var ty3 = ioContext.lastY; 123 | 124 | // from http://www.timotheegroleau.com/Flash/articles/cubic_bezier/bezier_lib.as 125 | 126 | var pa_x = Interp(tx0,tx1,0.75); 127 | var pa_y = Interp(ty0,ty1,0.75); 128 | var pb_x = Interp(tx3,tx2,0.75); 129 | var pb_y = Interp(ty3,ty2,0.75); 130 | 131 | // get 1/16 of the [P3, P0] segment 132 | var dx = (tx3 - tx0)/16; 133 | var dy = (ty3 - ty0)/16; 134 | 135 | // calculates control point 1 136 | var pcx_1 = Interp(tx0, tx1, 3/8); 137 | var pcy_1 = Interp(ty0, ty1, 3/8); 138 | 139 | // calculates control point 2 140 | var pcx_2 = Interp(pa_x, pb_x, 3/8) - dx; 141 | var pcy_2 = Interp(pa_y, pb_y, 3/8) - dy; 142 | 143 | // calculates control point 3 144 | var pcx_3 = Interp(pb_x, pa_x, 3/8) + dx; 145 | var pcy_3 = Interp(pb_y, pa_y, 3/8) + dy; 146 | 147 | // calculates control point 4 148 | var pcx_4 = Interp(tx3, tx2, 3/8); 149 | var pcy_4 = Interp(ty3, ty2, 3/8); 150 | 151 | // calculates the 3 anchor points 152 | var pax_1 = (pcx_1+pcx_2) * 0.5; 153 | var pay_1 = (pcy_1+pcy_2) * 0.5; 154 | 155 | var pax_2 = (pa_x+pb_x) * 0.5; 156 | var pay_2 = (pa_y+pb_y) * 0.5; 157 | 158 | var pax_3 = (pcx_3+pcx_4) * 0.5; 159 | var pay_3 = (pcy_3+pcy_4) * 0.5; 160 | 161 | // draw the four quadratic subsegments 162 | inGfx.curveTo(pcx_1, pcy_1, pax_1, pay_1); 163 | inGfx.curveTo(pcx_2, pcy_2, pax_2, pay_2); 164 | inGfx.curveTo(pcx_3, pcy_3, pax_3, pay_3); 165 | inGfx.curveTo(pcx_4, pcy_4, tx3, ty3); 166 | 167 | } 168 | 169 | public function toQuadratics(tx0:Float,ty0:Float) : Array 170 | { 171 | var result = new Array(); 172 | // from http://www.timotheegroleau.com/Flash/articles/cubic_bezier/bezier_lib.as 173 | 174 | var pa_x = Interp(tx0,cx1,0.75); 175 | var pa_y = Interp(ty0,cy1,0.75); 176 | var pb_x = Interp(x,cx2,0.75); 177 | var pb_y = Interp(y,cy2,0.75); 178 | 179 | // get 1/16 of the [P3, P0] segment 180 | var dx = (x - tx0)/16; 181 | var dy = (y - ty0)/16; 182 | 183 | // calculates control point 1 184 | var pcx_1 = Interp(tx0, cx1, 3/8); 185 | var pcy_1 = Interp(ty0, cy1, 3/8); 186 | 187 | // calculates control point 2 188 | var pcx_2 = Interp(pa_x, pb_x, 3/8) - dx; 189 | var pcy_2 = Interp(pa_y, pb_y, 3/8) - dy; 190 | 191 | // calculates control point 3 192 | var pcx_3 = Interp(pb_x, pa_x, 3/8) + dx; 193 | var pcy_3 = Interp(pb_y, pa_y, 3/8) + dy; 194 | 195 | // calculates control point 4 196 | var pcx_4 = Interp(x, cx2, 3/8); 197 | var pcy_4 = Interp(y, cy2, 3/8); 198 | 199 | // calculates the 3 anchor points 200 | var pax_1 = (pcx_1+pcx_2) * 0.5; 201 | var pay_1 = (pcy_1+pcy_2) * 0.5; 202 | 203 | var pax_2 = (pa_x+pb_x) * 0.5; 204 | var pay_2 = (pa_y+pb_y) * 0.5; 205 | 206 | var pax_3 = (pcx_3+pcx_4) * 0.5; 207 | var pay_3 = (pcy_3+pcy_4) * 0.5; 208 | 209 | // draw the four quadratic subsegments 210 | result.push(new QuadraticSegment(pcx_1, pcy_1, pax_1, pay_1)); 211 | result.push(new QuadraticSegment(pcx_2, pcy_2, pax_2, pay_2)); 212 | result.push(new QuadraticSegment(pcx_3, pcy_3, pax_3, pay_3)); 213 | result.push(new QuadraticSegment(pcx_4, pcy_4, x, y)); 214 | return result; 215 | } 216 | 217 | 218 | override public function getType() : Int { return PathSegment.CUBIC; } 219 | } 220 | 221 | class ArcSegment extends PathSegment 222 | { 223 | var x1:Float; 224 | var y1:Float; 225 | var rx:Float; 226 | var ry:Float; 227 | var phi:Float; 228 | var fA:Bool; 229 | var fS:Bool; 230 | 231 | public function new( inX1:Float, inY1:Float, inRX:Float, inRY:Float, inRotation:Float, 232 | inLargeArc:Bool, inSweep:Bool, x:Float, y:Float) 233 | { 234 | x1 = inX1; 235 | y1 = inY1; 236 | super(x,y); 237 | rx = inRX; 238 | ry = inRY; 239 | phi = inRotation; 240 | fA = inLargeArc; 241 | fS = inSweep; 242 | } 243 | 244 | override public function toGfx(inGfx:Gfx,ioContext:RenderContext) 245 | { 246 | if (x1==x && y1==y) 247 | return; 248 | ioContext.setLast(x,y); 249 | if (rx==0 || ry==0) 250 | { 251 | inGfx.lineTo(ioContext.lastX, ioContext.lastY); 252 | return; 253 | } 254 | if (rx<0) rx = -rx; 255 | if (ry<0) ry = -ry; 256 | 257 | // See: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes 258 | var p = phi*Math.PI/180.0; 259 | var cos = Math.cos(p); 260 | var sin = Math.sin(p); 261 | 262 | // Step 1, compute x', y' 263 | var dx = (x1-x)*0.5; 264 | var dy = (y1-y)*0.5; 265 | var x1_ = cos*dx + sin*dy; 266 | var y1_ = -sin*dx + cos*dy; 267 | 268 | // Step 2, compute cx', cy' 269 | var rx2 = rx*rx; 270 | var ry2 = ry*ry; 271 | var x1_2 = x1_*x1_; 272 | var y1_2 = y1_*y1_; 273 | var cr = x1_2/rx2 + y1_2/ry2; 274 | if (cr > 1.0) { 275 | var s = Math.sqrt(cr); 276 | rx = s * rx; 277 | ry = s * ry; 278 | rx2 = rx * rx; 279 | ry2 = ry * ry; 280 | } 281 | var s = (rx2*ry2 - rx2*y1_2 - ry2*x1_2) / 282 | (rx2*y1_2 + ry2*x1_2 ); 283 | if (s<0) 284 | s=0; 285 | else if (fA==fS) 286 | s = -Math.sqrt(s); 287 | else 288 | s = Math.sqrt(s); 289 | 290 | var cx_ = s*rx*y1_/ry; 291 | var cy_ = -s*ry*x1_/rx; 292 | 293 | // Step 3, compute cx,cy from cx',cy' 294 | // Something not quite right here. 295 | 296 | var xm = (x1+x)*0.5; 297 | var ym = (y1+y)*0.5; 298 | 299 | var cx = cos*cx_ - sin*cy_ + xm; 300 | var cy = sin*cx_ + cos*cy_ + ym; 301 | 302 | var theta = Math.atan2( (y1_-cy_)/ry, (x1_-cx_)/rx ); 303 | var dtheta = Math.atan2( (-y1_-cy_)/ry, (-x1_-cx_)/rx ) - theta; 304 | 305 | if (fS && dtheta<0) 306 | dtheta+=2.0*Math.PI; 307 | else if (!fS && dtheta>0) 308 | dtheta-=2.0*Math.PI; 309 | 310 | drawCurves(inGfx, ioContext, cx, cy, theta, dtheta, rx, ry, phi); 311 | } 312 | 313 | // Copyright (c) 2008 The Degrafa Team 314 | // convertToCurves() in com.degrafa.geometry.utilities.ArcUtils.as 315 | // SPDX-License-Identifier: MIT 316 | private inline function drawCurves(inGfx:Gfx, ioContext:RenderContext, x:Float, y:Float, startAngle:Float, arcAngle:Float, xRadius:Float, yRadius:Float, xAxisRotation:Float = 0):Void 317 | { 318 | // Circumvent drawing more than is needed 319 | if (Math.abs(arcAngle) > Math.PI) 320 | { 321 | arcAngle = Math.PI; 322 | } 323 | 324 | // Draw in a maximum of 45 degree segments. First we calculate how many 325 | // segments are needed for our arc. 326 | var segs:Int = Math.ceil(Math.abs(arcAngle) / (Math.PI / 4.0)); 327 | 328 | // Now calculate the sweep of each segment 329 | var segAngle:Float = arcAngle / segs; 330 | 331 | var currentAngle:Float = startAngle; 332 | 333 | // Draw as 45 degree segments 334 | if (segs > 0) 335 | { 336 | var beta:Float = (xAxisRotation) * Math.PI / 180.0; 337 | var sinbeta:Float = Math.sin(beta); 338 | var cosbeta:Float = Math.cos(beta); 339 | 340 | var m:Matrix = ioContext.matrix; 341 | var cx:Float; 342 | var cy:Float; 343 | var x1:Float; 344 | var y1:Float; 345 | 346 | // Loop for drawing arc segments 347 | for (i in 0...segs) 348 | { 349 | currentAngle += segAngle; 350 | 351 | var sinangle:Float = Math.sin(currentAngle-(segAngle/2)); 352 | var cosangle:Float = Math.cos(currentAngle-(segAngle/2)); 353 | 354 | var div:Float = Math.cos(segAngle/2); 355 | cx = x + (xRadius * cosangle * cosbeta - yRadius * sinangle * sinbeta) / div; //Why divide by Math.cos(segAngle/2)? 356 | cy = y + (xRadius * cosangle * sinbeta + yRadius * sinangle * cosbeta) / div; //Why divide by Math.cos(segAngle/2)? 357 | 358 | if (m != null) 359 | { 360 | cx = cx * m.a + cy * m.c + m.tx; 361 | cy = cx * m.b + cy * m.d + m.ty; 362 | } 363 | 364 | sinangle = Math.sin(currentAngle); 365 | cosangle = Math.cos(currentAngle); 366 | 367 | x1 = x + (xRadius * cosangle * cosbeta - yRadius * sinangle * sinbeta); 368 | y1 = y + (xRadius * cosangle * sinbeta + yRadius * sinangle * cosbeta); 369 | 370 | if (m != null) 371 | { 372 | x1 = x1 * m.a + y1 * m.c + m.tx; 373 | y1 = x1 * m.b + y1 * m.d + m.ty; 374 | } 375 | 376 | inGfx.curveTo(cx, cy, x1, y1); 377 | } 378 | } 379 | } 380 | 381 | override public function getType() : Int { return PathSegment.ARC; } 382 | } 383 | -------------------------------------------------------------------------------- /format/svg/RenderContext.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.geom.Rectangle; 5 | 6 | class RenderContext 7 | { 8 | public function new(inMatrix:Matrix,?inRect:Rectangle,?inW:Float, ?inH:Float) 9 | { 10 | matrix = inMatrix; 11 | rect = inRect; 12 | rectW = inW!=null ? inW : inRect!=null? inRect.width : 1; 13 | rectH = inH!=null ? inH : inRect!=null? inRect.height : 1; 14 | firstX = 0; 15 | firstY = 0; 16 | lastX = 0; 17 | lastY = 0; 18 | } 19 | public function transX(inX:Float, inY:Float) 20 | { 21 | if (rect!=null && inX>rect.x) 22 | { 23 | if (inX>rect.right) 24 | inX += rectW - rect.width; 25 | else 26 | inX = rect.x + rectW * (inX-rect.x)/rect.width; 27 | } 28 | return inX*matrix.a + inY*matrix.c + matrix.tx; 29 | } 30 | public function transY(inX:Float, inY:Float) 31 | { 32 | if (rect!=null && inY>rect.y) 33 | { 34 | if (inY>rect.right) 35 | inY += rectH - rect.height; 36 | else 37 | inY = rect.y + rectH * (inY-rect.y)/rect.height; 38 | } 39 | return inX*matrix.b + inY*matrix.d + matrix.ty; 40 | } 41 | 42 | 43 | public function setLast(inX:Float, inY:Float) 44 | { 45 | lastX = transX(inX,inY); 46 | lastY = transY(inX,inY); 47 | } 48 | public var matrix:Matrix; 49 | public var rect:Rectangle; 50 | public var rectW:Float; 51 | public var rectH:Float; 52 | 53 | public var firstX:Float; 54 | public var firstY:Float; 55 | public var lastX:Float; 56 | public var lastY:Float; 57 | } 58 | -------------------------------------------------------------------------------- /format/svg/SVG2Gfx.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import Xml; 4 | 5 | // To help old code... 6 | 7 | class SVG2Gfx 8 | { 9 | var renderer : SVGRenderer; 10 | 11 | public function new (inXml:Xml) 12 | { 13 | renderer = new SVGRenderer(new SVGData(inXml)); 14 | } 15 | 16 | public function CreateShape() 17 | { 18 | return renderer.createShape(); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /format/svg/SVGData.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import openfl.geom.Matrix; 4 | import openfl.geom.Rectangle; 5 | import openfl.display.GradientType; 6 | import openfl.display.Graphics; 7 | import openfl.display.SpreadMethod; 8 | import openfl.display.CapsStyle; 9 | import openfl.display.JointStyle; 10 | import format.svg.Grad; 11 | import format.svg.Group; 12 | import format.svg.FillType; 13 | import format.svg.PathParser; 14 | import format.svg.PathSegment; 15 | import format.svg.Path; 16 | import format.svg.SVGRenderer; 17 | import format.svg.Text; 18 | 19 | #if haxe3 20 | import haxe.ds.StringMap; 21 | #else 22 | typedef StringMap = Hash; 23 | #end 24 | 25 | 26 | class SVGData extends Group { 27 | 28 | 29 | private static inline var SIN45:Float = 0.70710678118654752440084436210485; 30 | private static inline var TAN22:Float = 0.4142135623730950488016887242097; 31 | private static var mStyleSplit = ~/;/g; 32 | private static var mStyleValue = ~/\s*(.*)\s*:\s*(.*)\s*/; 33 | private static var mTranslateMatch = ~/translate\((.*)[, ](.*)\)/; 34 | private static var mScaleMatch = ~/scale\((.*)\)/; 35 | private static var mMatrixMatch = ~/matrix\((.*?)[, ]+(.*?)[, ]+(.*?)[, ]+(.*?)[, ]+(.*?)[, ]+(.*?)\)/; 36 | private static var mRotationMatch = ~/rotate\(([0-9\.]+)(\s+([0-9\.]+)\s*[, ]\s*([0-9\.]+))?\)/; 37 | private static var mURLMatch = ~/url\(('|"?)#(.*)\1\)/; 38 | private static var mRGBMatch = ~/rgb\s*\(\s*(\d+)\s*(%)?\s*,\s*(\d+)\s*(%)?\s*,\s*(\d+)\s*(%)?\s*\)/; 39 | private static var defaultFill = FillSolid(0x000000); 40 | 41 | public var height (default, null):Float; 42 | public var width (default, null):Float; 43 | 44 | private var mConvertCubics:Bool; 45 | private var mGrads:GradHash; 46 | private var mPathParser:PathParser; 47 | 48 | 49 | public function new (inXML:Xml, inConvertCubics:Bool = false) { 50 | 51 | super(); 52 | 53 | var svg = inXML.firstElement(); 54 | 55 | if (svg == null || (svg.nodeName != "svg" && svg.nodeName != "svg:svg")) 56 | throw "Not an SVG file (" + (svg==null ? "null" : svg.nodeName) + ")"; 57 | 58 | mGrads = new GradHash (); 59 | mPathParser = new PathParser (); 60 | mConvertCubics = inConvertCubics; 61 | 62 | width = getFloatStyle ("width", svg, null, 0.0); 63 | height = getFloatStyle ("height", svg, null, 0.0); 64 | 65 | if (width == 0) 66 | width = height; 67 | else if (height == 0) 68 | height = width; 69 | 70 | var viewBox:Rectangle; 71 | 72 | if (svg.exists("viewBox")) 73 | { 74 | var vbox = svg.get("viewBox"); 75 | var params = vbox.indexOf(",") != -1 ? vbox.split(",") : vbox.split(" "); 76 | viewBox = new Rectangle( trimToFloat(params[0]), trimToFloat(params[1]), trimToFloat(params[2]), trimToFloat(params[3]) ); 77 | 78 | if (width == 0 && height == 0) 79 | { 80 | width = viewBox.width; 81 | height = viewBox.height; 82 | } 83 | } 84 | else 85 | { 86 | if (width == 0 && height == 0) 87 | width = height = 400; 88 | viewBox = new Rectangle(0, 0, width, height); 89 | } 90 | 91 | loadGroup(this, svg, new Matrix (1, 0, 0, 1, -viewBox.x, -viewBox.y), null); 92 | 93 | } 94 | 95 | 96 | inline function trimToFloat (value:String) { 97 | 98 | return Std.parseFloat( StringTools.trim(value) ); 99 | 100 | } 101 | 102 | 103 | // Applies the transformation specified in inTrans to ioMatrix. Returns the new scale 104 | // value after applying the transformation. 105 | private function applyTransform (ioMatrix:Matrix, inTrans:String):Float { 106 | 107 | var scale = 1.0; 108 | 109 | if (mTranslateMatch.match(inTrans)) 110 | { 111 | // TODO: Pre-translate 112 | 113 | ioMatrix.translate (Std.parseFloat (mTranslateMatch.matched (1)), Std.parseFloat (mTranslateMatch.matched (2))); 114 | 115 | } else if (mScaleMatch.match (inTrans)) { 116 | 117 | // TODO: Pre-scale 118 | var s = Std.parseFloat (mScaleMatch.matched (1)); 119 | ioMatrix.scale (s, s); 120 | scale = s; 121 | 122 | } else if (mMatrixMatch.match (inTrans)) { 123 | 124 | var m = new Matrix ( 125 | Std.parseFloat (mMatrixMatch.matched (1)), 126 | Std.parseFloat (mMatrixMatch.matched (2)), 127 | Std.parseFloat (mMatrixMatch.matched (3)), 128 | Std.parseFloat (mMatrixMatch.matched (4)), 129 | Std.parseFloat (mMatrixMatch.matched (5)), 130 | Std.parseFloat (mMatrixMatch.matched (6)) 131 | ); 132 | 133 | m.concat (ioMatrix); 134 | 135 | ioMatrix.a = m.a; 136 | ioMatrix.b = m.b; 137 | ioMatrix.c = m.c; 138 | ioMatrix.d = m.d; 139 | ioMatrix.tx = m.tx; 140 | ioMatrix.ty = m.ty; 141 | 142 | scale = Math.sqrt (ioMatrix.a * ioMatrix.a + ioMatrix.c * ioMatrix.c); 143 | } else if (mRotationMatch.match (inTrans)) { 144 | 145 | var degrees = Std.parseFloat (mRotationMatch.matched (1)); 146 | 147 | var rotationX = Std.parseFloat (mRotationMatch.matched (2)); 148 | if (Math.isNaN(rotationX)) { 149 | rotationX = 0; 150 | } 151 | var rotationY = Std.parseFloat (mRotationMatch.matched (3)); 152 | if (Math.isNaN(rotationY)) { 153 | rotationY = 0; 154 | } 155 | 156 | var radians = degrees * Math.PI / 180; 157 | 158 | ioMatrix.translate (-rotationX, -rotationY); 159 | ioMatrix.rotate(radians); 160 | ioMatrix.translate (rotationX, rotationY); 161 | } else { 162 | 163 | trace("Warning, unknown transform:" + inTrans); 164 | 165 | } 166 | 167 | return scale; 168 | 169 | } 170 | 171 | 172 | private function dumpGroup (g:Group, indent:String) { 173 | 174 | trace (indent + "Group:" + g.name); 175 | indent += " "; 176 | 177 | for (e in g.children) { 178 | 179 | switch (e) { 180 | 181 | case DisplayPath (path): trace (indent + "Path" + " " + path.matrix); 182 | case DisplayGroup (group): dumpGroup (group, indent+" "); 183 | case DisplayText (text): trace (indent + "Text " + text.text); 184 | 185 | } 186 | 187 | } 188 | 189 | } 190 | 191 | 192 | private function getColorStyle (inKey:String, inNode:Xml, inStyles:StringMap , inDefault:Int) { 193 | 194 | var s = getStyle (inKey, inNode, inStyles, ""); 195 | 196 | if (s == "") { 197 | 198 | return inDefault; 199 | 200 | } 201 | 202 | if (s.charAt (0) == '#') { 203 | 204 | return parseHex(s.substr(1)); 205 | 206 | } 207 | 208 | if (mRGBMatch.match (s)) { 209 | 210 | return parseRGBMatch(mRGBMatch); 211 | 212 | } 213 | 214 | return Std.parseInt (s); 215 | 216 | } 217 | 218 | 219 | private function getFillStyle (inKey:String, inNode:Xml, inStyles:StringMap) { 220 | 221 | var s = getStyle (inKey, inNode, inStyles, ""); 222 | 223 | if (s == "") { 224 | 225 | return defaultFill; 226 | 227 | } 228 | 229 | if (s.charAt (0) == '#') { 230 | 231 | return FillSolid (parseHex(s.substr(1))); 232 | 233 | } 234 | 235 | if (mRGBMatch.match (s)) { 236 | 237 | return FillSolid ( parseRGBMatch(mRGBMatch) ); 238 | 239 | } 240 | 241 | if (s == "none") { 242 | 243 | return FillNone; 244 | 245 | } 246 | 247 | if (mURLMatch.match (s)) { 248 | 249 | var url = mURLMatch.matched (2); 250 | 251 | if (mGrads.exists (url)) { 252 | 253 | return FillGrad(mGrads.get(url)); 254 | 255 | } 256 | 257 | throw ("Unknown url:" + url); 258 | 259 | } 260 | 261 | throw ("Unknown fill string:" + s); 262 | 263 | return FillNone; 264 | 265 | } 266 | 267 | 268 | private function getFloat (inXML:Xml, inName:String, inDef:Float = 0.0):Float { 269 | 270 | if (inXML.exists (inName)) 271 | return Std.parseFloat (inXML.get (inName)); 272 | 273 | return inDef; 274 | 275 | } 276 | 277 | 278 | private function getFloatStyle (inKey:String, inNode:Xml, inStyles:StringMap, inDefault:Float) { 279 | 280 | var s = getStyle (inKey, inNode, inStyles, ""); 281 | 282 | if (s == "") { 283 | 284 | return inDefault; 285 | 286 | } 287 | 288 | return Std.parseFloat (s); 289 | 290 | } 291 | 292 | 293 | private function getStyleAndConvert(inKey:String, inNode:Xml, inStyles:StringMap, inDefault:T, inConvert:StringMap) : T { 294 | 295 | var s = getStyle (inKey, inNode, inStyles, ""); 296 | 297 | if (s == "" || !inConvert.exists(s)) { 298 | 299 | return inDefault; 300 | 301 | } 302 | 303 | return inConvert.get(s); 304 | 305 | } 306 | 307 | 308 | private function getStrokeStyle (inKey:String, inNode:Xml, inStyles:StringMap , inDefault:Null) { 309 | 310 | var s = getStyle (inKey, inNode, inStyles, ""); 311 | 312 | if (s == "") { 313 | 314 | return inDefault; 315 | 316 | } 317 | 318 | 319 | if (mRGBMatch.match (s)) { 320 | 321 | return parseRGBMatch(mRGBMatch); 322 | 323 | } 324 | 325 | if (s == "none") { 326 | 327 | return null; 328 | 329 | } 330 | 331 | if (s.charAt (0) == '#') { 332 | 333 | return parseHex(s.substr(1)); 334 | 335 | } 336 | 337 | return Std.parseInt (s); 338 | 339 | } 340 | 341 | 342 | private function getStyle (inKey:String, inNode:Xml, inStyles:StringMap , inDefault:String) { 343 | 344 | if (inNode != null && inNode.exists (inKey)) { 345 | 346 | return inNode.get (inKey); 347 | 348 | } 349 | 350 | if (inStyles != null && inStyles.exists (inKey)) { 351 | 352 | return inStyles.get (inKey); 353 | 354 | } 355 | 356 | return inDefault; 357 | 358 | } 359 | 360 | 361 | private function getStyles (inNode:Xml, inPrevStyles:StringMap):StringMap { 362 | 363 | if (!inNode.exists ("style")) 364 | return inPrevStyles; 365 | 366 | var styles = new StringMap (); 367 | 368 | if (inPrevStyles != null) { 369 | 370 | for (s in inPrevStyles.keys ()) { 371 | 372 | styles.set (s, inPrevStyles.get (s)); 373 | 374 | } 375 | 376 | } 377 | 378 | var style = inNode.get ("style"); 379 | var strings = mStyleSplit.split (style); 380 | 381 | for (s in strings) { 382 | 383 | if (mStyleValue.match (s)) { 384 | 385 | styles.set (mStyleValue.matched (1), mStyleValue.matched (2)); 386 | 387 | } 388 | 389 | } 390 | 391 | return styles; 392 | 393 | } 394 | 395 | 396 | private function loadDefs (inXML:Xml) { 397 | 398 | // Two passes - to allow forward xlinks 399 | 400 | for (pass in 0...2) { 401 | 402 | for (def in inXML.elements ()) { 403 | 404 | var name = def.nodeName; 405 | 406 | if (name.substr (0, 4) == "svg:") { 407 | 408 | name = name.substr (4); 409 | 410 | } 411 | 412 | if (name == "linearGradient") { 413 | 414 | loadGradient (def, GradientType.LINEAR, pass == 1); 415 | 416 | } else if (name == "radialGradient") { 417 | 418 | loadGradient (def, GradientType.RADIAL, pass == 1); 419 | 420 | } 421 | 422 | } 423 | 424 | } 425 | 426 | } 427 | 428 | 429 | private function loadGradient (inGrad:Xml, inType:GradientType, inCrossLink:Bool) { 430 | 431 | var name = inGrad.get ("id"); 432 | var grad = new Grad (inType); 433 | 434 | if (inCrossLink && inGrad.exists("xlink:href")) { 435 | 436 | var xlink = inGrad.get ("xlink:href"); 437 | 438 | if (xlink.charAt(0) != "#") 439 | throw ("xlink - unkown syntax : " + xlink); 440 | 441 | var base = mGrads.get (xlink.substr (1)); 442 | 443 | if (base != null) { 444 | 445 | grad.colors = base.colors; 446 | grad.alphas = base.alphas; 447 | grad.ratios = base.ratios; 448 | grad.gradMatrix = base.gradMatrix.clone (); 449 | grad.spread = base.spread; 450 | grad.interp = base.interp; 451 | grad.radius = base.radius; 452 | 453 | } else { 454 | 455 | throw ("Unknown xlink : " + xlink); 456 | 457 | } 458 | 459 | } 460 | 461 | if (inGrad.exists ("x1")) { 462 | 463 | grad.x1 = getFloat (inGrad, "x1"); 464 | grad.y1 = getFloat (inGrad, "y1"); 465 | grad.x2 = getFloat (inGrad, "x2"); 466 | grad.y2 = getFloat (inGrad, "y2"); 467 | 468 | } else { 469 | 470 | grad.x1 = getFloat (inGrad, "cx"); 471 | grad.y1 = getFloat (inGrad, "cy"); 472 | grad.x2 = getFloat (inGrad, "fx", grad.x1); 473 | grad.y2 = getFloat (inGrad, "fy", grad.y1); 474 | 475 | } 476 | 477 | grad.radius = getFloat (inGrad, "r"); 478 | 479 | if (inGrad.exists ("gradientTransform")) { 480 | 481 | applyTransform (grad.gradMatrix, inGrad.get ("gradientTransform")); 482 | 483 | } 484 | 485 | // todo - grad.spread = base.spread; 486 | 487 | for (stop in inGrad.elements ()) { 488 | 489 | var styles = getStyles (stop, null); 490 | 491 | grad.colors.push (getColorStyle ("stop-color", stop, styles, 0x000000)); 492 | grad.alphas.push (getFloatStyle ("stop-opacity", stop, styles, 1.0)); 493 | var percent = 0.0; 494 | if (stop.exists("offset")) { 495 | var offset = stop.get("offset"); 496 | if (offset.indexOf("%") != -1) { 497 | // 0% - 100% 498 | percent = Std.parseFloat (offset) / 100.0; 499 | } else { 500 | // 0.0 - 1.0 501 | percent = Std.parseFloat(offset); 502 | } 503 | if (Math.isNaN(percent)) { 504 | throw ("Unknown stop offset:" + offset); 505 | } 506 | } 507 | grad.ratios.push (Std.int(percent * 255.0)); 508 | 509 | } 510 | 511 | mGrads.set (name, grad); 512 | 513 | } 514 | 515 | 516 | public function loadGroup (g:Group, inG:Xml, matrix:Matrix, inStyles:StringMap ):Group { 517 | 518 | if (inG.exists ("transform")) { 519 | 520 | matrix = matrix.clone (); 521 | applyTransform (matrix, inG.get ("transform")); 522 | 523 | } 524 | 525 | if (inG.exists ("inkscape:label")) { 526 | 527 | g.name = inG.get ("inkscape:label"); 528 | 529 | } else if (inG.exists ("id")) { 530 | 531 | g.name = inG.get ("id"); 532 | 533 | } 534 | 535 | var styles = getStyles (inG, inStyles); 536 | 537 | /* 538 | supports eg: 539 | 540 | 541 | 542 | 543 | 544 | 545 | */ 546 | if (inG.exists("opacity")) { 547 | 548 | var opacity = inG.get("opacity"); 549 | 550 | if (styles == null) 551 | styles = new StringMap(); 552 | 553 | if (styles.exists("opacity")) 554 | opacity = Std.string( Std.parseFloat(opacity) * Std.parseFloat(styles.get("opacity")) ); 555 | 556 | styles.set("opacity", opacity); 557 | 558 | } 559 | if (inG.exists("stroke")) { 560 | 561 | var stroke = inG.get("stroke"); 562 | 563 | if (styles == null) 564 | styles = new StringMap(); 565 | 566 | styles.set("stroke", stroke); 567 | 568 | } 569 | if (inG.exists("stroke-width")) { 570 | 571 | var strokeWidth = inG.get("stroke-width"); 572 | 573 | if (styles == null) 574 | styles = new StringMap(); 575 | 576 | styles.set("stroke-width", strokeWidth); 577 | 578 | } 579 | if (inG.exists("fill")) { 580 | 581 | var fill = inG.get("fill"); 582 | 583 | if (styles == null) 584 | styles = new StringMap(); 585 | 586 | styles.set("fill", fill); 587 | 588 | } 589 | 590 | for (el in inG.elements ()) { 591 | 592 | var name = el.nodeName; 593 | 594 | if (name.substr (0, 4) == "svg:") { 595 | 596 | name = name.substr(4); 597 | 598 | } 599 | 600 | if (el.exists("display") && el.get("display") == "none") continue; 601 | 602 | if (name == "defs") { 603 | 604 | loadDefs (el); 605 | 606 | } else if (name == "g") { 607 | 608 | if (!(el.exists("display") && el.get("display") == "none")) { 609 | 610 | g.children.push (DisplayGroup (loadGroup (new Group (), el, matrix, styles))); 611 | 612 | } 613 | 614 | } else if (name == "path" || name == "line" || name == "polyline") { 615 | 616 | g.children.push (DisplayPath (loadPath (el, matrix, styles, false, false))); 617 | 618 | } else if (name == "rect") { 619 | 620 | g.children.push (DisplayPath (loadPath (el, matrix, styles, true, false))); 621 | 622 | } else if (name == "polygon") { 623 | 624 | g.children.push (DisplayPath (loadPath (el, matrix, styles, false, false))); 625 | 626 | } else if (name == "ellipse") { 627 | 628 | g.children.push (DisplayPath (loadPath (el, matrix, styles, false, true))); 629 | 630 | } else if (name == "circle") { 631 | 632 | g.children.push (DisplayPath (loadPath (el, matrix, styles, false, true, true))); 633 | 634 | } else if (name == "text") { 635 | 636 | g.children.push (DisplayText (loadText (el, matrix, styles))); 637 | 638 | } else if (name == "linearGradient") { 639 | 640 | loadGradient (el, GradientType.LINEAR, true); 641 | 642 | } else if (name == "radialGradient") { 643 | 644 | loadGradient (el, GradientType.RADIAL, true); 645 | 646 | } else { 647 | 648 | // throw("Unknown child : " + el.nodeName ); 649 | 650 | } 651 | 652 | } 653 | 654 | return g; 655 | 656 | } 657 | 658 | 659 | public function loadPath (inPath:Xml, matrix:Matrix, inStyles:StringMap, inIsRect:Bool, inIsEllipse:Bool, inIsCircle:Bool=false):Path { 660 | 661 | if (inPath.exists ("transform")) { 662 | 663 | matrix = matrix.clone (); 664 | applyTransform (matrix, inPath.get ("transform")); 665 | 666 | } 667 | 668 | var styles = getStyles (inPath, inStyles); 669 | var name = inPath.exists ("id") ? inPath.get ("id") : ""; 670 | var path = new Path (); 671 | 672 | path.fill = getFillStyle ("fill", inPath, styles); 673 | path.alpha = getFloatStyle ("opacity", inPath, styles, 1.0); 674 | path.fill_alpha = getFloatStyle ("fill-opacity", inPath, styles, 1.0); 675 | path.stroke_alpha = getFloatStyle ("stroke-opacity", inPath, styles, 1.0); 676 | path.stroke_colour = getStrokeStyle ("stroke", inPath, styles, null); 677 | path.stroke_width = getFloatStyle ("stroke-width", inPath, styles, 1.0); 678 | path.stroke_caps = getStyleAndConvert ("stroke-linecap", inPath, styles, CapsStyle.NONE, 679 | ["round" => CapsStyle.ROUND, "square" => CapsStyle.SQUARE, "butt" => CapsStyle.NONE]); 680 | path.joint_style = getStyleAndConvert ("stroke-linejoin", inPath, styles, JointStyle.MITER, 681 | ["bevel" => JointStyle.BEVEL, "round" => JointStyle.ROUND, "miter" => JointStyle.MITER]); 682 | path.miter_limit = getFloatStyle ("stroke-miterlimit", inPath, styles, 3.0); 683 | path.segments = []; 684 | path.matrix = matrix; 685 | path.name = name; 686 | 687 | if (inIsRect) { 688 | 689 | var x = inPath.exists ("x") ? Std.parseFloat (inPath.get ("x")) : 0; 690 | var y = inPath.exists ("y") ? Std.parseFloat (inPath.get ("y")) : 0; 691 | var w = Std.parseFloat (inPath.get ("width")); 692 | var h = Std.parseFloat (inPath.get ("height")); 693 | var rx = inPath.exists ("rx") ? Std.parseFloat (inPath.get ("rx")) : 0.0; 694 | var ry = inPath.exists ("ry") ? Std.parseFloat (inPath.get ("ry")) : 0.0; 695 | 696 | if (rx == 0 || ry == 0) { 697 | 698 | path.segments.push (new MoveSegment (x , y)); 699 | path.segments.push (new DrawSegment (x + w, y)); 700 | path.segments.push (new DrawSegment (x + w, y + h)); 701 | path.segments.push (new DrawSegment (x, y + h)); 702 | path.segments.push (new DrawSegment (x, y)); 703 | 704 | } else { 705 | 706 | path.segments.push (new MoveSegment (x, y + ry)); 707 | 708 | // top-left 709 | path.segments.push (new QuadraticSegment (x, y, x + rx, y)); 710 | path.segments.push (new DrawSegment (x + w - rx, y)); 711 | 712 | // top-right 713 | path.segments.push (new QuadraticSegment (x + w, y, x + w, y + rx)); 714 | path.segments.push (new DrawSegment (x + w, y + h - ry)); 715 | 716 | // bottom-right 717 | path.segments.push (new QuadraticSegment (x + w, y + h, x + w - rx, y + h)); 718 | path.segments.push (new DrawSegment (x + rx, y + h)); 719 | 720 | // bottom-left 721 | path.segments.push (new QuadraticSegment (x, y + h, x, y + h - ry)); 722 | path.segments.push (new DrawSegment (x, y + ry)); 723 | 724 | } 725 | 726 | } else if (inIsEllipse) { 727 | 728 | var x = inPath.exists ("cx") ? Std.parseFloat (inPath.get ("cx")) : 0; 729 | var y = inPath.exists ("cy") ? Std.parseFloat (inPath.get ("cy")) : 0; 730 | var r = inIsCircle && inPath.exists ("r") ? Std.parseFloat (inPath.get ("r")) : 0.0; 731 | var w = inIsCircle ? r : inPath.exists ("rx") ? Std.parseFloat (inPath.get ("rx")) : 0.0; 732 | var w_ = w * SIN45; 733 | var cw_ = w * TAN22; 734 | var h = inIsCircle ? r : inPath.exists ("ry") ? Std.parseFloat (inPath.get ("ry")) : 0.0; 735 | var h_ = h * SIN45; 736 | var ch_ = h * TAN22; 737 | 738 | path.segments.push (new MoveSegment (x + w, y)); 739 | path.segments.push (new QuadraticSegment (x + w, y + ch_, x + w_, y + h_)); 740 | path.segments.push (new QuadraticSegment (x + cw_, y + h, x, y + h)); 741 | path.segments.push (new QuadraticSegment (x - cw_, y + h, x - w_, y + h_)); 742 | path.segments.push (new QuadraticSegment (x - w, y + ch_, x - w, y)); 743 | path.segments.push (new QuadraticSegment (x - w, y - ch_, x - w_, y - h_)); 744 | path.segments.push (new QuadraticSegment (x - cw_, y - h, x, y - h)); 745 | path.segments.push (new QuadraticSegment (x + cw_, y - h, x + w_, y - h_)); 746 | path.segments.push (new QuadraticSegment (x + w, y - ch_, x + w, y)); 747 | 748 | } else { 749 | 750 | var d = inPath.exists ("points") ? ("M" + inPath.get ("points") + "z") : 751 | inPath.exists ("x1") ? ("M" + inPath.get ("x1") + "," + inPath.get ("y1") + " " + inPath.get ("x2") + "," + inPath.get ("y2") + "z") : 752 | inPath.get ("d"); 753 | 754 | for (segment in mPathParser.parse (d, mConvertCubics)) { 755 | 756 | path.segments.push (segment); 757 | 758 | } 759 | 760 | } 761 | 762 | return path; 763 | 764 | } 765 | 766 | 767 | public function loadText (inText:Xml, matrix:Matrix, inStyles:StringMap ):Text { 768 | 769 | if (inText.exists ("transform")) { 770 | 771 | matrix = matrix.clone (); 772 | applyTransform (matrix, inText.get ("transform")); 773 | 774 | } 775 | 776 | var styles = getStyles (inText, inStyles); 777 | var text = new Text (); 778 | 779 | text.matrix = matrix; 780 | text.name = inText.exists ("id") ? inText.get ("id") : ""; 781 | text.x = getFloat (inText, "x", 0.0); 782 | text.y = getFloat (inText, "y", 0.0); 783 | text.fill = getFillStyle ("fill", inText, styles); 784 | text.fill_alpha = getFloatStyle ("fill-opacity", inText, styles, 1.0); 785 | text.stroke_alpha = getFloatStyle ("stroke-opacity", inText, styles, 1.0); 786 | text.stroke_colour = getStrokeStyle ("stroke", inText, styles, null); 787 | text.stroke_width = getFloatStyle ("stroke-width", inText, styles, 1.0); 788 | text.font_family = getStyle ("font-family", inText, styles, ""); 789 | text.font_size = getFloatStyle ("font-size", inText, styles, 12); 790 | text.letter_spacing = getFloatStyle ("letter-spacing", inText, styles, 0); 791 | text.kerning = getFloatStyle ("kerning", inText, styles, 0); 792 | text.text_align = getStyle ("text-align", inText, styles, "start"); 793 | 794 | var string = ""; 795 | 796 | for (el in inText.elements ()) { 797 | 798 | string += el.toString(); 799 | 800 | } 801 | 802 | //trace(string); 803 | text.text = string; 804 | return text; 805 | 806 | } 807 | 808 | private static inline function parseHex(hex:String):Int 809 | { 810 | // Support 3-character hex color shorthand 811 | // e.g. #RGB -> #RRGGBB 812 | if (hex.length == 3) { 813 | hex = hex.substr(0,1) + hex.substr(0,1) + 814 | hex.substr(1,1) + hex.substr(1,1) + 815 | hex.substr(2,1) + hex.substr(2,1); 816 | } 817 | 818 | return Std.parseInt ("0x" + hex); 819 | } 820 | 821 | private static inline function parseRGBMatch(rgbMatch:EReg):Int 822 | { 823 | // CSS2 rgb color definition, matches 0-255 or 0-100% 824 | // e.g. rgb(255,127,0) == rgb(100%,50%,0) 825 | 826 | inline function range(val:Float):Int { 827 | // constrain to Int 0-255 828 | if (val < 0) { val = 0; } 829 | if (val > 255) { val = 255; } 830 | return Std.int( val ); 831 | } 832 | 833 | var r = Std.parseFloat(rgbMatch.matched (1)); 834 | if (rgbMatch.matched(2)=='%') { r = r * 255 / 100; } 835 | 836 | var g = Std.parseFloat(rgbMatch.matched (3)); 837 | if (rgbMatch.matched(4)=='%') { g = g * 255 / 100; } 838 | 839 | var b = Std.parseFloat(rgbMatch.matched (5)); 840 | if (rgbMatch.matched(6)=='%') { b = b * 255 / 100; } 841 | 842 | return ( range(r)<<16 ) | ( range(g)<<8 ) | range(b); 843 | } 844 | } 845 | -------------------------------------------------------------------------------- /format/svg/SVGRenderer.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import format.svg.PathParser; 4 | import format.svg.PathSegment; 5 | 6 | import openfl.geom.Matrix; 7 | import openfl.geom.Rectangle; 8 | import openfl.display.Graphics; 9 | 10 | import openfl.display.Shape; 11 | import openfl.display.Sprite; 12 | import openfl.display.DisplayObject; 13 | import openfl.display.GradientType; 14 | import openfl.display.SpreadMethod; 15 | import openfl.display.InterpolationMethod; 16 | import openfl.display.CapsStyle; 17 | import openfl.display.JointStyle; 18 | import openfl.display.LineScaleMode; 19 | 20 | import format.svg.Grad; 21 | import format.svg.Group; 22 | import format.svg.FillType; 23 | import format.gfx.Gfx; 24 | import openfl.geom.Rectangle; 25 | 26 | 27 | typedef GroupPath = Array; 28 | typedef ObjectFilter = String->GroupPath->Bool; 29 | 30 | class SVGRenderer 31 | { 32 | public static var SQRT2:Float = Math.sqrt(2); 33 | public var width(default,null):Float; 34 | public var height(default,null):Float; 35 | 36 | var mSvg:SVGData; 37 | var mRoot:Group; 38 | var mGfx : Gfx; 39 | var mMatrix : Matrix; 40 | var mScaleRect:Rectangle; 41 | var mScaleW:Null; 42 | var mScaleH:Null; 43 | var mFilter : ObjectFilter; 44 | var mGroupPath : GroupPath; 45 | 46 | public function new(inSvg:SVGData,?inLayer:String) 47 | { 48 | mSvg = inSvg; 49 | 50 | width = mSvg.width; 51 | height = mSvg.height; 52 | mRoot = mSvg; 53 | if (inLayer!=null) 54 | { 55 | mRoot = mSvg.findGroup(inLayer); 56 | if (mRoot==null) 57 | throw "Could not find SVG group: " + inLayer; 58 | } 59 | } 60 | 61 | public static function toHaxe(inXML:Xml,?inFilter:ObjectFilter) : Array 62 | { 63 | return new SVGRenderer(new SVGData(inXML,true)).iterate(new format.gfx.Gfx2Haxe(),inFilter).commands; 64 | } 65 | 66 | public static function toBytes(inXML:Xml,?inFilter:ObjectFilter) : format.gfx.GfxBytes 67 | { 68 | return new SVGRenderer(new SVGData(inXML,true)).iterate(new format.gfx.GfxBytes(),inFilter); 69 | } 70 | 71 | 72 | public function iterate(inGfx:T, ?inFilter:ObjectFilter) : T 73 | { 74 | mGfx = cast inGfx; 75 | mMatrix = new Matrix(); 76 | mFilter = inFilter; 77 | mGroupPath = []; 78 | mGfx.size(width,height); 79 | iterateGroup(mRoot,true); 80 | mGfx.eof(); 81 | return inGfx; 82 | } 83 | public function hasGroup(inName:String) 84 | { 85 | return mRoot.hasGroup(inName); 86 | } 87 | 88 | public function iterateText(inText:Text) 89 | { 90 | if (mFilter!=null && !mFilter(inText.name,mGroupPath)) 91 | return; 92 | mGfx.renderText(inText); 93 | } 94 | 95 | public function iteratePath(inPath:Path) 96 | { 97 | if (mFilter!=null && !mFilter(inPath.name,mGroupPath)) 98 | return; 99 | 100 | if (inPath.segments.length==0 || mGfx==null) 101 | return; 102 | var px = 0.0; 103 | var py = 0.0; 104 | 105 | var m:Matrix = inPath.matrix.clone(); 106 | m.concat(mMatrix); 107 | var context = new RenderContext(m,mScaleRect,mScaleW,mScaleH); 108 | 109 | var geomOnly = mGfx.geometryOnly(); 110 | if (!geomOnly) 111 | { 112 | // Move to avoid the case of: 113 | // 1. finish drawing line on last path 114 | // 2. set fill=something 115 | // 3. move (this draws in the fill) 116 | // 4. continue with "real" drawing 117 | inPath.segments[0].toGfx(mGfx, context); 118 | 119 | switch(inPath.fill) 120 | { 121 | case FillGrad(grad): 122 | grad.updateMatrix(m); 123 | mGfx.beginGradientFill(grad); 124 | case FillSolid(colour): 125 | mGfx.beginFill(colour,inPath.fill_alpha*inPath.alpha); 126 | case FillNone: 127 | //mGfx.endFill(); 128 | } 129 | 130 | 131 | if (inPath.stroke_colour==null) 132 | { 133 | //mGfx.lineStyle(); 134 | } 135 | else 136 | { 137 | var style = new format.gfx.LineStyle(); 138 | var scale = Math.sqrt(m.a*m.a + m.d*m.d)/SQRT2; 139 | style.thickness = inPath.stroke_width*scale; 140 | style.alpha = inPath.stroke_alpha*inPath.alpha; 141 | style.color = inPath.stroke_colour; 142 | style.capsStyle = inPath.stroke_caps; 143 | style.jointStyle = inPath.joint_style; 144 | style.miterLimit = inPath.miter_limit; 145 | mGfx.lineStyle(style); 146 | } 147 | } 148 | 149 | 150 | for(segment in inPath.segments) 151 | segment.toGfx(mGfx, context); 152 | 153 | 154 | // endFill automatically close an open path 155 | // by putting endLineStyle before endFill, the closing line is not drawn 156 | // so an open path in inkscape stay open in openfl 157 | // this does not affect closed path 158 | mGfx.endLineStyle(); 159 | mGfx.endFill(); 160 | } 161 | 162 | 163 | 164 | public function iterateGroup(inGroup:Group,inIgnoreDot:Bool) 165 | { 166 | // Convention for hidden layers ... 167 | if (inIgnoreDot && inGroup.name!=null && inGroup.name.substr(0,1)==".") 168 | return; 169 | 170 | mGroupPath.push(inGroup.name); 171 | 172 | // if (mFilter!=null && !mFilter(inGroup.name)) return; 173 | 174 | for(child in inGroup.children) 175 | { 176 | switch(child) 177 | { 178 | case DisplayGroup(group): 179 | iterateGroup(group,inIgnoreDot); 180 | case DisplayPath(path): 181 | iteratePath(path); 182 | case DisplayText(text): 183 | iterateText(text); 184 | } 185 | } 186 | 187 | mGroupPath.pop(); 188 | } 189 | 190 | 191 | 192 | 193 | 194 | public function render(inGfx:Graphics,?inMatrix:Matrix, ?inFilter:ObjectFilter, ?inScaleRect:Rectangle,?inScaleW:Float, ?inScaleH:Float ) 195 | { 196 | 197 | mGfx = new format.gfx.GfxGraphics(inGfx); 198 | if (inMatrix==null) 199 | mMatrix = new Matrix(); 200 | else 201 | mMatrix = inMatrix.clone(); 202 | 203 | mScaleRect = inScaleRect; 204 | mScaleW = inScaleW; 205 | mScaleH = inScaleH; 206 | mFilter = inFilter; 207 | mGroupPath = []; 208 | 209 | iterateGroup(mRoot,inFilter==null); 210 | } 211 | public function renderRect(inGfx:Graphics,inFilter:ObjectFilter,scaleRect:Rectangle,inBounds:Rectangle,inRect:Rectangle) : Void 212 | { 213 | var matrix = new Matrix(); 214 | matrix.tx = inRect.x-(inBounds.x); 215 | matrix.ty = inRect.y-(inBounds.y); 216 | if (scaleRect!=null) 217 | { 218 | var extraX = inRect.width-(inBounds.width-scaleRect.width); 219 | var extraY = inRect.height-(inBounds.height-scaleRect.height); 220 | render(inGfx,matrix,inFilter,scaleRect, extraX, extraY ); 221 | } 222 | else 223 | render(inGfx,matrix,inFilter); 224 | } 225 | 226 | public function renderRect0(inGfx:Graphics,inFilter:ObjectFilter,scaleRect:Rectangle,inBounds:Rectangle,inRect:Rectangle) : Void 227 | { 228 | var matrix = new Matrix(); 229 | matrix.tx = -(inBounds.x); 230 | matrix.ty = -(inBounds.y); 231 | if (scaleRect!=null) 232 | { 233 | var extraX = inRect.width-(inBounds.width-scaleRect.width); 234 | var extraY = inRect.height-(inBounds.height-scaleRect.height); 235 | render(inGfx,matrix,inFilter,scaleRect, extraX, extraY ); 236 | } 237 | else 238 | render(inGfx,matrix,inFilter); 239 | } 240 | 241 | 242 | 243 | 244 | public function getExtent(?inMatrix:Matrix, ?inFilter:ObjectFilter, ?inIgnoreDot:Bool ) : 245 | Rectangle 246 | { 247 | if (inIgnoreDot==null) 248 | inIgnoreDot = inFilter==null; 249 | var gfx = new format.gfx.GfxExtent(); 250 | mGfx = gfx; 251 | if (inMatrix==null) 252 | mMatrix = new Matrix(); 253 | else 254 | mMatrix = inMatrix.clone(); 255 | 256 | mFilter = inFilter; 257 | mGroupPath = []; 258 | 259 | iterateGroup(mRoot,inIgnoreDot); 260 | 261 | return gfx.extent; 262 | } 263 | 264 | public function findText(?inFilter:ObjectFilter) 265 | { 266 | mFilter = inFilter; 267 | mGroupPath = []; 268 | var finder = new format.gfx.GfxTextFinder(); 269 | mGfx = finder; 270 | iterateGroup(mRoot,false); 271 | return finder.text; 272 | } 273 | 274 | public function getMatchingRect(inMatch:EReg) : Rectangle 275 | { 276 | return getExtent(null, function(_,groups) { 277 | return groups[1]!=null && inMatch.match(groups[1]); 278 | }, false ); 279 | } 280 | 281 | public function renderObject(inObj:DisplayObject,inGfx:Graphics, 282 | ?inMatrix:Matrix,?inFilter:ObjectFilter,inScale9:Rectangle) 283 | { 284 | render(inGfx,inMatrix,inFilter,inScale9); 285 | var rect = getExtent(inMatrix, function(_,groups) { return groups[1]==".scale9"; } ); 286 | // TODO: 287 | /* 288 | if (rect!=null) 289 | inObj.scale9Grid = rect; 290 | #if !flash 291 | inObj.cacheAsBitmap = neash.Lib.IsOpenGL(); 292 | #end 293 | */ 294 | } 295 | 296 | public function renderSprite(inObj:Sprite, ?inMatrix:Matrix,?inFilter:ObjectFilter, ?inScale9:Rectangle) 297 | { 298 | renderObject(inObj,inObj.graphics,inMatrix,inFilter,inScale9); 299 | } 300 | 301 | public function createShape(?inMatrix:Matrix,?inFilter:ObjectFilter, ?inScale9:Rectangle) : Shape 302 | { 303 | var shape = new Shape(); 304 | renderObject(shape,shape.graphics,inMatrix,inFilter,inScale9); 305 | return shape; 306 | } 307 | 308 | public function namedShape(inName:String) : Shape 309 | { 310 | return createShape(null, function(name,_) { return name==inName; } ); 311 | } 312 | 313 | 314 | public function renderBitmap(?inRect:Rectangle,inScale:Float = 1.0) 315 | { 316 | mMatrix = new Matrix(inScale,0,0,inScale, -inRect.x*inScale, -inRect.y*inScale); 317 | 318 | var w = Std.int(Math.ceil( inRect==null ? width : inRect.width*inScale )); 319 | var h = Std.int(Math.ceil( inRect==null ? width : inRect.height*inScale )); 320 | 321 | var bmp = new openfl.display.BitmapData(w,h,true,#if (neko && !haxe3) { a: 0x00, rgb: 0x000000 } #else 0x00000000 #end); 322 | 323 | var shape = new openfl.display.Shape(); 324 | mGfx = new format.gfx.GfxGraphics(shape.graphics); 325 | 326 | mGroupPath = []; 327 | iterateGroup(mRoot,true); 328 | 329 | bmp.draw(shape); 330 | mGfx = null; 331 | 332 | return bmp; 333 | } 334 | } 335 | 336 | -------------------------------------------------------------------------------- /format/svg/Text.hx: -------------------------------------------------------------------------------- 1 | package format.svg; 2 | 3 | import openfl.geom.Matrix; 4 | 5 | 6 | class Text 7 | { 8 | public function new() { } 9 | 10 | public var name:String; 11 | public var x:Float; 12 | public var y:Float; 13 | public var matrix:Matrix; 14 | public var text:String; 15 | public var fill:FillType; 16 | public var fill_alpha:Float; 17 | public var stroke_alpha:Float; 18 | public var stroke_colour:Null; 19 | public var stroke_width:Float; 20 | public var font_family:String; 21 | public var font_size:Float; 22 | public var kerning:Float; 23 | public var letter_spacing:Float; 24 | public var text_align:String; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg", 3 | "url": "http://github.com/openfl/svg", 4 | "license": "MIT", 5 | "tags": [ "cpp", "flash", "neko", "js" ], 6 | "description": "Provides support for parsing and rendering SVG content", 7 | "version": "1.1.3", 8 | "releasenote": "Fixed a crash in Cairo and Haxe 4 RC4 compilation support", 9 | "contributors": [ 10 | "singmajesty", 11 | "gamehaxe", 12 | "bowlerhat", 13 | "Dimensionscape" 14 | ], 15 | "dependencies": { 16 | } 17 | } -------------------------------------------------------------------------------- /scripts/docs.hxml: -------------------------------------------------------------------------------- 1 | # Generate platform-specific XML for documentation output 2 | 3 | # -xml xml/Flash.xml 4 | # -swf obj/docs 5 | # -swf-version 17.0 6 | # -D display=usage 7 | # -D doc_gen 8 | # --macro include("starling") 9 | # -lib lime 10 | # -lib openfl 11 | # -cp .. 12 | # --no-output 13 | 14 | -xml xml/Flash.xml 15 | -cpp obj/docs 16 | -D display=usage 17 | -D doc_gen 18 | -D flash_doc_gen 19 | -D nocffi 20 | --macro include("format") 21 | -lib lime 22 | -lib openfl 23 | -cp .. 24 | --no-output 25 | 26 | --next 27 | 28 | -xml xml/Windows.xml 29 | -cpp obj/docs 30 | -D display=usage 31 | -D windows 32 | -D doc_gen 33 | -D nocffi 34 | --macro include("format") 35 | -lib lime 36 | -lib openfl 37 | -cp .. 38 | --no-output 39 | 40 | --next 41 | 42 | -xml xml/macOS.xml 43 | -cpp obj/docs 44 | -D display=usage 45 | -D mac 46 | -D doc_gen 47 | -D nocffi 48 | --macro include("format") 49 | -lib lime 50 | -lib openfl 51 | -cp .. 52 | --no-output 53 | 54 | --next 55 | 56 | -xml xml/Linux.xml 57 | -cpp obj/docs 58 | -D display=usage 59 | -D linux 60 | -D doc_gen 61 | -D nocffi 62 | --macro include("format") 63 | -lib lime 64 | -lib openfl 65 | -cp .. 66 | --no-output 67 | 68 | --next 69 | 70 | -xml xml/Neko.xml 71 | -neko obj/docs 72 | -D display=usage 73 | -D doc_gen 74 | -D nocffi 75 | --macro include("format") 76 | -lib lime 77 | -lib openfl 78 | -cp .. 79 | --no-output 80 | 81 | --next 82 | 83 | -xml xml/iOS.xml 84 | -cpp obj/docs 85 | -D display=usage 86 | -D ios 87 | -D doc_gen 88 | -D nocffi 89 | --macro include("format") 90 | -lib lime 91 | -lib openfl 92 | -cp .. 93 | --no-output 94 | 95 | --next 96 | 97 | -xml xml/Android.xml 98 | -cpp obj/docs 99 | -D display=usage 100 | -D android 101 | -D doc_gen 102 | -D nocffi 103 | --macro include("format") 104 | -lib lime 105 | -lib openfl 106 | -cp .. 107 | --no-output 108 | 109 | --next 110 | 111 | -xml xml/HTML5.xml 112 | -js obj/docs 113 | -D display=usage 114 | -D html5 115 | -D doc_gen 116 | --macro include("format") 117 | -lib lime 118 | -lib openfl 119 | -cp .. 120 | --no-output 121 | 122 | --next 123 | 124 | -cmd haxelib run dox -o ../docs -i xml -in format --toplevel-package format -D website "http://www.openfl.org" -D source-path "https://github.com/openfl/svg/tree/master/src/" --title "SVG API Reference" -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # SVG Tests 2 | 3 | `svg` includes unit tests. These tests render SVGs at different sizes, and compare those images against known, "good" versions. If the images are similar enough, the test passes; otherwise, they fail. 4 | 5 | To run the tests, run of the following commands. 6 | 7 | - Windows: `openfl test windows` 8 | - macOS: `openfl test windows` 9 | - Linux: `openfl test linux` 10 | - Neko: `openfl test neko` 11 | - HashLink: `openfl test hl` 12 | 13 | On Haxe `sys` targets, this generates an `svg-tests.html` file in the repository root, which shows a row per image. For each row, it displays: 14 | 15 | - The SVG 16 | - The actual (expected) PNG rendering 17 | - The rendered PNG version 18 | 19 | The actual (expected) PNG is pre-generated ahead of time using GIMP. 20 | 21 | # Adding New Tests 22 | 23 | To add a new test, you need to: 24 | 25 | - Drop the SVG in `test/images` (eg. `apple.png`) 26 | - Open the SVG in GIMP, and render it to PNG at whatever size you want to render the SVG to. Save it with the filename `-x.png` (eg. `apple-64x64.png`). 27 | - Open up `test/SvgGeneration.hx` and add a new test. Something like this: 28 | 29 | ```haxe 30 | public function testAppleScalesUpCorrectly() 31 | { 32 | generateAndCompare("apple.svg"); // 56x56 (SVG size) 33 | generateAndCompare("apple.svg", 256, 256); 34 | generateAndCompare("apple.svg", 137, 137); 35 | } 36 | ``` 37 | 38 | This sample test renders `apple.svg` three times: at its original size (56x56), at 256x256, and 137x137. You need to add three new files to the `test/images` directory: 39 | 40 | - `apple-56x56.png` 41 | - `apple-137x137.png` 42 | - `apple-256x256.png` 43 | 44 | Run the test and make sure it passes before you commit/push! 45 | 46 | # Difference Algorithm 47 | 48 | Currently, the tests look at the number of different pixels between the actual and expected PNGs. For example, if an image is 10x10 pixels, and 5 pixels are different, the total image difference is 5%. 49 | 50 | If the number of different pixels exceeds a threshold, the rendered PNG is deemed disimilar and the test fails. -------------------------------------------------------------------------------- /test/images/all_rights_reserved_white-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/all_rights_reserved_white-256x256.png -------------------------------------------------------------------------------- /test/images/all_rights_reserved_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/images/alphachannel-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/alphachannel-100x100.png -------------------------------------------------------------------------------- /test/images/alphachannel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/images/arc-radius-too-small-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/arc-radius-too-small-256x256.png -------------------------------------------------------------------------------- /test/images/arc-radius-too-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/images/arc-rotate-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/arc-rotate-256x256.png -------------------------------------------------------------------------------- /test/images/arc-rotate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/images/disabled_test1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/images/disabled_test2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/images/fancy-sun-256x255.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/fancy-sun-256x255.png -------------------------------------------------------------------------------- /test/images/fancy-sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/images/fill_rgb-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/fill_rgb-100x100.png -------------------------------------------------------------------------------- /test/images/fill_rgb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/images/group-inherited-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/group-inherited-256x256.png -------------------------------------------------------------------------------- /test/images/group-inherited.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/images/layer_test1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/images/layer_test2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/images/matrix-rotated-square-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/matrix-rotated-square-100x100.png -------------------------------------------------------------------------------- /test/images/matrix-rotated-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/images/nested_layer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/images/path-two-float-decimals-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/path-two-float-decimals-256x256.png -------------------------------------------------------------------------------- /test/images/path-two-float-decimals.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/images/rotated-square-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/rotated-square-100x100.png -------------------------------------------------------------------------------- /test/images/rotated-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/images/scale_rect-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/scale_rect-256x256.png -------------------------------------------------------------------------------- /test/images/scale_rect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/images/ubuntu-logo-orange-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfl/svg/3e4c08a60837049ef92e4815d5c2d5cc04496536/test/images/ubuntu-logo-orange-256x256.png -------------------------------------------------------------------------------- /test/images/ubuntu-logo-orange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 11 | 12 | 14 | 27 | 28 | -------------------------------------------------------------------------------- /test/project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/src/MacroTest.hx: -------------------------------------------------------------------------------- 1 | import format.SVG; 2 | 3 | import utest.Assert; 4 | import utest.Test; 5 | 6 | /** 7 | Tests that using SVGs inside macros actually compiles. If the `flash` 8 | package is used instead of the `openfl` package. 9 | **/ 10 | class MacroTest extends Test 11 | { 12 | macro static function makeSvg() 13 | { 14 | // This macro will fail if the `flash` package is used. 15 | return macro new SVG(""); 16 | } 17 | 18 | public function testCompilesInMacro() 19 | { 20 | Assert.notNull(makeSvg()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/src/SvgGenerationTest.hx: -------------------------------------------------------------------------------- 1 | import format.SVG; 2 | 3 | import openfl.Assets; 4 | import openfl.display.Shape; 5 | import openfl.display.PNGEncoderOptions; 6 | import openfl.display.BitmapData; 7 | import openfl.display.Bitmap; 8 | import openfl.utils.Object; 9 | 10 | #if sys 11 | import sys.io.File; 12 | #end 13 | 14 | import utest.Assert; 15 | import utest.Test; 16 | 17 | using StringTools; 18 | 19 | /** 20 | Tests SVG generation. We run through a list of known (working) SVGs, 21 | generating them as (usually 256x256, unless the source is smaller) images, 22 | and comparing those to expected PNGs. 23 | 24 | To aid troubleshooting, this test generates a generation.html file which 25 | shows both expected and actual values side-by-side, so it's easy to see what 26 | went wrong in the SVG generation. 27 | **/ 28 | class SvgGenerationTest extends Test 29 | { 30 | private static inline var RESULTS_HTML_FILE:String = "svg-tests.html"; 31 | private static inline var IMAGES_PATH:String = "images"; 32 | private static inline var GENERATED_IMAGES_PATH:String = "generated"; 33 | // Maximum size we render the expected image to 34 | private static inline var MAX_IMAGE_SIZE:Int = 256; 35 | // Percentage difference allowable between expected/actual images 36 | // Ranges from 0 to 1 (0.1 = 10% diff) 37 | // Currently at 15% because anti-aliasing artifacts on small images makes a big difference 38 | private static inline var SVG_DIFF_TOLERANCE_PERCENT:Float = 0.10; 39 | 40 | private var results:GenerationResults; 41 | 42 | public function new() 43 | { 44 | super(); 45 | } 46 | 47 | public function testUbuntuLogoRendersCorrectly() 48 | { 49 | generateAndCompare("ubuntu-logo-orange.svg", 256, 256); 50 | } 51 | 52 | public function testAllRightsReservedRendersCorrectly() 53 | { 54 | // This file is a small circle; it has a lot of anti-aliasing artifacts in the diff. 55 | // This might be problematic. Hence, we render a larger version (4x). 56 | generateAndCompare("all_rights_reserved_white.svg", 256, 256); 57 | } 58 | 59 | public function testArcRotateRendersCorrectly() 60 | { 61 | generateAndCompare("arc-rotate.svg"); 62 | } 63 | 64 | public function testArcRadiusTooSmallRendersCorrectly() 65 | { 66 | generateAndCompare("arc-radius-too-small.svg"); 67 | } 68 | 69 | public function testPathWithTwoFloatDecimals() 70 | { 71 | generateAndCompare("path-two-float-decimals.svg"); 72 | } 73 | 74 | public function testGroupInherited() 75 | { 76 | generateAndCompare("group-inherited.svg"); 77 | } 78 | 79 | public function testFancySunIconRendersCorrectly() 80 | { 81 | generateAndCompare("fancy-sun.svg"); 82 | } 83 | 84 | public function testScaleRectStrokeWidth() 85 | { 86 | generateAndCompare("scale_rect.svg", 256, 256); 87 | } 88 | 89 | public function testAlphaChannelAnd3CharHexColors() 90 | { 91 | generateAndCompare("alphachannel.svg", 100, 100); 92 | } 93 | 94 | public function testFillColorRGB() 95 | { 96 | generateAndCompare("fill_rgb.svg", 100, 100); 97 | } 98 | 99 | public function testRotatedSquareRendersRotated() 100 | { 101 | generateAndCompare("rotated-square.svg", 100, 100); 102 | } 103 | 104 | public function testMatrixRotatedSquare() 105 | { 106 | generateAndCompare("matrix-rotated-square.svg", 100, 100); 107 | } 108 | 109 | public function testLayerFiltering() 110 | { 111 | generateAndCompareWithLayerFilter("layer_test1.svg", null, "layer_test2.svg", "red"); 112 | } 113 | 114 | public function testNestedLayerFiltering() 115 | { 116 | generateAndCompareWithLayerFilter("layer_test2.svg", "red", "nested_layer.svg", "red"); 117 | } 118 | 119 | public function testDisabledLayers() 120 | { 121 | generateAndCompareWithLayerFilter("layer_test1.svg", null, "disabled_test1.svg", null); 122 | generateAndCompareWithLayerFilter("layer_test1.svg", null, "disabled_test2.svg", null); 123 | } 124 | 125 | public function setupClass():Void { 126 | cleanPreviousTestRunResults(); 127 | } 128 | 129 | public function teardownClass():Void { 130 | createHtmlReport(); 131 | } 132 | 133 | private function cleanPreviousTestRunResults() { 134 | 135 | this.results = { 136 | passedTests: new Array(), failedTests: new Array() 137 | }; 138 | 139 | #if sys 140 | // Delete the old report 141 | if (sys.FileSystem.exists(RESULTS_HTML_FILE)) 142 | { 143 | sys.FileSystem.deleteFile(RESULTS_HTML_FILE); 144 | } 145 | 146 | // Delete generated images path (if it exists) 147 | if (sys.FileSystem.exists(GENERATED_IMAGES_PATH)) { 148 | for (file in sys.FileSystem.readDirectory(GENERATED_IMAGES_PATH)) { 149 | sys.FileSystem.deleteFile('${GENERATED_IMAGES_PATH}/${file}'); 150 | } 151 | } else { 152 | sys.FileSystem.createDirectory(GENERATED_IMAGES_PATH); 153 | } 154 | #end 155 | } 156 | 157 | private function createHtmlReport() { 158 | var total = results.failedTests.length + results.passedTests.length; 159 | 160 | var html:String = ' 161 | ${results.failedTests.length}/${total} failures | SVG Generation Tests 162 | '; 163 | 164 | html += this.getReportHtml(); 165 | 166 | html += ""; 167 | 168 | #if sys 169 | sys.io.File.saveContent(RESULTS_HTML_FILE, html); 170 | #else 171 | trace(html); 172 | #end 173 | } 174 | 175 | private function generateAndCompare(svgName:String, pngWidth:Int = 0, pngHeight:Int = 0) 176 | { 177 | #if sys 178 | var pngName = generatePng(svgName, pngWidth, pngHeight); 179 | compareGeneratedToExpected(svgName, pngName); 180 | #else 181 | var svgData = openfl.Assets.getText('${IMAGES_PATH}/${svgName}'); 182 | var actualBitmapData = generateBitmap(svgName, svgData, pngWidth, pngHeight); 183 | var pngName = svgName.replace(".svg", '-${actualBitmapData.width}x${actualBitmapData.height}.png'); 184 | var expectedBitmapData = openfl.Assets.getBitmapData('${IMAGES_PATH}/${pngName}'); 185 | compareGeneratedBitmapToExpectedBitmap(svgName, pngName, expectedBitmapData, actualBitmapData); 186 | #end 187 | } 188 | 189 | private function generateAndCompareWithLayerFilter(svgName1:String, layerId1:String, svgName2:String, layerId2:String) 190 | { 191 | #if sys 192 | var pngName1 = generatePng(svgName1, 0 ,0, layerId1); 193 | var pngName2 = generatePng(svgName2, 0, 0, layerId2); 194 | compareGeneratedPNGs(pngName1, pngName2); 195 | #else 196 | var svgData1 = openfl.Assets.getText('${IMAGES_PATH}/${svgName1}'); 197 | var svgData2 = openfl.Assets.getText('${IMAGES_PATH}/${svgName1}'); 198 | var bitmap1 = generateBitmap(svgName1, svgData1); 199 | var bitmap2 = generateBitmap(svgName2, svgData2); 200 | var pngName1 = svgName1.replace(".svg", '-${bitmap1.width}x${bitmap1.height}.png'); 201 | var pngName2 = svgName2.replace(".svg", '-${bitmap2.width}x${bitmap2.height}.png'); 202 | compareGeneratedBitmaps(pngName1, pngName2, bitmap1, bitmap2); 203 | #end 204 | } 205 | 206 | // Generates a BitmapData from an SVG at the specified width/height. 207 | private function generateBitmap(svgName:String, svgData:String, pngWidth:Int = 0, pngHeight:Int = 0, ?pLayerID:String = null):BitmapData 208 | { 209 | var svg = new SVG(svgData); 210 | 211 | // Render to the size of the PNG image representing our "expected" value. 212 | // If the user passed in a width/height, we use that. Otherwise, we use 213 | // the original SVG height. If the image is over 256x256, we render it at 256x256 214 | // (this makes it easier to see in the report). 215 | var width:Int = pngWidth; 216 | var height:Int = pngHeight; 217 | var layerId:String = pLayerID; 218 | 219 | if (pngWidth == 0 || pngHeight == 0) 220 | { 221 | width = Math.round(svg.data.width); 222 | height = Math.round(svg.data.height); 223 | } 224 | 225 | // Scale down (proportionally). 226 | if (width > 256) 227 | { 228 | var scale:Float = 256 / width; 229 | width = 256; 230 | height = Math.round(height * scale); 231 | } 232 | 233 | if (height > 256) 234 | { 235 | var scale:Float = 256 / height; 236 | height = 256; 237 | width = Math.round(width * scale); 238 | } 239 | 240 | // Fully-transparent and white 241 | var backgroundColor = 0x00FFFFFF; 242 | var shape = new Shape(); 243 | // scale/render the SVG to this size 244 | svg.render(shape.graphics, 0, 0, width, height, layerId); 245 | 246 | // generated image size 247 | var actualBitmapData = new BitmapData(width, height, true, backgroundColor); 248 | actualBitmapData.draw(shape); 249 | return actualBitmapData; 250 | } 251 | 252 | #if sys 253 | // Generates a PNG file from an SVG at the specified width/height. 254 | private function generatePng(svgName:String, pngWidth:Int = 0, pngHeight:Int = 0, ?pLayerID:String = null):String 255 | { 256 | var svgData = File.getContent('${IMAGES_PATH}/${svgName}'); 257 | 258 | var actualBitmapData = generateBitmap(svgName, svgData, pngWidth, pngHeight, pLayerID); 259 | 260 | var pngFileName = svgName.replace(".svg", '-${actualBitmapData.width}x${actualBitmapData.height}.png'); 261 | var outputFile = '${GENERATED_IMAGES_PATH}/${pngFileName}'; 262 | 263 | File.saveBytes(outputFile, actualBitmapData.encode(actualBitmapData.rect, new PNGEncoderOptions())); 264 | 265 | return pngFileName; 266 | } 267 | #end 268 | 269 | #if sys 270 | // Compares pixels from the generated PNG to the hand-made PNG. 271 | private function compareGeneratedToExpected(svgName:String, pngName:String) 272 | { 273 | var expectedImage:String = '${IMAGES_PATH}/${pngName}'; 274 | if (sys.FileSystem.exists(pngName)) 275 | { 276 | throw '${expectedImage} doesn\'t exist. Please create it.'; 277 | } 278 | var expectedBitmapData:BitmapData = BitmapData.fromFile(expectedImage); 279 | var actualImage:String = '${GENERATED_IMAGES_PATH}/${pngName}'; 280 | var actualBitmapData:BitmapData = BitmapData.fromFile(actualImage); 281 | 282 | compareGeneratedBitmapToExpectedBitmap(svgName, pngName, expectedBitmapData, actualBitmapData); 283 | } 284 | #end 285 | 286 | // Compares pixels from the generated BitmapData to the hand-made BitmapData. 287 | private function compareGeneratedBitmapToExpectedBitmap(svgName:String, pngName:String, expectedBitmapData:BitmapData, actualBitmapData:BitmapData) 288 | { 289 | var test = newSvgTest(svgName); 290 | 291 | if (expectedBitmapData.width != actualBitmapData.width || expectedBitmapData.height != actualBitmapData.height) 292 | { 293 | test.diffPercentage = 1; 294 | results.failedTests.push(test); 295 | Assert.fail('${svgName} generated at the wrong size (expected ${expectedBitmapData.width}x${expectedBitmapData.height}, got ${actualBitmapData.width}x${actualBitmapData.height})'); 296 | } 297 | else 298 | { 299 | test.expectedWidth = expectedBitmapData.width; 300 | test.expectedHeight = expectedBitmapData.height; 301 | 302 | var result:Object = actualBitmapData.compare(expectedBitmapData); 303 | var culmulativeDiff = comparePixels(result, test, svgName.replace(".svg", '-${test.expectedWidth}x${test.expectedHeight}-diff.png')); 304 | if (culmulativeDiff >= SVG_DIFF_TOLERANCE_PERCENT) 305 | { 306 | results.failedTests.push(test); 307 | Assert.fail('${svgName} has ${Math.round(culmulativeDiff * 100)}% pixels different, which is over the threshold of ${SVG_DIFF_TOLERANCE_PERCENT * 100}%'); 308 | } 309 | else 310 | { 311 | results.passedTests.push(test); 312 | Assert.pass(); 313 | } 314 | } 315 | } 316 | 317 | // Compares 2 generated bitmaps 318 | private function compareGeneratedBitmaps(pngName1:String, pngName2:String, actualBitmapData1:BitmapData, actualBitmapData2:BitmapData) 319 | { 320 | var test = newSvgTest(pngName1); 321 | 322 | var result:Object = actualBitmapData1.compare(actualBitmapData2); 323 | var culmulativeDiff = comparePixels(result, test, pngName1.replace(".png", '-diff.png')); 324 | if (culmulativeDiff >= SVG_DIFF_TOLERANCE_PERCENT) 325 | { 326 | results.failedTests.push(test); 327 | Assert.fail('${pngName2} differs from ${pngName1} by ${Math.round(culmulativeDiff * 100)}% pixels, which is over the threshold of ${SVG_DIFF_TOLERANCE_PERCENT * 100}%'); 328 | } 329 | else 330 | { 331 | results.passedTests.push(test); 332 | Assert.pass(); 333 | } 334 | } 335 | 336 | // Compares 2 generated PNGs 337 | private function compareGeneratedPNGs(pngName1:String, pngName2:String) 338 | { 339 | var actualImage1:String = '${GENERATED_IMAGES_PATH}/${pngName1}'; 340 | var actualBitmapData1:BitmapData = BitmapData.fromFile(actualImage1); 341 | 342 | var actualImage2:String = '${GENERATED_IMAGES_PATH}/${pngName2}'; 343 | var actualBitmapData2:BitmapData = BitmapData.fromFile(actualImage2); 344 | 345 | compareGeneratedBitmaps(pngName1, pngName2, actualBitmapData1, actualBitmapData2); 346 | } 347 | 348 | // Calculate the number of pixels that are different from what they should be. 349 | // Since we're averaging across the entire image, even if a few pixels are 350 | // drastically different, if the overall images are similar, we get a small diff. 351 | // We use BitmapData.compare to generate the "diff image" between two images. 352 | // The diff image has one non-transparent pixel for every pixel that differs. 353 | // Because of the way it calculates transparency, the safest way to know if 354 | // there's a diff is to get the pixel RGB values (not RGBA). 355 | // To see how this diff image works, just replace any SVG with a coloured rectangle and re-run the tests. 356 | private function comparePixels(result:Object, test:SvgTest, diffFilename:String):Float { 357 | var numPixelsThatAreDifferent:Int = 0; 358 | 359 | if (result == 0) 360 | { 361 | // A rare, but awesome, exact match! 362 | return 0; 363 | } 364 | else 365 | { 366 | var diffPixels = cast(result, BitmapData); 367 | 368 | for (y in 0 ... diffPixels.height) 369 | { 370 | for (x in 0 ... diffPixels.width) 371 | { 372 | var diffPixel = diffPixels.getPixel(x, y); 373 | // Extract RGB values 374 | var components = getComponents(diffPixel); 375 | var red = components[0]; 376 | var green = components[1]; 377 | var blue = components[2]; 378 | if (red > 0 || green > 0 || blue > 0) 379 | { 380 | numPixelsThatAreDifferent++; 381 | } 382 | } 383 | } 384 | 385 | // Average over all pixels in the image 386 | var culmulativeDiff:Float = numPixelsThatAreDifferent / (diffPixels.width * diffPixels.height); 387 | test.diffPixels = diffPixels; 388 | test.diffPercentage = culmulativeDiff; 389 | var diffFile:String = '${GENERATED_IMAGES_PATH}/${diffFilename}'; 390 | #if sys 391 | File.saveBytes(diffFile, diffPixels.encode(diffPixels.rect, new PNGEncoderOptions())); 392 | #end 393 | 394 | return culmulativeDiff; 395 | } 396 | } 397 | 398 | // Given a pixel (0xRRGGBB), return an array [RR, GG, BB] 399 | // Components are integer values from 0..255 400 | private function getComponents(pixel:Int):Array 401 | { 402 | // No difference (empty pixel)? Skip calculations. 403 | if (pixel <= 0) 404 | { 405 | return [0, 0, 0]; 406 | } 407 | 408 | var blue:Int = pixel & 0xFF; // BB 409 | var green:Int = (pixel >> 8) & 0xFF; // GG 410 | var red:Int = (pixel >> 16) & 0xFF; // RR 411 | var toReturn = [red, green, blue]; 412 | return toReturn; 413 | } 414 | 415 | // Returns the HTML report 416 | private function getReportHtml():String 417 | { 418 | // Failures first, because we care about fixing those 419 | var html:String = createTableFor(results.failedTests, "Failures"); 420 | html += createTableFor(results.passedTests, "Successes"); 421 | return html; 422 | } 423 | 424 | private function createTableFor(tests:Array, header:String):String 425 | { 426 | var html:String = '${tests.length} ${header} 427 | 428 | Expected (PNG) 429 | Actual (PNG) 430 | Diff Image 431 | Average Pixel Diff % 432 | '; 433 | 434 | for (test in tests) { 435 | var pngFile = test.fileName.replace('.svg', '-${test.expectedWidth}x${test.expectedHeight}.png'); 436 | var diffFile = test.fileName.replace('.svg', '-${test.expectedWidth}x${test.expectedHeight}-diff.png'); 437 | html += ' 438 | 439 | '; 440 | var diffFilePath = '${GENERATED_IMAGES_PATH}/${diffFile}'; 441 | #if sys 442 | if (sys.FileSystem.exists(diffFilePath)) 443 | { 444 | html += ''; 445 | } 446 | else 447 | { 448 | html += '(No difference)'; 449 | } 450 | #end 451 | 452 | html += '${test.diffPercentage * 100}%'; 453 | } 454 | html += ""; 455 | return html; 456 | } 457 | 458 | private function newSvgTest(fileName:String):SvgTest 459 | { 460 | return {fileName: fileName, 461 | expectedWidth: 0, expectedHeight: 0, 462 | diffPixels: null, diffPercentage: 0 }; 463 | } 464 | } 465 | 466 | /** 467 | * Encapsulates everything we need to test a single SVG 468 | */ 469 | typedef SvgTest = 470 | { 471 | // SVG filename, with extension (eg. sun.svg) 472 | var fileName:String; 473 | var expectedWidth:Int; 474 | var expectedHeight:Int; 475 | var diffPixels:BitmapData; 476 | var diffPercentage:Float; 477 | } 478 | 479 | typedef GenerationResults = 480 | { 481 | var passedTests:Array; 482 | var failedTests:Array; 483 | } 484 | -------------------------------------------------------------------------------- /test/src/TestMain.hx: -------------------------------------------------------------------------------- 1 | 2 | import openfl.display.Sprite; 3 | import utest.Runner; 4 | import utest.ui.Report; 5 | 6 | class TestMain extends Sprite 7 | { 8 | public function new() 9 | { 10 | super(); 11 | 12 | var runner = new Runner(); 13 | #if !(js && html5) 14 | runner.addCase(new MacroTest()); 15 | #end 16 | runner.addCase(new SvgGenerationTest()); 17 | 18 | Report.create(runner); 19 | 20 | #if (js && html5) 21 | runner.onComplete.add((results) -> { 22 | cast(js.Lib.global, js.html.Window).document.getElementById("openfl-content").style.display = "none"; 23 | cast(js.Lib.global, js.html.Window).document.body.style.overflow = "auto"; 24 | }); 25 | #end 26 | runner.run(); 27 | } 28 | } 29 | --------------------------------------------------------------------------------