plugins/raphael.export.js
[raphael] / plugins / raphael.export.js
1 /**
2  * Raphael.Export https://github.com/ElbertF/Raphael.Export
3  *
4  * Licensed under the MIT license:
5  * http://www.opensource.org/licenses/mit-license.php
6  *
7  */
8
9 (function (R) {
10     /**
11      * Escapes string for XML interpolation
12      * @param value string or number value to escape
13      * @returns string escaped
14      */
15     function escapeXML(s) {
16         if (typeof s === 'number')
17             return s.toString();
18
19         var replace = {
20             '&': 'amp',
21             '<': 'lt',
22             '>': 'gt',
23             '"': 'quot',
24             '\'': 'apos'
25         };
26
27         for (var entity in replace) {
28             s = s.replace(new RegExp(entity, 'g'), '&' + replace[entity] + ';');
29         }
30
31         return s;
32     }
33
34     /**
35      * Generic map function
36      * @param iterable the array or object to be mapped
37      * @param callback the callback function(element, key)
38      * @returns array
39      */
40     function map(iterable, callback) {
41         var mapped = new Array;
42
43         for (var i in iterable) {
44             if (iterable.hasOwnProperty(i)) {
45                 var value = callback.call(this, iterable[i], i);
46
47                 if (value !== null)
48                     mapped.push(value);
49             }
50         }
51
52         return mapped;
53     }
54
55     /**
56      * Generic reduce function
57      * @param iterable array or object to be reduced
58      * @param callback the callback function(initial, element, i)
59      * @param initial the initial value
60      * @return the reduced value
61      */
62     function reduce(iterable, callback, initial) {
63         for (var i in iterable) {
64             if (iterable.hasOwnProperty(i)) {
65                 initial = callback.call(this, initial, iterable[i], i);
66             }
67         }
68
69         return initial;
70     }
71
72     /**
73      * Utility method for creating a tag
74      * @param name the tag name, e.g., 'text'
75      * @param attrs the attribute string, e.g., name1="val1" name2="val2"
76      * or attribute map, e.g., { name1 : 'val1', name2 : 'val2' }
77      * @param content the content string inside the tag
78      * @returns string of the tag
79      */
80     function tag(name, attrs, matrix, content) {
81         if (typeof content === 'undefined' || content === null) {
82             content = '';
83         }
84
85         if (typeof attrs === 'object') {
86             attrs = map(attrs, function (element, name) {
87                 if (name === 'transform')
88                     return;
89
90                 return name + '="' + escapeXML(element) + '"';
91             }).join(' ');
92         }
93
94         return '<' + name + (matrix ? ' transform="matrix(' + matrix.toString().replace(/^matrix\(|\)$/g, '') + ')" ' : ' ') + attrs + '>' +
95                 content +
96                 '</' + name + '>' + "\n";
97     }
98
99     /**
100      * @return object the style object
101      */
102     function extractStyle(node) {
103         return {
104             font: {
105                 family: node.attrs.font.replace(/^.*?"(\w+)".*$/, '$1'),
106                 size: typeof node.attrs['font-size'] === 'undefined' ? null : node.attrs['font-size'],
107                 anchor: typeof node.attrs['text-anchor'] === 'undefined' ? null : node.attrs['text-anchor'],
108             }
109         };
110     }
111
112     /**
113      * @param style object from style()
114      * @return string
115      */
116     function styleToString(style) {
117         // TODO figure out what is 'normal'
118
119         var r = [
120             'font-family:' + style.font.family,
121             'font-weight:normal',
122             'font-style:normal',
123             'font-stretch:normal',
124             'font-variant:normal'
125         ];
126         if (style.font.size !== null) {
127             r.push('font-size: ' + style.font.size + 'px')
128         }
129
130         return r.join(';')
131
132     }
133
134     /**
135      * Computes tspan dy using font size. This formula was empircally determined
136      * using a best-fit line. Works well in both VML and SVG browsers.
137      * @param fontSize number
138      * @return number
139      */
140     function computeTSpanDy(fontSize, line, lines) {
141         if (fontSize === null)
142             fontSize = 10;
143
144         //return fontSize * 4.5 / 13
145         return fontSize * 4.5 / 13 * (line - .2 - lines / 2) * 3.5;
146     }
147
148     var serializer = {
149         'text': function (node) {
150             Roo.log(node);
151             style = extractStyle(node);
152             Roo.log(style);
153             var tags = new Array;
154             
155             var content = [];
156             
157             Roo.log(node.attrs['text']);
158             Roo.log(node.attrs['text'].split('\n'));
159             
160             map(node.attrs['text'].split('\n'), function (text, line, iterable) {
161                 Roo.log([text, iterable, line]);
162                 Roo.log([style.font.size, line + 1, node.attrs['text'].split('\n').length]);
163                 content.push(tag('tspan',
164                     {
165                         x: node.attrs.x,
166                         dy: computeTSpanDy(style.font.size, line + 1, node.attrs['text'].split('\n').length)
167                     },
168                     null,
169                     escapeXML(text)
170                 ))
171             });
172             
173             Roo.log(content);
174             tags.push(tag(
175                 'text',
176                 reduce(
177                     node.attrs,
178                     function (initial, value, name) {
179                         if (name !== 'text' && name !== 'w' && name !== 'h') {
180                             if (name === 'font-size')
181                                 value = value + 'px';
182
183                             initial[name] = escapeXML(value.toString());
184                         }
185
186                         return initial;
187                     },
188                     {
189                         style: 'text-anchor: ' + (style.font.anchor ? (style.font.anchor + ';') : 'middle;') +
190                                 styleToString(style) + ';'
191                     }
192                 ),
193                 node.matrix,
194                 content.join("")
195             ));
196             
197 //            map(node.attrs['text'].split('\n'), function (text, iterable, line) {
198 //                line = line || 0;
199 //                tags.push(tag(
200 //                        'text',
201 //                        reduce(
202 //                                node.attrs,
203 //                                function (initial, value, name) {
204 //                                    if (name !== 'text' && name !== 'w' && name !== 'h') {
205 //                                        if (name === 'font-size')
206 //                                            value = value + 'px';
207 //
208 //                                        initial[name] = escapeXML(value.toString());
209 //                                    }
210 //
211 //                                    return initial;
212 //                                },
213 //                                {
214 //                                    style: 'text-anchor: ' + (style.font.anchor ? (style.font.anchor + ';') : 'middle;') +
215 //                                            styleToString(style) + ';'
216 //                                }
217 //                        ),
218 //                        node.matrix,
219 //                        tag('tspan',
220 //                                {
221 //                                    dy: computeTSpanDy(style.font.size, line + 1, node.attrs['text'].split('\n').length)
222 //                                },
223 //                                null,
224 //                                escapeXML(text)
225 //                                )
226 //                        ));
227 //            });
228
229             return tags;
230         },
231         'path': function (node) {
232             var initial = (node.matrix.a === 1 && node.matrix.d === 1) ? {} : {'transform': node.matrix.toString()};
233
234
235
236             return tag(
237                     'path',
238                     reduce(
239                             node.attrs,
240                             function (initial, value, name) {
241                                 if (name === 'path') {
242                                     name = 'd';
243                                 }
244
245                                 initial[name] = (typeof (value) == 'undefined') ? '' : value.toString();
246
247                                 return initial;
248                             },
249                             {
250                                 style: 'fill:' + Raphael.color(node.attrs.fill).hex + ';'
251                             }
252                     ),
253                     node.matrix
254                     );
255         }
256         // Other serializers should go here
257     };
258
259     R.fn.toSVG = function () {
260         var
261                 paper = this,
262                 restore = {svg: R.svg, vml: R.vml},
263                 svg = '<svg style="overflow: hidden; position: relative;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' + paper.width + '" version="1.1" height="' + paper.height + '">';
264
265         R.svg = true;
266         R.vml = false;
267
268         for (var node = paper.bottom; node != null; node = node.next) {
269             if (node.node.style.display === 'none')
270                 continue;
271
272             var attrs = '';
273
274             // Use serializer
275             if (typeof serializer[node.type] === 'function') {
276                 svg += serializer[node.type](node);
277
278                 continue;
279             }
280
281             switch (node.type) {
282                 case 'image':
283                     attrs += ' preserveAspectRatio="none"';
284                     break;
285             }
286
287             for (i in node.attrs) {
288                 var name = i;
289                 var val = node.attrs[i].toString();
290                 switch (i) {
291                     case 'src':
292                         name = 'xlink:href';
293
294                         break;
295                     case 'transform':
296                         name = '';
297                         break;
298
299                     case 'stroke':
300                     case 'fill':
301                         val = Raphael.getRGB(val).hex;
302                         break;
303                 }
304
305                 if (name) {
306                     attrs += ' ' + name + '="' + escapeXML(val) + '"';
307                 }
308             }
309
310             svg += '<' + node.type + ' transform="matrix(' + node.matrix.toString().replace(/^matrix\(|\)$/g, '') + ')"' + attrs + '></' + node.type + '>';
311         }
312
313         svg += '</svg>';
314
315         R.svg = restore.svg;
316         R.vml = restore.vml;
317
318         return svg;
319     };
320 })(window.Raphael);