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 }