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 }