1 /** 2 PDF 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.pdfrender; 8 9 import core.stdc..string: strlen; 10 import core.stdc.stdio: snprintf; 11 12 import std..string; 13 import std.conv; 14 import std.math; 15 import std.zlib; 16 import printed.canvas.irenderer; 17 import printed.font.fontregistry; 18 import printed.font.opentype; 19 20 class PDFException : Exception 21 { 22 public 23 { 24 @safe pure nothrow this(string message, 25 string file =__FILE__, 26 size_t line = __LINE__, 27 Throwable next = null) 28 { 29 super(message, file, line, next); 30 } 31 } 32 } 33 34 35 final class PDFDocument : IRenderingContext2D 36 { 37 this(float pageWidthMm = 210, float pageHeightMm = 297) 38 { 39 _pageWidthMm = pageWidthMm; 40 _pageHeightMm = pageHeightMm; 41 42 _pageTreeId = _pool.allocateObjectId(); 43 _extGStateId = _pool.allocateObjectId(); 44 45 // header 46 output("%PDF-1.4\n"); 47 48 // "If a PDF file contains binary data, as most do (see 7.2, "Lexical Conventions"), 49 // the header line shall be immediately followed by a comment line containing at least 50 // four binary characters—that is, characters whose codes are 128 or greater. 51 // This ensures proper behaviour of file transfer applications that inspect data near 52 // the beginning of a file to determine whether to treat the file’s contents as text or as binary." 53 output("%¥±ë\n"); 54 55 // Start the first page 56 beginPage(); 57 } 58 59 override float pageWidth() 60 { 61 return _pageWidthMm; 62 } 63 64 override float pageHeight() 65 { 66 return _pageHeightMm; 67 } 68 69 override void newPage() 70 { 71 // end the current page, and add one 72 endPage(); 73 beginPage(); 74 } 75 76 void end() 77 { 78 if (_finished) 79 throw new PDFException("PDFDocument already finalized."); 80 81 _finished = true; 82 83 // end the current page 84 endPage(); 85 86 // Add all deferred object and finalize the PDF output 87 finalizeOutput(); 88 } 89 90 const(ubyte)[] bytes() 91 { 92 if (!_finished) 93 end(); 94 return _bytes; 95 } 96 97 // <Graphics operations> 98 99 // Text drawing 100 101 override void fontFace(string face) 102 { 103 _fontFace = face; 104 } 105 106 override void fontSize(float size) 107 { 108 _fontSize = convertPointsToMillimeters(size); 109 } 110 111 override void textAlign(TextAlign alignment) 112 { 113 _textAlign = alignment; 114 } 115 116 override void textBaseline(TextBaseline baseline) 117 { 118 _textBaseline = baseline; 119 } 120 121 override void fontWeight(FontWeight weight) 122 { 123 _fontWeight = weight; 124 } 125 126 override void fontStyle(FontStyle style) 127 { 128 _fontStyle = style; 129 } 130 131 override TextMetrics measureText(string text) 132 { 133 string fontPDFName; 134 object_id fontObjectId; 135 OpenTypeFont font; 136 getFont(_fontFace, _fontWeight, _fontStyle, fontPDFName, fontObjectId, font); 137 OpenTypeTextMetrics otMetrics = font.measureText(text); 138 TextMetrics metrics; 139 metrics.width = _fontSize * otMetrics.horzAdvance * font.invUPM(); // convert to millimeters 140 metrics.lineGap = _fontSize * font.lineGap() * font.invUPM(); 141 return metrics; 142 } 143 144 override void fillText(string text, float x, float y) 145 { 146 string fontPDFName; 147 object_id fontObjectId; 148 149 OpenTypeFont font; 150 getFont(_fontFace, _fontWeight, _fontStyle, fontPDFName, fontObjectId, font); 151 152 // We need a baseline offset in millimeters 153 float textBaselineInGlyphUnits = font.getBaselineOffset(cast(FontBaseline)_textBaseline); 154 float textBaselineInMm = _fontSize * textBaselineInGlyphUnits * font.invUPM(); 155 y += textBaselineInMm; 156 157 // Get width aka horizontal advance 158 OpenTypeTextMetrics otMetrics = font.measureText(text); 159 float horzAdvanceMm = _fontSize * otMetrics.horzAdvance * font.invUPM(); 160 final switch(_textAlign) with (TextAlign) 161 { 162 case start: // TODO bidir text 163 case left: 164 break; 165 case end: 166 case right: 167 x -= horzAdvanceMm; 168 break; 169 case center: 170 x -= horzAdvanceMm * 0.5f; 171 } 172 173 // Mark the current page as using this font 174 currentPage.markAsUsingThisFont(fontPDFName, fontObjectId); 175 176 177 178 // save CTM 179 outDelim(); 180 output('q'); 181 182 // Note: text has to be flipped vertically since we have flipped PDF coordinates vertically 183 scale(1, -1); 184 185 // begin text object 186 outDelim(); 187 output("BT"); 188 189 outName(fontPDFName); 190 outFloat(_fontSize); 191 output(" Tf"); 192 193 outFloat(x); 194 outFloat(-y); // inverted else text is not positionned rightly 195 output(" Td"); 196 197 outStringForDisplay(text); 198 output(" Tj"); 199 200 // end text object 201 outDelim(); 202 output("ET"); 203 204 // restore CTM 205 outDelim(); 206 output('Q'); 207 } 208 209 // State stack 210 211 override void save() 212 { 213 outDelim(); 214 output('q'); 215 } 216 217 override void restore() 218 { 219 outDelim(); 220 output('Q'); 221 } 222 223 // Transformations 224 override void scale(float x, float y) 225 { 226 if (x == 1 && y == 1) return; 227 transform(x, 0, 228 0, y, 229 0, 0); 230 } 231 232 override void translate(float dx, float dy) 233 { 234 if (dx == 0 && dy == 0) return; 235 transform(1, 0, 236 0, 1, 237 dx, dy); 238 } 239 240 override void rotate(float angle) 241 { 242 if (angle == 0) return; 243 float cosi = cos(angle); 244 float sine = sin(angle); 245 transform(cosi, sine, 246 -sine, cosi, 247 0, 0); 248 } 249 250 /// Multiply current transformation matrix by: 251 /// [a b 0 252 /// c d 0 253 /// e f 1] 254 void transform(float a, float b, float c, float d, float e, float f) 255 { 256 outFloat(a); 257 outFloat(b); 258 outFloat(c); 259 outFloat(d); 260 outFloat(e); 261 outFloat(f); 262 output(" cm"); 263 } 264 265 // Images 266 267 override void drawImage(Image image, float x, float y) 268 { 269 float widthMm = image.printWidth; 270 float heightMm = image.printHeight; 271 drawImage(image, x, y, widthMm, heightMm); 272 } 273 274 override void drawImage(Image image, float x, float y, float width, float height) 275 { 276 string imageName; 277 object_id id; 278 getImage(image, imageName, id); 279 280 // save CTM 281 outDelim(); 282 output('q'); 283 284 translate(x, y + height); 285 286 // Note: image has to be flipped vertically, since PDF is bottom to up 287 scale(width, -height); 288 289 outName(imageName); 290 output(" Do"); 291 292 // restore CTM 293 outDelim(); 294 output('Q'); 295 296 // Mark the current page as using this image 297 currentPage.markAsUsingThisImage(imageName, id); 298 } 299 300 // Color selection 301 302 override void fillStyle(Brush brush) 303 { 304 ubyte[4] c = brush.toRGBAColor(); 305 outFloat(c[0] / 255.0f); 306 outFloat(c[1] / 255.0f); 307 outFloat(c[2] / 255.0f); 308 output(" rg"); 309 setNonStrokeAlpha(c[3]); 310 } 311 312 override void strokeStyle(Brush brush) 313 { 314 ubyte[4] c = brush.toRGBAColor(); 315 outFloat(c[0] / 255.0f); 316 outFloat(c[1] / 255.0f); 317 outFloat(c[2] / 255.0f); 318 output(" RG"); 319 setStrokeAlpha(c[3]); 320 } 321 322 // Basic shapes 323 // UB if you are into a path. 324 325 override void fillRect(float x, float y, float width, float height) 326 { 327 outFloat(x); 328 outFloat(y); 329 outFloat(width); 330 outFloat(height); 331 output(" re"); 332 fill(); 333 } 334 335 override void strokeRect(float x, float y, float width, float height) 336 { 337 outFloat(x); 338 outFloat(y); 339 outFloat(width); 340 outFloat(height); 341 output(" re"); 342 stroke(); 343 } 344 345 // Path construction 346 347 override void beginPath(float x, float y) 348 { 349 outFloat(x); 350 outFloat(y); 351 output(" m"); 352 } 353 354 override void lineTo(float x, float y) 355 { 356 outFloat(x); 357 outFloat(y); 358 output(" l"); 359 } 360 361 // line parameters 362 363 override void lineWidth(float width) 364 { 365 outFloat(width); 366 output(" w"); 367 } 368 369 // Path painting operators 370 371 override void fill() 372 { 373 outDelim(); 374 output("f"); 375 } 376 377 override void stroke() 378 { 379 outDelim(); 380 output("S"); 381 } 382 383 override void fillAndStroke() 384 { 385 outDelim(); 386 output("B"); 387 } 388 389 override void closePath() 390 { 391 outDelim(); 392 output(" h"); 393 } 394 395 // </Graphics operations> 396 397 private: 398 399 bool _finished = false; 400 401 // Current font size 402 float _fontSize = convertPointsToMillimeters(11.0f); 403 404 // Current font face 405 string _fontFace = "Helvetica"; 406 407 // Current font weight 408 FontWeight _fontWeight = FontWeight.normal; 409 410 // Current font style 411 FontStyle _fontStyle = FontStyle.normal; 412 413 // Current font baseline 414 TextBaseline _textBaseline = TextBaseline.alphabetic; 415 416 // Current text alignment 417 TextAlign _textAlign = TextAlign.start; 418 419 // <alpha support> 420 421 object_id _extGStateId; 422 423 /// Whether this opacity value is used at all in the document (stroke operations). 424 bool[256] _strokeAlpha; 425 426 /// Whether this opacity value is used at all in the document (non-stroke operations). 427 bool[256] _nonStrokeAlpha; 428 429 void setStrokeAlpha(ubyte alpha) 430 { 431 _strokeAlpha[alpha] = true; 432 char[3] gsName; 433 makeStrokeAlphaName(alpha, gsName); 434 outName(gsName[]); 435 output(" gs"); 436 } 437 438 void setNonStrokeAlpha(ubyte alpha) 439 { 440 _nonStrokeAlpha[alpha] = true; 441 char[3] gsName; 442 makeNonStrokeAlphaName(alpha, gsName); 443 outName(gsName[]); 444 output(" gs"); 445 } 446 447 // </alpha support> 448 449 450 void finalizeOutput() 451 { 452 // Add every page object 453 foreach(i; 0..numberOfPages()) 454 { 455 beginDictObject(_pageDescriptions[i].id); 456 outName("Type"); outName("Page"); 457 outName("Parent"); outReference(_pageTreeId); 458 outName("Contents"); outReference(_pageDescriptions[i].contentId); 459 460 // Necessary for transparent PNGs 461 { 462 outName("Group"); 463 outBeginDict(); 464 outName("Type"); outName("Group"); 465 outName("S"); outName("Transparency"); 466 outName("CS"); outName("DeviceRGB"); 467 outEndDict(); 468 } 469 470 outName("Resources"); 471 outBeginDict(); 472 473 // List all fonts used by that page 474 outName("Font"); 475 outBeginDict(); 476 foreach(f; _pageDescriptions[i].fontUsed.byKeyValue) 477 { 478 outName(f.key); 479 outReference(f.value); 480 } 481 outEndDict(); 482 483 // List all images used by that page 484 outName("XObject"); 485 outBeginDict(); 486 foreach(iu; _pageDescriptions[i].imageUsed.byKeyValue) 487 { 488 outName(iu.key); 489 outReference(iu.value); 490 } 491 outEndDict(); 492 493 // Point to extended graphics state (shared across pages) 494 outName("ExtGState"); 495 outReference(_extGStateId); 496 outEndDict(); 497 endDictObject(); 498 } 499 500 // Generates ExtGState object with alpha graphics states 501 beginDictObject(_extGStateId); 502 foreach(alpha; 0..256) 503 if (_strokeAlpha[alpha]) 504 { 505 char[3] gs; 506 makeStrokeAlphaName(cast(ubyte)alpha, gs); 507 outName(gs[]); 508 outBeginDict(); 509 outName("CA"); outFloat(alpha / 255.0f); 510 outEndDict(); 511 } 512 513 foreach(alpha; 0..256) 514 if (_nonStrokeAlpha[alpha]) 515 { 516 char[3] gs; 517 makeNonStrokeAlphaName(cast(ubyte)alpha, gs); 518 outName(gs[]); 519 outBeginDict(); 520 outName("ca"); outFloat(alpha / 255.0f); 521 outEndDict(); 522 } 523 endDictObject(); 524 525 // Generates /Image objects 526 foreach(pair; _imagePDFInfos.byKeyValue()) 527 { 528 Image image = pair.key; 529 ImagePDFInfo info = pair.value; 530 531 bool isPNG = image.MIME == "image/png"; 532 bool isJPEG = image.MIME == "image/jpeg"; 533 if (!isPNG && !isJPEG) 534 throw new Exception("Unsupported image as PDF embed"); 535 536 const(ubyte)[] originalEncodedData = image.encodedData(); 537 538 // For JPEG, we can use the JPEG-encoded original image directly. 539 // For PNG, we need to decode it, and reencode using DEFLATE 540 541 string filter; 542 if (isJPEG) 543 filter = "DCTDecode"; 544 else if (isPNG) 545 filter = "FlateDecode"; 546 else 547 assert(false); 548 549 const(ubyte)[] pdfData = originalEncodedData; // what content will be embeded 550 const(ubyte)[] smaskData = null; // optional smask object 551 object_id smaskId; 552 if (isPNG) 553 { 554 import dplug.graphics.pngload; // because it's one of the fastest PNG decoder in D world 555 import core.stdc.stdlib: free; 556 557 // decode to RGBA 558 int width, height, origComponents; 559 ubyte* decoded = stbi_load_png_from_memory(originalEncodedData, width, height, origComponents, 4); 560 if (origComponents != 3 && origComponents != 4) 561 throw new Exception("Only support embed of RGB or RGBA PNG"); 562 563 int size = width * height * 4; 564 ubyte[] decodedRGBA = decoded[0..size]; 565 scope(exit) free(decoded); 566 567 // Extract RGB data 568 ubyte[] rgbData = new ubyte[width * height * 3]; 569 foreach(i; 0 .. width*height) 570 rgbData[3*i..3*i+3] = decodedRGBA[4*i..4*i+3]; 571 pdfData = compress(rgbData); 572 573 // if PNG has actual alpha information, use separate PDF image as mask 574 if (origComponents == 4) 575 { 576 // Eventually extract alpha data to a plane, that will be in a separate PNG image 577 ubyte[] alphaData = new ubyte[width * height]; 578 foreach(i; 0 .. width*height) 579 alphaData[i] = decodedRGBA[4*i+3]; 580 smaskData = compress(alphaData); 581 smaskId = _pool.allocateObjectId(); // MAYDO: allocate this on first use, detect PNG with alpha before 582 } 583 } 584 585 beginObject(info.streamId); 586 outBeginDict(); 587 outName("Type"); outName("XObject"); 588 outName("Subtype"); outName("Image"); 589 outName("Width"); outFloat(image.width()); 590 outName("Height"); outFloat(image.height()); 591 outName("ColorSpace"); outName("DeviceRGB"); 592 outName("BitsPerComponent"); outInteger(8); 593 outName("Length"); outInteger(cast(int)(pdfData.length)); 594 outName("Filter"); outName(filter); 595 if (smaskData) 596 { 597 outName("SMask"); outReference(smaskId); 598 } 599 outEndDict(); 600 outBeginStream(); 601 outputBytes(pdfData); 602 outEndStream(); 603 endObject(); 604 605 if (smaskData) 606 { 607 beginObject(smaskId); 608 outBeginDict(); 609 outName("Type"); outName("XObject"); 610 outName("Subtype"); outName("Image"); 611 outName("Width"); outFloat(image.width()); 612 outName("Height"); outFloat(image.height()); 613 outName("ColorSpace"); outName("DeviceGray"); 614 outName("BitsPerComponent"); outInteger(8); 615 outName("Length"); outInteger(cast(int)(smaskData.length)); 616 outName("Filter"); outName("FlateDecode"); 617 outEndDict(); 618 outBeginStream(); 619 outputBytes(smaskData); 620 outEndStream(); 621 endObject(); 622 } 623 } 624 625 // Generates /Font object 626 foreach(pair; _fontPDFInfos.byKeyValue()) 627 { 628 OpenTypeFont font = pair.key; 629 FontPDFInfo info = pair.value; 630 631 // Important: the font sizes given in the PDF have to be in the default glyph space where 1em = 1000 units 632 float scale = font.scaleFactorForPDF(); 633 634 beginDictObject(info.compositeFontId); 635 outName("Type"); outName("Font"); 636 outName("Subtype"); outName("Type0"); 637 outName("BaseFont"); outName(info.baseFont); 638 outName("DescendantFonts"); 639 outBeginArray(); 640 outReference(info.cidFontId); 641 outEndArray(); 642 643 // TODO ToUnicode? 644 outName("Encoding"); outName("Identity-H"); // map character to same CID 645 endDictObject(); 646 647 beginDictObject(info.cidFontId); 648 outName("Type"); outName("Font"); 649 outName("Subtype"); outName("CIDFontType2"); 650 outName("BaseFont"); outName(info.baseFont); 651 outName("FontDescriptor"); outReference(info.descriptorId); 652 653 // Export text advance ("widths") of glyphs in the font 654 outName("W"); 655 outBeginArray(); 656 foreach(crange; font.charRanges()) 657 { 658 outInteger(crange.start); // first glyph index is always 0 659 outBeginArray(); 660 foreach(dchar ch; crange.start .. crange.stop) 661 { 662 int glyph = font.glyphIndexFor(ch); 663 outFloat(scale * font.horizontalAdvanceForGlyph(glyph)); 664 } 665 outEndArray(); 666 } 667 outEndArray(); 668 669 outName("CIDToGIDMap"); outReference(info.cidToGidId); 670 671 outName("CIDSystemInfo"); 672 outBeginDict(); 673 outName("Registry"); outLiteralString("Adobe"); 674 outName("Ordering"); outLiteralString("Identity"); 675 outName("Supplement"); outInteger(0); 676 outEndDict(); 677 endDictObject(); 678 679 beginDictObject(info.descriptorId); 680 outName("Type"); outName("FontDescriptor"); 681 outName("FontName"); outName(info.baseFont); 682 outName("Flags"); outInteger( font.isMonospaced ? 5 : 4); 683 684 outName("FontBBox"); 685 outBeginArray(); 686 int[4] bb = font.boundingBox(); 687 outFloat(scale * bb[0]); 688 outFloat(scale * bb[1]); 689 outFloat(scale * bb[2]); 690 outFloat(scale * bb[3]); 691 outEndArray(); 692 693 outName("ItalicAngle"); outFloat(font.postScriptItalicAngle); 694 695 outName("Ascent"); outFloat(scale * font.ascent); 696 outName("Descent"); outFloat(scale * font.descent); 697 outName("Leading"); outFloat(scale * font.lineGap); 698 outName("CapHeight"); outFloat(scale * font.capHeight); 699 700 // See_also: https://stackoverflow.com/questions/35485179/stemv-value-of-the-truetype-font 701 outName("StemV"); outFloat(scale * 120); // since the font is always embedded in the PDF, we do not feel obligated with a valid value 702 703 outName("FontFile2"); outReference(info.streamId); 704 endDictObject(); 705 706 // font embedded as stream 707 outStream(info.streamId, font.fileData); 708 709 // CIDToGIDMap as a stream 710 // this can take quite some space 711 { 712 dchar N = font.maxAvailableChar()+1; 713 ubyte[] cidToGid = new ubyte[N*2]; 714 foreach(dchar ch; 0..N) 715 { 716 ushort gid = font.glyphIndexFor(ch); 717 cidToGid[ch*2] = (gid >> 8); 718 cidToGid[ch*2+1] = (gid & 255); 719 } 720 outStream(info.cidToGidId, cidToGid[]); 721 } 722 } 723 724 725 // Add the pages object 726 beginDictObject(_pageTreeId); 727 outName("Type"); outName("Pages"); 728 outName("Count"); outInteger(numberOfPages()); 729 outName("MediaBox"); 730 outBeginArray(); 731 outInteger(0); 732 outInteger(0); 733 // Note: device space is in point by default 734 outFloat(convertMillimetersToPoints(_pageWidthMm)); 735 outFloat(convertMillimetersToPoints(_pageHeightMm)); 736 outEndArray(); 737 outName("Kids"); 738 outBeginArray(); 739 foreach(i; 0..numberOfPages()) 740 outReference(_pageDescriptions[i].id); 741 outEndArray(); 742 endDictObject(); 743 744 // Add the root object 745 object_id rootId = _pool.allocateObjectId(); 746 beginDictObject(rootId); 747 outName("Type"); outName("Catalog"); 748 outName("Pages"); outReference(_pageTreeId); 749 endDictObject(); 750 output("\n"); 751 752 // Note: at this point all indirect objects should have been added to the output 753 byte_offset offsetOfXref = generatexrefTable(); 754 755 output("trailer\n"); 756 outBeginDict(); 757 outName("Size"); 758 outInteger(_pool.numberOfObjects()); 759 outName("Root"); 760 outReference(rootId); 761 outEndDict(); 762 763 output("\nstartxref\n"); 764 output(format("%s\n", offsetOfXref)); 765 output("%%EOF\n"); 766 } 767 768 alias object_id = int; 769 alias byte_offset = int; 770 771 // <pages> 772 773 void beginPage() 774 { 775 PageDesc p; 776 p.id = _pool.allocateObjectId(); 777 p.contentId = _pool.allocateObjectId(); 778 p.fontUsed = null; 779 780 _pageDescriptions ~= p; 781 782 _currentStreamLengthId = _pool.allocateObjectId(); 783 784 // start a new stream object with this id, referencing a future length object 785 // (as described in the PDF 32000-1:2008 specification section 7.3.2) 786 beginObject(p.contentId); 787 outBeginDict(); 788 outName("Length"); outReference(_currentStreamLengthId); 789 outEndDict(); 790 _currentStreamStart = outBeginStream(); 791 792 // Change coordinate system to match CSS, SVG, and general intuition 793 float scale = kMillimetersToPoints; 794 transform(scale, 0.0f, 795 0.0f, -1 * scale, 796 0.0f, scale * _pageHeightMm); 797 } 798 799 byte_offset _currentStreamStart; 800 object_id _currentStreamLengthId; 801 802 void endPage() 803 { 804 // end the stream object started in beginPage() 805 byte_offset streamStop = outEndStream(); 806 int streamBytes = streamStop - _currentStreamStart; 807 endObject(); 808 809 // Create a new object with the length 810 beginObject(_currentStreamLengthId); 811 outInteger(streamBytes); 812 endObject(); 813 814 // close stream object 815 } 816 817 object_id _pageTreeId; 818 819 struct PageDesc 820 { 821 object_id id; // page id 822 object_id contentId; // content of the page id 823 object_id[string] fontUsed; // a map of font objects used in that page 824 object_id[string] imageUsed; // a map of image objects used in that page 825 826 void markAsUsingThisImage(string imagePDFName, object_id imageObjectId) 827 { 828 imageUsed[imagePDFName] = imageObjectId; 829 } 830 831 void markAsUsingThisFont(string fontPDFName, object_id fontObjectId) 832 { 833 fontUsed[fontPDFName] = fontObjectId; 834 } 835 } 836 837 PageDesc[] _pageDescriptions; 838 839 int numberOfPages() 840 { 841 return cast(int)(_pageDescriptions.length); 842 } 843 844 PageDesc* currentPage() 845 { 846 return &_pageDescriptions[$-1]; 847 } 848 849 float _pageWidthMm, _pageHeightMm; 850 851 // </pages> 852 853 // <Mid-level syntax>, knows about indirect objects 854 855 // Opens a new dict object. 856 // Returns: the object ID. 857 void beginDictObject(object_id id) 858 { 859 beginObject(id); 860 outBeginDict(); 861 } 862 863 // Closes a dict object. 864 void endDictObject() 865 { 866 outEndDict(); 867 endObject(); 868 } 869 870 // Opens a new indirect object. 871 // Returns: the object ID. 872 void beginObject(object_id id) 873 { 874 outDelim(); 875 876 _pool.setObjectOffset(id, currentOffset()); 877 878 // Note: all objects are generation zero 879 output(format("%s 0 obj", id)); 880 } 881 882 // Closes an indirect object. 883 void endObject() 884 { 885 outDelim(); 886 output("endobj"); 887 } 888 889 void outStream(object_id id, const(ubyte)[] content) 890 { 891 // Note: there is very little to win between compression level 6 and 9 892 ubyte[] compressedContent = compress(content); 893 894 // Only compress if this actually reduce sizes 895 bool compressed = true; 896 if (compressedContent.length > content.length) 897 compressed = false; 898 899 const(ubyte)[] streamData = compressed ? compressedContent : content; 900 901 beginObject(id); 902 outBeginDict(); 903 outName("Length"); outInteger(cast(int)(streamData.length)); 904 if (compressed) 905 { 906 outName("Filter"); outName("FlateDecode"); 907 } 908 outEndDict(); 909 outBeginStream(); 910 outputBytes(streamData); 911 outEndStream(); 912 endObject(); 913 } 914 915 /// Returns: the offset of the xref table generated 916 byte_offset generatexrefTable() 917 { 918 int numberOfObjects = _pool.numberOfObjects; 919 byte_offset offsetOfLastXref = currentOffset(); 920 output("xref\n"); 921 output(format("0 %s\n", numberOfObjects+1)); 922 923 // special object 0, head of the freelist of objects 924 output("0000000000 65535 f \n"); 925 926 // writes all labelled objects 927 foreach(id; 1..numberOfObjects+1) 928 { 929 // Writing offset of object (i+1), not (i) 930 // Note: all objects are generation 0 931 output(format("%010s 00000 n \n", _pool.offsetOfObject(id))); 932 } 933 return offsetOfLastXref; 934 } 935 936 // </Mid-level syntax> 937 938 939 // <Low-level syntax>, don't know about objects 940 941 static immutable bool[256] spaceOrdelimiterFlag = 942 (){ 943 bool[256] t; 944 945 // space characters 946 t[0] = true; 947 t[9] = true; 948 t[10] = true; 949 t[12] = true; 950 t[13] = true; 951 t[32] = true; 952 953 // delimiter 954 t['('] = true; 955 t[')'] = true; 956 t['<'] = true; 957 t['>'] = true; 958 t['['] = true; 959 t[']'] = true; 960 t['{'] = true; 961 t['}'] = true; 962 t['/'] = true; // Note: % left out 963 return t; 964 }(); 965 966 bool isDelimiter(char c) 967 { 968 return spaceOrdelimiterFlag[c]; 969 } 970 971 // insert delimiter only if necessary 972 void outDelim() 973 { 974 char lastChar = _bytes[$-1]; 975 if (!isDelimiter(lastChar)) 976 output(' '); // space separates entities 977 } 978 979 void outReference(object_id id) 980 { 981 outDelim(); 982 output( format("%d 0 R", id) ); 983 } 984 985 ubyte[] _bytes; 986 987 byte_offset currentOffset() 988 { 989 return cast(byte_offset) _bytes.length; 990 } 991 992 void output(ubyte b) 993 { 994 _bytes ~= b; 995 } 996 997 void outputBytes(const(ubyte)[] b) 998 { 999 _bytes ~= b; 1000 } 1001 1002 void output(const(char)[] s) 1003 { 1004 _bytes ~= s.representation; 1005 } 1006 1007 void outStringForDisplay(string s) 1008 { 1009 // TODO: selection of shortest encoding instead of always UTF16-BE 1010 1011 bool allCharUnder512 = true; 1012 1013 foreach(dchar ch; s) 1014 { 1015 if (ch >= 512) 1016 { 1017 allCharUnder512 = false; 1018 break; 1019 } 1020 } 1021 1022 /* if (allCharUnder512) 1023 { 1024 outDelim(); 1025 output('('); 1026 1027 output(')'); 1028 } 1029 else */ 1030 { 1031 // Using encoding UTF16-BE 1032 output('<'); 1033 foreach(dchar ch; s) 1034 { 1035 ushort CID = cast(ushort)(ch); 1036 ubyte hi = (CID >> 8) & 255; 1037 ubyte lo = CID & 255; 1038 static immutable string hex = "0123456789ABCDEF"; 1039 output(hex[hi >> 4]); 1040 output(hex[hi & 15]); 1041 output(hex[lo >> 4]); 1042 output(hex[lo & 15]); 1043 } 1044 output('>'); 1045 } 1046 } 1047 1048 void outLiteralString(string s) 1049 { 1050 outLiteralString(cast(ubyte[])s); 1051 } 1052 1053 void outLiteralString(const(ubyte)[] s) 1054 { 1055 outDelim(); 1056 output('('); 1057 foreach(ubyte b; s) 1058 { 1059 if (b == '(') 1060 { 1061 output('\\'); 1062 output('('); 1063 } 1064 else if (b == ')') 1065 { 1066 output('\\'); 1067 output(')'); 1068 } 1069 else if (b == '\\') 1070 { 1071 output('\\'); 1072 output('\\'); 1073 } 1074 else 1075 output(b); 1076 } 1077 output(')'); 1078 } 1079 1080 void outBool(bool b) 1081 { 1082 outDelim(); 1083 output(b ? "true" : "false"); 1084 } 1085 1086 void outInteger(int d) 1087 { 1088 outDelim(); 1089 output(format("%d", d)); 1090 } 1091 1092 void outFloat(float f) 1093 { 1094 outDelim(); 1095 char[] fstr = format("%f", f).dup; 1096 replaceCommaPerDot(fstr); 1097 output(stripNumber(fstr)); 1098 } 1099 1100 void outName(const(char)[] name) 1101 { 1102 // no delimiter needed as '/' is a delimiter 1103 output('/'); 1104 output(name); 1105 } 1106 1107 // begins a stream, return the current byte offset 1108 byte_offset outBeginStream() 1109 { 1110 outDelim(); 1111 output("stream\n"); 1112 return currentOffset(); 1113 } 1114 1115 byte_offset outEndStream() 1116 { 1117 byte_offset here = currentOffset(); 1118 output("endstream"); 1119 return here; 1120 } 1121 1122 void outBeginDict() 1123 { 1124 output("<<"); 1125 } 1126 1127 void outEndDict() 1128 { 1129 output(">>"); 1130 } 1131 1132 void outBeginArray() 1133 { 1134 output("["); 1135 } 1136 1137 void outEndArray() 1138 { 1139 output("]"); 1140 } 1141 // </Low-level syntax> 1142 1143 1144 // <Object Pool> 1145 // The object pool stores id and offsets of indirect objects 1146 // exluding the "zero object". 1147 // There is support for allocating objects in advance, in order to reference them 1148 // in the stream before they appear. 1149 ObjectPool _pool; 1150 1151 static struct ObjectPool 1152 { 1153 public: 1154 1155 enum invalidOffset = cast(byte_offset)-1; 1156 1157 // Return a new object ID 1158 object_id allocateObjectId() 1159 { 1160 _currentObject += 1; 1161 _offsetsOfIndirectObjects ~= invalidOffset; 1162 assert(_currentObject == _offsetsOfIndirectObjects.length); 1163 return _currentObject; 1164 } 1165 1166 byte_offset offsetOfObject(object_id id) 1167 { 1168 assert(id > 0); 1169 assert(id <= _currentObject); 1170 return _offsetsOfIndirectObjects[id - 1]; 1171 } 1172 1173 int numberOfObjects() 1174 { 1175 assert(_currentObject == _offsetsOfIndirectObjects.length); 1176 return _currentObject; 1177 } 1178 1179 void setObjectOffset(object_id id, byte_offset offset) 1180 { 1181 assert(id > 0); 1182 assert(id <= _currentObject); 1183 _offsetsOfIndirectObjects[id - 1] = offset; 1184 } 1185 1186 private: 1187 byte_offset[] _offsetsOfIndirectObjects; // offset of 1188 object_id _currentObject = 0; 1189 } 1190 1191 static struct ImagePDFInfo 1192 { 1193 string pdfName; // "Ixx", associated name in the PDF (will be of the form /Ixx) 1194 object_id streamId; 1195 } 1196 1197 /// Associates with each open image information about 1198 /// the PDF embedding of that image. 1199 /// Note: they key act as reference that keeps the Image alive 1200 ImagePDFInfo[Image] _imagePDFInfos; 1201 1202 void getImage(Image image, 1203 out string imagePdfName, 1204 out object_id imageObjectId) 1205 { 1206 // Is this image known already? lazily create it 1207 ImagePDFInfo* pInfo = image in _imagePDFInfos; 1208 if (pInfo is null) 1209 { 1210 // Give a PDF name, and object id for this image 1211 ImagePDFInfo info; 1212 info.pdfName = format("I%d", _imagePDFInfos.length); 1213 info.streamId = _pool.allocateObjectId(); 1214 _imagePDFInfos[image] = info; 1215 pInfo = image in _imagePDFInfos; 1216 } 1217 imagePdfName = pInfo.pdfName; 1218 imageObjectId = pInfo.streamId; 1219 } 1220 1221 // Enough data to describe a font resource in a PDF 1222 static struct FontPDFInfo 1223 { 1224 object_id compositeFontId; // ID for the composite Type0 /Font object 1225 object_id cidFontId; // ID for the CID /Font object 1226 object_id descriptorId; // ID for the /FontDescriptor object 1227 object_id streamId; // ID for the file stream 1228 object_id cidToGidId; // ID for the /CIDToGIDMap stream object 1229 string pdfName; // "Fxx", associated name in the PDF (will be of the form /Fxx) 1230 string baseFont; 1231 } 1232 1233 // Ensure this font exist, generate a /name and give it back 1234 // Only PDF builtin fonts supported. 1235 // TODO: bold and oblique support 1236 void getFont(string fontFamily, 1237 FontWeight weight, 1238 FontStyle style, 1239 out string fontPDFName, 1240 out object_id fontObjectId, 1241 out OpenTypeFont outFont) 1242 { 1243 auto otWeight = cast(OpenTypeFontWeight)weight; 1244 auto otStyle = cast(OpenTypeFontStyle)style; 1245 OpenTypeFont font = theFontRegistry().findBestMatchingFont(fontFamily, otWeight, otStyle); 1246 outFont = font; 1247 1248 // is this font known already? 1249 FontPDFInfo* info = font in _fontPDFInfos; 1250 1251 // lazily create the font object in the PDF 1252 if (info is null) 1253 { 1254 // Give a PDF name, and object id for this font 1255 FontPDFInfo f; 1256 f.compositeFontId = _pool.allocateObjectId(); 1257 f.cidFontId = _pool.allocateObjectId(); 1258 f.descriptorId = _pool.allocateObjectId(); 1259 f.streamId = _pool.allocateObjectId(); 1260 f.cidToGidId = _pool.allocateObjectId(); 1261 f.pdfName = format("F%d", _fontPDFInfos.length); // technically this is only namespaced at the /Page resource level 1262 1263 /* 1264 This is tricky. The specification says: 1265 1266 "The PostScript name for the value of BaseFont 1267 may be determined in one of two ways: 1268 - If the TrueType font program's “name” table contains a 1269 PostScript name, it shall be used. 1270 - In the absence of such an entry in the “name” table, a 1271 PostScript name shall be derived from the name by 1272 which the font is known in the host operating system. 1273 On a Windows system, the name shall be based on 1274 the lfFaceName field in a LOGFONT structure; in the Mac OS, 1275 it shall be based on the name of the FOND resource. If the 1276 name contains any SPACEs, the SPACEs shall be removed." 1277 */ 1278 f.baseFont = font.postScriptName(); 1279 1280 // FIXME: follow the above instruction if no PostScript name in the truetype file. 1281 if (f.baseFont is null) 1282 throw new Exception("Couldn't find a PostScript name in the %s font."); 1283 1284 // TODO: throw if the PostScript name is not a valid PDF name 1285 1286 _fontPDFInfos[font] = f; 1287 info = font in _fontPDFInfos; 1288 assert(info !is null); 1289 } 1290 1291 fontObjectId = info.compositeFontId; 1292 fontPDFName = info.pdfName; 1293 } 1294 1295 /// Associates with each open font information about 1296 /// the PDF embedding of that font. 1297 FontPDFInfo[OpenTypeFont] _fontPDFInfos; 1298 } 1299 1300 private: 1301 1302 void replaceCommaPerDot(char[] s) 1303 { 1304 foreach(ref char ch; s) 1305 { 1306 if (ch == ',') 1307 { 1308 ch = '.'; 1309 break; 1310 } 1311 } 1312 } 1313 unittest 1314 { 1315 char[] s = "1,5".dup; 1316 replaceCommaPerDot(s); 1317 assert(s == "1.5"); 1318 } 1319 1320 // Strip number of non-significant characters. 1321 // "1.10000" => "1.1" 1322 // "1.00000" => "1" 1323 // "4" => "4" 1324 const(char)[] stripNumber(const(char)[] s) 1325 { 1326 assert(s.length > 0); 1327 1328 // Remove leading + 1329 // "+0.4" => "0.4" 1330 if (s[0] == '+') 1331 s = s[1..$]; 1332 1333 // if there is a dot, remove all trailing zeroes 1334 // ".45000" => ".45" 1335 int positionOfDot = -1; 1336 foreach(size_t i, char c; s) 1337 { 1338 if (c == '.') 1339 positionOfDot = cast(int)i; 1340 } 1341 if (positionOfDot != -1) 1342 { 1343 for (size_t i = s.length - 1; i > positionOfDot ; --i) 1344 { 1345 bool isZero = (s[i] == '0'); 1346 if (isZero) 1347 s = s[0..$-1]; // drop last char 1348 else 1349 break; 1350 } 1351 } 1352 1353 // if the final character is a dot, drop it 1354 if (s.length >= 2 && s[$-1] == '.') 1355 s = s[0..$-1]; 1356 1357 // Remove useless zero 1358 // "-0.1" => "-.1" 1359 // "0.1" => ".1" 1360 if (s.length >= 2 && s[0..2] == "0.") 1361 s = "." ~ s[2..$]; // TODO: this allocates 1362 else if (s.length >= 3 && s[0..3] == "-0.") 1363 s = "-." ~ s[3..$]; // TODO: this allocates 1364 1365 return s; 1366 } 1367 1368 unittest 1369 { 1370 assert(stripNumber("1.10000") == "1.1"); 1371 assert(stripNumber("1.0000") == "1"); 1372 assert(stripNumber("4") == "4"); 1373 assert(stripNumber("+0.4") == ".4"); 1374 assert(stripNumber("-0.4") == "-.4"); 1375 assert(stripNumber("0.0") == "0"); 1376 } 1377 1378 /// Returns: scale factor to convert from glyph space to the PDF glyph space which is fixed for the CIFFont we use. 1379 float scaleFactorForPDF(OpenTypeFont font) 1380 { 1381 return 1000.0f * font.invUPM(); 1382 } 1383 1384 enum float kMillimetersToPoints = 2.83465f; 1385 1386 1387 /// Returns: scale factor to convert from glyph space to the PDF glyph space which is fixed for the CIFFont we use. 1388 float convertMillimetersToPoints(float x) pure 1389 { 1390 return x * kMillimetersToPoints; 1391 } 1392 1393 static immutable string HEX = "0123456789abcdef"; 1394 1395 // Name /S80 means a stroke alpha value of 128.0 / 255.0 1396 void makeStrokeAlphaName(ubyte alpha, ref char[3] outName) pure 1397 { 1398 outName[0] = 'S'; 1399 outName[1] = HEX[(alpha >> 4)]; 1400 outName[2] = HEX[alpha & 15]; 1401 } 1402 1403 // Name /T80 means a non-stroke alpha value of 128.0 / 255.0 1404 void makeNonStrokeAlphaName(ubyte alpha, ref char[3] outName) pure 1405 { 1406 outName[0] = 'T'; 1407 outName[1] = HEX[(alpha >> 4)]; 1408 outName[2] = HEX[alpha & 15]; 1409 }