1 /**
2 SVG renderer.
3 
4 Copyright: Guillaume Piolat 2018.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module printed.canvas.svgrender;
8 
9 import std..string;
10 import std.file;
11 import std.math;
12 import std.base64;
13 
14 import printed.canvas.irenderer;
15 import printed.font.fontregistry;
16 import printed.font.opentype;
17 
18 class SVGException : Exception
19 {
20     public
21     {
22         @safe pure nothrow this(string message,
23                                 string file =__FILE__,
24                                 size_t line = __LINE__,
25                                 Throwable next = null)
26         {
27             super(message, file, line, next);
28         }
29     }
30 }
31 
32 /// Renders 2D commands in a SVG file.
33 /// For comparisons between PDF and SVG.
34 class SVGDocument : IRenderingContext2D
35 {
36 public:
37     this(float pageWidthMm = 210, float pageHeightMm = 297)
38     {
39         _pageWidthMm = pageWidthMm;
40         _pageHeightMm = pageHeightMm;
41         beginPage();
42     }
43 
44     const(ubyte)[] bytes()
45     {
46         if (!_finished)
47             end();
48         auto header = cast(const(ubyte)[])( getHeader() );
49         auto defs = cast(const(ubyte)[])( getDefinitions() );
50 
51         return header ~ defs ~ _bytes;
52     }
53 
54     override float pageWidth()
55     {
56         return _pageWidthMm;
57     }
58 
59     override float pageHeight()
60     {
61         return _pageHeightMm;
62     }
63 
64     override void save()
65     {
66         _numberOfNestedGroups += 1;
67         output("<g>");
68     }
69 
70     /// Restore the graphical contect: transformation matrices.
71     override void restore()
72     {        
73         foreach(i; 0.._numberOfNestedGroups)
74         {
75             output("</g>");
76         }
77         _numberOfNestedGroups = 0;
78     }
79 
80     /// Start a new page, finish the previous one.
81     override void newPage()
82     {
83         endPage();
84         _numberOfPage += 1;
85         beginPage();        
86     }
87 
88     override void fillStyle(Brush brush)
89     {
90         _currentFill = brush.toSVGColor();
91     }
92 
93     override void strokeStyle(Brush brush)
94     {
95         _currentStroke = brush.toSVGColor();
96     }
97 
98     override void fillRect(float x, float y, float width, float height)
99     {
100         output(format(`<rect x="%s" y="%s" width="%s" height="%s" fill="%s"/>`, 
101                       convertFloatToText(x), convertFloatToText(y), convertFloatToText(width), convertFloatToText(height), _currentFill));
102     }
103 
104     override void strokeRect(float x, float y, float width, float height)
105     {
106         output(format(`<rect x="%s" y="%s" width="%s" height="%s" stroke="%s" fill="none"/>`, 
107                       convertFloatToText(x), convertFloatToText(y), convertFloatToText(width), convertFloatToText(height), _currentStroke));
108     }
109 
110     override TextMetrics measureText(string text)
111     {
112         string svgFamilyName;
113         OpenTypeFont font;
114         getFont(_fontFace, _fontWeight, _fontStyle, svgFamilyName, font);    
115         OpenTypeTextMetrics otMetrics = font.measureText(text);
116         TextMetrics metrics;
117         metrics.width = _fontSize * otMetrics.horzAdvance * font.invUPM(); // convert to millimeters
118         metrics.lineGap = _fontSize * font.lineGap() * font.invUPM();
119         return metrics;
120     }
121 
122     override void fillText(string text, float x, float y)
123     {
124         string svgFamilyName;
125         OpenTypeFont font;
126         getFont(_fontFace, _fontWeight, _fontStyle, svgFamilyName, font);    
127 
128         // We need a baseline offset in millimeters
129         float textBaselineInGlyphUnits = font.getBaselineOffset(cast(FontBaseline)_textBaseline);
130         float textBaselineInMm = _fontSize * textBaselineInGlyphUnits * font.invUPM();
131 
132         // Get width aka horizontal advance
133         // TODO: instead of relying on the SVG viewer, compute the right x here.
134         version(manualHorzAlign)
135         {
136             OpenTypeTextMetrics otMetrics = font.measureText(text);
137             float horzAdvanceMm = _fontSize * otMetrics.horzAdvance * font.invUPM();
138         }
139 
140         string textAnchor="start";
141         final switch(_textAlign) with (TextAlign)
142         {
143             case start: // TODO bidir text
144             case left:
145                 textAnchor="start";
146                 break;
147             case end:
148             case right:
149                 textAnchor="end";
150                 break;
151             case center:
152                 textAnchor="middle";
153         }
154 
155         output(format(`<text x="%s" y="%s" font-family="%s" font-size="%s" fill="%s" text-anchor="%s">%s</text>`, 
156                       convertFloatToText(x), convertFloatToText(y + textBaselineInMm), svgFamilyName, convertFloatToText(_fontSize), _currentFill, textAnchor, text)); 
157         // TODO escape XML sequences in text
158     }
159 
160     override void beginPath(float x, float y)
161     {
162         _currentPath = format("M%s %s", convertFloatToText(x), convertFloatToText(y));
163     }
164 
165     override void lineWidth(float width)
166     {
167         _currentLineWidth = width;
168     }
169 
170     override void lineTo(float dx, float dy)
171     {
172         _currentPath ~= format(" L%s %s", convertFloatToText(dx), convertFloatToText(dy));
173     }
174 
175     override void fill()
176     {
177         output(format(`<path d="%s" fill="%s"/>`, _currentPath, _currentFill));
178     }
179 
180     override void stroke()
181     {
182         output(format(`<path d="%s" stroke="%s" stroke-width="%s"/>`, _currentPath, _currentStroke, convertFloatToText(_currentLineWidth)));
183     }
184 
185     override void fillAndStroke()
186     {
187         output(format(`<path d="%s" fill="%s" stroke="%s" stroke-width="%s"/>`, _currentPath, _currentFill, _currentStroke, convertFloatToText(_currentLineWidth)));
188     }
189 
190     override void closePath()
191     {
192         _currentPath ~= " Z";
193     }
194 
195     override void fontFace(string fontFace)
196     {
197         _fontFace = fontFace;
198     }
199 
200     override void fontWeight(FontWeight fontWeight)
201     {
202         _fontWeight = fontWeight;
203     }
204 
205     override void fontStyle(FontStyle fontStyle)
206     {
207         _fontStyle = fontStyle;
208     }
209 
210     override void fontSize(float size)
211     {
212         _fontSize = convertPointsToMillimeters(size);
213     }
214 
215     override void textAlign(TextAlign alignment)
216     {
217         _textAlign = alignment;
218     }
219 
220     override void textBaseline(TextBaseline baseline)
221     {
222         _textBaseline = baseline;
223     }
224 
225     override void scale(float x, float y)
226     {
227         output(format(`<g transform="scale(%s %s)">`, convertFloatToText(x), convertFloatToText(y)));
228         _numberOfNestedGroups++;
229     }
230 
231     override void translate(float dx, float dy)
232     {
233         output(format(`<g transform="translate(%s %s)">`, convertFloatToText(dx), convertFloatToText(dy)));
234         _numberOfNestedGroups++;
235     }
236 
237     override void rotate(float angle)
238     {
239         float angleInDegrees = (angle * 180) / PI;
240         output(format(`<g transform="rotate(%s)">`, convertFloatToText(angleInDegrees)));
241         _numberOfNestedGroups++;
242     }
243 
244     override void drawImage(Image image, float x, float y)
245     {
246         drawImage(image, x, y, image.printWidth(), image.printHeight());
247     }
248 
249     override void drawImage(Image image, float x, float y, float width, float height)
250     {
251         output(format(`<image xlink:href="%s" x="%s" y="%s" width="%s" height="%s" preserveAspectRatio="none"/>`, 
252                       image.toDataURI(), convertFloatToText(x), convertFloatToText(y), convertFloatToText(width), convertFloatToText(height)));
253     }
254 
255 protected:
256     string getXMLHeader()
257     {
258         return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>`;
259     }
260 
261 private:
262 
263     bool _finished = false;
264     ubyte[] _bytes;
265 
266     string _currentFill = "#000";
267     string _currentStroke = "#000";
268     float _currentLineWidth = 1;
269     int _numberOfNestedGroups = 0;
270     int _numberOfPage = 1;
271     float _pageWidthMm;
272     float _pageHeightMm;
273 
274     string _currentPath;
275 
276     string _fontFace = "Helvetica";    
277     FontWeight _fontWeight = FontWeight.normal;
278     FontStyle _fontStyle = FontStyle.normal;
279     float _fontSize = convertPointsToMillimeters(11.0f);
280     TextAlign _textAlign = TextAlign.start;
281     TextBaseline _textBaseline = TextBaseline.alphabetic;
282 
283     void output(ubyte b)
284     {
285         _bytes ~= b;
286     }
287 
288     void outputBytes(const(ubyte)[] b)
289     {
290         _bytes ~= b;
291     }
292 
293     void output(string s)
294     {
295         _bytes ~= s.representation;
296     }
297 
298     void endPage()
299     {
300         restore();
301     }
302 
303     void beginPage()
304     {        
305         output(format(`<g transform="translate(0,%s)">`, convertFloatToText(_pageHeightMm * (_numberOfPage-1))));
306         _numberOfNestedGroups = 1;
307     }
308 
309     void end()
310     {
311         if (_finished)
312             throw new SVGException("SVGDocument already finalized.");
313 
314         _finished = true;
315 
316         endPage();
317         output(`</svg>`);
318     }    
319 
320     string getHeader()
321     {
322         float heightInMm = _pageHeightMm * _numberOfPage;
323         return getXMLHeader()
324             ~ format(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink= "http://www.w3.org/1999/xlink"`
325                      ~` width="%smm" height="%smm" viewBox="0 0 %s %s" version="1.1">`,
326                      convertFloatToText(_pageWidthMm), convertFloatToText(heightInMm), convertFloatToText(_pageWidthMm), convertFloatToText(heightInMm));
327     }
328 
329     static struct FontSVGInfo
330     {
331         string svgFamilyName; // name used as family name in this SVG, doesn't have to be the real one
332     }
333     
334     /// Associates with each open font information about
335     /// the SVG embedding of that font.
336     FontSVGInfo[OpenTypeFont] _fontSVGInfos;
337 
338     // Generates the <defs> section.
339     string getDefinitions()
340     {
341         string defs;
342         defs ~= 
343         `<defs>` ~
344             `<style type="text/css">` ~
345                 "<![CDATA[\n";
346 
347                 // Embed this font into the SVG as a base64 data URI
348                 foreach(pair; _fontSVGInfos.byKeyValue())
349                 {
350                     OpenTypeFont font = pair.key;
351                     FontSVGInfo info = pair.value;
352 
353                     const(ubyte)[] fontContent = font.fileData;
354                     const(char)[] base64font = Base64.encode(fontContent);
355                     defs ~= 
356                         `@font-face` ~
357                         `{` ~
358                             `font-family: ` ~ info.svgFamilyName ~ `;` ~
359                             `src: url('data:application/x-font-ttf;charset=utf-8;base64,` ~ base64font ~ `');` ~
360                         "}\n";
361                 }
362 
363         defs ~= `]]>`~
364             `</style>` ~
365         `</defs>`;
366         return defs;
367     }
368 
369     // Ensure this font exist, generate a /name and give it back
370     // Only PDF builtin fonts supported.
371     // TODO: bold and oblique support
372     void getFont(string fontFamily,
373                  FontWeight weight,
374                  FontStyle style,
375                  out string svgFamilyName,
376                  out OpenTypeFont outFont)
377     {
378         auto otWeight = cast(OpenTypeFontWeight)weight;
379         auto otStyle = cast(OpenTypeFontStyle)style;
380         OpenTypeFont font = theFontRegistry().findBestMatchingFont(fontFamily, otWeight, otStyle);
381         outFont = font;
382 
383         // is this font known already?
384         FontSVGInfo* info = font in _fontSVGInfos;
385 
386         // lazily create the font object in the PDF
387         if (info is null)
388         {
389             // Give a family name for this font
390             FontSVGInfo f;
391             f.svgFamilyName = format("f%d", cast(int)(_fontSVGInfos.length));
392             _fontSVGInfos[font] = f;
393             info = font in _fontSVGInfos;
394             assert(info !is null);
395         }
396 
397         svgFamilyName = info.svgFamilyName;
398     }
399 }
400 
401 private:
402 
403 const(char)[] convertFloatToText(float f)
404 {
405     char[] fstr = format("%f", f).dup;
406     replaceCommaPerDot(fstr);
407     return stripNumber(fstr);
408 }
409 
410 const(char)[] stripNumber(const(char)[] s)
411 {
412     assert(s.length > 0);
413 
414     // Remove leading +
415     // "+0.4" => "0.4"
416     if (s[0] == '+')
417         s = s[1..$];
418 
419     // if there is a dot, remove all trailing zeroes
420     // ".45000" => ".45"
421     int positionOfDot = -1;
422     foreach(size_t i, char c; s)
423     {
424         if (c == '.')
425             positionOfDot = cast(int)i;
426     }
427     if (positionOfDot != -1)
428     {
429         for (size_t i = s.length - 1; i > positionOfDot ; --i)
430         {
431             bool isZero = (s[i] == '0');
432             if (isZero)
433                 s = s[0..$-1]; // drop last char
434             else
435                 break;
436         }
437     }
438 
439     // if the final character is a dot, drop it
440     if (s.length >= 2 && s[$-1] == '.')
441         s = s[0..$-1];
442 
443     // Remove useless zero
444     // "-0.1" => "-.1"
445     // "0.1" => ".1"
446     if (s.length >= 2 && s[0..2] == "0.")
447         s = "." ~ s[2..$]; // TODO: this allocates
448     else if (s.length >= 3 && s[0..3] == "-0.")
449         s = "-." ~ s[3..$]; // TODO: this allocates
450 
451     return s;
452 }
453 
454 void replaceCommaPerDot(char[] s)
455 {
456     foreach(ref char ch; s)
457     {
458         if (ch == ',')
459         {
460             ch = '.';
461             break;
462         }
463     }
464 }
465 unittest
466 {
467     char[] s = "1,5".dup;
468     replaceCommaPerDot(s);
469     assert(s == "1.5");
470 }