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