The OpenD Programming Language

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 import printed.canvas.internals;
18 
19 class SVGException : Exception
20 {
21     public
22     {
23         @safe pure nothrow this(string message,
24                                 string file =__FILE__,
25                                 size_t line = __LINE__,
26                                 Throwable next = null)
27         {
28             super(message, file, line, next);
29         }
30     }
31 }
32 
33 /// Renders 2D commands in a SVG file.
34 /// For comparisons between PDF and SVG.
35 class SVGDocument : IRenderingContext2D
36 {
37 public:
38     this(float pageWidthMm = 210, float pageHeightMm = 297, RenderOptions options = defaultRenderOptions)
39     {
40         _pageWidthMm = pageWidthMm;
41         _pageHeightMm = pageHeightMm;
42         _options = options;
43 
44         _stateStack = [ State(0) ];
45         beginPage();
46     }
47 
48     const(ubyte)[] bytes()
49     {
50         if (!_finished)
51             end();
52         auto header = cast(const(ubyte)[])( getHeader() );
53         auto defs = cast(const(ubyte)[])( getDefinitions() );
54 
55         return header ~ defs ~ _bytes;
56     }
57 
58     override float pageWidth()
59     {
60         return _pageWidthMm;
61     }
62 
63     override float pageHeight()
64     {
65         return _pageHeightMm;
66     }
67 
68     override void save()
69     {
70         _stateStack ~= State(currentOpenedNestedGroups() + 1);
71         output("<g>");
72     }
73 
74     /// Restore the graphical contect: transformation matrices.
75     override void restore()
76     {
77         // if you crash here => too much restore() without save()
78         assert(_stateStack.length > 1);
79 
80         int nestedGroupsBefore = currentOpenedNestedGroups();
81         _stateStack = _stateStack[0..$-1]; // pop
82         int nestedGroupsAfter = currentOpenedNestedGroups();
83 
84         for (int n = nestedGroupsBefore; n > nestedGroupsAfter; --n)
85         {
86             output("</g>");
87         }
88     }
89 
90     /// Start a new page, finish the previous one.
91     override void newPage()
92     {
93         endPage();
94         _numberOfPage += 1;
95         beginPage();
96     }
97 
98     override void fillStyle(Brush brush)
99     {
100         _currentFill = brush.toSVGColor();
101     }
102 
103     override void fillStyle(const(char)[] color)
104     {
105         fillStyle(brush(color));
106     }
107 
108     override void strokeStyle(Brush brush)
109     {
110         _currentStroke = brush.toSVGColor();
111     }
112 
113     override void strokeStyle(const(char)[] color)
114     {
115         strokeStyle(brush(color));
116     }
117 
118     override void setLineDash(float[] segments = [])
119     {
120         if (isValidLineDashPattern(segments))
121             _dashSegments = normalizeLineDashPattern(segments);
122     }
123 
124     override float[] getLineDash()
125     {
126         return _dashSegments.dup;
127     }
128 
129     override void lineDashOffset(float offset)
130     {
131         _dashOffset = offset;
132     }
133 
134     override float lineDashOffset()
135     {
136         return _dashOffset;
137     }
138 
139     override void fillRect(float x, float y, float width, float height)
140     {
141         output(format(`<rect x="%s" y="%s" width="%s" height="%s" fill="%s"/>`,
142                       convertFloatToText(x), convertFloatToText(y), convertFloatToText(width), convertFloatToText(height), _currentFill));
143     }
144 
145     override void strokeRect(float x, float y, float width, float height)
146     {
147         output(format(`<rect x="%s" y="%s" width="%s" height="%s" stroke="%s" stroke-width="%s" stroke-dasharray="%-(%f %)" stroke-dashoffset="%f" fill="none"/>`,
148                       convertFloatToText(x), convertFloatToText(y), convertFloatToText(width), convertFloatToText(height), 
149                       _currentStroke, convertFloatToText(_currentLineWidth), _dashSegments, _dashOffset));
150     }
151 
152 
153 
154     override TextMetrics measureText(const(char)[] text)
155     {
156         string svgFamilyName;
157         OpenTypeFont font;
158         getFont(_fontFace, _fontWeight, _fontStyle, svgFamilyName, font);
159         OpenTypeTextMetrics otMetrics = font.measureText(text);
160         TextMetrics metrics;
161 
162         float scale = _fontSize * font.invUPM(); // convert from glyph to millimeters
163         float baseline = font.getBaselineOffset(cast(FontBaseline)_textBaseline) * scale;
164 
165         // TODO: extent or advance? keep in sync with fillText
166         float horzAdvance = otMetrics.horzAdvance * scale; 
167         float horzOffset = 0;
168         final switch(_textAlign) with (TextAlign)
169         {
170             case start:
171             case left:
172                 break;
173             case end:
174             case right:
175                 horzOffset -= horzAdvance;
176                 break;
177             case center:
178                 horzOffset -= horzAdvance * 0.5f;
179         }
180 
181         metrics.width                  = otMetrics.horzAdvance * scale; // convert to millimeters
182         metrics.actualBoundingBoxLeft  = -horzOffset + otMetrics.xmin * scale;
183         metrics.actualBoundingBoxRight = -horzOffset + otMetrics.xmax * scale;
184         metrics.actualBoundingBoxWidth = (otMetrics.xmax - otMetrics.xmin) * scale;
185         metrics.fontBoundingBoxAscent  = -baseline + font.ascent() * scale;
186         metrics.fontBoundingBoxDescent = -baseline - font.descent() * scale;
187         metrics.fontBoundingBoxHeight  = (font.ascent() - font.descent()) * scale;
188         metrics.lineGap                = font.lineGap() * scale;
189         return metrics;
190     }
191 
192     override void fillText(const(char)[] text, float x, float y)
193     {
194         string svgFamilyName;
195         OpenTypeFont font;
196         getFont(_fontFace, _fontWeight, _fontStyle, svgFamilyName, font);
197 
198         // We need a baseline offset in millimeters
199         float textBaselineInGlyphUnits = font.getBaselineOffset(cast(FontBaseline)_textBaseline);
200         float textBaselineInMm = _fontSize * textBaselineInGlyphUnits * font.invUPM();
201 
202         // Get width aka horizontal advance
203         // TODO: instead of relying on the SVG viewer, compute the right x here.
204         version(manualHorzAlign)
205         {
206             OpenTypeTextMetrics otMetrics = font.measureText(text);
207             float horzAdvanceMm = _fontSize * otMetrics.horzAdvance * font.invUPM();
208         }
209 
210         string textAnchor="start";
211         final switch(_textAlign) with (TextAlign)
212         {
213             case start: // TODO bidir text
214             case left:
215                 textAnchor="start";
216                 break;
217             case end:
218             case right:
219                 textAnchor="end";
220                 break;
221             case center:
222                 textAnchor="middle";
223         }
224 
225         output(format(`<text x="%s" y="%s" font-family="%s" font-size="%s" fill="%s" text-anchor="%s">%s</text>`,
226                       convertFloatToText(x), convertFloatToText(y + textBaselineInMm), svgFamilyName, convertFloatToText(_fontSize), _currentFill, textAnchor, text));
227         // TODO escape XML sequences in text
228     }
229 
230     override void beginPath(float x, float y)
231     {
232         _currentPath = format("M%s %s", convertFloatToText(x), convertFloatToText(y));
233     }
234 
235     override void lineWidth(float width)
236     {
237         _currentLineWidth = width;
238     }
239 
240     override void lineTo(float dx, float dy)
241     {
242         _currentPath ~= format(" L%s %s", convertFloatToText(dx), convertFloatToText(dy));
243     }
244 
245     override void fill()
246     {
247         output(format(`<path d="%s" fill="%s"/>`, _currentPath, _currentFill));
248     }
249 
250     override void stroke()
251     {
252         output(format(`<path d="%s" fill="none" stroke="%s" stroke-width="%s" stroke-dasharray="%-(%f %)" stroke-dashoffset="%f"/>`, _currentPath, _currentStroke, convertFloatToText(_currentLineWidth), _dashSegments, _dashOffset));
253     }
254 
255     override void fillAndStroke()
256     {
257         output(format(`<path d="%s" fill="%s" stroke="%s" stroke-width="%s" stroke-dasharray="%-(%f %)" stroke-dashoffset="%f"/>`, _currentPath, _currentFill, _currentStroke, convertFloatToText(_currentLineWidth), _dashSegments, _dashOffset));
258     }
259 
260     override void closePath()
261     {
262         _currentPath ~= " Z";
263     }
264 
265     override void fontFace(string fontFace)
266     {
267         _fontFace = fontFace;
268     }
269 
270     override void fontWeight(FontWeight fontWeight)
271     {
272         _fontWeight = fontWeight;
273     }
274 
275     override void fontStyle(FontStyle fontStyle)
276     {
277         _fontStyle = fontStyle;
278     }
279 
280     override void fontSize(float size)
281     {
282         _fontSize = convertPointsToMillimeters(size);
283     }
284 
285     override void textAlign(TextAlign alignment)
286     {
287         _textAlign = alignment;
288     }
289 
290     override void textBaseline(TextBaseline baseline)
291     {
292         _textBaseline = baseline;
293     }
294 
295     override void scale(float x, float y)
296     {
297         output(format(`<g transform="scale(%s %s)">`, convertFloatToText(x), convertFloatToText(y)));
298         currentState().openedNestedGroups += 1;
299     }
300 
301     override void translate(float dx, float dy)
302     {
303         output(format(`<g transform="translate(%s %s)">`, convertFloatToText(dx), convertFloatToText(dy)));
304         currentState().openedNestedGroups += 1;
305     }
306 
307     override void rotate(float angle)
308     {
309         float angleInDegrees = (angle * 180) / PI;
310         output(format(`<g transform="rotate(%s)">`, convertFloatToText(angleInDegrees)));
311         currentState().openedNestedGroups += 1;
312     }
313 
314     override void drawImage(Image image, float x, float y)
315     {
316         drawImage(image, x, y, image.printWidth(), image.printHeight());
317     }
318 
319     override void drawImage(Image image, float x, float y, float width, float height)
320     {
321         output(format(`<image xlink:href="%s" x="%s" y="%s" width="%s" height="%s" preserveAspectRatio="none"/>`,
322                       image.toDataURI(), convertFloatToText(x), convertFloatToText(y), convertFloatToText(width), convertFloatToText(height)));
323     }
324 
325 protected:
326     string getXMLHeader()
327     {
328         return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>`;
329     }
330 
331 private:
332 
333     bool _finished = false;
334     ubyte[] _bytes;
335     RenderOptions _options;
336 
337     string _currentFill = "#000";
338     string _currentStroke = "#000";
339     float _currentLineWidth = 1;
340 
341     int _numberOfPage = 1;
342     float _pageWidthMm;
343     float _pageHeightMm;
344 
345     string _currentPath;
346     float[] _dashSegments;
347     float _dashOffset = 0f;
348 
349     string _fontFace = "Helvetica";
350     FontWeight _fontWeight = FontWeight.normal;
351     FontStyle _fontStyle = FontStyle.normal;
352     float _fontSize = convertPointsToMillimeters(11.0f);
353     TextAlign _textAlign = TextAlign.start;
354     TextBaseline _textBaseline = TextBaseline.alphabetic;
355 
356     static struct State
357     {
358         int openedNestedGroups; // Number of opened <g> at the point `save()` is called.
359     }
360     State[] _stateStack;
361 
362     ref State currentState()
363     {
364         assert(_stateStack.length > 0);
365         return _stateStack[$-1];
366     }
367 
368     int currentOpenedNestedGroups()
369     {
370         return _stateStack[$-1].openedNestedGroups;
371     }
372 
373     void output(ubyte b)
374     {
375         _bytes ~= b;
376     }
377 
378     void outputBytes(const(ubyte)[] b)
379     {
380         _bytes ~= b;
381     }
382 
383     void output(string s)
384     {
385         _bytes ~= s.representation;
386     }
387 
388     void endPage()
389     {
390         restore();
391         assert(_stateStack.length == 1);
392     }
393 
394     void beginPage()
395     {
396         _stateStack ~= State(currentOpenedNestedGroups() + 1);
397         output(format(`<g transform="translate(0,%s)">`, convertFloatToText(_pageHeightMm * (_numberOfPage-1))));
398     }
399 
400     void end()
401     {
402         if (_finished)
403             throw new SVGException("SVGDocument already finalized.");
404 
405         _finished = true;
406 
407         endPage();
408         output(`</svg>`);
409     }
410 
411     string getHeader()
412     {
413         float heightInMm = _pageHeightMm * _numberOfPage;
414         return getXMLHeader()
415             ~ 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"`
416                      ~` width="%smm" height="%smm" viewBox="0 0 %s %s" version="1.1">`,
417                      convertFloatToText(_pageWidthMm), convertFloatToText(heightInMm), convertFloatToText(_pageWidthMm), convertFloatToText(heightInMm));
418     }
419 
420     static struct FontSVGInfo
421     {
422         string svgFamilyName; // name used as family name in this SVG, doesn't have to be the real one
423     }
424 
425     /// Associates with each open font information about
426     /// the SVG embedding of that font.
427     FontSVGInfo[OpenTypeFont] _fontSVGInfos;
428 
429     // Generates the <defs> section.
430     string getDefinitions()
431     {
432         string defs;
433         defs ~=
434         `<defs>` ~
435             `<style type="text/css">` ~
436                 "<![CDATA[\n";
437 
438                 // Embed this font into the SVG as a base64 data URI
439                 foreach(pair; _fontSVGInfos.byKeyValue())
440                 {
441                     OpenTypeFont font = pair.key;
442                     FontSVGInfo info = pair.value;
443 
444                     const(ubyte)[] fontContent = font.fileData;
445                     const(char)[] base64font = Base64.encode(fontContent);
446                     defs ~=
447                         `@font-face` ~
448                         `{` ~
449                             `font-family: ` ~ info.svgFamilyName ~ `;`;
450 
451                     if (_options.embedFonts)
452                         defs ~= `src: url('data:application/x-font-ttf;charset=utf-8;base64,` ~ base64font ~ `');`; 
453                     else
454                     {
455                         /// Ref: MDN
456                         /// "Specifies the name of a locally-installed font face using the local() function, 
457                         ///  which uniquely identifies a single font face within a larger family."
458                         string fullFontName = font.fullFontName();
459                         assert(fullFontName !is null); // if false, it would mean not all font have this table and name and we have to chnge our method
460                         defs ~= `src: local('` ~ fullFontName ~ `');`;
461                     }
462                         
463                     defs ~= "}\n";
464                 }
465 
466         defs ~= `]]>`~
467             `</style>` ~
468         `</defs>`;
469         return defs;
470     }
471 
472     // Ensure this font exist, generate a /name and give it back
473     // Only PDF builtin fonts supported.
474     // TODO: bold and oblique support
475     void getFont(string fontFamily,
476                  FontWeight weight,
477                  FontStyle style,
478                  out string svgFamilyName,
479                  out OpenTypeFont outFont)
480     {
481         auto otWeight = cast(OpenTypeFontWeight)weight;
482         auto otStyle = cast(OpenTypeFontStyle)style;
483         OpenTypeFont font = theFontRegistry().findBestMatchingFont(fontFamily, otWeight, otStyle);
484         outFont = font;
485 
486         // is this font known already?
487         FontSVGInfo* info = font in _fontSVGInfos;
488 
489         // lazily create the font object in the PDF
490         if (info is null)
491         {
492             // Give a family name for this font
493             FontSVGInfo f;
494             f.svgFamilyName = format("f%d", cast(int)(_fontSVGInfos.length));
495             _fontSVGInfos[font] = f;
496             info = font in _fontSVGInfos;
497             assert(info !is null);
498         }
499 
500         svgFamilyName = info.svgFamilyName;
501     }
502 }
503 
504 private:
505 
506 const(char)[] convertFloatToText(float f)
507 {
508     char[] fstr = format("%f", f).dup;
509     replaceCommaPerDot(fstr);
510     return stripNumber(fstr);
511 }
512 
513 const(char)[] stripNumber(const(char)[] s)
514 {
515     assert(s.length > 0);
516 
517     // Remove leading +
518     // "+0.4" => "0.4"
519     if (s[0] == '+')
520         s = s[1..$];
521 
522     // if there is a dot, remove all trailing zeroes
523     // ".45000" => ".45"
524     int positionOfDot = -1;
525     foreach(size_t i, char c; s)
526     {
527         if (c == '.')
528             positionOfDot = cast(int)i;
529     }
530     if (positionOfDot != -1)
531     {
532         for (size_t i = s.length - 1; i > positionOfDot ; --i)
533         {
534             bool isZero = (s[i] == '0');
535             if (isZero)
536                 s = s[0..$-1]; // drop last char
537             else
538                 break;
539         }
540     }
541 
542     // if the final character is a dot, drop it
543     if (s.length >= 2 && s[$-1] == '.')
544         s = s[0..$-1];
545 
546     // Remove useless zero
547     // "-0.1" => "-.1"
548     // "0.1" => ".1"
549     if (s.length >= 2 && s[0..2] == "0.")
550         s = "." ~ s[2..$]; // TODO: this allocates
551     else if (s.length >= 3 && s[0..3] == "-0.")
552         s = "-." ~ s[3..$]; // TODO: this allocates
553 
554     return s;
555 }
556 
557 void replaceCommaPerDot(char[] s)
558 {
559     foreach(ref char ch; s)
560     {
561         if (ch == ',')
562         {
563             ch = '.';
564             break;
565         }
566     }
567 }
568 unittest
569 {
570     char[] s = "1,5".dup;
571     replaceCommaPerDot(s);
572     assert(s == "1.5");
573 }