├── .gitignore ├── objects ├── diamond.path ├── document.path ├── storage.path ├── cloud.path └── computer.path ├── logo.txt ├── Makefile ├── mk52.pl ├── LICENSE ├── tests └── odd_lines.txt ├── svg-path.lex ├── logo.svg ├── a2s ├── svg-path.y ├── colors.php ├── jlex.php ├── svg-path.lex.php ├── README.md ├── svg-path.php └── a2s52.php /.gitignore: -------------------------------------------------------------------------------- 1 | svg-path.h 2 | svg-path.out 3 | .*.swp 4 | -------------------------------------------------------------------------------- /objects/diamond.path: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /objects/document.path: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /objects/storage.path: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /logo.txt: -------------------------------------------------------------------------------- 1 | .-------------------------. 2 | |[Logo] | 3 | | .---.-. .-----. .-----. | 4 | | | .-. | +--> | | <--+ | 5 | | | '-' | | <--+ +--> | | 6 | | '---'-' '-----' '-----' | 7 | | ascii 2 svg | 8 | | | 9 | '-------------------------' 10 | https://9vx.org/~dho/a2s/ 11 | 12 | [Logo]: {"fill":"#88d","a2s:delref":true} 13 | -------------------------------------------------------------------------------- /objects/cloud.path: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: svg-path.lex svg-path.y 2 | ../lemon-php/lemon -lPHP svg-path.y 3 | java -cp ../jlexphp/JLexPHP.jar JLexPHP.Main svg-path.lex 4 | sed -e 's/Yylex/A2S_Yylex/' -e 's/JLexBase/A2S_JLexBase/' < svg-path.lex.php > .tmp.php 5 | mv .tmp.php svg-path.lex.php 6 | rm -f .tmp.php 7 | ./mk52.pl < ASCIIToSVG.php > a2s52.php 8 | php a2s -ilogo.txt -ologo.svg 9 | 10 | some: 11 | ./mk52.pl < ASCIIToSVG.php > a2s52.php 12 | php a2s -ilogo.txt -ologo.svg 13 | 14 | clean: 15 | rm -f svg-path.h svg-path.out 16 | 17 | distclean: 18 | rm -f svg-path.lex.php svg-path.php svg-path.h svg-path.out a2s52.php 19 | -------------------------------------------------------------------------------- /objects/computer.path: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mk52.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=2:sw=2:et: 3 | # Converts a PHP file with namespaces to a PHP 5.2 compatible form. 4 | # Reads a PHP file from stdin and emits the "fixed" version on stdout 5 | use strict; 6 | 7 | local $/ = undef; 8 | my $src = <>; 9 | 10 | # Comment out namespace specification 11 | $src =~ s/^namespace/#namespace/smg; 12 | 13 | 14 | # Find all class names, but ignore "class" in comments 15 | my $nocomments = $src; 16 | $nocomments =~ s,/\*.*?\*/,,smg; 17 | # For each class, fix the namespace up to a class prefix 18 | while ($nocomments =~ m/class\s+(\S+)/g) { 19 | my $class = $1; 20 | $src =~ s/\b$class\b/A2S_$class/g; 21 | } 22 | 23 | # Get rid of \A2S namespace usage for scanner / parser. 24 | $src =~ s/\\A2S_SVGPathParser/A2S_SVGPathParser/g; 25 | $src =~ s/\\A2S_Yylex/A2S_Yylex/g; 26 | 27 | # Not perfect, since it changes some things in comments in generated output 28 | print $src; 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2012 Devon H. O'Dell 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | o Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | o Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | POSSIBILITY OF SUCH DAMAGE. 25 | 26 | -------------------------------------------------------------------------------- /tests/odd_lines.txt: -------------------------------------------------------------------------------- 1 | blork! 2 | +---+ +---+ ^ ^ 3 | | | | | blork! -+| / 4 | --+ +---+ +--> | || / 5 | | <------+----++--> + 6 | .-.---+----. | || | 7 | | | | | v v+------> | 8 | | +---+----+ v 9 | | | v +------------> 10 | | | | .-------+--------. 11 | '-'--------' | some text | 12 | +----------------+ 13 | <--. | * more text | 14 | | | * other text | 15 | | | * that's all | 16 | '----------------' 17 | .---------. 18 | | | +------------------------------+ 19 | | | | .-------------. | + 20 | '---------' |[1] ^ | | / \ 21 | | \ .----' / \ 22 | + | \ | / + 23 | / \ #----. \'----. / 24 | / \ | \ | | \ / 25 | \ '----+---' | \ / 26 | \ \ +--+ ----+----- 27 | +------+-----------------+ \ / / 28 | | \ | \ / / 29 | +---- \ + / 30 | 31 | 32 | [1]: {"fill":"#933","a2s:delref":true} 33 | -------------------------------------------------------------------------------- /svg-path.lex: -------------------------------------------------------------------------------- 1 | [0|1] { return $this->createToken(A2S_SVGPathParser::TK_FLAG); } 17 | [+]?{D}+ { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 18 | "-"{D}+ { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 19 | [+]?{D}*"."{D}+({E})? { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 20 | [+]?{D}+"."{D}*({E})? { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 21 | "-"{D}*"."{D}+({E})? { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 22 | "-"{D}+"."{D}*({E})? { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 23 | [M|m] { return $this->createToken(A2S_SVGPathParser::TK_MCMD); } 24 | [Z|z] { return $this->createToken(A2S_SVGPathParser::TK_ZCMD); } 25 | [L|l] { return $this->createToken(A2S_SVGPathParser::TK_LCMD); } 26 | [H|h] { return $this->createToken(A2S_SVGPathParser::TK_HCMD); } 27 | [V|v] { return $this->createToken(A2S_SVGPathParser::TK_VCMD); } 28 | [Q|q] { return $this->createToken(A2S_SVGPathParser::TK_QCMD); } 29 | [C|c] { return $this->createToken(A2S_SVGPathParser::TK_CCMD); } 30 | [S|s] { return $this->createToken(A2S_SVGPathParser::TK_SCMD); } 31 | [T|t] { return $this->createToken(A2S_SVGPathParser::TK_TCMD); } 32 | [A|a] { return $this->createToken(A2S_SVGPathParser::TK_ACMD); } 33 | [ ,\t\v\n\f\r] { } 34 | . { } 35 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | https://9vx.org/~dho/a2s/ 72 | 73 | 74 | -------------------------------------------------------------------------------- /a2s: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 6 | * All rights reserved. 7 | * 8 | * Redistribution and use in source and binary forms, with or without 9 | * modification, are permitted provided that the following conditions 10 | * are met: 11 | * 12 | * o Redistributions of source code must retain the above copyright notice, 13 | * this list of conditions and the following disclaimer. 14 | * o Redistributions in binary form must reproduce the above copyright notice, 15 | * this list of conditions and the following disclaimer in the documentation 16 | * and/or other materials provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | * POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | namespace org\dh0\a2s; 32 | 33 | require_once 'ASCIIToSVG.php'; 34 | 35 | error_reporting(E_ALL|E_STRICT); 36 | 37 | $opts = getopt("bf::hi::o::s::"); 38 | 39 | if (isset($opts['h'])) { 40 | echo <<blurDropShadow = false; 114 | } 115 | 116 | if (isset($opts['f'])) { 117 | $o->fontFamily = $opts['f']; 118 | } 119 | 120 | $o->setDimensionScale($scale[0], $scale[1]); 121 | $o->parseGrid(); 122 | 123 | file_put_contents($outFile, $o->render()); 124 | 125 | /* vim:ts=2:sw=2:et: 126 | * * */ 127 | -------------------------------------------------------------------------------- /svg-path.y: -------------------------------------------------------------------------------- 1 | %name A2S_SVGPath 2 | %token_prefix TK_ 3 | %wildcard ANY. 4 | 5 | svg_path ::= moveto_drawto_command_groups. 6 | 7 | moveto_drawto_command_groups ::= moveto_drawto_command_groups moveto_drawto_command_group. 8 | moveto_drawto_command_groups ::= moveto_drawto_command_group. 9 | 10 | moveto_drawto_command_group ::= moveto drawto_commands. 11 | 12 | drawto_commands ::= drawto_commands drawto_command. 13 | drawto_commands ::= drawto_command. 14 | 15 | drawto_command ::= closepath. 16 | drawto_command ::= lineto. 17 | drawto_command ::= horizontal_lineto. 18 | drawto_command ::= vertical_lineto. 19 | drawto_command ::= curveto. 20 | drawto_command ::= smooth_curveto. 21 | drawto_command ::= quadratic_bezier_curveto. 22 | drawto_command ::= smooth_quadratic_bezier_curveto. 23 | drawto_command ::= elliptical_arc. 24 | 25 | moveto ::= MCMD(A) moveto_argument_sequence(B). 26 | { 27 | if (count(B) == 2) { 28 | $this->commands[] = array_merge(array(A), B); 29 | } else { 30 | if (A->value == 'm') { 31 | $arr = array ('value' => 'l'); 32 | } else { 33 | $arr = array ('value' => 'L'); 34 | } 35 | $c = array_splice(B, 2); 36 | $this->commands[] = array_merge(array(A), B); 37 | $this->commands[] = array_merge(array($arr), $c); 38 | } 39 | } 40 | 41 | moveto_argument_sequence(A) ::= lineto_argument_sequence(B) 42 | coordinate_pair(C). { A = array_merge(B, C); } 43 | moveto_argument_sequence(A) ::= coordinate_pair(B). { A = B; } 44 | 45 | closepath ::= ZCMD(A). { $this->commands[] = array(A); } 46 | 47 | lineto ::= LCMD(A) lineto_argument_sequence(B). 48 | { $this->commands[] = array_merge(array(A), B); } 49 | 50 | lineto_argument_sequence(A) ::= lineto_argument_sequence(B) coordinate_pair(C). 51 | { A = array_merge(B, C); } 52 | lineto_argument_sequence(A) ::= coordinate_pair(B). { A = B; } 53 | 54 | horizontal_lineto ::= HCMD(A) horizontal_lineto_argument_sequence(B). 55 | { $this->commands[] = array_merge(array(A), B); } 56 | 57 | horizontal_lineto_argument_sequence(A) ::= 58 | horizontal_lineto_argument_sequence(B) coordinate(C). 59 | { A = array_merge(B, array(C)); } 60 | horizontal_lineto_argument_sequence(A) ::= coordinate(B). { A = array(B); } 61 | 62 | vertical_lineto ::= VCMD(A) vertical_lineto_argument_sequence(B). 63 | { $this->commands[] = array_merge(array(A), B); } 64 | 65 | vertical_lineto_argument_sequence(A) ::= vertical_lineto_argument_sequence(B) 66 | coordinate(C). 67 | { A = array_merge(B, array(C)); } 68 | vertical_lineto_argument_sequence(A) ::= coordinate(B). { A = array(B); } 69 | 70 | curveto ::= CCMD(A) curveto_argument_sequence(B). 71 | { $this->commands[] = array_merge(array(A), B); } 72 | 73 | curveto_argument_sequence(A) ::= curveto_argument_sequence(B) 74 | curveto_argument(C). 75 | { A = array_merge(B, C); } 76 | curveto_argument_sequence(A) ::= curveto_argument(B). { A = B; } 77 | 78 | curveto_argument(A) ::= coordinate_pair(B) coordinate_pair(C) 79 | coordinate_pair(D). 80 | { A = array_merge(B, C, D); } 81 | 82 | smooth_curveto ::= SCMD(A) smooth_curveto_argument_sequence(B). 83 | { $this->commands[] = array_merge(array(A), B); } 84 | 85 | smooth_curveto_argument_sequence(A) ::= smooth_curveto_argument_sequence(B) 86 | smooth_curveto_argument(C). 87 | { A = array_merge(B, C); } 88 | smooth_curveto_argument_sequence(A) ::= smooth_curveto_argument(B). { A = B; } 89 | 90 | smooth_curveto_argument(A) ::= coordinate_pair(B) coordinate_pair(C). 91 | { A = array_merge(B, C); } 92 | 93 | quadratic_bezier_curveto ::= QCMD(A) 94 | quadratic_bezier_curveto_argument_sequence(B). 95 | { $this->commands[] = array_merge(array(A), B); } 96 | 97 | quadratic_bezier_curveto_argument_sequence(A) ::= 98 | quadratic_bezier_curveto_argument_sequence(B) 99 | quadratic_bezier_curveto_argument(C). 100 | { A = array_merge(B, C); } 101 | 102 | quadratic_bezier_curveto_argument_sequence(A) ::= 103 | quadratic_bezier_curveto_argument(B). 104 | { A = B; } 105 | 106 | quadratic_bezier_curveto_argument(A) ::= coordinate_pair(B) coordinate_pair(C). 107 | { A = array_merge(B, C); } 108 | 109 | smooth_quadratic_bezier_curveto ::= TCMD(A) 110 | smooth_quadratic_bezier_curveto_argument_sequence(B). 111 | { $this->commands[] = array_merge(array(A), B); } 112 | 113 | smooth_quadratic_bezier_curveto_argument_sequence(A) ::= 114 | smooth_quadratic_bezier_curveto_argument_sequence(B) 115 | coordinate_pair(C). 116 | { A = array_merge(B, C); } 117 | smooth_quadratic_bezier_curveto_argument_sequence(A) ::= coordinate_pair(B). 118 | { A = B; } 119 | 120 | elliptical_arc ::= ACMD(A) elliptical_arc_argument_sequence(B). 121 | { $this->commands[] = array_merge(array(A), B); } 122 | 123 | elliptical_arc_argument_sequence(A) ::= elliptical_arc_argument_sequence(B) 124 | elliptical_arc_argument(C). 125 | { A = array_merge(B, C); } 126 | elliptical_arc_argument_sequence(A) ::= elliptical_arc_argument(B). 127 | { A = B; } 128 | 129 | elliptical_arc_argument(A) ::= POSNUM(B) POSNUM(C) number(D) FLAG(E) FLAG(F) 130 | coordinate_pair(G). 131 | { A = array_merge(array(B, C, D, E, F), G); } 132 | 133 | coordinate_pair(A) ::= coordinate(B) coordinate(C). { A = array(B, C); } 134 | 135 | coordinate(A) ::= number(B). { A = B; } 136 | 137 | number(A) ::= POSNUM(B). { A = B; } 138 | number(A) ::= FLAG(B). { A = B; } 139 | number(A) ::= NEGNUM(B). { A = B; } 140 | 141 | -------------------------------------------------------------------------------- /colors.php: -------------------------------------------------------------------------------- 1 | "#cd5c5c", 5 | "light coral" => "#f08080", 6 | "salmon" => "#fa8072", 7 | "dark salmon" => "#e9967a", 8 | "light salmon" => "#ffa07a", 9 | "crimson" => "#dc143c", 10 | "red" => "#ff0000", 11 | "fire brick" => "#b22222", 12 | "dark red" => "#8b0000", 13 | 14 | "pink" => "#ffc0cb", 15 | "light pink" => "#ffb6c1", 16 | "hot pink" => "#ff69b4", 17 | "deep pink" => "#ff1493", 18 | "medium violet red" => "#c71585", 19 | "pale violet red" => "#db7093", 20 | 21 | "light salmon" => "#ffa07a", 22 | "coral" => "#ff7f50", 23 | "tomato" => "#ff6347", 24 | "orange red" => "#ff4500", 25 | "dark orange" => "#ff8c00", 26 | "orange" => "#ffa500", 27 | 28 | "gold" => "#ffd700", 29 | "yellow" => "#ffff00", 30 | "light yellow" => "#ffffe0", 31 | "lemon chiffon" => "#fffacd", 32 | "light goldenrod yellow" => "#fafad2", 33 | "papaya whip" => "#ffefd5", 34 | "moccasin" => "#ffe4b5", 35 | "peach puff" => "#ffdab9", 36 | "pale goldenrod" => "#eee8aa", 37 | "khaki" => "#f0e68c", 38 | "dark khaki" => "#bdb76b", 39 | 40 | "lavender" => "#e6e6fa", 41 | "thistle" => "#d8bfd8", 42 | "plum" => "#dda0dd", 43 | "violet" => "#ee82ee", 44 | "orchid" => "#da70d6", 45 | "fuchsia" => "#ff00ff", 46 | "magenta" => "#ff00ff", 47 | "medium orchid" => "#ba55d3", 48 | "medium purple" => "#9370db", 49 | "blue violet" => "#8a2be2", 50 | "dark violet" => "#9400d3", 51 | "dark orchid" => "#9932cc", 52 | "dark magenta" => "#8b008b", 53 | "purple" => "#800080", 54 | "indigo" => "#4b0082", 55 | "slate blue" => "#6a5acd", 56 | "dark slate blue" => "#483d8b", 57 | 58 | "green yellow" => "#adff2f", 59 | "chartreuse" => "#7fff00", 60 | "lawn green" => "#7cfc00", 61 | "lime" => "#00ff00", 62 | "lime green" => "#32cd32", 63 | "pale green" => "#98fb98", 64 | "light green" => "#90ee90", 65 | "medium spring green" => "#00fa9a", 66 | "spring green" => "#00ff7f", 67 | "medium sea green" => "#3cb371", 68 | "sea green" => "#2e8b57", 69 | "forest green" => "#228b22", 70 | "green" => "#008000", 71 | "dark green" => "#006400", 72 | "yellow green" => "#9acd32", 73 | "olive drab" => "#6b8e23", 74 | "olive" => "#808000", 75 | "dark olive green" => "#556b2f", 76 | "medium aquamarine" => "#66cdaa", 77 | "dark sea green" => "#8fbc8f", 78 | "light sea green" => "#20b2aa", 79 | "dark cyan" => "#008b8b", 80 | "teal" => "#008080", 81 | 82 | "aqua" => "#00ffff", 83 | "cyan" => "#00ffff", 84 | "light cyan" => "#e0ffff", 85 | "pale turquoise" => "#afeeee", 86 | "aquamarine" => "#7fffd4", 87 | "turquoise" => "#40e0d0", 88 | "medium turquoise" => "#48d1cc", 89 | "dark turquoise" => "#00ced1", 90 | "cadet blue" => "#5f9ea0", 91 | "steel blue" => "#4682b4", 92 | "light steel blue" => "#b0c4de", 93 | "powder blue" => "#b0e0e6", 94 | "light blue" => "#add8e6", 95 | "sky blue" => "#87ceeb", 96 | "light sky blue" => "#87cefa", 97 | "deep sky blue" => "#00bfff", 98 | "dodger blue" => "#1e90ff", 99 | "cornflower blue" => "#6495ed", 100 | "medium slate blue" => "#7b68ee", 101 | "royal blue" => "#4169e1", 102 | "blue" => "#0000ff", 103 | "medium blue" => "#0000cd", 104 | "dark blue" => "#00008b", 105 | "navy" => "#000080", 106 | "midnight blue" => "#191970", 107 | 108 | "cornsilk" => "#fff8dc", 109 | "blanched almond" => "#ffebcd", 110 | "bisque" => "#ffe4c4", 111 | "navajo white" => "#ffdead", 112 | "wheat" => "#f5deb3", 113 | "burly wood" => "#deb887", 114 | "tan" => "#d2b48c", 115 | "rosy brown" => "#bc8f8f", 116 | "sandy brown" => "#f4a460", 117 | "goldenrod" => "#daa520", 118 | "dark goldenrod" => "#b8860b", 119 | "peru" => "#cd853f", 120 | "chocolate" => "#d2691e", 121 | "saddle brown" => "#8b4513", 122 | "sienna" => "#a0522d", 123 | "brown" => "#a52a2a", 124 | "maroon" => "#800000", 125 | 126 | "white" => "#ffffff", 127 | "snow" => "#fffafa", 128 | "honeydew" => "#f0fff0", 129 | "mint cream" => "#f5fffa", 130 | "azure" => "#f0ffff", 131 | "alice blue" => "#f0f8ff", 132 | "ghost white" => "#f8f8ff", 133 | "white smoke" => "#f5f5f5", 134 | "seashell" => "#fff5ee", 135 | "beige" => "#f5f5dc", 136 | "old lace" => "#fdf5e6", 137 | "floral white" => "#fffaf0", 138 | "ivory" => "#fffff0", 139 | "antique white" => "#faebd7", 140 | "linen" => "#faf0e6", 141 | "lavender blush" => "#fff0f5", 142 | "misty rose" => "#ffe4e1", 143 | 144 | "gainsboro" => "#dcdcdc", 145 | "light grey" => "#d3d3d3", 146 | "silver" => "#c0c0c0", 147 | "dark gray" => "#a9a9a9", 148 | "gray" => "#808080", 149 | "dim gray" => "#696969", 150 | "light slate gray" => "#778899", 151 | "slate gray" => "#708090", 152 | "dark slate gray" => "#2f4f4f", 153 | "black" => "#000000" 154 | ); 155 | 156 | /* vim:ts=2:sw=2:et: 157 | * * */ 158 | -------------------------------------------------------------------------------- /jlex.php: -------------------------------------------------------------------------------- 1 | line = $line; 36 | $this->col = $col; 37 | $this->value = $value; 38 | $this->type = $type; 39 | } 40 | } 41 | 42 | class A2S_JLexBase { 43 | const YY_F = -1; 44 | const YY_NO_STATE = -1; 45 | const YY_NOT_ACCEPT = 0; 46 | const YY_START = 1; 47 | const YY_END = 2; 48 | const YY_NO_ANCHOR = 4; 49 | const YYEOF = -1; 50 | 51 | protected $YY_BOL; 52 | protected $YY_EOF; 53 | 54 | protected $yy_reader; 55 | protected $yy_buffer; 56 | protected $yy_buffer_read; 57 | protected $yy_buffer_index; 58 | protected $yy_buffer_start; 59 | protected $yy_buffer_end; 60 | protected $yychar = 0; 61 | protected $yycol = 0; 62 | protected $yyline = 0; 63 | protected $yy_at_bol; 64 | protected $yy_lexical_state; 65 | protected $yy_last_was_cr = false; 66 | protected $yy_count_lines = false; 67 | protected $yy_count_chars = false; 68 | protected $yyfilename = null; 69 | 70 | function __construct($stream) { 71 | $this->yy_reader = $stream; 72 | $meta = stream_get_meta_data($stream); 73 | if (!isset($meta['uri'])) { 74 | $this->yyfilename = '<>'; 75 | } else { 76 | $this->yyfilename = $meta['uri']; 77 | } 78 | 79 | $this->yy_buffer = ""; 80 | $this->yy_buffer_read = 0; 81 | $this->yy_buffer_index = 0; 82 | $this->yy_buffer_start = 0; 83 | $this->yy_buffer_end = 0; 84 | $this->yychar = 0; 85 | $this->yyline = 1; 86 | $this->yy_at_bol = true; 87 | } 88 | 89 | protected function yybegin($state) { 90 | $this->yy_lexical_state = $state; 91 | } 92 | 93 | protected function yy_advance() { 94 | if ($this->yy_buffer_index < $this->yy_buffer_read) { 95 | if (!isset($this->yy_buffer[$this->yy_buffer_index])) { 96 | return $this->YY_EOF; 97 | } 98 | return ord($this->yy_buffer[$this->yy_buffer_index++]); 99 | } 100 | if ($this->yy_buffer_start != 0) { 101 | /* shunt */ 102 | $j = $this->yy_buffer_read - $this->yy_buffer_start; 103 | $this->yy_buffer = substr($this->yy_buffer, $this->yy_buffer_start, $j); 104 | $this->yy_buffer_end -= $this->yy_buffer_start; 105 | $this->yy_buffer_start = 0; 106 | $this->yy_buffer_read = $j; 107 | $this->yy_buffer_index = $j; 108 | 109 | $data = fread($this->yy_reader, 8192); 110 | if ($data === false || !strlen($data)) return $this->YY_EOF; 111 | $this->yy_buffer .= $data; 112 | $this->yy_buffer_read .= strlen($data); 113 | } 114 | 115 | while ($this->yy_buffer_index >= $this->yy_buffer_read) { 116 | $data = fread($this->yy_reader, 8192); 117 | if ($data === false || !strlen($data)) return $this->YY_EOF; 118 | $this->yy_buffer .= $data; 119 | $this->yy_buffer_read .= strlen($data); 120 | } 121 | return ord($this->yy_buffer[$this->yy_buffer_index++]); 122 | } 123 | 124 | protected function yy_move_end() { 125 | if ($this->yy_buffer_end > $this->yy_buffer_start && 126 | $this->yy_buffer[$this->yy_buffer_end-1] == "\n") 127 | $this->yy_buffer_end--; 128 | if ($this->yy_buffer_end > $this->yy_buffer_start && 129 | $this->yy_buffer[$this->yy_buffer_end-1] == "\r") 130 | $this->yy_buffer_end--; 131 | } 132 | 133 | protected function yy_mark_start() { 134 | if ($this->yy_count_lines || $this->yy_count_chars) { 135 | if ($this->yy_count_lines) { 136 | for ($i = $this->yy_buffer_start; $i < $this->yy_buffer_index; ++$i) { 137 | if ("\n" == $this->yy_buffer[$i] && !$this->yy_last_was_cr) { 138 | ++$this->yyline; 139 | $this->yycol = 0; 140 | } 141 | if ("\r" == $this->yy_buffer[$i]) { 142 | ++$yyline; 143 | $this->yycol = 0; 144 | $this->yy_last_was_cr = true; 145 | } else { 146 | $this->yy_last_was_cr = false; 147 | } 148 | } 149 | } 150 | if ($this->yy_count_chars) { 151 | $this->yychar += $this->yy_buffer_index - $this->yy_buffer_start; 152 | $this->yycol += $this->yy_buffer_index - $this->yy_buffer_start; 153 | } 154 | } 155 | $this->yy_buffer_start = $this->yy_buffer_index; 156 | } 157 | 158 | protected function yy_mark_end() { 159 | $this->yy_buffer_end = $this->yy_buffer_index; 160 | } 161 | 162 | protected function yy_to_mark() { 163 | #echo "yy_to_mark: setting buffer index to ", $this->yy_buffer_end, "\n"; 164 | $this->yy_buffer_index = $this->yy_buffer_end; 165 | $this->yy_at_bol = ($this->yy_buffer_end > $this->yy_buffer_start) && 166 | ("\r" == $this->yy_buffer[$this->yy_buffer_end-1] || 167 | "\n" == $this->yy_buffer[$this->yy_buffer_end-1] || 168 | 2028 /* unicode LS */ == $this->yy_buffer[$this->yy_buffer_end-1] || 169 | 2029 /* unicode PS */ == $this->yy_buffer[$this->yy_buffer_end-1]); 170 | } 171 | 172 | protected function yytext() { 173 | return substr($this->yy_buffer, $this->yy_buffer_start, 174 | $this->yy_buffer_end - $this->yy_buffer_start); 175 | } 176 | 177 | protected function yylength() { 178 | return $this->yy_buffer_end - $this->yy_buffer_start; 179 | } 180 | 181 | static $yy_error_string = array( 182 | 'INTERNAL' => "Error: internal error.\n", 183 | 'MATCH' => "Error: Unmatched input.\n" 184 | ); 185 | 186 | protected function yy_error($code, $fatal) { 187 | print self::$yy_error_string[$code]; 188 | flush(); 189 | if ($fatal) throw new Exception("JLex fatal error " . self::$yy_error_string[$code]); 190 | } 191 | 192 | /* creates an annotated token */ 193 | function createToken($type = null) { 194 | if ($type === null) $type = $this->yytext(); 195 | $tok = new A2S_JLexToken($type); 196 | $this->annotateToken($tok); 197 | return $tok; 198 | } 199 | 200 | /* annotates a token with a value and source positioning */ 201 | function annotateToken(A2S_JLexToken $tok) { 202 | $tok->value = $this->yytext(); 203 | $tok->col = $this->yycol; 204 | $tok->line = $this->yyline; 205 | $tok->filename = $this->yyfilename; 206 | } 207 | } 208 | 209 | -------------------------------------------------------------------------------- /svg-path.lex.php: -------------------------------------------------------------------------------- 1 | yy_lexical_state = self::YYINITIAL; 22 | } 23 | 24 | const YYINITIAL = 0; 25 | static $yy_state_dtrans = array( 26 | 0 27 | ); 28 | static $yy_acpt = array( 29 | /* 0 */ self::YY_NOT_ACCEPT, 30 | /* 1 */ self::YY_NO_ANCHOR, 31 | /* 2 */ self::YY_NO_ANCHOR, 32 | /* 3 */ self::YY_NO_ANCHOR, 33 | /* 4 */ self::YY_NO_ANCHOR, 34 | /* 5 */ self::YY_NO_ANCHOR, 35 | /* 6 */ self::YY_NO_ANCHOR, 36 | /* 7 */ self::YY_NO_ANCHOR, 37 | /* 8 */ self::YY_NO_ANCHOR, 38 | /* 9 */ self::YY_NO_ANCHOR, 39 | /* 10 */ self::YY_NO_ANCHOR, 40 | /* 11 */ self::YY_NO_ANCHOR, 41 | /* 12 */ self::YY_NO_ANCHOR, 42 | /* 13 */ self::YY_NO_ANCHOR, 43 | /* 14 */ self::YY_NO_ANCHOR, 44 | /* 15 */ self::YY_NO_ANCHOR, 45 | /* 16 */ self::YY_NO_ANCHOR, 46 | /* 17 */ self::YY_NO_ANCHOR, 47 | /* 18 */ self::YY_NO_ANCHOR, 48 | /* 19 */ self::YY_NO_ANCHOR, 49 | /* 20 */ self::YY_NO_ANCHOR, 50 | /* 21 */ self::YY_NOT_ACCEPT, 51 | /* 22 */ self::YY_NO_ANCHOR, 52 | /* 23 */ self::YY_NO_ANCHOR, 53 | /* 24 */ self::YY_NO_ANCHOR, 54 | /* 25 */ self::YY_NO_ANCHOR, 55 | /* 26 */ self::YY_NO_ANCHOR, 56 | /* 27 */ self::YY_NO_ANCHOR, 57 | /* 28 */ self::YY_NOT_ACCEPT, 58 | /* 29 */ self::YY_NO_ANCHOR, 59 | /* 30 */ self::YY_NOT_ACCEPT, 60 | /* 31 */ self::YY_NO_ANCHOR, 61 | /* 32 */ self::YY_NOT_ACCEPT, 62 | /* 33 */ self::YY_NOT_ACCEPT, 63 | /* 34 */ self::YY_NOT_ACCEPT, 64 | /* 35 */ self::YY_NOT_ACCEPT, 65 | /* 36 */ self::YY_NOT_ACCEPT, 66 | /* 37 */ self::YY_NOT_ACCEPT, 67 | /* 38 */ self::YY_NOT_ACCEPT 68 | ); 69 | static $yy_cmap = array( 70 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 18, 18, 19, 18, 18, 19, 19, 19, 19, 19, 19, 71 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 18, 19, 19, 19, 19, 19, 19, 19, 72 | 19, 19, 19, 2, 18, 5, 6, 19, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 19, 19, 73 | 19, 19, 19, 19, 19, 17, 19, 14, 19, 7, 19, 19, 11, 19, 19, 19, 10, 8, 19, 19, 74 | 19, 13, 19, 15, 16, 19, 12, 19, 19, 19, 9, 19, 19, 19, 19, 19, 19, 17, 19, 14, 75 | 19, 7, 19, 19, 11, 19, 19, 19, 10, 8, 19, 19, 19, 13, 19, 15, 16, 19, 12, 19, 76 | 19, 19, 9, 19, 1, 19, 19, 19, 0, 0,); 77 | 78 | static $yy_rmap = array( 79 | 0, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 5, 6, 7, 80 | 8, 9, 3, 10, 11, 12, 13, 14, 15, 9, 16, 1, 17, 11, 18, 19, 12, 13, 14,); 81 | 82 | static $yy_nxt = array( 83 | array( 84 | 1, 2, 3, 22, 4, 23, 29, 31, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 31, 85 | 86 | ), 87 | array( 88 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 89 | 90 | ), 91 | array( 92 | -1, -1, -1, 4, 4, -1, 21, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 93 | 94 | ), 95 | array( 96 | -1, -1, -1, 4, 4, -1, 16, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 97 | 98 | ), 99 | array( 100 | -1, -1, -1, 18, 18, -1, -1, 30, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 101 | 102 | ), 103 | array( 104 | -1, -1, -1, 17, 17, -1, 19, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 105 | 106 | ), 107 | array( 108 | -1, -1, -1, 18, 18, -1, -1, 32, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 109 | 110 | ), 111 | array( 112 | -1, -1, -1, 20, 20, -1, -1, 34, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 113 | 114 | ), 115 | array( 116 | -1, -1, -1, 20, 20, -1, -1, 35, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 117 | 118 | ), 119 | array( 120 | -1, -1, -1, 18, 18, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 121 | 122 | ), 123 | array( 124 | -1, -1, -1, 17, 17, -1, 28, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 125 | 126 | ), 127 | array( 128 | -1, -1, -1, 24, 24, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 129 | 130 | ), 131 | array( 132 | -1, -1, -1, 25, 25, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 133 | 134 | ), 135 | array( 136 | -1, -1, -1, 26, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 137 | 138 | ), 139 | array( 140 | -1, -1, -1, 27, 27, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 141 | 142 | ), 143 | array( 144 | -1, -1, -1, 20, 20, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 145 | 146 | ), 147 | array( 148 | -1, -1, 33, 24, 24, 33, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 149 | 150 | ), 151 | array( 152 | -1, -1, 36, 25, 25, 36, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 153 | 154 | ), 155 | array( 156 | -1, -1, 37, 26, 26, 37, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 157 | 158 | ), 159 | array( 160 | -1, -1, 38, 27, 27, 38, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 161 | 162 | ), 163 | ); 164 | 165 | public function /*Yytoken*/ nextToken () 166 | { 167 | $yy_anchor = self::YY_NO_ANCHOR; 168 | $yy_state = self::$yy_state_dtrans[$this->yy_lexical_state]; 169 | $yy_next_state = self::YY_NO_STATE; 170 | $yy_last_accept_state = self::YY_NO_STATE; 171 | $yy_initial = true; 172 | 173 | $this->yy_mark_start(); 174 | $yy_this_accept = self::$yy_acpt[$yy_state]; 175 | if (self::YY_NOT_ACCEPT != $yy_this_accept) { 176 | $yy_last_accept_state = $yy_state; 177 | $this->yy_mark_end(); 178 | } 179 | while (true) { 180 | if ($yy_initial && $this->yy_at_bol) $yy_lookahead = self::YY_BOL; 181 | else $yy_lookahead = $this->yy_advance(); 182 | $yy_next_state = self::$yy_nxt[self::$yy_rmap[$yy_state]][self::$yy_cmap[$yy_lookahead]]; 183 | if ($this->YY_EOF == $yy_lookahead && true == $yy_initial) { 184 | return null; 185 | } 186 | if (self::YY_F != $yy_next_state) { 187 | $yy_state = $yy_next_state; 188 | $yy_initial = false; 189 | $yy_this_accept = self::$yy_acpt[$yy_state]; 190 | if (self::YY_NOT_ACCEPT != $yy_this_accept) { 191 | $yy_last_accept_state = $yy_state; 192 | $this->yy_mark_end(); 193 | } 194 | } 195 | else { 196 | if (self::YY_NO_STATE == $yy_last_accept_state) { 197 | throw new Exception("Lexical Error: Unmatched Input."); 198 | } 199 | else { 200 | $yy_anchor = self::$yy_acpt[$yy_last_accept_state]; 201 | if (0 != (self::YY_END & $yy_anchor)) { 202 | $this->yy_move_end(); 203 | } 204 | $this->yy_to_mark(); 205 | switch ($yy_last_accept_state) { 206 | case 1: 207 | 208 | case -2: 209 | break; 210 | case 2: 211 | { return $this->createToken(A2S_SVGPathParser::TK_FLAG); } 212 | case -3: 213 | break; 214 | case 3: 215 | { } 216 | case -4: 217 | break; 218 | case 4: 219 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 220 | case -5: 221 | break; 222 | case 5: 223 | { return $this->createToken(A2S_SVGPathParser::TK_MCMD); } 224 | case -6: 225 | break; 226 | case 6: 227 | { return $this->createToken(A2S_SVGPathParser::TK_ZCMD); } 228 | case -7: 229 | break; 230 | case 7: 231 | { return $this->createToken(A2S_SVGPathParser::TK_LCMD); } 232 | case -8: 233 | break; 234 | case 8: 235 | { return $this->createToken(A2S_SVGPathParser::TK_HCMD); } 236 | case -9: 237 | break; 238 | case 9: 239 | { return $this->createToken(A2S_SVGPathParser::TK_VCMD); } 240 | case -10: 241 | break; 242 | case 10: 243 | { return $this->createToken(A2S_SVGPathParser::TK_QCMD); } 244 | case -11: 245 | break; 246 | case 11: 247 | { return $this->createToken(A2S_SVGPathParser::TK_CCMD); } 248 | case -12: 249 | break; 250 | case 12: 251 | { return $this->createToken(A2S_SVGPathParser::TK_SCMD); } 252 | case -13: 253 | break; 254 | case 13: 255 | { return $this->createToken(A2S_SVGPathParser::TK_TCMD); } 256 | case -14: 257 | break; 258 | case 14: 259 | { return $this->createToken(A2S_SVGPathParser::TK_ACMD); } 260 | case -15: 261 | break; 262 | case 15: 263 | { } 264 | case -16: 265 | break; 266 | case 16: 267 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 268 | case -17: 269 | break; 270 | case 17: 271 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 272 | case -18: 273 | break; 274 | case 18: 275 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 276 | case -19: 277 | break; 278 | case 19: 279 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 280 | case -20: 281 | break; 282 | case 20: 283 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 284 | case -21: 285 | break; 286 | case 22: 287 | { return $this->createToken(A2S_SVGPathParser::TK_FLAG); } 288 | case -22: 289 | break; 290 | case 23: 291 | { } 292 | case -23: 293 | break; 294 | case 24: 295 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 296 | case -24: 297 | break; 298 | case 25: 299 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 300 | case -25: 301 | break; 302 | case 26: 303 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 304 | case -26: 305 | break; 306 | case 27: 307 | { return $this->createToken(A2S_SVGPathParser::TK_POSNUM); } 308 | case -27: 309 | break; 310 | case 29: 311 | { } 312 | case -28: 313 | break; 314 | case 31: 315 | { } 316 | case -29: 317 | break; 318 | default: 319 | $this->yy_error('INTERNAL',false); 320 | case -1: 321 | } 322 | $yy_initial = true; 323 | $yy_state = self::$yy_state_dtrans[$this->yy_lexical_state]; 324 | $yy_next_state = self::YY_NO_STATE; 325 | $yy_last_accept_state = self::YY_NO_STATE; 326 | $this->yy_mark_start(); 327 | $yy_this_accept = self::$yy_acpt[$yy_state]; 328 | if (self::YY_NOT_ACCEPT != $yy_this_accept) { 329 | $yy_last_accept_state = $yy_state; 330 | $this->yy_mark_end(); 331 | } 332 | } 333 | } 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASCIIToSVG ################################################################ 2 | 3 | .-------------------------. 4 | | | 5 | | .---.-. .-----. .-----. | 6 | | | .-. | +--> | | <--| | 7 | | | '-' | | <--| +--> | | 8 | | '---'-' '-----' '-----' | 9 | | ascii 2 svg | 10 | | | 11 | '-------------------------' 12 | https://9vx.org/~dho/a2s/ 13 | 14 | # WARNING ################################################################### 15 | 16 | I haven't written PHP in years, and this implementation has numerous 17 | drawbacks. I recommend you look into using the 18 | [Go implementation](https://github.com/asciitosvg/asciitosvg) instead. I will 19 | continue to accept, review, and merge PRs to this repository, and (where 20 | appropriate) mirror any new functionality in the Go version, but I do not 21 | intend to identify new bugs, fux old bugs, or self-author new functionality 22 | in this repository. 23 | 24 | ## Introduction ############################################################# 25 | 26 | ### License ################################################################# 27 | 28 | ASCIIToSVG is copyright © 2012 Devon H. O'Dell and is distributed under an 29 | awesome 2-clause BSD license. All code without explicit copyright retains 30 | this license. Any code not adhering to this license is explicit in its own 31 | copyright. 32 | 33 | ### What does it do? ######################################################## 34 | 35 | ASCIIToSVG is a pretty simple PHP library for parsing ASCII art diagrams and 36 | converting them to a pretty SVG output format. 37 | 38 | ### But... why? ############################################################# 39 | 40 | There are a few reasons, mostly to do with things I really like: 41 | 42 | * I like markdown a lot. If I could write everything in markdown, I 43 | probably would. 44 | * I like ASCII art a lot. I've been playing around with drawing ASCII art 45 | since I was both a wee lad and a BBS user. 46 | * I like pretty pictures. People say they're worth a thousand words. 47 | 48 | So I thought, "What if I could combine all these things and start writing 49 | markdown documents for my technical designs -- complete with ASCII art 50 | diagrams -- that I could then prettify for presentation purposes?" 51 | 52 | ### Aren't there already things that do this? ############################### 53 | 54 | Well, yes. There is a project called [ditaa][1] that has this functionality. 55 | Sort of. But it's written in Java and it generates PNG output. I needed 56 | something that would generate scalable output, and would be a bit easier to 57 | integrate into a wiki (like the one in [mtrack][2], which we use at work). 58 | We got it integrated, but I ran into a few bugs and rendering nits that I 59 | didn't like. Additionally, it takes a long time to shell out the process 60 | and JVM to generate the PNG. If you're doing inline edits on an image, you 61 | can imagine that this gets to be a heavy load on both the client and 62 | server, just from a data transfer perspective alone. 63 | 64 | So I reimplemented it in PHP. There are some things it can do better, and 65 | I'm aware of a few rendering bugs already, but so far it works better and 66 | is more extensible than ditaa, and I'm enjoying it more. 67 | 68 | ## Building ASCIIToSVG ###################################################### 69 | 70 | What?! Building?!?! Isn't this PHP? 71 | 72 | Well, yes. But some of it is tool-generated. If you check out the code, you 73 | will get the latest generated code, don't worry! This information is more 74 | for people who want to contribute and need to make changes to the SVG path 75 | parser. 76 | 77 | You will need [JLexPHP][7] and [lemon-php][8] to build the tokenizer and 78 | parser for the SVG paths. (This parser was implemented [from the SVG 1.1 79 | paths BNF][9].) JLexPHP requires the JDK to build; lemon-php requires a C 80 | compiler. 81 | 82 | You should have the following structure one your repository: 83 | 84 | . 85 | ├── JLexPHP 86 | ├── asciitosvg 87 | └── lemon-php 88 | 89 | Simply running `make` on asciitosvg should be enough to get all the autogenerated 90 | stuff running. It will also update the logo. Please make sure to view the logo and 91 | test files to make sure that you haven't broken any rendering. 92 | 93 | Make clean will remove extraneous files that aren't necessary for running; 94 | make distclean removes everything autogenerated. 95 | 96 | ## Using ASCIIToSVG ######################################################### 97 | 98 | ### Command Line ############################################################ 99 | 100 | A command line utility exists to convert the ASCII file to SVG. You can put 101 | this somewhere inside your `$PATH` and set its permissions to +x. You can also symlink 102 | to it by adding `ln -s /Users/asciitosvg/a2s a2s` in `/usr/local/bin`. 103 | Windows users will have to make their own batch file to run this or remove the first 104 | line. Usage: 105 | 106 | Usage: a2s [-i[-|filename]] [-o[-|filename]] [-sx-scale,y-scale] 107 | -h: This usage screen. 108 | -i: Path to input text file. If unspecified, or set to "-" (hyphen), 109 | stdin is used. 110 | -o: Path to output SVG file. If unspecified or set to "-" (hyphen), 111 | stdout is used. 112 | -s: Grid scale in pixels. If unspecified, each grid unit on the X 113 | axis is set to 9 pixels; each grid unit on the Y axis is 16 pixels. 114 | 115 | Note that this uses PHP's `getopt` which handles short options stupidly. You 116 | need to put the actual argument right next to the flag; no space. For 117 | example: `a2s -i- -oout.svg -s10,17` would be a valid command line 118 | invocation for this utility. 119 | 120 | ### Class API ############################################################### 121 | 122 | There are plenty of comments explaining how everything works (and I'm sure 123 | several bugs negating the truth of some of those explanations) in the source 124 | file. Basic flow through the API is: 125 | 126 | :::php 127 | $asciiDiagram = file_get_contents('some_pretty_art'); 128 | $o = new org\dh0\a2s\ASCIIToSVG($asciiDiagram); 129 | $o->setDimensionScale(9, 16); 130 | $o->parseGrid(); 131 | file_put_contents('some_pretty_art.svg', $o->render()); 132 | 133 | I'll put more detailed information in the [wiki][3] and this `README` as I 134 | have time and inclination to do so. 135 | 136 | ## How Do I Draw? ########################################################### 137 | 138 | Enough yammering about the impetus, code, and functionality. I bet you want 139 | to draw something. ASCIIToSVG supports a few different ways to do that. If 140 | you have more questions, take a look at some of the files in the `test` 141 | subdirectory. (If you have a particularly nice diagram you'd like to see 142 | included for demo, feel free to send it my way!) 143 | 144 | ### Basics: Boxes and Lines ################################################# 145 | 146 | Boxes can be polygons of almost any shape (don't try anything too wacky 147 | just yet; I haven't). Horizontal, vertical, and diagonal lines are all 148 | supported, giving ASCIIToSVG basically complete support for output from 149 | [App::Asciio][4]. Edges of boxes and lines can be drawn by using the 150 | following characters: 151 | 152 | * `-` or `=`: Horizontal lines (dash, equals) 153 | * `|` or `:`: Vertical line (pipe, colon) 154 | * `*`: Line of ambiguous direction (asterisk) 155 | * `\` or `/`: Diagonal line (backward slash, forward slash) 156 | * `+`: Edge of line passing through a box or line corner (plus) 157 | 158 | The existence of a `:` or `=` edge on a line causes that line to be 159 | rendered as a dashed line. 160 | 161 | To draw a box or turn a line, you need to specify corners. The following 162 | characters are valid corner characters: 163 | 164 | * `'` and `.`: Quadratic Bézier corners (apostrophe, period) 165 | * `#`: Boxed, angular, 90 degree corners for boxes (hash) 166 | * `+`: Boxed, angular, 90 degree corners for lines (plus) 167 | 168 | The `+` token is a control point for lines. It denotes an area where a line 169 | intersects another line or traverses a box boundary. 170 | 171 | A simple box with 3 rounded corners and a line pointing at it: 172 | 173 | #----------. 174 | | | <--------- 175 | '----------' 176 | 177 | #### A Quick Note on Diagonals ############################################## 178 | 179 | Diagonal lines cannot be made to form a closed box. If you would like to 180 | make diagrams using parallelograms / diamonds (perhaps for flow charts), it 181 | is recommended that you annotate regular boxes and use custom objects (see 182 | below) instead. Diagonal lines also may not use `'` or `.` smoothed corners. 183 | 184 | ### Basics: Markers ######################################################### 185 | 186 | Markers can be attached at the end of a line to give it a nice arrow by 187 | using one of the following characters: 188 | 189 | * `<`: Left marker (less than) 190 | * `>`: Right marker (greater than) 191 | * `^`: Up marker (carat) 192 | * `v`: Down marker (letter v) 193 | 194 | ### Basics: Text ############################################################ 195 | 196 | Text can be inserted at almost any point in the image. Text is rendered in 197 | a monospaced font. There aren't many restrictions, but obviously anything 198 | you type that looks like it's actually a line or a box is going to be a good 199 | candidate for turning into some pretty SVG path. Here is a box with some 200 | plain black text in it: 201 | 202 | .-------------------------------------. 203 | | Hello here and there and everywhere | 204 | '-------------------------------------' 205 | 206 | ### Basics: Formatting ##################################################### 207 | 208 | It's possible to change the format of any boxes / polygons you create. This 209 | is done (true to markdown form) by providing a reference on the top left 210 | edge of the object, and defining that reference at the bottom of the input. 211 | Let me reiterate that the references *must* be defined at the *bottom* of 212 | the input. An example: 213 | 214 | .-------------. .--------------. 215 | |[Red Box] | |[Blue Box] | 216 | '-------------' '--------------' 217 | 218 | [Red Box]: {"fill":"#aa4444"} 219 | [Blue Box]: {"fill":"#ccccff"} 220 | 221 | Text appearing within a stylized box automatically tries to fix the color 222 | contrast if the black text would be too dark on the background. The 223 | reference commands can take any valid SVG properties / settings for a 224 | [path element][5]. The commands are specified in JSON form, one per line. 225 | Reference commands do not accept nested JSON objects -- don't try to 226 | place additional curly braces inside! 227 | 228 | The text of a reference is left in the polygon, making it useful as an 229 | object title. You can have the reference removed by adding an option 230 | `a2s:delref` to the options JSON object. Any value will suffice to have 231 | the reference fully removed from the polygon. You can also replace the 232 | text in the label by specifying `a2s:label`. 233 | 234 | This method is (in my opinion) much nicer than the one provided by ditaa: 235 | 236 | * It eats up less space. 237 | * It fits in smaller boxes. 238 | * It allows more customization. 239 | * It's easier to see what formatting changes happened in diffs. 240 | * It's easier to read any text nested in the box itself. 241 | 242 | But that's just me. If you have thoughts on how to do this for lines, 243 | please do let me know. 244 | 245 | #### Special References ##################################################### 246 | 247 | It is possible to reference an object by its starting row and column. The 248 | rows and columns start at (0, 0) in the top left of the diagram. Such 249 | references should only be made for stable diagrams, and only if you *really* 250 | need to style text or a line in some particular way. Such references are 251 | marked by starting the line with `[R,C]` where `R` is the numeric row and 252 | `C` is the numeric column. 253 | 254 | Note that boxes start from their top left corner. Text starts from the first 255 | non-whitespace character on a line (and only traverses a single space in 256 | between text points). Lines start from the left-most point on the line that 257 | is not adjoined to any other objects. 258 | 259 | ### Basics: Special Objects ################################################# 260 | 261 | There was some pretty nifty functionality in ditaa for special box types. 262 | The ones I was most interested in were the `storage` and `document` box, 263 | though I recently learned there are more than are advertised on the website. 264 | To do this, one adds an `a2s:type` option to the box and specifies one of 265 | the supported object types: 266 | 267 | * `storage`: The standard "storage" cylinder. 268 | * `document`: The standard document box with a wavy bottom. 269 | * `cloud`: A network cloud. 270 | * `computer`: Something that looks kind of like an iMac. 271 | 272 | ### Creating Special Objects ################################################ 273 | 274 | Although a tutorial on SVG is out of the scope of this document, it is 275 | possible to create your own custom objects for your own graphs! Fire up your 276 | favorite vector editor that is capable of producing SVG output (if you don't 277 | have one, [Inkscape][10] will work fine). Draw to your heart's content and 278 | save your drawing as an SVG image (you will need to use the path tool; any 279 | premade shapes aren't supported). Open the image and extract all the path 280 | tags; put them in a file called `object.path`. Place this file in the 281 | `objects` directory. Any time you reference `"a2s:type":"object"`, your new 282 | object will take the place of the box you created. 283 | 284 | There are a few caveats to this approach: 285 | 286 | * You *must* specify the width and height of the original canvas in every 287 | path, although there are no minimum or maximum values, nor is there any 288 | required aspect ratio. 289 | * The path (or any group it is in) of the original SVG file must not be 290 | transformed in any way. (Inkscape will do this if you change your 291 | document size after you start drawing, for instance.) 292 | * Extraneous tags (i.e. things that aren't ``) will probably 293 | break the object parser. 294 | * Objects are cached in `$TMPDIR/.a2s.objcache`. If multiple users are 295 | using ASCIIToSVG on the same machine, this probably isn't very safe. 296 | * Reference options are currently only applied to the first path; 297 | additional paths are drawn with the default options of the group. This 298 | means you probably want your first object to be an outline. 299 | 300 | If you're particularly happy with an object and want to share it, feel free 301 | to submit it! 302 | 303 | ## External Resources ####################################################### 304 | 305 | There are some interesting sites that you might be interested in; these are 306 | mildly related to the goals of ASCIIToSVG: 307 | 308 | * [ditaa][1] (previously mentioned) is a Java utility with a very similar 309 | syntax that translates ASCII diagrams into PNG files. 310 | * [App::Asciio][4] (previously mentioned) allows you to programmatically 311 | draw ASCII diagrams. 312 | * [Asciiflow][6] is a web front-end to designing ASCII flowcharts. 313 | 314 | If you have something really cool that is related to ASCIIToSVG, and I have 315 | failed to list it here, do please let me know. 316 | 317 | ## References ############################################################### 318 | 319 | If there's nothing here, you're looking at this README post-markdown-ified. 320 | 321 | [1]: http://ditaa.sourceforge.net/ "ditaa - DIagrams Through ASCII Art" 322 | [2]: http://mtrack.wezfurlong.org/ "mtrack project management software" 323 | [3]: https://bitbucket.org/dhobsd/asciitosvg/wiki/Home "a2s wiki page" 324 | [4]: http://search.cpan.org/dist/App-Asciio/lib/App/Asciio.pm "App::Asciio" 325 | [5]: http://www.w3.org/TR/SVG/paths.html "SVG Paths" 326 | [6]: http://www.asciiflow.com/ "Asciiflow" 327 | [7]: http://bitbucket.org/wez/jlexphp "JLexPHP" 328 | [8]: https://bitbucket.org/wez/lemon-php "lemon-php" 329 | [9]: http://www.w3.org/TR/SVG/paths.html#PathDataBNF "SVG Path Grammar" 330 | [10]: http://inkscape.org/download "Download Inkscape" 331 | -------------------------------------------------------------------------------- /svg-path.php: -------------------------------------------------------------------------------- 1 | 240 | **
  • A FILE* to which trace output should be written. 241 | ** If NULL, then tracing is turned off. 242 | **
  • A prefix string written at the beginning of every 243 | ** line of trace output. If NULL, then tracing is 244 | ** turned off. 245 | ** 246 | ** 247 | ** Outputs: 248 | ** None. 249 | */ 250 | function A2S_SVGPathTrace(/* stream */ $TraceFILE, /* string */ $zTracePrompt){ 251 | $this->yyTraceFILE = $TraceFILE; 252 | $this->yyTracePrompt = $zTracePrompt; 253 | if( $this->yyTraceFILE===null ) $this->yyTracePrompt = null; 254 | else if( $this->yyTracePrompt===null ) $this->yyTraceFILE = null; 255 | } 256 | 257 | /* For tracing shifts, the names of all terminals and nonterminals 258 | ** are required. The following table supplies these names */ 259 | static $yyTokenName = array( 260 | '$', 'ANY', 'MCMD', 'ZCMD', 261 | 'LCMD', 'HCMD', 'VCMD', 'CCMD', 262 | 'SCMD', 'QCMD', 'TCMD', 'ACMD', 263 | 'POSNUM', 'FLAG', 'NEGNUM', 'error', 264 | 'svg_path', 'moveto_drawto_command_groups', 'moveto_drawto_command_group', 'moveto', 265 | 'drawto_commands', 'drawto_command', 'closepath', 'lineto', 266 | 'horizontal_lineto', 'vertical_lineto', 'curveto', 'smooth_curveto', 267 | 'quadratic_bezier_curveto', 'smooth_quadratic_bezier_curveto', 'elliptical_arc', 'moveto_argument_sequence', 268 | 'lineto_argument_sequence', 'coordinate_pair', 'horizontal_lineto_argument_sequence', 'coordinate', 269 | 'vertical_lineto_argument_sequence', 'curveto_argument_sequence', 'curveto_argument', 'smooth_curveto_argument_sequence', 270 | 'smooth_curveto_argument', 'quadratic_bezier_curveto_argument_sequence', 'quadratic_bezier_curveto_argument', 'smooth_quadratic_bezier_curveto_argument_sequence', 271 | 'elliptical_arc_argument_sequence', 'elliptical_arc_argument', 'number', 272 | ); 273 | 274 | /* For tracing reduce actions, the names of all rules are required. 275 | */ 276 | static $yyRuleName = array( 277 | /* 0 */ "svg_path ::= moveto_drawto_command_groups", 278 | /* 1 */ "moveto_drawto_command_groups ::= moveto_drawto_command_groups moveto_drawto_command_group", 279 | /* 2 */ "moveto_drawto_command_groups ::= moveto_drawto_command_group", 280 | /* 3 */ "moveto_drawto_command_group ::= moveto drawto_commands", 281 | /* 4 */ "drawto_commands ::= drawto_commands drawto_command", 282 | /* 5 */ "drawto_commands ::= drawto_command", 283 | /* 6 */ "drawto_command ::= closepath", 284 | /* 7 */ "drawto_command ::= lineto", 285 | /* 8 */ "drawto_command ::= horizontal_lineto", 286 | /* 9 */ "drawto_command ::= vertical_lineto", 287 | /* 10 */ "drawto_command ::= curveto", 288 | /* 11 */ "drawto_command ::= smooth_curveto", 289 | /* 12 */ "drawto_command ::= quadratic_bezier_curveto", 290 | /* 13 */ "drawto_command ::= smooth_quadratic_bezier_curveto", 291 | /* 14 */ "drawto_command ::= elliptical_arc", 292 | /* 15 */ "moveto ::= MCMD moveto_argument_sequence", 293 | /* 16 */ "moveto_argument_sequence ::= lineto_argument_sequence coordinate_pair", 294 | /* 17 */ "moveto_argument_sequence ::= coordinate_pair", 295 | /* 18 */ "closepath ::= ZCMD", 296 | /* 19 */ "lineto ::= LCMD lineto_argument_sequence", 297 | /* 20 */ "lineto_argument_sequence ::= lineto_argument_sequence coordinate_pair", 298 | /* 21 */ "lineto_argument_sequence ::= coordinate_pair", 299 | /* 22 */ "horizontal_lineto ::= HCMD horizontal_lineto_argument_sequence", 300 | /* 23 */ "horizontal_lineto_argument_sequence ::= horizontal_lineto_argument_sequence coordinate", 301 | /* 24 */ "horizontal_lineto_argument_sequence ::= coordinate", 302 | /* 25 */ "vertical_lineto ::= VCMD vertical_lineto_argument_sequence", 303 | /* 26 */ "vertical_lineto_argument_sequence ::= vertical_lineto_argument_sequence coordinate", 304 | /* 27 */ "vertical_lineto_argument_sequence ::= coordinate", 305 | /* 28 */ "curveto ::= CCMD curveto_argument_sequence", 306 | /* 29 */ "curveto_argument_sequence ::= curveto_argument_sequence curveto_argument", 307 | /* 30 */ "curveto_argument_sequence ::= curveto_argument", 308 | /* 31 */ "curveto_argument ::= coordinate_pair coordinate_pair coordinate_pair", 309 | /* 32 */ "smooth_curveto ::= SCMD smooth_curveto_argument_sequence", 310 | /* 33 */ "smooth_curveto_argument_sequence ::= smooth_curveto_argument_sequence smooth_curveto_argument", 311 | /* 34 */ "smooth_curveto_argument_sequence ::= smooth_curveto_argument", 312 | /* 35 */ "smooth_curveto_argument ::= coordinate_pair coordinate_pair", 313 | /* 36 */ "quadratic_bezier_curveto ::= QCMD quadratic_bezier_curveto_argument_sequence", 314 | /* 37 */ "quadratic_bezier_curveto_argument_sequence ::= quadratic_bezier_curveto_argument_sequence quadratic_bezier_curveto_argument", 315 | /* 38 */ "quadratic_bezier_curveto_argument_sequence ::= quadratic_bezier_curveto_argument", 316 | /* 39 */ "quadratic_bezier_curveto_argument ::= coordinate_pair coordinate_pair", 317 | /* 40 */ "smooth_quadratic_bezier_curveto ::= TCMD smooth_quadratic_bezier_curveto_argument_sequence", 318 | /* 41 */ "smooth_quadratic_bezier_curveto_argument_sequence ::= smooth_quadratic_bezier_curveto_argument_sequence coordinate_pair", 319 | /* 42 */ "smooth_quadratic_bezier_curveto_argument_sequence ::= coordinate_pair", 320 | /* 43 */ "elliptical_arc ::= ACMD elliptical_arc_argument_sequence", 321 | /* 44 */ "elliptical_arc_argument_sequence ::= elliptical_arc_argument_sequence elliptical_arc_argument", 322 | /* 45 */ "elliptical_arc_argument_sequence ::= elliptical_arc_argument", 323 | /* 46 */ "elliptical_arc_argument ::= POSNUM POSNUM number FLAG FLAG coordinate_pair", 324 | /* 47 */ "coordinate_pair ::= coordinate coordinate", 325 | /* 48 */ "coordinate ::= number", 326 | /* 49 */ "number ::= POSNUM", 327 | /* 50 */ "number ::= FLAG", 328 | /* 51 */ "number ::= NEGNUM", 329 | ); 330 | 331 | /* 332 | ** This function returns the symbolic name associated with a token 333 | ** value. 334 | */ 335 | function A2S_SVGPathTokenName(/* int */ $tokenType){ 336 | if (isset(self::$yyTokenName[$tokenType])) 337 | return self::$yyTokenName[$tokenType]; 338 | return "Unknown"; 339 | } 340 | 341 | /* The following function deletes the value associated with a 342 | ** symbol. The symbol can be either a terminal or nonterminal. 343 | ** "yymajor" is the symbol code, and "yypminor" is a pointer to 344 | ** the value. 345 | */ 346 | private function yy_destructor($yymajor, $yypminor){ 347 | switch( $yymajor ){ 348 | /* Here is inserted the actions which take place when a 349 | ** terminal or non-terminal is destroyed. This can happen 350 | ** when the symbol is popped from the stack during a 351 | ** reduce or during error processing or when a parser is 352 | ** being destroyed before it is finished parsing. 353 | ** 354 | ** Note: during a reduce, the only symbols destroyed are those 355 | ** which appear on the RHS of the rule, but which are not used 356 | ** inside the C code. 357 | */ 358 | default: break; /* If no destructor action specified: do nothing */ 359 | } 360 | } 361 | 362 | /* 363 | ** Pop the parser's stack once. 364 | ** 365 | ** If there is a destructor routine associated with the token which 366 | ** is popped from the stack, then call it. 367 | ** 368 | ** Return the major token number for the symbol popped. 369 | */ 370 | private function yy_pop_parser_stack() { 371 | if ($this->yyidx < 0) return 0; 372 | $yytos = $this->yystack[$this->yyidx]; 373 | if( $this->yyTraceFILE ) { 374 | fprintf($this->yyTraceFILE,"%sPopping %s\n", 375 | $this->yyTracePrompt, 376 | self::$yyTokenName[$yytos->major]); 377 | } 378 | $this->yy_destructor( $yytos->major, $yytos->minor); 379 | unset($this->yystack[$this->yyidx]); 380 | $this->yyidx--; 381 | return $yytos->major; 382 | } 383 | 384 | /* 385 | ** Deallocate and destroy a parser. Destructors are all called for 386 | ** all stack elements before shutting the parser down. 387 | ** 388 | ** Inputs: 389 | **
      390 | **
    • A pointer to the parser. This should be a pointer 391 | ** obtained from A2S_SVGPathAlloc. 392 | **
    • A pointer to a function used to reclaim memory obtained 393 | ** from malloc. 394 | **
    395 | */ 396 | function __destruct() 397 | { 398 | while($this->yyidx >= 0) 399 | $this->yy_pop_parser_stack(); 400 | } 401 | 402 | /* 403 | ** Find the appropriate action for a parser given the terminal 404 | ** look-ahead token iLookAhead. 405 | ** 406 | ** If the look-ahead token is YYNOCODE, then check to see if the action is 407 | ** independent of the look-ahead. If it is, return the action, otherwise 408 | ** return YY_NO_ACTION. 409 | */ 410 | private function yy_find_shift_action( 411 | $iLookAhead /* The look-ahead token */ 412 | ){ 413 | $i = 0; 414 | $stateno = $this->yystack[$this->yyidx]->stateno; 415 | 416 | if( $stateno>self::YY_SHIFT_MAX || 417 | ($i = self::$yy_shift_ofst[$stateno])==self::YY_SHIFT_USE_DFLT ){ 418 | return self::$yy_default[$stateno]; 419 | } 420 | if( $iLookAhead==self::YYNOCODE ){ 421 | return $this->YY_NO_ACTION; 422 | } 423 | $i += $iLookAhead; 424 | if( $i<0 || $i>=count(self::$yy_action) || self::$yy_lookahead[$i]!=$iLookAhead ){ 425 | if( $iLookAhead>0 ){ 426 | if (isset(self::$yyFallback[$iLookAhead]) && 427 | ($iFallback = self::$yyFallback[$iLookAhead]) != 0) { 428 | if( $this->yyTraceFILE ){ 429 | fprintf($this->yyTraceFILE, "%sFALLBACK %s => %s\n", 430 | $this->yyTracePrompt, self::$yyTokenName[$iLookAhead], 431 | self::$yyTokenName[$iFallback]); 432 | } 433 | return $this->yy_find_shift_action($iFallback); 434 | } 435 | { 436 | $j = $i - $iLookAhead + self::YYWILDCARD; 437 | if( $j>=0 && $jyyTraceFILE ){ 439 | fprintf($this->yyTraceFILE, "%sWILDCARD %s => %s\n", 440 | $this->yyTracePrompt, self::$yyTokenName[$iLookAhead], 441 | self::$yyTokenName[self::YYWILDCARD]); 442 | } 443 | return self::$yy_action[$j]; 444 | } 445 | } 446 | } 447 | return self::$yy_default[$stateno]; 448 | }else{ 449 | return self::$yy_action[$i]; 450 | } 451 | } 452 | 453 | /* 454 | ** Find the appropriate action for a parser given the non-terminal 455 | ** look-ahead token iLookAhead. 456 | ** 457 | ** If the look-ahead token is YYNOCODE, then check to see if the action is 458 | ** independent of the look-ahead. If it is, return the action, otherwise 459 | ** return YY_NO_ACTION. 460 | */ 461 | private function yy_find_reduce_action( 462 | $stateno, /* Current state number */ 463 | $iLookAhead /* The look-ahead token */ 464 | ){ 465 | $i = 0; 466 | 467 | if( $stateno>self::YY_REDUCE_MAX || 468 | ($i = self::$yy_reduce_ofst[$stateno])==self::YY_REDUCE_USE_DFLT ){ 469 | return self::$yy_default[$stateno]; 470 | } 471 | if( $iLookAhead==self::YYNOCODE ){ 472 | return $this->YY_NO_ACTION; 473 | } 474 | $i += $iLookAhead; 475 | if( $i<0 || $i>=count(self::$yy_action) || self::$yy_lookahead[$i]!=$iLookAhead ){ 476 | return self::$yy_default[$stateno]; 477 | }else{ 478 | return self::$yy_action[$i]; 479 | } 480 | } 481 | 482 | /* 483 | ** Perform a shift action. 484 | */ 485 | private function yy_shift( 486 | $yyNewState, /* The new state to shift in */ 487 | $yyMajor, /* The major token to shift in */ 488 | $yypMinor /* Pointer ot the minor token to shift in */ 489 | ){ 490 | $this->yyidx++; 491 | if (isset($this->yystack[$this->yyidx])) { 492 | $yytos = $this->yystack[$this->yyidx]; 493 | } else { 494 | $yytos = new A2S_SVGPathyyStackEntry; 495 | $this->yystack[$this->yyidx] = $yytos; 496 | } 497 | $yytos->stateno = $yyNewState; 498 | $yytos->major = $yyMajor; 499 | $yytos->minor = $yypMinor; 500 | if( $this->yyTraceFILE) { 501 | fprintf($this->yyTraceFILE,"%sShift %d\n",$this->yyTracePrompt,$yyNewState); 502 | fprintf($this->yyTraceFILE,"%sStack:",$this->yyTracePrompt); 503 | for ($i = 1; $i <= $this->yyidx; $i++) { 504 | $ent = $this->yystack[$i]; 505 | fprintf($this->yyTraceFILE," %s",self::$yyTokenName[$ent->major]); 506 | } 507 | fprintf($this->yyTraceFILE,"\n"); 508 | } 509 | } 510 | 511 | private function __overflow_dead_code() { 512 | /* if the stack can overflow (it can't in the PHP implementation) 513 | * Then the following code would be emitted */ 514 | } 515 | 516 | /* The following table contains information about every rule that 517 | ** is used during the reduce. 518 | ** Rather than pollute memory with a large number of arrays, 519 | ** we store both data points in the same array, indexing by 520 | ** rule number * 2. 521 | static const struct { 522 | YYCODETYPE lhs; // Symbol on the left-hand side of the rule 523 | unsigned char nrhs; // Number of right-hand side symbols in the rule 524 | } yyRuleInfo[] = { 525 | */ 526 | static $yyRuleInfo = array( 527 | 16, 1, 528 | 17, 2, 529 | 17, 1, 530 | 18, 2, 531 | 20, 2, 532 | 20, 1, 533 | 21, 1, 534 | 21, 1, 535 | 21, 1, 536 | 21, 1, 537 | 21, 1, 538 | 21, 1, 539 | 21, 1, 540 | 21, 1, 541 | 21, 1, 542 | 19, 2, 543 | 31, 2, 544 | 31, 1, 545 | 22, 1, 546 | 23, 2, 547 | 32, 2, 548 | 32, 1, 549 | 24, 2, 550 | 34, 2, 551 | 34, 1, 552 | 25, 2, 553 | 36, 2, 554 | 36, 1, 555 | 26, 2, 556 | 37, 2, 557 | 37, 1, 558 | 38, 3, 559 | 27, 2, 560 | 39, 2, 561 | 39, 1, 562 | 40, 2, 563 | 28, 2, 564 | 41, 2, 565 | 41, 1, 566 | 42, 2, 567 | 29, 2, 568 | 43, 2, 569 | 43, 1, 570 | 30, 2, 571 | 44, 2, 572 | 44, 1, 573 | 45, 6, 574 | 33, 2, 575 | 35, 1, 576 | 46, 1, 577 | 46, 1, 578 | 46, 1, 579 | ); 580 | 581 | /* 582 | ** Perform a reduce action and the shift that must immediately 583 | ** follow the reduce. 584 | */ 585 | private function yy_reduce( 586 | $yyruleno /* Number of the rule by which to reduce */ 587 | ){ 588 | $yygoto = 0; /* The next state */ 589 | $yyact = 0; /* The next action */ 590 | $yygotominor = null; /* The LHS of the rule reduced */ 591 | $yymsp = null; /* The top of the parser's stack */ 592 | $yysize = 0; /* Amount to pop the stack */ 593 | 594 | $yymsp = $this->yystack[$this->yyidx]; 595 | if( $this->yyTraceFILE && isset(self::$yyRuleName[$yyruleno])) { 596 | fprintf($this->yyTraceFILE, "%sReduce [%s].\n", $this->yyTracePrompt, 597 | self::$yyRuleName[$yyruleno]); 598 | } 599 | 600 | switch( $yyruleno ){ 601 | /* Beginning here are the reduction cases. A typical example 602 | ** follows: 603 | ** case 0: 604 | ** #line 605 | ** { ... } // User supplied code 606 | ** #line 607 | ** break; 608 | */ 609 | case 15: 610 | #line 26 "svg-path.y" 611 | { 612 | if (count($this->yystack[$this->yyidx + 0]->minor) == 2) { 613 | $this->commands[] = array_merge(array($this->yystack[$this->yyidx + -1]->minor), $this->yystack[$this->yyidx + 0]->minor); 614 | } else { 615 | if ($this->yystack[$this->yyidx + -1]->minor->value == 'm') { 616 | $arr = array ('value' => 'l'); 617 | } else { 618 | $arr = array ('value' => 'L'); 619 | } 620 | $c = array_splice($this->yystack[$this->yyidx + 0]->minor, 2); 621 | $this->commands[] = array_merge(array($this->yystack[$this->yyidx + -1]->minor), $this->yystack[$this->yyidx + 0]->minor); 622 | $this->commands[] = array_merge(array($arr), $c); 623 | } 624 | } 625 | #line 604 "svg-path.php" 626 | break; 627 | case 16: 628 | case 20: 629 | case 29: 630 | case 33: 631 | case 35: 632 | case 37: 633 | case 39: 634 | case 41: 635 | case 44: 636 | #line 42 "svg-path.y" 637 | { $yygotominor = array_merge($this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + 0]->minor); } 638 | #line 617 "svg-path.php" 639 | break; 640 | case 17: 641 | case 21: 642 | case 30: 643 | case 34: 644 | case 38: 645 | case 42: 646 | case 45: 647 | case 48: 648 | case 49: 649 | case 50: 650 | case 51: 651 | #line 43 "svg-path.y" 652 | { $yygotominor = $this->yystack[$this->yyidx + 0]->minor; } 653 | #line 632 "svg-path.php" 654 | break; 655 | case 18: 656 | #line 45 "svg-path.y" 657 | { $this->commands[] = array($this->yystack[$this->yyidx + 0]->minor); } 658 | #line 637 "svg-path.php" 659 | break; 660 | case 19: 661 | case 22: 662 | case 25: 663 | case 28: 664 | case 32: 665 | case 36: 666 | case 40: 667 | case 43: 668 | #line 48 "svg-path.y" 669 | { $this->commands[] = array_merge(array($this->yystack[$this->yyidx + -1]->minor), $this->yystack[$this->yyidx + 0]->minor); } 670 | #line 649 "svg-path.php" 671 | break; 672 | case 23: 673 | case 26: 674 | #line 59 "svg-path.y" 675 | { $yygotominor = array_merge($this->yystack[$this->yyidx + -1]->minor, array($this->yystack[$this->yyidx + 0]->minor)); } 676 | #line 655 "svg-path.php" 677 | break; 678 | case 24: 679 | case 27: 680 | #line 60 "svg-path.y" 681 | { $yygotominor = array($this->yystack[$this->yyidx + 0]->minor); } 682 | #line 661 "svg-path.php" 683 | break; 684 | case 31: 685 | #line 80 "svg-path.y" 686 | { $yygotominor = array_merge($this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + 0]->minor); } 687 | #line 666 "svg-path.php" 688 | break; 689 | case 46: 690 | #line 131 "svg-path.y" 691 | { $yygotominor = array_merge(array($this->yystack[$this->yyidx + -5]->minor, $this->yystack[$this->yyidx + -4]->minor, $this->yystack[$this->yyidx + -3]->minor, $this->yystack[$this->yyidx + -2]->minor, $this->yystack[$this->yyidx + -1]->minor), $this->yystack[$this->yyidx + 0]->minor); } 692 | #line 671 "svg-path.php" 693 | break; 694 | case 47: 695 | #line 133 "svg-path.y" 696 | { $yygotominor = array($this->yystack[$this->yyidx + -1]->minor, $this->yystack[$this->yyidx + 0]->minor); } 697 | #line 676 "svg-path.php" 698 | break; 699 | }; 700 | $yygoto = self::$yyRuleInfo[2*$yyruleno]; 701 | $yysize = self::$yyRuleInfo[(2*$yyruleno)+1]; 702 | 703 | $state_for_reduce = $this->yystack[$this->yyidx - $yysize]->stateno; 704 | 705 | $this->yyidx -= $yysize; 706 | $yyact = $this->yy_find_reduce_action($state_for_reduce,$yygoto); 707 | if( $yyact < self::YYNSTATE ){ 708 | $this->yy_shift($yyact, $yygoto, $yygotominor); 709 | }else if( $yyact == self::YYNSTATE + self::YYNRULE + 1 ){ 710 | $this->yy_accept(); 711 | } 712 | } 713 | 714 | /* 715 | ** The following code executes when the parse fails 716 | */ 717 | private function yy_parse_failed( 718 | ){ 719 | if( $this->yyTraceFILE ){ 720 | fprintf($this->yyTraceFILE,"%sFail!\n",$this->yyTracePrompt); 721 | } 722 | while( $this->yyidx>=0 ) $this->yy_pop_parser_stack(); 723 | /* Here code is inserted which will be executed whenever the 724 | ** parser fails */ 725 | } 726 | 727 | /* 728 | ** The following code executes when a syntax error first occurs. 729 | */ 730 | private function yy_syntax_error( 731 | $yymajor, /* The major type of the error token */ 732 | $yyminor /* The minor type of the error token */ 733 | ){ 734 | } 735 | 736 | /* 737 | ** The following is executed when the parser accepts 738 | */ 739 | private function yy_accept( 740 | ){ 741 | if( $this->yyTraceFILE ){ 742 | fprintf($this->yyTraceFILE,"%sAccept!\n",$this->yyTracePrompt); 743 | } 744 | while( $this->yyidx>=0 ) $this->yy_pop_parser_stack(); 745 | /* Here code is inserted which will be executed whenever the 746 | ** parser accepts */ 747 | } 748 | 749 | /* The main parser program. 750 | ** The first argument is a pointer to a structure obtained from 751 | ** "A2S_SVGPathAlloc" which describes the current state of the parser. 752 | ** The second argument is the major token number. The third is 753 | ** the minor token. The fourth optional argument is whatever the 754 | ** user wants (and specified in the grammar) and is available for 755 | ** use by the action routines. 756 | ** 757 | ** Inputs: 758 | **
      759 | **
    • A pointer to the parser (an opaque structure.) 760 | **
    • The major token number. 761 | **
    • The minor token number. 762 | **
    • An option argument of a grammar-specified type. 763 | **
    764 | ** 765 | ** Outputs: 766 | ** None. 767 | */ 768 | function A2S_SVGPath( 769 | $yymajor, /* The major token code number */ 770 | $yyminor = null /* The value for the token */ 771 | ){ 772 | $yyact = 0; /* The parser action. */ 773 | $yyendofinput = 0; /* True if we are at the end of input */ 774 | $yyerrorhit = 0; /* True if yymajor has invoked an error */ 775 | 776 | /* (re)initialize the parser, if necessary */ 777 | if( $this->yyidx<0 ){ 778 | $this->yyidx = 0; 779 | $this->yyerrcnt = -1; 780 | $ent = new A2S_SVGPathyyStackEntry; 781 | $ent->stateno = 0; 782 | $ent->major = 0; 783 | $this->yystack = array( 0 => $ent ); 784 | 785 | $this->YY_NO_ACTION = self::YYNSTATE + self::YYNRULE + 2; 786 | $this->YY_ACCEPT_ACTION = self::YYNSTATE + self::YYNRULE + 1; 787 | $this->YY_ERROR_ACTION = self::YYNSTATE + self::YYNRULE; 788 | } 789 | $yyendofinput = ($yymajor==0); 790 | 791 | if( $this->yyTraceFILE ){ 792 | fprintf($this->yyTraceFILE,"%sInput %s\n",$this->yyTracePrompt, 793 | self::$yyTokenName[$yymajor]); 794 | } 795 | 796 | do{ 797 | $yyact = $this->yy_find_shift_action($yymajor); 798 | if( $yyactyy_shift($yyact,$yymajor,$yyminor); 800 | $this->yyerrcnt--; 801 | if( $yyendofinput && $this->yyidx>=0 ){ 802 | $yymajor = 0; 803 | }else{ 804 | $yymajor = self::YYNOCODE; 805 | } 806 | }else if( $yyact < self::YYNSTATE + self::YYNRULE ){ 807 | $this->yy_reduce($yyact-self::YYNSTATE); 808 | }else if( $yyact == $this->YY_ERROR_ACTION ){ 809 | if( $this->yyTraceFILE ){ 810 | fprintf($this->yyTraceFILE,"%sSyntax Error!\n",$this->yyTracePrompt); 811 | } 812 | if (self::YYERRORSYMBOL) { 813 | /* A syntax error has occurred. 814 | ** The response to an error depends upon whether or not the 815 | ** grammar defines an error token "ERROR". 816 | ** 817 | ** This is what we do if the grammar does define ERROR: 818 | ** 819 | ** * Call the %syntax_error function. 820 | ** 821 | ** * Begin popping the stack until we enter a state where 822 | ** it is legal to shift the error symbol, then shift 823 | ** the error symbol. 824 | ** 825 | ** * Set the error count to three. 826 | ** 827 | ** * Begin accepting and shifting new tokens. No new error 828 | ** processing will occur until three tokens have been 829 | ** shifted successfully. 830 | ** 831 | */ 832 | if( $this->yyerrcnt<0 ){ 833 | $this->yy_syntax_error($yymajor, $yyminor); 834 | } 835 | $yymx = $this->yystack[$this->yyidx]->major; 836 | if( $yymx==self::YYERRORSYMBOL || $yyerrorhit ){ 837 | if( $this->yyTraceFILE ){ 838 | fprintf($this->yyTraceFILE,"%sDiscard input token %s\n", 839 | $this->yyTracePrompt,self::$yyTokenName[$yymajor]); 840 | } 841 | $this->yy_destructor($yymajor,$yyminor); 842 | $yymajor = self::YYNOCODE; 843 | }else{ 844 | while( 845 | $this->yyidx >= 0 && 846 | $yymx != self::YYERRORSYMBOL && 847 | ($yyact = $this->yy_find_reduce_action( 848 | $this->yystack[$this->yyidx]->stateno, 849 | self::YYERRORSYMBOL)) >= self::YYNSTATE 850 | ){ 851 | $this->yy_pop_parser_stack(); 852 | } 853 | if( $this->yyidx < 0 || $yymajor==0 ){ 854 | $this->yy_destructor($yymajor,$yyminor); 855 | $this->yy_parse_failed(); 856 | $yymajor = self::YYNOCODE; 857 | }else if( $yymx!=self::YYERRORSYMBOL ){ 858 | $this->yy_shift($yyact,self::YYERRORSYMBOL,0); 859 | } 860 | } 861 | $this->yyerrcnt = 3; 862 | $yyerrorhit = 1; 863 | } else { /* YYERRORSYMBOL is not defined */ 864 | /* This is what we do if the grammar does not define ERROR: 865 | ** 866 | ** * Report an error message, and throw away the input token. 867 | ** 868 | ** * If the input token is $, then fail the parse. 869 | ** 870 | ** As before, subsequent error messages are suppressed until 871 | ** three input tokens have been successfully shifted. 872 | */ 873 | if( $this->yyerrcnt<=0 ){ 874 | $this->yy_syntax_error($yymajor, $yyminor); 875 | } 876 | $this->yyerrcnt = 3; 877 | $this->yy_destructor($yymajor,$yyminor); 878 | if( $yyendofinput ){ 879 | $this->yy_parse_failed(); 880 | } 881 | $yymajor = self::YYNOCODE; 882 | } 883 | }else{ 884 | $this->yy_accept(); 885 | $yymajor = self::YYNOCODE; 886 | } 887 | }while( $yymajor!=self::YYNOCODE && $this->yyidx>=0 ); 888 | } 889 | 890 | } 891 | -------------------------------------------------------------------------------- /a2s52.php: -------------------------------------------------------------------------------- 1 | SVG art generator. 4 | * Copyright © 2012 Devon H. O'Dell 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * o Redistributions of source code must retain the above copyright notice, 12 | * this list of conditions and the following disclaimer. 13 | * o Redistributions in binary form must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | * POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | */ 30 | 31 | #namespace org\dh0\a2s; 32 | 33 | include 'svg-path.lex.php'; 34 | include 'colors.php'; 35 | 36 | /* 37 | * A2S_Scale is a singleton class that is instantiated to apply scale 38 | * transformations on the text -> canvas grid geometry. We could probably use 39 | * SVG's native scaling for this, but I'm not sure how yet. 40 | */ 41 | class A2S_Scale { 42 | private static $instance = null; 43 | 44 | public $xScale; 45 | public $yScale; 46 | 47 | private function __construct() {} 48 | private function __clone() {} 49 | 50 | public static function getInstance() { 51 | if (self::$instance == null) { 52 | self::$instance = new A2S_Scale(); 53 | } 54 | 55 | return self::$instance; 56 | } 57 | 58 | public function setScale($x, $y) { 59 | $o = self::getInstance(); 60 | $o->xScale = $x; 61 | $o->yScale = $y; 62 | } 63 | } 64 | 65 | /* 66 | * A2S_CustomObjects allows users to create their own custom SVG paths and use 67 | * them as box types with a2s:type references. 68 | * 69 | * Paths must have width and height set, and must not span multiple lines. 70 | * Multiple paths can be specified, one path per line. All objects must 71 | * reside in the same directory. 72 | * 73 | * File operations are horribly slow, so we make a best effort to avoid 74 | * as many as possible: 75 | * 76 | * * If the directory mtime hasn't changed, we attempt to load our 77 | * objects from a cache file. 78 | * 79 | * * If this file doesn't exist, can't be read, or the mtime has 80 | * changed, we scan the directory and update files that have changed 81 | * based on their mtime. 82 | * 83 | * * We attempt to save our cache in a temporary directory. It's volatile 84 | * but also requires no configuration. 85 | * 86 | * We could do a bit better by utilizing APC's shared memory storage, which 87 | * would help greatly when running on a server. 88 | * 89 | * Note that the path parser isn't foolproof, mostly because PHP isn't the 90 | * greatest language ever for implementing a parser. 91 | */ 92 | class A2S_CustomObjects { 93 | public static $objects = array(); 94 | 95 | /* 96 | * Closures / callable function names / whatever for integrating non-default 97 | * loading and storage functionality. 98 | */ 99 | public static $loadCacheFn = null; 100 | public static $storCacheFn = null; 101 | public static $loadObjsFn = null; 102 | 103 | 104 | public static function loadObjects() { 105 | $cacheFile = sys_get_temp_dir() . "/.a2s.objcache"; 106 | $dir = './objects'; 107 | 108 | if (is_callable(self::$loadCacheFn)) { 109 | /* 110 | * Should return exactly what was given to the $storCacheFn when it was 111 | * last called, or null if nothing can be loaded. 112 | */ 113 | $fn = self::$loadCacheFn; 114 | self::$objects = $fn(); 115 | return; 116 | } else { 117 | if (is_readable($cacheFile) && is_readable($dir)) { 118 | $cacheTime = filemtime($cacheFile); 119 | 120 | if (filemtime($dir) <= filemtime($cacheFile)) { 121 | self::$objects = unserialize(file_get_contents($cacheFile)); 122 | return; 123 | } 124 | } else { 125 | return; 126 | } 127 | } 128 | 129 | if (is_callable(self::$loadObjsFn)) { 130 | /* 131 | * Returns an array of arrays of path information. The innermost arrays 132 | * (containing the path information) contain the path name, the width of 133 | * the bounding box, the height of the bounding box, and the path 134 | * command. This interface does *not* want the path's XML tag. An array 135 | * returned from here containing two objects that each have 1 line would 136 | * look like: 137 | * 138 | * array ( 139 | * array ( 140 | * name => 'pathA', 141 | * paths => array ( 142 | * array ('width' => 10, 'height' => 10, 'path' => 'M 0 0 L 10 10'), 143 | * array ('width' => 10, 'height' => 10, 'path' => 'M 0 10 L 10 0'), 144 | * ), 145 | * ), 146 | * array ( 147 | * name => 'pathB', 148 | * paths => array ( 149 | * array ('width' => 10, 'height' => 10, 'path' => 'M 0 5 L 5 10'), 150 | * array ('width' => 10, 'height' => 10, 'path' => 'M 5 10 L 10 5'), 151 | * ), 152 | * ), 153 | * ); 154 | */ 155 | $fn = self::$loadObjsFn; 156 | $objs = $fn(); 157 | 158 | $i = 0; 159 | foreach ($objs as $obj) { 160 | foreach ($obj['paths'] as $path) { 161 | self::$objects[$obj['name']][$i]['width'] = $path['width']; 162 | self::$objects[$obj['name']][$i]['height'] = $path['height']; 163 | self::$objects[$obj['name']][$i++]['path'] = 164 | self::parsePath($path['path']); 165 | } 166 | } 167 | } else { 168 | $ents = scandir($dir); 169 | foreach ($ents as $ent) { 170 | $file = "{$dir}/{$ent}"; 171 | $base = substr($ent, 0, -5); 172 | if (substr($ent, -5) == '.path' && is_readable($file)) { 173 | if (isset(self::$objects[$base]) && 174 | filemtime($file) <= self::$cacheTime) { 175 | continue; 176 | } 177 | 178 | $lines = file($file); 179 | 180 | $i = 0; 181 | foreach ($lines as $line) { 182 | preg_match('/width="(\d+)/', $line, $m); 183 | $width = $m[1]; 184 | preg_match('/height="(\d+)/', $line, $m); 185 | $height = $m[1]; 186 | preg_match('/d="([^"]+)"/', $line, $m); 187 | $path = $m[1]; 188 | 189 | self::$objects[$base][$i]['width'] = $width; 190 | self::$objects[$base][$i]['height'] = $height; 191 | self::$objects[$base][$i++]['path'] = self::parsePath($path); 192 | } 193 | } 194 | } 195 | } 196 | 197 | if (is_callable(self::$storCacheFn)) { 198 | $fn = self::$storCacheFn; 199 | $fn(self::$objects); 200 | } else { 201 | file_put_contents($cacheFile, serialize(self::$objects)); 202 | } 203 | } 204 | 205 | private static function parsePath($path) { 206 | $stream = fopen("data://text/plain,{$path}", 'r'); 207 | 208 | $P = new A2S_SVGPathParser(); 209 | $S = new A2S_Yylex($stream); 210 | 211 | while ($t = $S->nextToken()) { 212 | $P->A2S_SVGPath($t->type, $t); 213 | } 214 | /* Force shift/reduce of last token. */ 215 | $P->A2S_SVGPath(0); 216 | 217 | fclose($stream); 218 | 219 | $cmdArr = array(); 220 | $i = 0; 221 | foreach ($P->commands as $cmd) { 222 | foreach ($cmd as $arg) { 223 | $arg = (array)$arg; 224 | $cmdArr[$i][] = $arg['value']; 225 | } 226 | $i++; 227 | } 228 | 229 | return $cmdArr; 230 | } 231 | } 232 | 233 | /* 234 | * All lines and polygons are represented as a series of point coordinates 235 | * along a path. Points can have different properties; markers appear on 236 | * edges of lines and control points denote that a bezier curve should be 237 | * calculated for the corner represented by this point. 238 | */ 239 | class A2S_Point { 240 | public $gridX; 241 | public $gridY; 242 | 243 | public $x; 244 | public $y; 245 | 246 | public $flags; 247 | 248 | const POINT = 0x1; 249 | const CONTROL = 0x2; 250 | const SMARKER = 0x4; 251 | const IMARKER = 0x8; 252 | const TICK = 0x10; 253 | const DOT = 0x20; 254 | 255 | public function __construct($x, $y) { 256 | $this->flags = 0; 257 | 258 | $s = A2S_Scale::getInstance(); 259 | $this->x = ($x * $s->xScale) + ($s->xScale / 2); 260 | $this->y = ($y * $s->yScale) + ($s->yScale / 2); 261 | 262 | $this->gridX = $x; 263 | $this->gridY = $y; 264 | } 265 | } 266 | 267 | /* 268 | * Groups objects together and sets common properties for the objects in the 269 | * group. 270 | */ 271 | class A2S_SVGGroup { 272 | private $groups; 273 | private $curGroup; 274 | private $groupStack; 275 | private $options; 276 | 277 | public function __construct() { 278 | $this->groups = array(); 279 | $this->groupStack = array(); 280 | $this->options = array(); 281 | } 282 | 283 | public function getGroup($groupName) { 284 | return $this->groups[$groupName]; 285 | } 286 | 287 | public function pushGroup($groupName) { 288 | if (!isset($this->groups[$groupName])) { 289 | $this->groups[$groupName] = array(); 290 | $this->options[$groupName] = array(); 291 | } 292 | 293 | $this->groupStack[] = $groupName; 294 | $this->curGroup = $groupName; 295 | } 296 | 297 | public function popGroup() { 298 | /* 299 | * Remove the last group and fetch the current one. array_pop will return 300 | * NULL for an empty array, so this is safe to do when only one element 301 | * is left. 302 | */ 303 | array_pop($this->groupStack); 304 | $this->curGroup = array_pop($this->groupStack); 305 | } 306 | 307 | public function addObject($o) { 308 | $this->groups[$this->curGroup][] = $o; 309 | } 310 | 311 | public function setOption($opt, $val) { 312 | $this->options[$this->curGroup][$opt] = $val; 313 | } 314 | 315 | public function render() { 316 | $out = ''; 317 | 318 | foreach($this->groups as $groupName => $objects) { 319 | $out .= "options[$groupName] as $opt => $val) { 321 | if (strpos($opt, 'a2s:', 0) === 0) { 322 | continue; 323 | } 324 | $out .= "$opt=\"$val\" "; 325 | } 326 | $out .= ">\n"; 327 | 328 | foreach($objects as $obj) { 329 | $out .= $obj->render(); 330 | } 331 | 332 | $out .= "\n"; 333 | } 334 | 335 | return $out; 336 | } 337 | } 338 | 339 | /* 340 | * The Path class represents lines and polygons. 341 | */ 342 | class A2S_SVGPath { 343 | private $options; 344 | private $points; 345 | private $ticks; 346 | private $flags; 347 | private $text; 348 | private $name; 349 | 350 | private static $id = 0; 351 | 352 | const CLOSED = 0x1; 353 | 354 | public function __construct() { 355 | $this->options = array(); 356 | $this->points = array(); 357 | $this->text = array(); 358 | $this->ticks = array(); 359 | $this->flags = 0; 360 | $this->name = self::$id++; 361 | } 362 | 363 | /* 364 | * Making sure that we always started at the top left coordinate 365 | * makes so many things so much easier. First, find the lowest Y 366 | * position. Then, of all matching Y positions, find the lowest X 367 | * position. This is the top left. 368 | * 369 | * As far as the points are considered, they're definitely on the 370 | * top somewhere, but not necessarily the most left. This could 371 | * happen if there was a corner connector in the top edge (perhaps 372 | * for a line to connect to). Since we couldn't turn right there, 373 | * we have to try now. 374 | * 375 | * This should only be called when we close a polygon. 376 | */ 377 | public function orderPoints() { 378 | $pPoints = count($this->points); 379 | 380 | $minY = $this->points[0]->y; 381 | $minX = $this->points[0]->x; 382 | $minIdx = 0; 383 | for ($i = 1; $i < $pPoints; $i++) { 384 | if ($this->points[$i]->y <= $minY) { 385 | $minY = $this->points[$i]->y; 386 | 387 | if ($this->points[$i]->x < $minX) { 388 | $minX = $this->points[$i]->x; 389 | $minIdx = $i; 390 | } 391 | } 392 | } 393 | 394 | /* 395 | * If our top left isn't at the 0th index, it is at the end. If 396 | * there are bits after it, we need to cut those and put them at 397 | * the front. 398 | */ 399 | if ($minIdx != 0) { 400 | $startPoints = array_splice($this->points, $minIdx); 401 | $this->points = array_merge($startPoints, $this->points); 402 | } 403 | } 404 | 405 | /* 406 | * Useful for recursive walkers when speculatively trying a direction. 407 | */ 408 | public function popPoint() { 409 | array_pop($this->points); 410 | } 411 | 412 | public function addPoint($x, $y, $flags = A2S_Point::POINT) { 413 | $p = new A2S_Point($x, $y); 414 | 415 | /* 416 | * If we attempt to add our original point back to the path, the polygon 417 | * must be closed. 418 | */ 419 | if (count($this->points) > 0) { 420 | if ($this->points[0]->x == $p->x && $this->points[0]->y == $p->y) { 421 | $this->flags |= self::CLOSED; 422 | return true; 423 | } 424 | 425 | /* 426 | * For the purposes of this library, paths should never intersect each 427 | * other. Even in the case of closing the polygon, we do not store the 428 | * final coordinate twice. 429 | */ 430 | foreach ($this->points as $point) { 431 | if ($point->x == $p->x && $point->y == $p->y) { 432 | return true; 433 | } 434 | } 435 | } 436 | 437 | $p->flags |= $flags; 438 | $this->points[] = $p; 439 | 440 | return false; 441 | } 442 | 443 | /* 444 | * It's useful to be able to know the points in a shape. 445 | */ 446 | public function getPoints() { 447 | return $this->points; 448 | } 449 | 450 | /* 451 | * Add a marker to a line. The third argument specifies which marker to use, 452 | * and this depends on the orientation of the line. Due to the way the line 453 | * parser works, we may have to use an inverted representation. 454 | */ 455 | public function addMarker($x, $y, $t) { 456 | $p = new A2S_Point($x, $y); 457 | $p->flags |= $t; 458 | $this->points[] = $p; 459 | } 460 | 461 | public function addTick($x, $y, $t) { 462 | $p = new A2S_Point($x, $y); 463 | $p->flags |= $t; 464 | $this->ticks[] = $p; 465 | } 466 | 467 | /* 468 | * Is this path closed? 469 | */ 470 | public function isClosed() { 471 | return ($this->flags & self::CLOSED); 472 | } 473 | 474 | public function addText($t) { 475 | $this->text[] = $t; 476 | } 477 | 478 | public function getText() { 479 | return $this->text; 480 | } 481 | 482 | public function setID($id) { 483 | $this->name = str_replace(' ', '_', str_replace('"', '_', $id)); 484 | } 485 | 486 | public function getID() { 487 | return $this->name; 488 | } 489 | 490 | /* 491 | * Set options as a JSON string. Specified as a merge operation so that it 492 | * can be called after an individual setOption call. 493 | */ 494 | public function setOptions($opt) { 495 | $this->options = array_merge($this->options, $opt); 496 | } 497 | 498 | public function setOption($opt, $val) { 499 | $this->options[$opt] = $val; 500 | } 501 | 502 | public function getOption($opt) { 503 | if (isset($this->options[$opt])) { 504 | return $this->options[$opt]; 505 | } 506 | 507 | return null; 508 | } 509 | 510 | /* 511 | * Does the given point exist within this polygon? Since we can 512 | * theoretically have some complex concave and convex polygon edges in the 513 | * same shape, we need to do a full point-in-polygon test. This algorithm 514 | * seems like the standard one. See: http://alienryderflex.com/polygon/ 515 | */ 516 | public function hasPoint($x, $y) { 517 | if ($this->isClosed() == false) { 518 | return false; 519 | } 520 | 521 | $oddNodes = false; 522 | 523 | $bound = count($this->points); 524 | for ($i = 0, $j = count($this->points) - 1; $i < $bound; $i++) { 525 | if (($this->points[$i]->gridY < $y && $this->points[$j]->gridY >= $y || 526 | $this->points[$j]->gridY < $y && $this->points[$i]->gridY >= $y) && 527 | ($this->points[$i]->gridX <= $x || $this->points[$j]->gridX <= $x)) { 528 | if ($this->points[$i]->gridX + ($y - $this->points[$i]->gridY) / 529 | ($this->points[$j]->gridY - $this->points[$i]->gridY) * 530 | ($this->points[$j]->gridX - $this->points[$i]->gridX) < $x) { 531 | $oddNodes = !$oddNodes; 532 | } 533 | } 534 | 535 | $j = $i; 536 | } 537 | 538 | return $oddNodes; 539 | } 540 | 541 | /* 542 | * Apply a matrix transformation to the coordinates ($x, $y). The 543 | * multiplication is implemented on the matrices: 544 | * 545 | * | a b c | | x | 546 | * | d e f | * | y | 547 | * | 0 0 1 | | 1 | 548 | * 549 | * Additional information on the transformations and what each R,C in the 550 | * transformation matrix represents, see: 551 | * 552 | * http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined 553 | */ 554 | private function matrixTransform($matrix, $x, $y) { 555 | $xyMat = array(array($x), array($y), array(1)); 556 | $newXY = array(array()); 557 | 558 | for ($i = 0; $i < 3; $i++) { 559 | for ($j = 0; $j < 1; $j++) { 560 | $sum = 0; 561 | 562 | for ($k = 0; $k < 3; $k++) { 563 | $sum += $matrix[$i][$k] * $xyMat[$k][$j]; 564 | } 565 | 566 | $newXY[$i][$j] = $sum; 567 | } 568 | } 569 | 570 | /* Return the coordinates as a vector */ 571 | return array($newXY[0][0], $newXY[1][0], $newXY[2][0]); 572 | } 573 | 574 | /* 575 | * Translate the X and Y coordinates. tX and tY specify the distance to 576 | * transform. 577 | */ 578 | private function translateTransform($tX, $tY, $x, $y) { 579 | $matrix = array(array(1, 0, $tX), array(0, 1, $tY), array(0, 0, 1)); 580 | return $this->matrixTransform($matrix, $x, $y); 581 | } 582 | 583 | /* 584 | * A2S_Scale transformations are implemented by applying the scale to the X and 585 | * Y coordinates. One unit in the new coordinate system equals $s[XY] units 586 | * in the old system. Thus, if you want to double the size of an object on 587 | * both axes, you sould call scaleTransform(0.5, 0.5, $x, $y) 588 | */ 589 | private function scaleTransform($sX, $sY, $x, $y) { 590 | $matrix = array(array($sX, 0, 0), array(0, $sY, 0), array(0, 0, 1)); 591 | return $this->matrixTransform($matrix, $x, $y); 592 | } 593 | 594 | /* 595 | * Rotate the coordinates around the center point cX and cY. If these 596 | * are not specified, the coordinate is rotated around 0,0. The angle 597 | * is specified in degrees. 598 | */ 599 | private function rotateTransform($angle, $x, $y, $cX = 0, $cY = 0) { 600 | $angle = $angle * (pi() / 180); 601 | if ($cX != 0 || $cY != 0) { 602 | list ($x, $y) = $this->translateTransform(-$cX, -$cY, $x, $y); 603 | } 604 | 605 | $matrix = array(array(cos($angle), -sin($angle), 0), 606 | array(sin($angle), cos($angle), 0), 607 | array(0, 0, 1)); 608 | $ret = $this->matrixTransform($matrix, $x, $y); 609 | 610 | if ($cX != 0 || $cY != 0) { 611 | list ($x, $y) = $this->translateTransform($cX, $cY, $ret[0], $ret[1]); 612 | $ret[0] = $x; 613 | $ret[1] = $y; 614 | } 615 | 616 | return $ret; 617 | } 618 | 619 | /* 620 | * Skews along the X axis at specified angle. The angle is specified in 621 | * degrees. 622 | */ 623 | private function skewXTransform($angle, $x, $y) { 624 | $angle = $angle * (pi() / 180); 625 | $matrix = array(array(1, tan($angle), 0), array(0, 1, 0), array(0, 0, 1)); 626 | return $this->matrixTransform($matrix, $x, $y); 627 | } 628 | 629 | /* 630 | * Skews along the Y axis at specified angle. The angle is specified in 631 | * degrees. 632 | */ 633 | private function skewYTransform($angle, $x, $y) { 634 | $angle = $angle * (pi() / 180); 635 | $matrix = array(array(1, 0, 0), array(tan($angle), 1, 0), array(0, 0, 1)); 636 | return $this->matrixTransform($matrix, $x, $y); 637 | } 638 | 639 | /* 640 | * Apply a transformation to a point $p. 641 | */ 642 | private function applyTransformToPoint($txf, $p, $args) { 643 | switch ($txf) { 644 | case 'translate': 645 | return $this->translateTransform($args[0], $args[1], $p->x, $p->y); 646 | 647 | case 'scale': 648 | return $this->scaleTransform($args[0], $args[1], $p->x, $p->y); 649 | 650 | case 'rotate': 651 | if (count($args) > 1) { 652 | return $this->rotateTransform($args[0], $p->x, $p->y, $args[1], $args[2]); 653 | } else { 654 | return $this->rotateTransform($args[0], $p->x, $p->y); 655 | } 656 | 657 | case 'skewX': 658 | return $this->skewXTransform($args[0], $p->x, $p->y); 659 | 660 | case 'skewY': 661 | return $this->skewYTransform($args[0], $p->x, $p->y); 662 | } 663 | } 664 | 665 | /* 666 | * Apply the transformation function $txf to all coordinates on path $p 667 | * providing $args as arguments to the transformation function. 668 | */ 669 | private function applyTransformToPath($txf, &$p, $args) { 670 | $pathCmds = count($p['path']); 671 | $curPoint = new A2S_Point(0, 0); 672 | $prevType = null; 673 | $curType = null; 674 | 675 | for ($i = 0; $i < $pathCmds; $i++) { 676 | $cmd = &$p['path'][$i]; 677 | 678 | $prevType = $curType; 679 | $curType = $cmd[0]; 680 | 681 | switch ($curType) { 682 | case 'z': 683 | case 'Z': 684 | /* Can't transform this */ 685 | break; 686 | 687 | case 'm': 688 | if ($prevType != null) { 689 | $curPoint->x += $cmd[1]; 690 | $curPoint->y += $cmd[2]; 691 | 692 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 693 | $curPoint->x = $x; 694 | $curPoint->y = $y; 695 | 696 | $cmd[1] = $x; 697 | $cmd[2] = $y; 698 | } else { 699 | $curPoint->x = $cmd[1]; 700 | $curPoint->y = $cmd[2]; 701 | 702 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 703 | $curPoint->x = $x; 704 | $curPoint->y = $y; 705 | 706 | $cmd[1] = $x; 707 | $cmd[2] = $y; 708 | $curType = 'l'; 709 | } 710 | 711 | break; 712 | 713 | case 'M': 714 | $curPoint->x = $cmd[1]; 715 | $curPoint->y = $cmd[2]; 716 | 717 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 718 | $curPoint->x = $x; 719 | $curPoint->y = $y; 720 | 721 | $cmd[1] = $x; 722 | $cmd[2] = $y; 723 | 724 | if ($prevType == null) { 725 | $curType = 'L'; 726 | } 727 | break; 728 | 729 | case 'l': 730 | $curPoint->x += $cmd[1]; 731 | $curPoint->y += $cmd[2]; 732 | 733 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 734 | $curPoint->x = $x; 735 | $curPoint->y = $y; 736 | 737 | $cmd[1] = $x; 738 | $cmd[2] = $y; 739 | 740 | break; 741 | 742 | case 'L': 743 | $curPoint->x = $cmd[1]; 744 | $curPoint->y = $cmd[2]; 745 | 746 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 747 | $curPoint->x = $x; 748 | $curPoint->y = $y; 749 | 750 | $cmd[1] = $x; 751 | $cmd[2] = $y; 752 | 753 | break; 754 | 755 | case 'v': 756 | $curPoint->y += $cmd[1]; 757 | $curPoint->x += 0; 758 | 759 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 760 | $curPoint->x = $x; 761 | $curPoint->y = $y; 762 | 763 | $cmd[1] = $y; 764 | 765 | break; 766 | 767 | case 'V': 768 | $curPoint->y = $cmd[1]; 769 | 770 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 771 | $curPoint->x = $x; 772 | $curPoint->y = $y; 773 | 774 | $cmd[1] = $y; 775 | 776 | break; 777 | 778 | case 'h': 779 | $curPoint->x += $cmd[1]; 780 | 781 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 782 | $curPoint->x = $x; 783 | $curPoint->y = $y; 784 | 785 | $cmd[1] = $x; 786 | 787 | break; 788 | 789 | case 'H': 790 | $curPoint->x = $cmd[1]; 791 | 792 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 793 | $curPoint->x = $x; 794 | $curPoint->y = $y; 795 | 796 | $cmd[1] = $x; 797 | 798 | break; 799 | 800 | case 'c': 801 | $tP = new A2S_Point(0, 0); 802 | $tP->x = $curPoint->x + $cmd[1]; $tP->y = $curPoint->y + $cmd[2]; 803 | list ($x, $y) = $this->applyTransformToPoint($txf, $tP, $args); 804 | $cmd[1] = $x; 805 | $cmd[2] = $y; 806 | 807 | $tP->x = $curPoint->x + $cmd[3]; $tP->y = $curPoint->y + $cmd[4]; 808 | list ($x, $y) = $this->applyTransformToPoint($txf, $tP, $args); 809 | $cmd[3] = $x; 810 | $cmd[4] = $y; 811 | 812 | $curPoint->x += $cmd[5]; 813 | $curPoint->y += $cmd[6]; 814 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 815 | 816 | $curPoint->x = $x; 817 | $curPoint->y = $y; 818 | $cmd[5] = $x; 819 | $cmd[6] = $y; 820 | 821 | break; 822 | case 'C': 823 | $curPoint->x = $cmd[1]; 824 | $curPoint->y = $cmd[2]; 825 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 826 | $cmd[1] = $x; 827 | $cmd[2] = $y; 828 | 829 | $curPoint->x = $cmd[3]; 830 | $curPoint->y = $cmd[4]; 831 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 832 | $cmd[3] = $x; 833 | $cmd[4] = $y; 834 | 835 | $curPoint->x = $cmd[5]; 836 | $curPoint->y = $cmd[6]; 837 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 838 | 839 | $curPoint->x = $x; 840 | $curPoint->y = $y; 841 | $cmd[5] = $x; 842 | $cmd[6] = $y; 843 | 844 | break; 845 | 846 | case 's': 847 | case 'S': 848 | 849 | case 'q': 850 | case 'Q': 851 | 852 | case 't': 853 | case 'T': 854 | 855 | case 'a': 856 | break; 857 | 858 | case 'A': 859 | /* 860 | * This radius is relative to the start and end points, so it makes 861 | * sense to scale, rotate, or skew it, but not translate it. 862 | */ 863 | if ($txf != 'translate') { 864 | $curPoint->x = $cmd[1]; 865 | $curPoint->y = $cmd[2]; 866 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 867 | $cmd[1] = $x; 868 | $cmd[2] = $y; 869 | } 870 | 871 | $curPoint->x = $cmd[6]; 872 | $curPoint->y = $cmd[7]; 873 | list ($x, $y) = $this->applyTransformToPoint($txf, $curPoint, $args); 874 | $curPoint->x = $x; 875 | $curPoint->y = $y; 876 | $cmd[6] = $x; 877 | $cmd[7] = $y; 878 | 879 | break; 880 | } 881 | } 882 | } 883 | 884 | public function render() { 885 | $startPoint = array_shift($this->points); 886 | $endPoint = $this->points[count($this->points) - 1]; 887 | 888 | $out = "name}\">\n"; 889 | 890 | /* 891 | * If someone has specified one of our special object types, we are going 892 | * to want to completely override any of the pathing that we would have 893 | * done otherwise, but we defer until here to do anything about it because 894 | * we need information about the object we're replacing. 895 | */ 896 | if (isset($this->options['a2s:type']) && 897 | isset(A2S_CustomObjects::$objects[$this->options['a2s:type']])) { 898 | $object = A2S_CustomObjects::$objects[$this->options['a2s:type']]; 899 | 900 | /* Again, if no fill was specified, specify one. */ 901 | if (!isset($this->options['fill'])) { 902 | $this->options['fill'] = '#fff'; 903 | } 904 | 905 | /* 906 | * We don't care so much about the area, but we do care about the width 907 | * and height of the object. All of our "custom" objects are implemented 908 | * in 100x100 space, which makes the transformation marginally easier. 909 | */ 910 | $minX = $startPoint->x; $maxX = $minX; 911 | $minY = $startPoint->y; $maxY = $minY; 912 | foreach ($this->points as $p) { 913 | if ($p->x < $minX) { 914 | $minX = $p->x; 915 | } elseif ($p->x > $maxX) { 916 | $maxX = $p->x; 917 | } 918 | if ($p->y < $minY) { 919 | $minY = $p->y; 920 | } elseif ($p->y > $maxY) { 921 | $maxY = $p->y; 922 | } 923 | } 924 | 925 | $objW = $maxX - $minX; 926 | $objH = $maxY - $minY; 927 | 928 | $i = 0; 929 | foreach ($object as $o) { 930 | $id = self::$id++; 931 | $out .= "\tname}\" d=\""; 932 | 933 | $oW = $o['width']; 934 | $oH = $o['height']; 935 | 936 | $this->applyTransformToPath('scale', $o, array($objW/$oW, $objH/$oH)); 937 | $this->applyTransformToPath('translate', $o, array($minX, $minY)); 938 | 939 | foreach ($o['path'] as $cmd) { 940 | $out .= join(' ', $cmd) . ' '; 941 | } 942 | $out .= '" '; 943 | 944 | /* Don't add options to sub-paths */ 945 | if ($i++ < 1) { 946 | foreach ($this->options as $opt => $val) { 947 | if (strpos($opt, 'a2s:', 0) === 0) { 948 | continue; 949 | } 950 | $out .= "$opt=\"$val\" "; 951 | } 952 | } 953 | 954 | $out .= " />\n"; 955 | } 956 | 957 | if (count($this->text) > 0) { 958 | foreach ($this->text as $text) { 959 | $out .= "\t" . $text->render() . "\n"; 960 | } 961 | } 962 | $out .= "\n"; 963 | 964 | /* Bazinga. */ 965 | return $out; 966 | } 967 | 968 | /* 969 | * Nothing fancy here -- this is just rendering for our standard 970 | * polygons. 971 | * 972 | * Our start point is represented by a single moveto command (unless the 973 | * start point is curved) as the shape will be closed with the Z command 974 | * automatically if it is a closed shape. If we have a control point, we 975 | * have to go ahead and draw the curve. 976 | */ 977 | if (($startPoint->flags & A2S_Point::CONTROL)) { 978 | $cX = $startPoint->x; 979 | $cY = $startPoint->y; 980 | $sX = $startPoint->x; 981 | $sY = $startPoint->y + 10; 982 | $eX = $startPoint->x + 10; 983 | $eY = $startPoint->y; 984 | 985 | $path = "M {$sX} {$sY} Q {$cX} {$cY} {$eX} {$eY} "; 986 | } else { 987 | $path = "M {$startPoint->x} {$startPoint->y} "; 988 | } 989 | 990 | $prevP = $startPoint; 991 | $bound = count($this->points); 992 | for ($i = 0; $i < $bound; $i++) { 993 | $p = $this->points[$i]; 994 | 995 | /* 996 | * Handle quadratic Bezier curves. NOTE: This algorithm for drawing 997 | * the curves only works if the shapes are drawn in a clockwise 998 | * manner. 999 | */ 1000 | if (($p->flags & A2S_Point::CONTROL)) { 1001 | /* Our control point is always the original corner */ 1002 | $cX = $p->x; 1003 | $cY = $p->y; 1004 | 1005 | /* Need next point to determine which way to turn */ 1006 | if ($i == count($this->points) - 1) { 1007 | $nP = $startPoint; 1008 | } else { 1009 | $nP = $this->points[$i + 1]; 1010 | } 1011 | 1012 | if ($prevP->x == $p->x) { 1013 | /* 1014 | * If we are on the same vertical axis, our starting X coordinate 1015 | * is the same as the control point coordinate. 1016 | */ 1017 | $sX = $p->x; 1018 | 1019 | /* Offset start point from control point in the proper direction */ 1020 | if ($prevP->y < $p->y) { 1021 | $sY = $p->y - 10; 1022 | } else { 1023 | $sY = $p->y + 10; 1024 | } 1025 | 1026 | $eY = $p->y; 1027 | /* Offset end point from control point in the proper direction */ 1028 | if ($nP->x < $p->x) { 1029 | $eX = $p->x - 10; 1030 | } else { 1031 | $eX = $p->x + 10; 1032 | } 1033 | } elseif ($prevP->y == $p->y) { 1034 | /* Horizontal decisions mirror vertical's above */ 1035 | $sY = $p->y; 1036 | if ($prevP->x < $p->x) { 1037 | $sX = $p->x - 10; 1038 | } else { 1039 | $sX = $p->x + 10; 1040 | } 1041 | 1042 | $eX = $p->x; 1043 | if ($nP->y <= $p->y) { 1044 | $eY = $p->y - 10; 1045 | } else { 1046 | $eY = $p->y + 10; 1047 | } 1048 | } 1049 | 1050 | $path .= "L {$sX} {$sY} Q {$cX} {$cY} {$eX} {$eY} "; 1051 | } else { 1052 | /* The excruciating difficulty of drawing a straight line */ 1053 | $path .= "L {$p->x} {$p->y} "; 1054 | } 1055 | 1056 | $prevP = $p; 1057 | } 1058 | 1059 | if ($this->isClosed()) { 1060 | $path .= 'Z'; 1061 | } 1062 | 1063 | $id = self::$id++; 1064 | 1065 | /* Add markers if necessary. */ 1066 | if ($startPoint->flags & A2S_Point::SMARKER) { 1067 | $this->options["marker-start"] = "url(#Pointer)"; 1068 | } elseif ($startPoint->flags & A2S_Point::IMARKER) { 1069 | $this->options["marker-start"] = "url(#iPointer)"; 1070 | } 1071 | 1072 | if ($endPoint->flags & A2S_Point::SMARKER) { 1073 | $this->options["marker-end"] = "url(#Pointer)"; 1074 | } elseif ($endPoint->flags & A2S_Point::IMARKER) { 1075 | $this->options["marker-end"] = "url(#iPointer)"; 1076 | } 1077 | 1078 | /* 1079 | * SVG objects without a fill will be transparent, and this looks so 1080 | * terrible with the drop-shadow effect. Any objects that aren't filled 1081 | * automatically get a white fill. 1082 | */ 1083 | if ($this->isClosed() && !isset($this->options['fill'])) { 1084 | $this->options['fill'] = '#fff'; 1085 | } 1086 | 1087 | 1088 | $out .= "\tname}\" "; 1089 | foreach ($this->options as $opt => $val) { 1090 | if (strpos($opt, 'a2s:', 0) === 0) { 1091 | continue; 1092 | } 1093 | $out .= "$opt=\"$val\" "; 1094 | } 1095 | $out .= "d=\"{$path}\" />\n"; 1096 | 1097 | if (count($this->text) > 0) { 1098 | foreach ($this->text as $text) { 1099 | $text->setID($this->name); 1100 | $out .= "\t" . $text->render() . "\n"; 1101 | } 1102 | } 1103 | 1104 | $bound = count($this->ticks); 1105 | for ($i = 0; $i < $bound; $i++) { 1106 | $t = $this->ticks[$i]; 1107 | if ($t->flags & A2S_Point::DOT) { 1108 | $out .= "x}\" cy=\"{$t->y}\" r=\"3\" fill=\"black\" />"; 1109 | } elseif ($t->flags & A2S_Point::TICK) { 1110 | $x1 = $t->x - 4; 1111 | $y1 = $t->y - 4; 1112 | $x2 = $t->x + 4; 1113 | $y2 = $t->y + 4; 1114 | $out .= ""; 1115 | 1116 | $x1 = $t->x + 4; 1117 | $y1 = $t->y - 4; 1118 | $x2 = $t->x - 4; 1119 | $y2 = $t->y + 4; 1120 | $out .= ""; 1121 | } 1122 | } 1123 | 1124 | $out .= "\n"; 1125 | return $out; 1126 | } 1127 | } 1128 | 1129 | /* 1130 | * Nothing really special here. Container for representing text bits. 1131 | */ 1132 | class A2S_SVGText { 1133 | private $options; 1134 | private $string; 1135 | private $point; 1136 | private $name; 1137 | 1138 | private static $id = 0; 1139 | 1140 | public function __construct($x, $y) { 1141 | $this->point = new A2S_Point($x, $y); 1142 | $this->name = self::$id++; 1143 | $this->options = array(); 1144 | } 1145 | 1146 | public function setOption($opt, $val) { 1147 | $this->options[$opt] = $val; 1148 | } 1149 | 1150 | public function setID($id) { 1151 | $this->name = str_replace(' ', '_', str_replace('"', '_', $id)); 1152 | } 1153 | 1154 | public function getID() { 1155 | return $this->name; 1156 | } 1157 | 1158 | public function getPoint() { 1159 | return $this->point; 1160 | } 1161 | 1162 | public function setString($string) { 1163 | $this->string = $string; 1164 | } 1165 | 1166 | public function render() { 1167 | $out = "point->x}\" y=\"{$this->point->y}\" id=\"text{$this->name}\" "; 1168 | foreach ($this->options as $opt => $val) { 1169 | if (strpos($opt, 'a2s:', 0) === 0) { 1170 | continue; 1171 | } 1172 | $out .= "$opt=\"$val\" "; 1173 | } 1174 | $out .= ">"; 1175 | $out .= htmlentities($this->string); 1176 | $out .= "\n"; 1177 | return $out; 1178 | } 1179 | } 1180 | 1181 | /* 1182 | * Main class for parsing ASCII and constructing the SVG output based on the 1183 | * above classes. 1184 | */ 1185 | class A2S_ASCIIToSVG { 1186 | public $blurDropShadow = true; 1187 | public $fontFamily = "Consolas,Monaco,Anonymous Pro,Anonymous,Bitstream Sans Mono,monospace"; 1188 | 1189 | private $rawData; 1190 | private $grid; 1191 | 1192 | private $svgObjects; 1193 | private $clearCorners; 1194 | 1195 | /* Directions for traversing lines in our grid */ 1196 | const DIR_UP = 0x1; 1197 | const DIR_DOWN = 0x2; 1198 | const DIR_LEFT = 0x4; 1199 | const DIR_RIGHT = 0x8; 1200 | const DIR_NE = 0x10; 1201 | const DIR_SE = 0x20; 1202 | 1203 | public function __construct($data) { 1204 | /* For debugging purposes */ 1205 | $this->rawData = $data; 1206 | 1207 | A2S_CustomObjects::loadObjects(); 1208 | 1209 | $this->clearCorners = array(); 1210 | 1211 | /* 1212 | * Parse out any command references. These need to be at the bottom of the 1213 | * diagram due to the way they're removed. Format is: 1214 | * [identifier] optional-colon optional-spaces ({json-blob})\n 1215 | * 1216 | * The JSON blob may not contain objects as values or the regex will break. 1217 | */ 1218 | $this->commands = array(); 1219 | preg_match_all('/^\[([^\]]+)\]:?\s+({[^}]+?})/ims', $data, $matches); 1220 | $bound = count($matches[1]); 1221 | for ($i = 0; $i < $bound; $i++) { 1222 | $this->commands[$matches[1][$i]] = json_decode($matches[2][$i], true); 1223 | } 1224 | 1225 | $data = preg_replace('/^\[([^\]]+)\](:?)\s+.*/ims', '', $data); 1226 | 1227 | /* 1228 | * Treat our ASCII field as a grid and store each character as a point in 1229 | * that grid. The (0, 0) coordinate on this grid is top-left, just as it 1230 | * is in images. 1231 | */ 1232 | $this->grid = explode("\n", $data); 1233 | 1234 | foreach ($this->grid as $k => $line) { 1235 | $this->grid[$k] = str_split($line); 1236 | } 1237 | 1238 | $this->svgObjects = new A2S_SVGGroup(); 1239 | } 1240 | 1241 | /* 1242 | * This is kind of a stupid and hacky way to do this, but this allows setting 1243 | * the default scale of one grid space on the X and Y axes. 1244 | */ 1245 | public function setDimensionScale($x, $y) { 1246 | $o = A2S_Scale::getInstance(); 1247 | $o->setScale($x, $y); 1248 | } 1249 | 1250 | public function dump() { 1251 | var_export($this); 1252 | } 1253 | 1254 | /* Render out what we've done! */ 1255 | public function render() { 1256 | $o = A2S_Scale::getInstance(); 1257 | 1258 | /* Figure out how wide we need to make the canvas */ 1259 | $canvasWidth = 0; 1260 | foreach($this->grid as $line) { 1261 | if (count($line) > $canvasWidth) { 1262 | $canvasWidth = count($line); 1263 | } 1264 | } 1265 | 1266 | $canvasWidth = $canvasWidth * $o->xScale; 1267 | $canvasHeight = count($this->grid) * $o->yScale; 1268 | 1269 | /* 1270 | * Boilerplate header with definitions that we might be using for markers 1271 | * and drop shadows. 1272 | */ 1273 | $out = << 1275 | 1277 | 1278 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1297 | 1298 | 1299 | 1304 | 1305 | 1306 | 1307 | SVG; 1308 | 1309 | /* Render the group, everything lives in there */ 1310 | $out .= $this->svgObjects->render(); 1311 | 1312 | $out .= "\n"; 1313 | 1314 | return $out; 1315 | } 1316 | 1317 | /* 1318 | * Parsing the grid is a multi-step process. We parse out boxes first, as 1319 | * this makes it easier to then parse lines. By parse out, I do mean we 1320 | * parse them and then remove them. This does mean that a complete line 1321 | * will not travel along the edge of a box, but you probably won't notice 1322 | * unless the box is curved anyway. While edges are removed, points are 1323 | * not. This means that you can cleanly allow lines to intersect boxes 1324 | * (as long as they do not bisect! 1325 | * 1326 | * After parsing boxes and lines, we remove the corners from the grid. At 1327 | * this point, all we have left should be text, which we can pick up and 1328 | * place. 1329 | */ 1330 | public function parseGrid() { 1331 | $this->parseBoxes(); 1332 | $this->parseLines(); 1333 | 1334 | foreach ($this->clearCorners as $corner) { 1335 | $this->grid[$corner[0]][$corner[1]] = ' '; 1336 | } 1337 | 1338 | $this->parseText(); 1339 | 1340 | $this->injectCommands(); 1341 | } 1342 | 1343 | /* 1344 | * Ahh, good ol' box parsing. We do this by scanning each row for points and 1345 | * attempting to close the shape. Since the approach is first horizontal, 1346 | * then vertical, we complete the shape in a clockwise order (which is 1347 | * important for the Bezier curve generation. 1348 | */ 1349 | private function parseBoxes() { 1350 | /* Set up our box group */ 1351 | $this->svgObjects->pushGroup('boxes'); 1352 | $this->svgObjects->setOption('stroke', 'black'); 1353 | $this->svgObjects->setOption('stroke-width', '2'); 1354 | $this->svgObjects->setOption('fill', 'none'); 1355 | 1356 | /* Scan the grid for corners */ 1357 | foreach ($this->grid as $row => $line) { 1358 | foreach ($line as $col => $char) { 1359 | if ($this->isCorner($char)) { 1360 | $path = new A2S_SVGPath(); 1361 | 1362 | if ($char == '.' || $char == "'") { 1363 | $path->addPoint($col, $row, A2S_Point::CONTROL); 1364 | } else { 1365 | $path->addPoint($col, $row); 1366 | } 1367 | 1368 | /* 1369 | * The wall follower is a left-turning, marking follower. See that 1370 | * function for more information on how it works. 1371 | */ 1372 | $this->wallFollow($path, $row, $col+1, self::DIR_RIGHT); 1373 | 1374 | /* We only care about closed polygons */ 1375 | if ($path->isClosed()) { 1376 | $path->orderPoints(); 1377 | 1378 | $skip = false; 1379 | /* 1380 | * The walking code can find the same box from a different edge: 1381 | * 1382 | * +---+ +---+ 1383 | * | | | | 1384 | * | +---+ | 1385 | * +-----------+ 1386 | * 1387 | * so ignore adding a box that we've already added. 1388 | */ 1389 | foreach($this->svgObjects->getGroup('boxes') as $box) { 1390 | $bP = $box->getPoints(); 1391 | $pP = $path->getPoints(); 1392 | $pPoints = count($pP); 1393 | $shared = 0; 1394 | 1395 | /* 1396 | * If the boxes don't have the same number of edges, they 1397 | * obviously cannot be the same box. 1398 | */ 1399 | if (count($bP) != $pPoints) { 1400 | continue; 1401 | } 1402 | 1403 | /* Traverse the vertices of this new box... */ 1404 | for ($i = 0; $i < $pPoints; $i++) { 1405 | /* ...and find them in this existing box. */ 1406 | for ($j = 0; $j < $pPoints; $j++) { 1407 | if ($pP[$i]->x == $bP[$j]->x && $pP[$i]->y == $bP[$j]->y) { 1408 | $shared++; 1409 | } 1410 | } 1411 | } 1412 | 1413 | /* If all the edges are in common, it's the same shape. */ 1414 | if ($shared == count($bP)) { 1415 | $skip = true; 1416 | break; 1417 | } 1418 | } 1419 | 1420 | if ($skip == false) { 1421 | /* Search for any references for styling this polygon; add it */ 1422 | if ($this->blurDropShadow) { 1423 | $path->setOption('filter', 'url(#dsFilter)'); 1424 | } else { 1425 | $path->setOption('filter', 'url(#dsFilterNoBlur)'); 1426 | } 1427 | 1428 | $name = $this->findCommands($path); 1429 | 1430 | if ($name != '') { 1431 | $path->setID($name); 1432 | } 1433 | 1434 | $this->svgObjects->addObject($path); 1435 | } 1436 | } 1437 | } 1438 | } 1439 | } 1440 | 1441 | /* 1442 | * Once we've found all the boxes, we want to remove them from the grid so 1443 | * that they don't confuse the line parser. However, we don't remove any 1444 | * corner characters because these might be shared by lines. 1445 | */ 1446 | foreach ($this->svgObjects->getGroup('boxes') as $box) { 1447 | $this->clearObject($box); 1448 | } 1449 | 1450 | /* Anything after this is not a subgroup */ 1451 | $this->svgObjects->popGroup(); 1452 | } 1453 | 1454 | /* 1455 | * Our line parser operates differently than the polygon parser. This is 1456 | * because lines are not intrinsically marked with starting points (markers 1457 | * are optional) -- they just sort of begin. Additionally, so that markers 1458 | * will work, we can't just construct a line from some random point: we need 1459 | * to start at the correct edge. 1460 | * 1461 | * Thus, the line parser traverses vertically first, then horizontally. Once 1462 | * a line is found, it is cleared immediately (but leaving any control points 1463 | * in case there were any intersections. 1464 | */ 1465 | private function parseLines() { 1466 | /* Set standard line options */ 1467 | $this->svgObjects->pushGroup('lines'); 1468 | $this->svgObjects->setOption('stroke', 'black'); 1469 | $this->svgObjects->setOption('stroke-width', '2'); 1470 | $this->svgObjects->setOption('fill', 'none'); 1471 | 1472 | /* The grid is not uniform, so we need to determine the longest row. */ 1473 | $maxCols = 0; 1474 | $bound = count($this->grid); 1475 | for ($r = 0; $r < $bound; $r++) { 1476 | $maxCols = max($maxCols, count($this->grid[$r])); 1477 | } 1478 | 1479 | for ($c = 0; $c < $maxCols; $c++) { 1480 | for ($r = 0; $r < $bound; $r++) { 1481 | /* This gets set if we find a line-start here. */ 1482 | $dir = false; 1483 | 1484 | $line = new A2S_SVGPath(); 1485 | 1486 | /* 1487 | * Since the column count isn't uniform, don't attempt to handle any 1488 | * rows that don't extend out this far. 1489 | */ 1490 | if (!isset($this->grid[$r][$c])) { 1491 | continue; 1492 | } 1493 | 1494 | $char = $this->getChar($r, $c); 1495 | switch ($char) { 1496 | /* 1497 | * Do marker characters first. These are the easiest because they are 1498 | * basically guaranteed to represent the start of the line. 1499 | */ 1500 | case '<': 1501 | $e = $this->getChar($r, $c + 1); 1502 | if ($this->isEdge($e, self::DIR_RIGHT) || $this->isCorner($e)) { 1503 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1504 | $dir = self::DIR_RIGHT; 1505 | } else { 1506 | $se = $this->getChar($r + 1, $c + 1); 1507 | $ne = $this->getChar($r - 1, $c + 1); 1508 | if ($se == "\\") { 1509 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1510 | $dir = self::DIR_SE; 1511 | } elseif ($ne == '/') { 1512 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1513 | $dir = self::DIR_NE; 1514 | } 1515 | } 1516 | break; 1517 | case '^': 1518 | $s = $this->getChar($r + 1, $c); 1519 | if ($this->isEdge($s, self::DIR_DOWN) || $this->isCorner($s)) { 1520 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1521 | $dir = self::DIR_DOWN; 1522 | } elseif ($this->getChar($r + 1, $c + 1) == "\\") { 1523 | /* Don't need to check west for diagonals. */ 1524 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1525 | $dir = self::DIR_SE; 1526 | } 1527 | break; 1528 | case '>': 1529 | $w = $this->getChar($r, $c - 1); 1530 | if ($this->isEdge($w, self::DIR_LEFT) || $this->isCorner($w)) { 1531 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1532 | $dir = self::DIR_LEFT; 1533 | } 1534 | /* All diagonals come from west, so we don't need to check */ 1535 | break; 1536 | case 'v': 1537 | $n = $this->getChar($r - 1, $c); 1538 | if ($this->isEdge($n, self::DIR_UP) || $this->isCorner($n)) { 1539 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1540 | $dir = self::DIR_UP; 1541 | } elseif ($this->getChar($r - 1, $c + 1) == '/') { 1542 | $line->addMarker($c, $r, A2S_Point::IMARKER); 1543 | $dir = self::DIR_NE; 1544 | } 1545 | break; 1546 | 1547 | /* 1548 | * Edges are handled specially. We have to look at the context of the 1549 | * edge to determine whether it's the start of a line. A vertical edge 1550 | * can appear as the start of a line in the following circumstances: 1551 | * 1552 | * +------------- +-------------- +---- | (s) 1553 | * | | | | 1554 | * | | (s) +-------+ |(s) | 1555 | * +------+ | (s) 1556 | * 1557 | * From this we can extrapolate that we are a starting edge if our 1558 | * southern neighbor is a vertical edge or corner, but we have no line 1559 | * material to our north (and vice versa). This logic does allow for 1560 | * the southern / northern neighbor to be part of a separate 1561 | * horizontal line. 1562 | */ 1563 | case ':': 1564 | $line->setOption('stroke-dasharray', '5 5'); 1565 | /* FALLTHROUGH */ 1566 | case '|': 1567 | $n = $this->getChar($r-1, $c); 1568 | $s = $this->getChar($r+1, $c); 1569 | if (($s == '|' || $s == ':' || $this->isCorner($s)) && 1570 | $n != '|' && $n != ':' && !$this->isCorner($n) && 1571 | $n != '^') { 1572 | $dir = self::DIR_DOWN; 1573 | } elseif (($n == '|' || $n == ':' || $this->isCorner($n)) && 1574 | $s != '|' && $s != ':' && !$this->isCorner($s) && 1575 | $s != 'v') { 1576 | $dir = self::DIR_UP; 1577 | } 1578 | break; 1579 | 1580 | /* 1581 | * Horizontal edges have the same properties for search as vertical 1582 | * edges, except we need to look east / west. The diagrams for the 1583 | * vertical case are still accurate to visualize this case; just 1584 | * mentally turn them 90 degrees clockwise. 1585 | */ 1586 | case '=': 1587 | $line->setOption('stroke-dasharray', '5 5'); 1588 | /* FALLTHROUGH */ 1589 | case '-': 1590 | $w = $this->getChar($r, $c-1); 1591 | $e = $this->getChar($r, $c+1); 1592 | if (($w == '-' || $w == '=' || $this->isCorner($w)) && 1593 | $e != '=' && $e != '-' && !$this->isCorner($e) && 1594 | $e != '>') { 1595 | $dir = self::DIR_LEFT; 1596 | } elseif (($e == '-' || $e == '=' || $this->isCorner($e)) && 1597 | $w != '=' && $w != '-' && !$this->isCorner($w) && 1598 | $w != '<') { 1599 | $dir = self::DIR_RIGHT; 1600 | } 1601 | break; 1602 | 1603 | /* 1604 | * We can only find diagonals going north or south and east. This is 1605 | * simplified due to the fact that they have no corners. We are 1606 | * guaranteed to run into their westernmost point or their relevant 1607 | * marker. 1608 | */ 1609 | case '/': 1610 | $ne = $this->getChar($r-1, $c+1); 1611 | if ($ne == '/' || $ne == '^' || $ne == '>') { 1612 | $dir = self::DIR_NE; 1613 | } 1614 | break; 1615 | 1616 | case "\\": 1617 | $se = $this->getChar($r+1, $c+1); 1618 | if ($se == "\\" || $se == "v" || $se == '>') { 1619 | $dir = self::DIR_SE; 1620 | } 1621 | break; 1622 | 1623 | /* 1624 | * The corner case must consider all four directions. Though a 1625 | * reasonable person wouldn't use slant corners for this, they are 1626 | * considered corners, so it kind of makes sense to handle them the 1627 | * same way. For this case, envision the starting point being a corner 1628 | * character in both the horizontal and vertical case. And then 1629 | * mentally overlay them and consider that :). 1630 | */ 1631 | case '+': 1632 | case '#': 1633 | $ne = $this->getChar($r-1, $c+1); 1634 | $se = $this->getChar($r+1, $c+1); 1635 | if ($ne == '/' || $ne == '^' || $ne == '>') { 1636 | $dir = self::DIR_NE; 1637 | } elseif ($se == "\\" || $se == "v" || $se == '>') { 1638 | $dir = self::DIR_SE; 1639 | } 1640 | /* FALLTHROUGH */ 1641 | 1642 | case '.': 1643 | case "'": 1644 | $n = $this->getChar($r-1, $c); 1645 | $w = $this->getChar($r, $c-1); 1646 | $s = $this->getChar($r+1, $c); 1647 | $e = $this->getChar($r, $c+1); 1648 | if (($w == '=' || $w == '-') && $n != '|' && $n != ':' && $w != '-' && 1649 | $e != '=' && $e != '|' && $s != ':') { 1650 | $dir = self::DIR_LEFT; 1651 | } elseif (($e == '=' || $e == '-') && $n != '|' && $n != ':' && 1652 | $w != '-' && $w != '=' && $s != '|' && $s != ':') { 1653 | $dir = self::DIR_RIGHT; 1654 | } elseif (($s == '|' || $s == ':') && $n != '|' && $n != ':' && 1655 | $w != '-' && $w != '=' && $e != '-' && $e != '=' && 1656 | (($char != '.' && $char != "'") || 1657 | ($char == '.' && $s != '.') || 1658 | ($char == "'" && $s != "'"))) { 1659 | $dir = self::DIR_DOWN; 1660 | } elseif (($n == '|' || $n == ':') && $s != '|' && $s != ':' && 1661 | $w != '-' && $w != '=' && $e != '-' && $e != '=' && 1662 | (($char != '.' && $char != "'") || 1663 | ($char == '.' && $s != '.') || 1664 | ($char == "'" && $s != "'"))) { 1665 | $dir = self::DIR_UP; 1666 | } 1667 | break; 1668 | } 1669 | 1670 | /* It does actually save lines! */ 1671 | if ($dir !== false) { 1672 | $rInc = 0; $cInc = 0; 1673 | if (!$this->isMarker($char)) { 1674 | $line->addPoint($c, $r); 1675 | } 1676 | 1677 | /* 1678 | * The walk routine may attempt to add the point again, so skip it. 1679 | * If we don't, we can miss the line or end up with just a point. 1680 | */ 1681 | if ($dir == self::DIR_UP) { 1682 | $rInc = -1; $cInc = 0; 1683 | } elseif ($dir == self::DIR_DOWN) { 1684 | $rInc = 1; $cInc = 0; 1685 | } elseif ($dir == self::DIR_RIGHT) { 1686 | $rInc = 0; $cInc = 1; 1687 | } elseif ($dir == self::DIR_LEFT) { 1688 | $rInc = 0; $cInc = -1; 1689 | } elseif ($dir == self::DIR_NE) { 1690 | $rInc = -1; $cInc = 1; 1691 | } elseif ($dir == self::DIR_SE) { 1692 | $rInc = 1; $cInc = 1; 1693 | } 1694 | 1695 | /* 1696 | * Walk the points of this line. Note we don't use wallFollow; we are 1697 | * operating under the assumption that lines do not meander. (And, in 1698 | * any event, that algorithm is intended to find a closed object.) 1699 | */ 1700 | $this->walk($line, $r+$rInc, $c+$cInc, $dir); 1701 | 1702 | /* 1703 | * Remove it so that we don't confuse any other lines. This leaves 1704 | * corners in tact, still. 1705 | */ 1706 | $this->clearObject($line); 1707 | $this->svgObjects->addObject($line); 1708 | 1709 | /* We may be able to find more lines starting from this same point */ 1710 | if ($this->isCorner($char)) { 1711 | $r--; 1712 | } 1713 | } 1714 | } 1715 | } 1716 | 1717 | $this->svgObjects->popGroup(); 1718 | } 1719 | 1720 | /* 1721 | * Look for text in a file. If the text appears in a box that has a dark 1722 | * fill, we want to give it a light fill (and vice versa). This means we 1723 | * have to figure out what box it lives in (if any) and do all sorts of 1724 | * color calculation magic. 1725 | */ 1726 | private function parseText() { 1727 | $o = A2S_Scale::getInstance(); 1728 | 1729 | /* 1730 | * The style options deserve some comments. The monospace and font-size 1731 | * choices are not accidental. This gives the best sort of estimation 1732 | * for font size to scale that I could come up with empirically. 1733 | * 1734 | * N.B. This might change with different scales. I kind of feel like this 1735 | * is a bug waiting to be filed, but whatever. 1736 | */ 1737 | $fSize = 0.95*$o->yScale; 1738 | $this->svgObjects->pushGroup('text'); 1739 | $this->svgObjects->setOption('fill', 'black'); 1740 | $this->svgObjects->setOption('style', 1741 | "font-family:{$this->fontFamily};font-size:{$fSize}px"); 1742 | 1743 | /* 1744 | * Text gets the same scanning treatment as boxes. We do left-to-right 1745 | * scanning, which should probably be configurable in case someone wants 1746 | * to use this with e.g. Arabic or some other right-to-left language. 1747 | * Either way, this isn't UTF-8 safe (thanks, PHP!!!), so that'll require 1748 | * thought regardless. 1749 | */ 1750 | $boxes = $this->svgObjects->getGroup('boxes'); 1751 | $bound = count($boxes); 1752 | 1753 | foreach ($this->grid as $row => $line) { 1754 | $cols = count($line); 1755 | for ($i = 0; $i < $cols; $i++) { 1756 | if ($this->getChar($row, $i) != ' ') { 1757 | /* More magic numbers that probably need research. */ 1758 | $t = new A2S_SVGText($i - .6, $row + 0.3); 1759 | 1760 | /* Time to figure out which (if any) box we live inside */ 1761 | $tP = $t->getPoint(); 1762 | 1763 | $maxPoint = new A2S_Point(-1, -1); 1764 | $boxQueue = array(); 1765 | 1766 | for ($j = 0; $j < $bound; $j++) { 1767 | if ($boxes[$j]->hasPoint($tP->gridX, $tP->gridY)) { 1768 | $boxPoints = $boxes[$j]->getPoints(); 1769 | $boxTL = $boxPoints[0]; 1770 | 1771 | /* 1772 | * This text is in this box, but it may still be in a more 1773 | * specific nested box. Find the box with the highest top 1774 | * left X,Y coordinate. Keep a queue of boxes in case the top 1775 | * most box doesn't have a fill. 1776 | */ 1777 | if ($boxTL->y > $maxPoint->y && $boxTL->x > $maxPoint->x) { 1778 | $maxPoint->x = $boxTL->x; 1779 | $maxPoint->y = $boxTL->y; 1780 | $boxQueue[] = $boxes[$j]; 1781 | } 1782 | } 1783 | } 1784 | 1785 | if (count($boxQueue) > 0) { 1786 | /* 1787 | * Work backwards through the boxes to find the box with the most 1788 | * specific fill. 1789 | */ 1790 | for ($j = count($boxQueue) - 1; $j >= 0; $j--) { 1791 | $fill = $boxQueue[$j]->getOption('fill'); 1792 | 1793 | if ($fill == 'none' || $fill == null) { 1794 | continue; 1795 | } 1796 | 1797 | if (substr($fill, 0, 1) != '#') { 1798 | if (!isset($GLOBALS['A2S_colors'][strtolower($fill)])) { 1799 | continue; 1800 | } else { 1801 | $fill = $GLOBALS['A2S_colors'][strtolower($fill)]; 1802 | } 1803 | } else { 1804 | if (strlen($fill) != 4 && strlen($fill) != 7) { 1805 | continue; 1806 | } 1807 | } 1808 | 1809 | 1810 | if ($fill) { 1811 | /* Attempt to parse the fill color */ 1812 | if (strlen($fill) == 4) { 1813 | $cR = hexdec(str_repeat($fill[1], 2)); 1814 | $cG = hexdec(str_repeat($fill[2], 2)); 1815 | $cB = hexdec(str_repeat($fill[3], 2)); 1816 | } elseif (strlen($fill) == 7) { 1817 | $cR = hexdec(substr($fill, 1, 2)); 1818 | $cG = hexdec(substr($fill, 3, 2)); 1819 | $cB = hexdec(substr($fill, 5, 2)); 1820 | } 1821 | 1822 | /* 1823 | * This magic is gleaned from the working group paper on 1824 | * accessibility at http://www.w3.org/TR/AERT. The recommended 1825 | * contrast is a brightness difference of at least 125 and a 1826 | * color difference of at least 500. Since our default color 1827 | * is black, that makes the color difference easier. 1828 | */ 1829 | $bFill = (($cR * 299) + ($cG * 587) + ($cB * 114)) / 1000; 1830 | $bDiff = $cR + $cG + $cB; 1831 | $bText = 0; 1832 | 1833 | if ($bFill - $bText < 125 || $bDiff < 500) { 1834 | /* If black is too dark, white will work */ 1835 | $t->setOption('fill', '#fff'); 1836 | } else { 1837 | $t->setOption('fill', '#000'); 1838 | } 1839 | 1840 | break; 1841 | } 1842 | } 1843 | 1844 | if ($j < 0) { 1845 | $t->setOption('fill', '#000'); 1846 | } 1847 | } else { 1848 | /* This text isn't inside a box; make it black */ 1849 | $t->setOption('fill', '#000'); 1850 | } 1851 | 1852 | /* We found a stringy character, eat it and the rest. */ 1853 | $str = $this->getChar($row, $i++); 1854 | while ($i < count($line) && $this->getChar($row, $i) != ' ') { 1855 | $str .= $this->getChar($row, $i++); 1856 | /* Eat up to 1 space */ 1857 | if ($this->getChar($row, $i) == ' ') { 1858 | $str .= ' '; 1859 | $i++; 1860 | } 1861 | } 1862 | 1863 | if ($str == '') { 1864 | continue; 1865 | } 1866 | 1867 | $t->setString($str); 1868 | 1869 | /* 1870 | * If we were in a box, group with the box. Otherwise it gets its 1871 | * own group. 1872 | */ 1873 | if (count($boxQueue) > 0) { 1874 | $t->setOption('stroke', 'none'); 1875 | $t->setOption('style', 1876 | "font-family:{$this->fontFamily};font-size:{$fSize}px"); 1877 | $boxQueue[count($boxQueue) - 1]->addText($t); 1878 | } else { 1879 | $this->svgObjects->addObject($t); 1880 | } 1881 | } 1882 | } 1883 | } 1884 | } 1885 | 1886 | /* 1887 | * Allow specifying references that target an object starting at grid point 1888 | * (ROW,COL). This allows styling of lines, boxes, or any text object. 1889 | */ 1890 | private function injectCommands() { 1891 | $boxes = $this->svgObjects->getGroup('boxes'); 1892 | $lines = $this->svgObjects->getGroup('lines'); 1893 | $text = $this->svgObjects->getGroup('text'); 1894 | 1895 | foreach ($boxes as $obj) { 1896 | $objPoints = $obj->getPoints(); 1897 | $pointCmd = "{$objPoints[0]->gridY},{$objPoints[0]->gridX}"; 1898 | 1899 | if (isset($this->commands[$pointCmd])) { 1900 | $obj->setOptions($this->commands[$pointCmd]); 1901 | } 1902 | 1903 | foreach ($obj->getText() as $text) { 1904 | $textPoint = $text->getPoint(); 1905 | $pointCmd = "{$textPoint->gridY},{$textPoint->gridX}"; 1906 | 1907 | if (isset($this->commands[$pointCmd])) { 1908 | $text->setOptions($this->commands[$pointCmd]); 1909 | } 1910 | } 1911 | } 1912 | 1913 | foreach ($lines as $obj) { 1914 | $objPoints = $obj->getPoints(); 1915 | $pointCmd = "{$objPoints[0]->gridY},{$objPoints[0]->gridX}"; 1916 | 1917 | if (isset($this->commands[$pointCmd])) { 1918 | $obj->setOptions($this->commands[$pointCmd]); 1919 | } 1920 | } 1921 | 1922 | foreach ($text as $obj) { 1923 | $objPoint = $obj->getPoint(); 1924 | $pointCmd = "{$objPoint->gridY},{$objPoint->gridX}"; 1925 | 1926 | if (isset($this->commands[$pointCmd])) { 1927 | $obj->setOptions($this->commands[$pointCmd]); 1928 | } 1929 | } 1930 | } 1931 | 1932 | /* 1933 | * A generic, recursive line walker. This walker makes the assumption that 1934 | * lines want to go in the direction that they are already heading. I'm 1935 | * sure that there are ways to formulate lines to screw this walker up, 1936 | * but it does a good enough job right now. 1937 | */ 1938 | private function walk($path, $row, $col, $dir, $d = 0) { 1939 | $d++; 1940 | $r = $row; 1941 | $c = $col; 1942 | 1943 | if ($dir == self::DIR_RIGHT || $dir == self::DIR_LEFT) { 1944 | $cInc = ($dir == self::DIR_RIGHT) ? 1 : -1; 1945 | $rInc = 0; 1946 | } elseif ($dir == self::DIR_DOWN || $dir == self::DIR_UP) { 1947 | $cInc = 0; 1948 | $rInc = ($dir == self::DIR_DOWN) ? 1 : -1; 1949 | } elseif ($dir == self::DIR_SE || $dir == self::DIR_NE) { 1950 | $cInc = 1; 1951 | $rInc = ($dir == self::DIR_SE) ? 1 : -1; 1952 | } 1953 | 1954 | /* Follow the edge for as long as we can */ 1955 | $cur = $this->getChar($r, $c); 1956 | while ($this->isEdge($cur, $dir)) { 1957 | if ($cur == ':' || $cur == '=') { 1958 | $path->setOption('stroke-dasharray', '5 5'); 1959 | } 1960 | 1961 | if ($this->isTick($cur)) { 1962 | $path->addTick($c, $r, ($cur == 'o') ? A2S_Point::DOT : A2S_Point::TICK); 1963 | $path->addPoint($c, $r); 1964 | } 1965 | 1966 | $c += $cInc; 1967 | $r += $rInc; 1968 | $cur = $this->getChar($r, $c); 1969 | } 1970 | 1971 | if ($this->isCorner($cur)) { 1972 | if ($cur == '.' || $cur == "'") { 1973 | $path->addPoint($c, $r, A2S_Point::CONTROL); 1974 | } else { 1975 | $path->addPoint($c, $r); 1976 | } 1977 | 1978 | if ($path->isClosed()) { 1979 | $path->popPoint(); 1980 | return; 1981 | } 1982 | 1983 | /* 1984 | * Attempt first to continue in the current direction. If we can't, 1985 | * try to go in any direction other than the one opposite of where 1986 | * we just came from -- no backtracking. 1987 | */ 1988 | $n = $this->getChar($r - 1, $c); 1989 | $s = $this->getChar($r + 1, $c); 1990 | $e = $this->getChar($r, $c + 1); 1991 | $w = $this->getChar($r, $c - 1); 1992 | $next = $this->getChar($r + $rInc, $c + $cInc); 1993 | 1994 | $se = $this->getChar($r + 1, $c + 1); 1995 | $ne = $this->getChar($r - 1, $c + 1); 1996 | 1997 | if ($this->isCorner($next) || $this->isEdge($next, $dir)) { 1998 | return $this->walk($path, $r + $rInc, $c + $cInc, $dir, $d); 1999 | } elseif ($dir != self::DIR_DOWN && 2000 | ($this->isCorner($n) || $this->isEdge($n, self::DIR_UP))) { 2001 | /* Can't turn up into bottom corner */ 2002 | if (($cur != '.' && $cur != "'") || ($cur == '.' && $n != '.') || 2003 | ($cur == "'" && $n != "'")) { 2004 | return $this->walk($path, $r - 1, $c, self::DIR_UP, $d); 2005 | } 2006 | } elseif ($dir != self::DIR_UP && 2007 | ($this->isCorner($s) || $this->isEdge($s, self::DIR_DOWN))) { 2008 | /* Can't turn down into top corner */ 2009 | if (($cur != '.' && $cur != "'") || ($cur == '.' && $s != '.') || 2010 | ($cur == "'" && $s != "'")) { 2011 | return $this->walk($path, $r + 1, $c, self::DIR_DOWN, $d); 2012 | } 2013 | } elseif ($dir != self::DIR_LEFT && 2014 | ($this->isCorner($e) || $this->isEdge($e, self::DIR_RIGHT))) { 2015 | return $this->walk($path, $r, $c + 1, self::DIR_RIGHT, $d); 2016 | } elseif ($dir != self::DIR_RIGHT && 2017 | ($this->isCorner($w) || $this->isEdge($w, self::DIR_LEFT))) { 2018 | return $this->walk($path, $r, $c - 1, self::DIR_LEFT, $d); 2019 | } elseif ($dir == self::DIR_SE && 2020 | ($this->isCorner($ne) || $this->isEdge($ne, self::DIR_NE))) { 2021 | return $this->walk($path, $r - 1, $c + 1, self::DIR_NE, $d); 2022 | } elseif ($dir == self::DIR_NE && 2023 | ($this->isCorner($se) || $this->isEdge($se, self::DIR_SE))) { 2024 | return $this->walk($path, $r + 1, $c + 1, self::DIR_SE, $d); 2025 | } 2026 | } elseif ($this->isMarker($cur)) { 2027 | /* We found a marker! Add it. */ 2028 | $path->addMarker($c, $r, A2S_Point::SMARKER); 2029 | return; 2030 | } else { 2031 | /* 2032 | * Not a corner, not a marker, and we already ate edges. Whatever this 2033 | * is, it is not part of the line. 2034 | */ 2035 | $path->addPoint($c - $cInc, $r - $rInc); 2036 | return; 2037 | } 2038 | } 2039 | 2040 | /* 2041 | * This function attempts to follow a line and complete it into a closed 2042 | * polygon. It assumes that we have been called from a top point, and in any 2043 | * case that the polygon can be found by moving clockwise along its edges. 2044 | * Any time this algorithm finds a corner, it attempts to turn right. If it 2045 | * cannot turn right, it goes in any direction other than the one it came 2046 | * from. If it cannot complete the polygon by continuing in any direction 2047 | * from a point, that point is removed from the path, and we continue on 2048 | * from the previous point (since this is a recursive function). 2049 | * 2050 | * Because the function assumes that it is starting from the top left, 2051 | * if its first turn cannot be a right turn to moving down, the object 2052 | * cannot be a valid polygon. It also maintains an internal list of points 2053 | * it has already visited, and refuses to visit any point twice. 2054 | */ 2055 | private function wallFollow($path, $r, $c, $dir, $bucket = array(), $d = 0) { 2056 | $d++; 2057 | 2058 | if ($dir == self::DIR_RIGHT || $dir == self::DIR_LEFT) { 2059 | $cInc = ($dir == self::DIR_RIGHT) ? 1 : -1; 2060 | $rInc = 0; 2061 | } elseif ($dir == self::DIR_DOWN || $dir == self::DIR_UP) { 2062 | $cInc = 0; 2063 | $rInc = ($dir == self::DIR_DOWN) ? 1 : -1; 2064 | } 2065 | 2066 | /* Traverse the edge in whatever direction we are going. */ 2067 | $cur = $this->getChar($r, $c); 2068 | while ($this->isBoxEdge($cur, $dir)) { 2069 | $r += $rInc; 2070 | $c += $cInc; 2071 | $cur = $this->getChar($r, $c); 2072 | } 2073 | 2074 | /* We 'key' our location by catting r and c together */ 2075 | $key = "{$r}{$c}"; 2076 | if (isset($bucket[$key])) { 2077 | return; 2078 | } 2079 | 2080 | /* 2081 | * When we run into a corner, we have to make a somewhat complicated 2082 | * decision about which direction to turn. 2083 | */ 2084 | if ($this->isBoxCorner($cur)) { 2085 | if (!isset($bucket[$key])) { 2086 | $bucket[$key] = 0; 2087 | } 2088 | 2089 | switch ($cur) { 2090 | case '.': 2091 | case "'": 2092 | $pointExists = $path->addPoint($c, $r, A2S_Point::CONTROL); 2093 | break; 2094 | 2095 | case '#': 2096 | $pointExists = $path->addPoint($c, $r); 2097 | break; 2098 | } 2099 | 2100 | if ($path->isClosed() || $pointExists) { 2101 | return; 2102 | } 2103 | 2104 | /* 2105 | * Special case: if we're looking for our first turn and we can't make it 2106 | * due to incompatible corners, keep looking, but don't adjust our call 2107 | * depth so that we can continue to make progress. 2108 | */ 2109 | if ($d == 1 && $cur == '.' && $this->getChar($r + 1, $c) == '.') { 2110 | return $this->wallFollow($path, $r, $c + 1, $dir, $bucket, 0); 2111 | } 2112 | 2113 | /* 2114 | * We need to make a decision here on where to turn. We may have multiple 2115 | * directions we can choose, and all of them might generate a closed 2116 | * object. Always try turning right first. 2117 | */ 2118 | $newDir = false; 2119 | $n = $this->getChar($r - 1, $c); 2120 | $s = $this->getChar($r + 1, $c); 2121 | $e = $this->getChar($r, $c + 1); 2122 | $w = $this->getChar($r, $c - 1); 2123 | 2124 | if ($dir == self::DIR_RIGHT) { 2125 | if (!($bucket[$key] & self::DIR_DOWN) && 2126 | ($this->isBoxEdge($s, self::DIR_DOWN) || $this->isBoxCorner($s))) { 2127 | /* We can't turn into another top edge. */ 2128 | if (($cur != '.' && $cur != "'") || ($cur == '.' && $s != '.') || 2129 | ($cur == "'" && $s != "'")) { 2130 | $newDir = self::DIR_DOWN; 2131 | } 2132 | } else { 2133 | /* There is no right hand turn for us; this isn't a valid start */ 2134 | if ($d == 1) { 2135 | return; 2136 | } 2137 | } 2138 | } elseif ($dir == self::DIR_DOWN) { 2139 | if (!($bucket[$key] & self::DIR_LEFT) && 2140 | ($this->isBoxEdge($w, self::DIR_LEFT) || $this->isBoxCorner($w))) { 2141 | $newDir == self::DIR_LEFT; 2142 | } 2143 | } elseif ($dir == self::DIR_LEFT) { 2144 | if (!($bucket[$key] & self::DIR_UP) && 2145 | ($this->isBoxEdge($n, self::DIR_UP) || $this->isBoxCorner($n))) { 2146 | /* We can't turn into another bottom edge. */ 2147 | if (($cur != '.' && $cur != "'") || ($cur == '.' && $n != '.') || 2148 | ($cur == "'" && $n != "'")) { 2149 | $newDir = self::DIR_UP; 2150 | } 2151 | } 2152 | } elseif ($dir == self::DIR_UP) { 2153 | if (!($bucket[$key] & self::DIR_RIGHT) && 2154 | ($this->isBoxEdge($e, self::DIR_RIGHT) || $this->isBoxCorner($e))) { 2155 | $newDir = self::DIR_RIGHT; 2156 | } 2157 | } 2158 | 2159 | if ($newDir != false) { 2160 | if ($newDir == self::DIR_RIGHT || $newDir == self::DIR_LEFT) { 2161 | $cMod = ($newDir == self::DIR_RIGHT) ? 1 : -1; 2162 | $rMod = 0; 2163 | } elseif ($newDir == self::DIR_DOWN || $newDir == self::DIR_UP) { 2164 | $cMod = 0; 2165 | $rMod = ($newDir == self::DIR_DOWN) ? 1 : -1; 2166 | } 2167 | 2168 | $bucket[$key] |= $newDir; 2169 | $this->wallFollow($path, $r+$rMod, $c+$cMod, $newDir, $bucket, $d); 2170 | if ($path->isClosed()) { 2171 | return; 2172 | } 2173 | } 2174 | 2175 | /* 2176 | * Unfortunately, we couldn't complete the search by turning right, 2177 | * so we need to pick a different direction. Note that this will also 2178 | * eventually cause us to continue in the direction we were already 2179 | * going. We make sure that we don't go in the direction opposite of 2180 | * the one in which we're already headed, or an any direction we've 2181 | * already travelled for this point (we may have hit it from an 2182 | * earlier branch). We accept the first closing polygon as the 2183 | * "correct" one for this object. 2184 | */ 2185 | if ($dir != self::DIR_RIGHT && !($bucket[$key] & self::DIR_LEFT) && 2186 | ($this->isBoxEdge($w, self::DIR_LEFT) || $this->isBoxCorner($w))) { 2187 | $bucket[$key] |= self::DIR_LEFT; 2188 | $this->wallFollow($path, $r, $c - 1, self::DIR_LEFT, $bucket, $d); 2189 | if ($path->isClosed()) { 2190 | return; 2191 | } 2192 | } 2193 | if ($dir != self::DIR_LEFT && !($bucket[$key] & self::DIR_RIGHT) && 2194 | ($this->isBoxEdge($e, self::DIR_RIGHT) || $this->isBoxCorner($e))) { 2195 | $bucket[$key] |= self::DIR_RIGHT; 2196 | $this->wallFollow($path, $r, $c + 1, self::DIR_RIGHT, $bucket, $d); 2197 | if ($path->isClosed()) { 2198 | return; 2199 | } 2200 | } 2201 | if ($dir != self::DIR_DOWN && !($bucket[$key] & self::DIR_UP) && 2202 | ($this->isBoxEdge($n, self::DIR_UP) || $this->isBoxCorner($n))) { 2203 | if (($cur != '.' && $cur != "'") || ($cur == '.' && $n != '.') || 2204 | ($cur == "'" && $n != "'")) { 2205 | /* We can't turn into another bottom edge. */ 2206 | $bucket[$key] |= self::DIR_UP; 2207 | $this->wallFollow($path, $r - 1, $c, self::DIR_UP, $bucket, $d); 2208 | if ($path->isClosed()) { 2209 | return; 2210 | } 2211 | } 2212 | } 2213 | if ($dir != self::DIR_UP && !($bucket[$key] & self::DIR_DOWN) && 2214 | ($this->isBoxEdge($s, self::DIR_DOWN) || $this->isBoxCorner($s))) { 2215 | if (($cur != '.' && $cur != "'") || ($cur == '.' && $s != '.') || 2216 | ($cur == "'" && $s != "'")) { 2217 | /* We can't turn into another top edge. */ 2218 | $bucket[$key] |= self::DIR_DOWN; 2219 | $this->wallFollow($path, $r + 1, $c, self::DIR_DOWN, $bucket, $d); 2220 | if ($path->isClosed()) { 2221 | return; 2222 | } 2223 | } 2224 | } 2225 | 2226 | /* 2227 | * If we get here, the path doesn't close in any direction from this 2228 | * point (it's probably a line extension). Get rid of the point from our 2229 | * path and go back to the last one. 2230 | */ 2231 | $path->popPoint(); 2232 | return; 2233 | } elseif ($this->isMarker($this->getChar($r, $c))) { 2234 | /* Marker is part of a line, not a wall to close. */ 2235 | return; 2236 | } else { 2237 | /* We landed on some whitespace or something; this isn't a closed path */ 2238 | return; 2239 | } 2240 | } 2241 | 2242 | /* 2243 | * Clears an object from the grid, erasing all edge and marker points. This 2244 | * function retains corners in "clearCorners" to be cleaned up before we do 2245 | * text parsing. 2246 | */ 2247 | private function clearObject($obj) { 2248 | $points = $obj->getPoints(); 2249 | $closed = $obj->isClosed(); 2250 | 2251 | $bound = count($points); 2252 | for ($i = 0; $i < $bound; $i++) { 2253 | $p = $points[$i]; 2254 | 2255 | if ($i == count($points) - 1) { 2256 | /* This keeps us from handling end of line to start of line */ 2257 | if ($closed) { 2258 | $nP = $points[0]; 2259 | } else { 2260 | $nP = null; 2261 | } 2262 | } else { 2263 | $nP = $points[$i+1]; 2264 | } 2265 | 2266 | /* If we're on the same vertical axis as our next point... */ 2267 | if ($nP != null && $p->gridX == $nP->gridX) { 2268 | /* ...traverse the vertical line from the minimum to maximum points */ 2269 | $maxY = max($p->gridY, $nP->gridY); 2270 | for ($j = min($p->gridY, $nP->gridY); $j <= $maxY; $j++) { 2271 | $char = $this->getChar($j, $p->gridX); 2272 | 2273 | if (!$this->isTick($char) && $this->isEdge($char) || $this->isMarker($char)) { 2274 | $this->grid[$j][$p->gridX] = ' '; 2275 | } elseif ($this->isCorner($char)) { 2276 | $this->clearCorners[] = array($j, $p->gridX); 2277 | } elseif ($this->isTick($char)) { 2278 | $this->grid[$j][$p->gridX] = '+'; 2279 | } 2280 | } 2281 | } elseif ($nP != null && $p->gridY == $nP->gridY) { 2282 | /* Same horizontal plane; traverse from min to max point */ 2283 | $maxX = max($p->gridX, $nP->gridX); 2284 | for ($j = min($p->gridX, $nP->gridX); $j <= $maxX; $j++) { 2285 | $char = $this->getChar($p->gridY, $j); 2286 | 2287 | if (!$this->isTick($char) && $this->isEdge($char) || $this->isMarker($char)) { 2288 | $this->grid[$p->gridY][$j] = ' '; 2289 | } elseif ($this->isCorner($char)) { 2290 | $this->clearCorners[] = array($p->gridY, $j); 2291 | } elseif ($this->isTick($char)) { 2292 | $this->grid[$p->gridY][$j] = '+'; 2293 | } 2294 | } 2295 | } elseif ($nP != null && $closed == false && $p->gridX != $nP->gridX && 2296 | $p->gridY != $nP->gridY) { 2297 | /* 2298 | * This is a diagonal line starting from the westernmost point. It 2299 | * must contain max(p->gridY, nP->gridY) - min(p->gridY, nP->gridY) 2300 | * segments, and we can tell whether to go north or south depending 2301 | * on which side of zero p->gridY - nP->gridY lies. There are no 2302 | * corners in diagonals, so we don't have to keep those around. 2303 | */ 2304 | $c = $p->gridX; 2305 | $r = $p->gridY; 2306 | $rInc = ($p->gridY > $nP->gridY) ? -1 : 1; 2307 | $bound = max($p->gridY, $nP->gridY) - min($p->gridY, $nP->gridY); 2308 | 2309 | /* 2310 | * This looks like an off-by-one, but it is not. This clears the 2311 | * corner, if one exists. 2312 | */ 2313 | for ($j = 0; $j <= $bound; $j++) { 2314 | $char = $this->getChar($r, $c); 2315 | if ($char == '/' || $char == "\\" || $this->isMarker($char)) { 2316 | $this->grid[$r][$c++] = ' '; 2317 | } elseif ($this->isCorner($char)) { 2318 | $this->clearCorners[] = array($r, $c++); 2319 | } elseif ($this->isTick($char)) { 2320 | $this->grid[$r][$c] = '+'; 2321 | } 2322 | $r += $rInc; 2323 | } 2324 | 2325 | $this->grid[$p->gridY][$p->gridX] = ' '; 2326 | break; 2327 | } 2328 | } 2329 | } 2330 | 2331 | /* 2332 | * Find style information for this polygon. This information is required to 2333 | * exist on the first line after the top, touching the left wall. It's kind 2334 | * of a pain requirement, but there's not a much better way to do it: 2335 | * ditaa's handling requires too much text flung everywhere and this way 2336 | * gives you a good method for specifying *tons* of information about the 2337 | * object. 2338 | */ 2339 | private function findCommands($box) { 2340 | $points = $box->getPoints(); 2341 | $sX = $points[0]->gridX + 1; 2342 | $sY = $points[0]->gridY + 1; 2343 | $ref = ''; 2344 | if ($this->getChar($sY, $sX++) == '[') { 2345 | $char = $this->getChar($sY, $sX++); 2346 | while ($char != ']') { 2347 | $ref .= $char; 2348 | $char = $this->getChar($sY, $sX++); 2349 | } 2350 | 2351 | if ($char == ']') { 2352 | $sX = $points[0]->gridX + 1; 2353 | $sY = $points[0]->gridY + 1; 2354 | 2355 | if (!isset($this->commands[$ref]['a2s:delref']) && 2356 | !isset($this->commands[$ref]['a2s:label'])) { 2357 | $this->grid[$sY][$sX] = ' '; 2358 | $this->grid[$sY][$sX + strlen($ref) + 1] = ' '; 2359 | } else { 2360 | if (isset($this->commands[$ref]['a2s:label'])) { 2361 | $label = $this->commands[$ref]['a2s:label']; 2362 | } else { 2363 | $label = null; 2364 | } 2365 | 2366 | $len = strlen($ref) + 2; 2367 | for ($i = 0; $i < $len; $i++) { 2368 | if (strlen($label) > $i) { 2369 | $this->grid[$sY][$sX + $i] = substr($label, $i, 1); 2370 | } else { 2371 | $this->grid[$sY][$sX + $i] = ' '; 2372 | } 2373 | } 2374 | } 2375 | 2376 | if (isset($this->commands[$ref])) { 2377 | $box->setOptions($this->commands[$ref]); 2378 | } 2379 | } 2380 | } 2381 | 2382 | return $ref; 2383 | } 2384 | 2385 | /* 2386 | * Extremely useful debugging information to figure out what has been 2387 | * parsed, especially when used in conjunction with clearObject. 2388 | */ 2389 | private function dumpGrid() { 2390 | foreach($this->grid as $lines) { 2391 | echo implode('', $lines) . "\n"; 2392 | } 2393 | } 2394 | 2395 | private function getChar($row, $col) { 2396 | if (isset($this->grid[$row][$col])) { 2397 | return $this->grid[$row][$col]; 2398 | } 2399 | 2400 | return null; 2401 | } 2402 | 2403 | private function isBoxEdge($char, $dir = null) { 2404 | if ($dir == null) { 2405 | return $char == '-' || $char == '|' || char == ':' || $char == '=' || $char == '*' || $char == '+'; 2406 | } elseif ($dir == self::DIR_UP || $dir == self::DIR_DOWN) { 2407 | return $char == '|' || $char == ':' || $char == '*' || $char == '+'; 2408 | } elseif ($dir == self::DIR_LEFT || $dir == self::DIR_RIGHT) { 2409 | return $char == '-' || $char == '=' || $char == '*' || $char == '+'; 2410 | } 2411 | } 2412 | 2413 | private function isEdge($char, $dir = null) { 2414 | if ($char == 'o' || $char == 'x') { 2415 | return true; 2416 | } 2417 | 2418 | if ($dir == null) { 2419 | return $char == '-' || $char == '|' || $char == ':' || $char == '=' || $char == '*' || $char == '/' || $char == "\\"; 2420 | } elseif ($dir == self::DIR_UP || $dir == self::DIR_DOWN) { 2421 | return $char == '|' || $char == ':' || $char == '*'; 2422 | } elseif ($dir == self::DIR_LEFT || $dir == self::DIR_RIGHT) { 2423 | return $char == '-' || $char == '=' || $char == '*'; 2424 | } elseif ($dir == self::DIR_NE) { 2425 | return $char == '/'; 2426 | } elseif ($dir == self::DIR_SE) { 2427 | return $char == "\\"; 2428 | } 2429 | } 2430 | 2431 | private function isBoxCorner($char) { 2432 | return $char == '.' || $char == "'" || $char == '#'; 2433 | } 2434 | 2435 | private function isCorner($char) { 2436 | return $char == '.' || $char == "'" || $char == '#' || $char == '+'; 2437 | } 2438 | 2439 | private function isMarker($char) { 2440 | return $char == 'v' || $char == '^' || $char == '<' || $char == '>'; 2441 | } 2442 | 2443 | private function isTick($char) { 2444 | return $char == 'o' || $char == 'x'; 2445 | } 2446 | } 2447 | 2448 | /* vim:ts=2:sw=2:et: 2449 | * * */ 2450 | --------------------------------------------------------------------------------