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 }