1 // Written in the D language.
2 /*
3  * Distributed under the Boost License V1.0.
4  *
5  * Original Author Olivier Plathey
6  * D translation by Jason den Dulk.
7  */
8 
9 module jaypha.fpdf;
10 
11 import jaypha.fpdf_fonts;
12 
13 import imageformats;
14 
15 import std.array;
16 import std.file;
17 import std.string;
18 import std.json;
19 import std.conv;
20 import std.zlib;
21 import std.traits;
22 import std.algorithm;
23 import std.datetime;
24 import std.stdio;
25 import std.bitmanip;   // endianness stuff
26 
27 enum fpdfVersion = 1.7;
28 
29 float[2][string] StdPageSizes;
30 
31 //----------------------------------------------------------------------------
32 
33 shared static this()
34 {
35   StdPageSizes = 
36   [
37     "a3" : [ 841.89, 1190.55 ],
38     "a4" : [ 595.28, 841.89 ],
39     "a5" : [ 420.94, 595.28 ],
40     "letter" : [ 612.0, 792.0 ],
41     "legal" : [ 612.0, 1008.0 ],
42   ];
43 }
44 
45 /****************************************************************************
46  * class Fpdf
47  ****************************************************************************/
48 
49 //----------------------------------------------------------------------------
50 class Fpdf
51 //----------------------------------------------------------------------------
52 {
53   struct LinkInfo
54   {
55     float x,y,w,h;
56     size_t link;
57     string externalLink;
58   }
59   struct LinkDest
60   {
61     size_t page;
62     float y;
63   }
64 
65   struct ImageInfo
66   {
67     size_t i,n;
68     ulong w,h;
69     string cs;  // Color space
70     uint bpc; // Bits per component.
71     string f; // Filter
72     string dp; // DecodeParams
73     string pal; // Palette
74     ubyte[] trns; // transparency
75     ubyte[] data;
76     ubyte[] smask;
77   }
78 
79   protected:
80     size_t page = 0;
81     size_t n = 2;
82     size_t[] offsets;
83     Appender!string buffer;
84 
85     uint state = 0;                // current document state
86 
87     Appender!(string)[size_t] pages;     // array containing pages
88     bool compress = true;                 // compression flag
89     float k;                       // scale factor (number of points in user unit)
90     char DefOrientation;    // default orientation
91     char CurOrientation;    // current orientation
92     float[2] DefPageSize;          // default page size
93     float[2] CurPageSize;          // current page size
94     float[2][size_t] PageSizes;             // used for pages with non default sizes or orientations
95     float wPt, hPt;                // dimensions of current page in points
96     float w, h;                    // dimensions of current page in user unit
97     float lMargin;                 // left margin
98     float tMargin;                 // top margin
99     float rMargin;                 // right margin
100     float bMargin;                 // page break margin
101     float cMargin;                 // cell margin
102     float x, y;                    // current position in user unit
103     float lasth;                   // height of last printed cell
104     float LineWidth;               // line width in user unit
105     string fontpath;               // path containing fonts
106     string[] CoreFonts;            // array of core font names
107     FontFileInfo[string] FontFiles;            // array of font files
108     string[] diffs;                // array of encoding differences
109     string FontFamily;             // current font family
110     string FontStyle;              // current font style
111     bool underline;                // underlining flag
112     FontInfo CurrentFont;          // current font info
113     float FontSizePt = 12.0;       // current font size in points
114     float FontSize;                // current font size in user unit
115     string DrawColor;              // commands for drawing color
116     string FillColor;              // commands for filling color
117     string TextColor;              // commands for text color
118     bool ColorFlag;                // indicates whether fill and text colors are different
119     float ws = 0.0;                // word spacing
120     LinkInfo[][size_t] PageLinks;     // array of links in pages
121     LinkDest[] links;                  // array of internal links
122     bool AutoPageBreak;            // automatic page breaking
123     float PageBreakTrigger;        // threshold used to trigger page breaks
124     bool InHeader;                 // flag set when processing header
125     bool InFooter;                 // flag set when processing footer
126     string zoomMode;               // zoom display mode
127     float zoom = 0;                // numeric zoom
128     string layoutMode;         // layout display mode
129     string title;                  // title
130     string subject;                // subject
131     string author;                 // author
132     string[] keywords;             // keywords
133     string creator;                // creator
134     string aliasNbPages;           // alias for total number of pages
135     float pdfVersion;             // PDF version number
136 
137     // Image files;
138     ImageInfo[] images;
139     size_t[string] imageInfoIdx;
140 
141     // Fonts
142     FontInfo[] fonts;
143     size_t[string] fontInfoIdx;
144 
145   public:
146 
147   //-----------------------------------
148 
149   private void setUnit(string unit)
150   {
151     // Scale factor;
152     switch (unit)
153     {
154       case "pt": k = 1; break;
155       case "mm": k = 72/25.4; break;
156       case "cm": k = 72/2.54; break;
157       case "in": k = 72; break;
158       default: throw new Exception("Unknown unit");
159     }
160   }
161 
162   //-----------------------------------
163 
164   this(char orientation = 'P', string unit = "mm", string size = "a4")
165   {
166     setUnit(unit);
167     setup(orientation, _getpagesize(size));
168   }
169 
170   this(char orientation, string unit, float[2] sz)
171   {
172     setUnit(unit);
173     setup(orientation, _getpagesize(sz));
174   }
175 
176   //-----------------------------------
177 
178   private void setup(char orientation, float[2] sz)
179   {
180     n = 2;
181     buffer = appender!string();
182     DrawColor = "0 G";
183     FillColor = "0 g";
184     TextColor = "0 g";
185 
186     // Font path
187     static if (__traits(compiles, fpdfFontPath))
188     {
189       fontpath = fpdfFontPath;
190       if (fontpath[$-1] != '/' && fontpath[$-1] != '\\')
191         fontpath ~= "/";
192     }
193 
194     // Core fonts
195     CoreFonts = [ "courier", "helvetica", "times", "symbol", "zapfdingbats" ];
196 
197     DefPageSize = sz;
198     CurPageSize = sz;
199 
200     // Page Orientation
201     if (orientation == 'P')
202     {
203       w = sz[0];
204       h = sz[1];
205     }
206     else if (orientation == 'L')
207     {
208       w = sz[1];
209       h = sz[0];
210     }
211     else
212       throw new Exception("Unkown orientation");
213 
214     DefOrientation = orientation;
215     CurOrientation = orientation;
216     wPt = w*k;
217     hPt = h*k;
218 
219     // Margins
220     auto margin = 28.35/k; // 1cm
221     SetMargins(margin,margin);
222     cMargin = margin/10; // 1mm
223     LineWidth = 0.567/k; // 0.2mm
224     SetAutoPageBreak(true, 2*margin);
225 
226     pdfVersion = 1.3;
227   }
228 
229   void SetMargins(float left, float top, float right = -1)
230   {
231     lMargin = left;
232     tMargin = top;
233     if (right < 0)
234       rMargin = left;
235     else
236       rMargin = right;
237   }
238 
239   void SetLeftMargin(float margin)
240   {
241     lMargin = margin;
242     if (page > 0 && x < margin)
243      x = margin;
244   }
245 
246   void SetTopMargin(float margin)
247   {
248     // Set top margin
249     tMargin = margin;
250   }
251 
252   void SetRightMargin(float margin)
253   {
254     // Set right margin
255     rMargin = margin;
256   }
257 
258   void SetAutoPageBreak(bool isAuto, float margin=0)
259   {
260     // Set auto page break mode and triggering margin
261     AutoPageBreak = isAuto;
262     bMargin = margin;
263     PageBreakTrigger = h-margin;
264   }
265 
266   void SetDisplayMode(string zoom, string layout="default")
267   {
268     // Set display mode in viewer
269     if (zoom!="fullpage" && zoom!="fullwidth" && zoom!="real" && zoom!="default")
270       throw new Exception("Incorrect zoom display mode "~ zoom);
271     if(layout!="single" && layout!="continuous" && layout!="two" && layout!="default")
272       throw new Exception("Incorrect layout display mode: "~layout);
273     zoomMode = zoom;
274     this.zoom = 0;
275     layoutMode = layout;
276   }
277 
278   void SetDisplayMode(float zoom, string layout="default")
279   {
280     // Set display mode in viewer
281     if(layout!="single" && layout!="continuous" && layout!="two" && layout!="default")
282       throw new Exception("Incorrect layout display mode: "~layout);
283     zoomMode = "custom";
284     this.zoom = zoom;
285     layoutMode = layout;
286   }
287 
288   void SetCompression(bool compress)
289   {
290     // Set page compression TODO is compress available?
291     this.compress = compress;
292   }
293 
294   void SetTitle(string title)
295   {
296     // Title of document
297     this.title = title;
298   }
299 
300   void SetSubject(string subject)
301   {
302     // Subject of document
303     this.subject = subject;
304   }
305 
306   void SetAuthor(string author)
307   {
308     // Author of document
309     this.author = author;
310   }
311 
312   void SetKeywords(string[] keywords)
313   {
314     // Keywords of document
315     this.keywords = keywords;
316   }
317 
318   void SetCreator(string creator)
319   {
320     // Creator of document
321     this.creator = creator;
322   }
323 
324   void AliasNbPages(string nbAlias="{nb}")
325   {
326     // Define an alias for total number of pages
327     aliasNbPages = nbAlias;
328   }
329 
330   void Open()
331   {
332     // Begin document
333     state = 1;
334   }
335 
336   void Close()
337   {
338     // Terminate document
339     if(state==3)
340       return;
341     if(page==0)
342       AddPage();
343     // Page footer
344     InFooter = true;
345     Footer();
346     InFooter = false;
347     // Close page
348     _endpage();
349     // Close document
350     _enddoc();
351   }
352 
353   //-----------------------------------
354 
355   void AddPage() { AddPage(DefOrientation, DefPageSize); }
356   void AddPage(char orientation, string size = null)
357   {
358     if (size is null) AddPage(orientation, DefPageSize);
359     else AddPage(orientation, _getpagesize(size));
360   }
361 
362   void AddPage(char orientation, float[2] size)
363   {
364     // Start a new page
365     if(state==0)
366       Open();
367     auto family = FontFamily;
368     auto style = FontStyle ~ (underline ? "U" : "");
369     auto fontsize = FontSizePt;
370     auto lw = LineWidth;
371     auto dc = DrawColor;
372     auto fc = FillColor;
373     auto tc = TextColor;
374     auto cf = ColorFlag;
375     if(page>0)
376     {
377       // Page footer
378       InFooter = true;
379       Footer();
380       InFooter = false;
381       // Close page
382       _endpage();
383     }
384 
385     // Start new page
386     _beginpage(orientation,size);
387     // Set line cap style to square
388     _out("2 J");
389     // Set line width
390     LineWidth = lw;
391     _out(format("%.2F w",lw*k));
392     // Set font
393     if(family)
394       SetFont(family,style,fontsize);
395     // Set colors
396     DrawColor = dc;
397     if (dc!="0 G")
398       _out(dc);
399     FillColor = fc;
400     if(fc!="0 g")
401       _out(fc);
402     TextColor = tc;
403     ColorFlag = cf;
404     // Page header
405     InHeader = true;
406     Header();
407     InHeader = false;
408     // Restore line width
409     if(LineWidth!=lw)
410     {
411       LineWidth = lw;
412       _out(format("%.2F w",lw*k));
413     }
414     // Restore font
415     if(family)
416       SetFont(family,style,fontsize);
417     // Restore colors
418     if(DrawColor!=dc)
419     {
420       DrawColor = dc;
421       _out(dc);
422     }
423     if(FillColor!=fc)
424     {
425       FillColor = fc;
426       _out(fc);
427     }
428     TextColor = tc;
429     ColorFlag = cf;
430   }
431 
432   //-----------------------------------
433 
434   void Header()
435   {
436     // To be implemented in your own inherited class
437   }
438 
439   void Footer()
440   {
441     // To be implemented in your own inherited class
442   }
443 
444   //-----------------------------------
445 
446   ulong PageNo()
447   {
448     // Get current page number
449     return page;
450   }
451 
452   //-----------------------------------
453 
454   void SetDrawColor(ulong r)
455   {
456     // Set color for all stroking operations
457     DrawColor = format("%.3F G",to!float(r)/255);
458     if(page>0)
459      _out(DrawColor);
460   }
461 
462   //-----------------------------------
463 
464   void SetDrawColor(ulong r, ulong g, ulong b)
465   {
466     // Set color for all stroking operations
467     DrawColor = format("%.3F %.3F %.3F RG",to!float(r)/255,to!float(g)/255,to!float(b)/255);
468     if(page>0)
469      _out(DrawColor);
470   }
471 
472   //-----------------------------------
473 
474   void SetFillColor(ulong r)
475   {
476     // Set color for all filling operations
477     FillColor = format("%.3F g",to!float(r)/255);
478     ColorFlag = (FillColor!=TextColor);
479     if(page>0)
480       _out(FillColor);
481   }
482 
483   void SetFillColor(ulong r, ulong g, ulong b)
484   {
485     // Set color for all filling operations
486     FillColor = format("%.3F %.3F %.3F rg",to!float(r)/255,to!float(g)/255,to!float(b)/255);
487     ColorFlag = (FillColor!=TextColor);
488     if(page>0)
489       _out(FillColor);
490   }
491 
492   //-----------------------------------
493 
494   void SetTextColor(ulong r)
495   {
496     TextColor = format("%.3F g",to!float(r)/255);
497     ColorFlag = (FillColor!=TextColor);
498   }
499 
500   void SetTextColor(ulong r, ulong g, ulong b)
501   {
502     TextColor = format("%.3F %.3F %.3F rg",to!float(r)/255,to!float(g)/255,to!float(b)/255);
503     ColorFlag = (FillColor!=TextColor);
504   }
505 
506   //-----------------------------------
507 
508   float GetStringWidth(string s)
509   {
510     // Get width of a string in the current font
511     auto cw = CurrentFont.cw;
512     float w = 0;
513 
514     for (auto i=0; i<s.length; ++i) // TODO check for UTF issues
515       w += cw[s[i]];
516     return w*FontSize/1000;
517   }
518 
519   //-----------------------------------
520 
521   void SetLineWidth(float width)
522   {
523     // Set line width
524     LineWidth = width;
525     if (page>0)
526       _out(format("%.2F w",width*k));
527   }
528 
529   //-----------------------------------
530 
531   void Line(float x1, float y1, float x2, float y2)
532   {
533     // Draw a line
534     _out(format("%.2F %.2F m %.2F %.2F l S",x1*k,(h-y1)*k,x2*k,(h-y2)*k));
535   }
536 
537   //-----------------------------------
538 
539   void Rect(float x, float y, float w, float h, string style="")
540   {
541     char op;
542     // Draw a rectangle
543     if(style=="F")
544       op = 'f';
545     else if (style=="FD" || style=="DF")
546       op = 'B';
547     else
548       op = 'S';
549     _out(format("%.2F %.2F %.2F %.2F re %c",x*k,(this.h-y)*k,w*k,-h*k,op));
550   }
551 
552   //-----------------------------------
553 
554   void AddFont(string family, string style, string file)
555   {
556 /+
557     // Add a TrueType, OpenType or Type1 font
558     family = toLower(family);
559     style = toUpper(style);
560     if (style=="IB")
561       style = "BI";
562     auto fontkey = family ~ style;
563     if (fontkey in fonts)
564       return;
565     auto info = _loadfont(file);
566     info.i = fonts.length+1;
567     if(!info.diff.empty)
568     {
569       long n = -1;
570       // Search existing encodings
571       for (auto i=0; i< diffs.length; ++i)
572         if (diffs[i] == info.diff)
573         { n = i; break; }
574           
575       if(n<0)
576       {
577         n = diffs.length+1;
578         diffs[n] = info.diff;
579       }
580       info.diffn = n;
581     }
582     if(!info.file.empty)
583     {
584       // Embedded font
585       if(info.type=="TrueType")
586         FontFiles[info.file] = FontFileInfo(info.originalsize);
587       else
588         FontFiles[info.file] = FontFileInfo(info.size1, info.size2);
589     }
590     fonts[fontkey] = info;
591 +/
592   }
593 
594   //-----------------------------------
595 
596   void SetFont(string family, string style=null, float size=0)
597   {
598 
599     // Select a font; size given in points
600     if (family.empty)
601       family = FontFamily;
602     else
603       family = toLower(family);
604     style = toUpper(style);
605     if(indexOf(style,'U') != -1)
606     {
607       underline = true;
608       style = replace(style,"U","");
609     }
610     else
611       underline = false;
612     if (style=="IB")
613       style = "BI";
614     if (size==0)
615       size = FontSizePt;
616     // Test if font is already selected
617     if (FontFamily==family && FontStyle==style && FontSizePt==size)
618       return;
619     // Test if font is already loaded
620     auto fontkey = family~style;
621     if(!(fontkey in fontInfoIdx))
622     {
623       // Test if one of the core fonts
624       if(family=="arial")
625         family = "helvetica";
626       if(canFind(CoreFonts, family))
627       {
628         if(family=="symbol" || family=="zapfdingbats")
629           style = "";
630         fontkey = family~style;
631         if(!(fontkey in fontInfoIdx))
632         {
633           FontInfo info = cast(FontInfo) coreFonts[fontkey];
634           fontInfoIdx[fontkey] = fonts.length;
635           info.i = fonts.length+1;
636           fonts ~= info;
637         }
638       }
639       else
640         throw new Exception("Undefined font: "~family~" "~style);
641     }
642     // Select it
643     FontFamily = family;
644     FontStyle = style;
645     FontSizePt = size;
646     FontSize = size/k;
647     CurrentFont = fonts[fontInfoIdx[fontkey]];
648     if(page>0)
649       _out(format("BT /F%d %.2F Tf ET",CurrentFont.i,FontSizePt));
650   }
651 
652   //---------------------------------
653 
654   void SetFontSize(float size)
655   {
656     // Set font size in points
657     if(FontSizePt==size)
658       return;
659     FontSizePt = size;
660     FontSize = size/k;
661     if(page>0)
662       _out(format("BT /F%d %.2F Tf ET",CurrentFont.i,FontSizePt));
663   }
664 
665   //---------------------------------
666 
667   size_t AddLink()
668   {
669     // Create a new internal link
670     links ~= LinkDest(0, 0);
671     return links.length;
672   }
673 
674   //---------------------------------
675 
676   void SetLink(size_t link, float y=-1, size_t page=0)
677   {
678     // Set destination of internal link
679     if (y==-1)
680       y = this.y;
681     if(page==0)
682       page = this.page;
683     links[link-1] = LinkDest(page, y);
684   }
685 
686   //---------------------------------
687 
688   void Link(float x, float y, float w, float h, size_t link)
689   {
690     // Put a link on the page
691     PageLinks[page] ~= LinkInfo(x*k, hPt-y*k, w*k, h*k, link);
692   }
693 
694   //---------------------------------
695 
696   void Link(float x, float y, float w, float h, string link)
697   {
698     // Put a link on the page
699     PageLinks[page] ~= LinkInfo(x*k, hPt-y*k, w*k, h*k, 0, link);
700   }
701 
702   //---------------------------------
703 
704   void Text(float x, float y, string txt)
705   {
706     // Output a string
707     auto s = format("BT %.2F %.2F Td (%s) Tj ET",x*k,(h-y)*k,_escape(txt));
708     if (underline && !txt.empty)
709       s ~= " " ~_dounderline(x,y,txt);
710     if (ColorFlag)
711       s = "q "~TextColor~" "~s~" Q";
712     _out(s);
713   }
714 
715   //---------------------------------
716   
717   bool AcceptPageBreak()
718   {
719     // Accept automatic page break or not
720     return AutoPageBreak;
721   }
722 
723   //---------------------------------
724 
725   struct _rect
726   {
727     float x,y,w,h;
728   }
729 
730 
731   void Cell(float w, float h, string txt, string border, size_t ln, string algn, bool fill, size_t link)
732   {
733     auto r = _cell(w, h, txt, border, ln, algn, fill);
734     if (!txt.empty)
735       Link(r.x,r.y,r.w,r.h,link);
736   }
737 
738   void Cell(float w, float h=0, string txt="", string border="0", size_t ln=0, string algn="", bool fill=false, string link=null)
739   {
740     auto r = _cell(w, h, txt, border, ln, algn, fill);
741     if (!link.empty && !txt.empty)
742       Link(r.x,r.y,r.w,r.h,link);
743   }
744 
745   _rect _cell(float w, float h, string txt, string border, size_t ln, string algn, bool fill)
746   {
747     // Output a cell
748 
749     if(y+h>PageBreakTrigger && !InHeader && !InFooter && AcceptPageBreak())
750     {
751       // Automatic page break
752       auto x = this.x;
753       auto ws = this.ws;
754       if (ws>0)
755       {
756         this.ws = 0;
757         _out("0 Tw");
758       }
759       AddPage(CurOrientation,CurPageSize);
760       this.x = x;
761       if (ws>0)
762       {
763         this.ws = ws;
764         _out(format("%.3F Tw",ws*k));
765       }
766     }
767     if (w==0)
768       w = this.w - rMargin - this.x;
769     auto s = "";
770     string op;
771     if (fill || border=="1")
772     {
773       if (fill)
774         op = (border=="1") ? "B" : "f";
775       else
776         op = "S";
777       s = format("%.2F %.2F %.2F %.2F re %s ",this.x*k,(this.h-this.y)*k,w*k,-h*k,op);
778     }
779 
780     if (indexOf(border,'L')!=-1)
781       s ~= format("%.2F %.2F m %.2F %.2F l S ",x*k,(this.h-y)*k,x*k,(this.h-(y+h))*k);
782     if (indexOf(border,'T')!=-1)
783       s ~= format("%.2F %.2F m %.2F %.2F l S ",x*k,(this.h-y)*k,(x+w)*k,(this.h-y)*k);
784     if (indexOf(border,'R')!=-1)
785       s ~= format("%.2F %.2F m %.2F %.2F l S ",(x+w)*k,(this.h-y)*k,(x+w)*k,(this.h-(y+h))*k);
786     if (indexOf(border,'B')!=-1)
787       s ~= format("%.2F %.2F m %.2F %.2F l S ",x*k,(this.h-(y+h))*k,(x+w)*k,(this.h-(y+h))*k);
788 
789     _rect r;
790 
791     if (!txt.empty)
792     {
793       float dx;
794       if(algn=="R")
795         dx = w-cMargin-GetStringWidth(txt);
796       else if (algn=="C")
797         dx = (w-GetStringWidth(txt))/2;
798       else
799         dx = cMargin;
800       if (ColorFlag)
801        s ~= "q "~TextColor~" ";
802       auto txt2 = txt.replace("\\","\\\\").replace(")","\\)").replace("(","\\(",);
803       s ~= format("BT %.2F %.2F Td (%s) Tj ET",(x+dx)*k,(this.h-(this.y+0.5*h+0.3*FontSize))*k,txt2);
804       if (underline)
805       s ~= " "~_dounderline(x+dx,y+0.5*h+0.3*FontSize,txt);
806       if (ColorFlag)
807         s ~= " Q";
808       r = _rect(this.x+dx, this.y+0.5*h-0.5*FontSize, GetStringWidth(txt),FontSize);
809     }
810     if(s)
811       _out(s);
812     lasth = h;
813     if (ln>0)
814     {
815       // Go to next line
816       y += h;
817       if (ln==1)
818         this.x = lMargin;
819     }
820     else
821       this.x += w;
822 
823     return r;
824   }
825 
826   //-----------------------------------
827 
828   void MultiCell(float w, float h, string txt,  string border="0", string algn="J", bool fill=false)
829   {
830     // Output text with automatic or explicit line breaks
831     auto cw = CurrentFont.cw;
832     if(w==0)
833       w = this.w-rMargin-x;
834     auto wmax = (w-2*cMargin)*1000/FontSize;
835     auto s = txt.replace("\r", "");
836     auto nb = s.length;
837     if(nb>0 && s[nb-1]=='\n')
838       nb--;
839     string b = null;
840     string b2= null;
841     if (border)
842     {
843       if(border=="1")
844       {
845         border = "LTRB";
846         b = "LRT";
847         b2 = "LR";
848       }
849       else
850       {
851         b2 = "";
852         if(indexOf(border,'L')!=-1)
853           b2 ~= "L";
854         if(indexOf(border,'R')!=-1)
855           b2 ~= "R";
856         b = (indexOf(border,'T')!=-1) ? b2~"T" : b2;
857       }
858     }
859     auto sep = -1;
860     auto i = 0;
861     auto j = 0;
862     auto l = 0;
863     auto ns = 0;
864     auto nl = 1;
865     auto ls = 0;
866 
867     while(i<nb)
868     {
869       // Get next character
870       auto c = s[i];
871       if(c=='\n')
872       {
873         // Explicit line break
874         if(ws>0)
875         {
876           ws = 0;
877           _out("0 Tw");
878         }
879         Cell(w,h,s[j..i],b,2,algn,fill);
880         i++;
881         sep = -1;
882         j = i;
883         l = 0;
884         ns = 0;
885         nl++;
886         if(border!="0" && nl==2)
887           b = b2;
888         continue;
889       }
890       if(c==' ')
891       {
892         sep = i;
893         ls = l;
894         ns++;
895       }
896       l += cw[c];
897       if(l>wmax)
898       {
899         // Automatic line break
900         if(sep==-1)
901         {
902           if(i==j)
903             i++;
904           if(ws>0)
905           {
906             ws = 0;
907             _out("0 Tw");
908           }
909           Cell(w,h,s[j..i],b,2,algn,fill);
910         }
911         else
912         {
913           if(algn=="J")
914           {
915             ws = (ns>1) ? (wmax-ls)/1000*FontSize/(ns-1) : 0;
916             _out(format("%.3F Tw",ws*k));
917           }
918           Cell(w,h,s[j..sep],b,2,algn,fill);
919           i = sep+1;
920         }
921         sep = -1;
922         j = i;
923         l = 0;
924         ns = 0;
925         nl++;
926         if(border!="0" && nl==2)
927           b = b2;
928       }
929       else
930         i++;
931     }
932     // Last chunk
933     if(ws>0)
934     {
935       ws = 0;
936       _out("0 Tw");
937     }
938     if(indexOf(border,"B")!=-1)
939       b ~= "B";
940     Cell(w,h,s[j..i],b,2,algn,fill);
941     x = lMargin;
942   }
943 
944   //-----------------------------------
945 
946   void Write(float h, string txt, string link=null)
947   {
948     _write(h, txt, link);
949   }
950 
951   void Write(float h, string txt, size_t link)
952   {
953     _write(h, txt, link);
954   }
955 
956   void _write(T)(float h, string txt, T link)
957   {
958     // Output text in flowing mode
959     auto cw = CurrentFont.cw;
960     auto w = this.w-rMargin-x;
961     auto wmax = (w-2*cMargin)*1000/FontSize;
962     auto s = txt.replace("\r","");
963     auto nb = s.length;
964     auto sep = -1;
965     auto i = 0;
966     auto j = 0;
967     auto l = 0;
968     auto nl = 1;
969     while(i<nb)
970     {
971       // Get next character
972       auto c = s[i];
973       if(c=='\n')
974       {
975         // Explicit line break
976         Cell(w,h,s[j..i],"0",2L,"",false,link);
977         i++;
978         sep = -1;
979         j = i;
980         l = 0;
981         if(nl==1)
982         {
983           x = lMargin;
984           w = this.w-rMargin-x;
985           wmax = (w-2*cMargin)*1000/FontSize;
986         }
987         nl++;
988         continue;
989       }
990       if(c==' ')
991       sep = i;
992       l += cw[c];
993       if (l>wmax)
994       {
995         // Automatic line break
996         if(sep==-1)
997         {
998           if(x>lMargin)
999           {
1000             // Move to next line
1001             x = lMargin;
1002             y += h;
1003             w = w-rMargin-x;
1004             wmax = (w-2*cMargin)*1000/FontSize;
1005             i++;
1006             nl++;
1007             continue;
1008           }
1009           if(i==j)
1010             i++;
1011           Cell(w,h,s[j..i],"0",2L,"",false,link);
1012         }
1013         else
1014         {
1015           Cell(w,h,s[j..sep],"0",2L,"",false,link);
1016           i = sep+1;
1017         }
1018         sep = -1;
1019         j = i;
1020         l = 0;
1021         if(nl==1)
1022         {
1023           x = lMargin;
1024           w = this.w-rMargin-x;
1025           wmax = (w-2*cMargin)*1000/FontSize;
1026         }
1027         nl++;
1028       }
1029       else
1030         i++;
1031     }
1032     // Last chunk
1033     if(i!=j)
1034       Cell(l/1000*FontSize,h,s[j..$],"0",0L,"",false,link);
1035   }
1036 
1037   //-----------------------------------
1038 
1039   void Ln()
1040   {
1041     Ln(lasth);
1042   }
1043 
1044   void Ln(float h)
1045   {
1046     // Line feed; default value is last cell height
1047     x = lMargin;
1048     y += h;
1049   }
1050 
1051   //-----------------------------------
1052 
1053   void Image(string file, float x, float y, float w, float h, string type, size_t link)
1054   {
1055     auto r = _image(file, x, y, w, h, type);
1056     Link(r.x,r.y,r.w,r.h,link);
1057   }
1058 
1059   void Image(string file, float x=-1, float y=-1, float w=0, float h=0, string type=null, string link=null)
1060   {
1061     auto r = _image(file, x, y, w, h, type);
1062     if(!link.empty)
1063       Link(r.x,r.y,r.w,r.h,link);
1064   }
1065 
1066   private _rect _image(string file, float x, float y, float w, float h, string type)
1067   {
1068 
1069     // Put an image on the page
1070     ImageInfo info;
1071 
1072     if(!(file in imageInfoIdx))
1073     {
1074       // First use of this image, get info
1075       if (type.empty)
1076       {
1077         auto pos = lastIndexOf(file,'.');
1078         if(pos <=0)
1079           throw new Exception("Image file has no extension and no type was specified: "~file);
1080         type = file[pos+1..$];
1081       }
1082       
1083       switch (toLower(type))
1084       {
1085         case "jpeg":
1086         case "jpg":
1087           info = _parsejpg(file);
1088           break;
1089         case "png":
1090           info = _parsepng(file);
1091           break;
1092         default:
1093           throw new Exception("Unsupported image type: "~type);
1094       }
1095       auto l = images.length;
1096       imageInfoIdx[file] = l;
1097       info.i = l+1;
1098       images ~= info;
1099     }
1100     else
1101       info = images[imageInfoIdx[file]];
1102 
1103     // Automatic width and height calculation if needed
1104     if (w==0 && h==0)
1105     {
1106       // Put image at 96 dpi
1107       w = -96;
1108       h = -96;
1109     }
1110     if(w<0)
1111       w = -to!float(info.w)*72/w/k;
1112     if(h<0)
1113       h = -to!float(info.h)*72/h/k;
1114     if(w==0)
1115       w = h*to!float(info.w)/to!float(info.h);
1116     if(h==0)
1117       h = w*to!float(info.h)/to!float(info.w);
1118 
1119     // Flowing mode
1120     if(y<0)
1121     {
1122       if(this.y+h>PageBreakTrigger && !InHeader && !InFooter && AcceptPageBreak())
1123       {
1124         // Automatic page break
1125         auto x2 = this.x;
1126         AddPage(CurOrientation,CurPageSize);
1127         this.x = x2;
1128       }
1129       y = this.y;
1130       this.y += h;
1131     }
1132 
1133     if(x<0)
1134       x = this.x;
1135     _out(format("q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q", w*k, h*k, x*k, (this.h-(y+h))*k, info.i));
1136 
1137     auto r = _rect(x,y,w,h);
1138     return r;
1139   }
1140 
1141   //-----------------------------------
1142 
1143   float GetX()        { return x; }
1144   void  SetX(float x) { this.x = (x>=0)? x : w+x; }
1145   float GetY()        { return y; }
1146   void  SetY(float y) { x = lMargin; this.y = (y >= 0) ? y : h+y; }
1147 
1148   //-----------------------------------
1149 
1150   void SetXY(float x, float y)
1151   {
1152     // Set x and y positions
1153     SetY(y);
1154     SetX(x);
1155   }
1156 
1157   //-----------------------------------
1158 
1159   immutable(ubyte)[] Output()
1160   {
1161     if(state<3)
1162       Close();
1163     return cast(immutable(ubyte)[]) buffer.data;
1164   }
1165 
1166   //------------------------------------------------------------
1167   // Protected methods
1168 
1169   protected:
1170 
1171     float[2] _getpagesize(string size)
1172     {
1173       size = toLower(size);
1174       if(!(size in StdPageSizes))
1175         throw new Exception("Unknown page size: "~size);
1176       auto a = StdPageSizes[size];
1177       return [ a[0]/k, a[1]/k ];
1178     }
1179 
1180     float[2] _getpagesize(float[2] size)
1181     {
1182       if(size[0]>size[1])
1183         return [ size[1], size[0] ];
1184       else
1185         return size;
1186     }
1187 
1188     //---------------------------------
1189 
1190     void _beginpage(char orientation, float[2] size)
1191     {
1192       ++page;
1193       pages[page] = appender!string();
1194       state = 2;
1195       x = lMargin;
1196       y = tMargin;
1197       FontFamily = "";
1198       // Check page size and orientation
1199       if (orientation!=CurOrientation || size[0]!=CurPageSize[0] || size[1]!=CurPageSize[1])
1200       {
1201         // New size or orientation
1202         if(orientation=='P')
1203         {
1204           w = size[0];
1205           h = size[1];
1206         }
1207         else
1208         {
1209           w = size[1];
1210           h = size[0];
1211         }
1212         wPt = w*k;
1213         hPt = h*k;
1214         PageBreakTrigger = h-bMargin;
1215         CurOrientation = orientation;
1216         CurPageSize = size;
1217       }
1218       if(orientation!=DefOrientation || size[0]!=DefPageSize[0] || size[1]!=DefPageSize[1])
1219         PageSizes[page] = [ wPt, hPt ];
1220     }
1221 
1222     //---------------------------------
1223 
1224     void _endpage()
1225     {
1226       state = 1;
1227     }
1228 
1229     //---------------------------------
1230 
1231 /+
1232     FontInfo _loadfont(string file)
1233     {
1234       // Reads a font definition from a JSON file. (Note different from original).
1235       FontInfo info;
1236       auto raw = readText(file);
1237 
1238       auto decoded = parseJSON(raw);
1239       auto tLvl = decoded.object;
1240       info.type = tLvl["type"].str;
1241       info.name = tLvl["num"].str;
1242       info.up = tLvl["up"].integer;
1243       info.ut = tLvl["ut"].integer;
1244       info.cw.length = tLvl["cw"].array.length;
1245       foreach(x; tLvl["cw"].array)
1246         info.cw ~= x.uinteger;
1247       return info;
1248     }
1249 +/
1250 
1251     string _escape(string s)
1252     {
1253       // Escape special characters in strings
1254       return s.replace("\\","\\\\")
1255               .replace("(","\\(")
1256               .replace(")","\\)")
1257               .replace("\r","\\r");
1258     }
1259 
1260     string _textstring(string s)
1261     {
1262       // Format a text string
1263       return "("~_escape(s)~")";
1264     }
1265 
1266     string _dounderline(float x, float y, string txt)
1267     {
1268       // Underline text
1269       auto up = CurrentFont.up;
1270       auto ut = CurrentFont.ut;
1271       auto w = GetStringWidth(txt)+ws*txt.length;
1272       return format("%.2F %.2F %.2F %.2F re f",x*k,(h-(y-up/1000*FontSize))*k,w*k,-ut/1000*FontSizePt);
1273     }
1274 
1275     ImageInfo _parsejpg(string file)
1276     {
1277       // Extract info from a JPEG file
1278       ImageInfo info;
1279 
1280       info.data = cast(ubyte[]) read(file);
1281       auto ifImage = read_jpeg_from_mem(info.data);
1282 
1283       info.w = cast(size_t) ifImage.w;
1284       info.h = cast(size_t) ifImage.h;
1285       switch(ifImage.c)
1286       {
1287         case ColFmt.Y:
1288           info.cs = "DeviceGray";
1289           break;
1290         case ColFmt.RGB:
1291           info.cs = "DeviceRGB";
1292           break;
1293         default: break;
1294       }
1295       info.bpc = 8; // something about bits?
1296       info.f = "DCTDecode";
1297       return info;
1298     }
1299 
1300 
1301     ImageInfo _parsepng(string filename)
1302     {
1303       // Extract info from a PNG file
1304 
1305       ImageInfo info;
1306 
1307       File file;
1308       file.open(filename);
1309       scope(exit) { file.close(); }
1310 
1311       auto sig = _readstream(file, 8);
1312       if (sig != [ 137, 'P','N','G', 13,10,26,10 ])
1313         throw new Exception("Not a PNG file");
1314 
1315       _readstream(file, 4);
1316       if (_readstream(file, 4) != cast(ubyte[]) "IHDR")
1317         throw new Exception("Unsupported PNG type");
1318 
1319       info.w = _readint(file);
1320       info.h = _readint(file);
1321       info.bpc = _readstream(file,1)[0];
1322       auto ct = _readstream(file,1)[0];
1323       switch (ct)
1324       {
1325         case 0:
1326         case 4:
1327           info.cs = "DeviceGrey";
1328           break;
1329         case 2:
1330         case 6:
1331           info.cs = "DeviceRGB";
1332           break;
1333         case 3:
1334           info.cs = "Indexed";
1335           break;
1336         default:
1337           throw new Exception("Unknown color type");
1338       }
1339       auto compMethod = _readstream(file,1)[0];
1340       auto filterMethod = _readstream(file,1)[0];
1341       auto interlaceMethod = _readstream(file,1)[0];
1342       _readstream(file, 4);
1343 
1344       info.dp = "/Predictor 15 /Colors "~(info.cs=="DeviceRGB" ? "3" : "1")~" /BitsPerComponent "~to!string(info.bpc)~" /Columns "~to!string(info.w);
1345       info.f = "FlateDecode";
1346 
1347 	// Scan chunks looking for palette, transparency and image data
1348       ubyte[] pal, data = [];
1349 
1350       auto finish = false;
1351       do
1352       {
1353         auto n = _readint(file);
1354         string type = cast(string) _readstream(file,4);
1355         switch (type)
1356         {
1357           case "PLTE":
1358             // Read palette
1359             pal = _readstream(file,n);
1360             _readstream(file,4);
1361             break;
1362           case "tRNS":
1363             // Read transparency info
1364             auto t = _readstream(file,n);
1365             if(ct == 0)
1366               info.trns = t[1..2];
1367             else if(ct == 2)
1368               info.trns = [ t[1], t[3], t[5] ];
1369             else
1370             {
1371               auto pos = indexOf(cast(string)t, '\0');
1372               if (pos != -1)
1373                 info.trns = [ cast(ubyte)pos ];
1374             }
1375             _readstream(file,4);
1376             break;
1377           case "IDAT":
1378             data ~= _readstream(file,n);
1379             _readstream(file,4);
1380             break;
1381           case "IEND":
1382             finish = true;
1383             break;
1384           default:
1385             _readstream(file,n+4);
1386         }
1387       } while (!finish);
1388 
1389       if (ct >= 4)
1390       {
1391         auto color = appender!(ubyte[])();
1392         auto alpha = appender!(ubyte[])();
1393         data = cast(ubyte[]) uncompress(data);
1394 
1395         if (ct == 4)
1396         {
1397           // Extract alpha imformation from Gray image
1398           auto len = cast(size_t) (2*info.w);
1399 
1400           for(size_t i=0;i<info.h;++i)
1401           {
1402             size_t pos = (1+len)*i;
1403             color.put(data[pos]);
1404             alpha.put(data[pos]);
1405             auto line = data[pos+1..pos+1+len];
1406             for (size_t j=0; j<line.length; j+=2)
1407             {
1408               color.put(line[j]);
1409               alpha.put(line[j+1]);
1410             }
1411           }
1412         }
1413         else
1414         {
1415           // Extract alpha imformation from RGB image
1416           auto len = cast(size_t) (4*info.w);
1417           for(size_t i=0;i<info.h;++i)
1418           {
1419             size_t pos = (1+len)*i;
1420             color.put(data[pos]);
1421             alpha.put(data[pos]);
1422             auto line = data[pos+1..pos+1+len];
1423             for (size_t j=0; j<line.length; j+=4)
1424             {
1425               color.put(line[j..j+3]);
1426               alpha.put(line[j+3]);
1427             }
1428           }
1429         }
1430 
1431         info.data = cast(ubyte[]) .compress(color.data);
1432         info.smask = cast(ubyte[]) .compress(alpha.data);
1433         if(pdfVersion<1.4)
1434           pdfVersion = 1.4;
1435       }
1436       else
1437         info.data = data;
1438 
1439       return info;
1440     }
1441 
1442 
1443     ubyte[] _readstream(ref File f, uint n)
1444     {
1445       ubyte[] buffer;
1446       buffer.length = n;
1447 
1448       return f.rawRead(buffer);
1449     }
1450 
1451     uint _readint(ref File f)
1452     {
1453       return bigEndianToNative!uint(_readstream(f,4)[0..4]);
1454     }
1455 
1456     /+
1457     void _parsegif(string file)
1458     {
1459     }
1460     +/
1461 
1462     void _newobj()
1463     {
1464       // Begin a new object
1465       ++n;
1466       offsets.length = n+1;
1467       offsets[n] = buffer.data.length;
1468       _out(to!string(n)~" 0 obj");
1469     }
1470 
1471     void _putstream(string s)
1472     {
1473       _out("stream");
1474       _out(s);
1475       _out("endstream");
1476     }
1477 
1478     //---------------------------------
1479 
1480     void _out(string s)
1481     {
1482       // Add a line to the document
1483       if(state==2)
1484       {
1485         pages[page].put(s);
1486         pages[page].put("\n");
1487       }
1488       else
1489       {
1490         buffer.put(s);
1491         buffer.put("\n");
1492       }
1493     }
1494 
1495     //---------------------------------
1496 
1497     void _putpages()
1498     {
1499       auto nb = page;
1500 
1501       if(DefOrientation=='P')
1502       {
1503         wPt = DefPageSize[0]*k;
1504         hPt = DefPageSize[1]*k;
1505       }
1506       else
1507       {
1508         wPt = DefPageSize[1]*k;
1509         hPt = DefPageSize[0]*k;
1510       }
1511       auto filter = (compress) ? "/Filter /FlateDecode " : "";
1512       for(auto n=1;n<=nb;n++)
1513       {
1514         // Page
1515         _newobj();
1516         _out("<</Type /Page");
1517         _out("/Parent 1 0 R");
1518         if(n in PageSizes)
1519           _out(format("/MediaBox [0 0 %.2F %.2F]",PageSizes[n][0],PageSizes[n][1]));
1520         _out("/Resources 2 0 R");
1521         if(n in PageLinks)
1522         {
1523           // Links
1524           auto annots = "/Annots [";
1525           foreach(link; PageLinks[n])
1526           {
1527             auto rect = format("%.2F %.2F %.2F %.2F",link.x,link.y,link.x+link.w,link.y-link.h);
1528             annots ~= "<</Type /Annot /Subtype /Link /Rect ["~rect~"] /Border [0 0 0] ";
1529             if(!link.externalLink.empty) 
1530               annots ~= "/A <</S /URI /URI "~_textstring(link.externalLink)~">>>>";
1531             else
1532             {
1533               auto l = links[link.link-1];
1534               h = (l.page in PageSizes) ? PageSizes[l.page][1] : hPt;
1535               annots ~= format("/Dest [%d 0 R /XYZ 0 %.2F null]>>",1+2*l.page,h-l.y*k);
1536             }
1537           }
1538           _out(annots~"]");
1539         }
1540         if(pdfVersion>1.3)
1541           _out("/Group <</Type /Group /S /Transparency /CS /DeviceRGB>>");
1542         _out("/Contents "~.to!string(this.n+1)~" 0 R>>");
1543         _out("endobj");
1544 
1545         // Page content
1546         auto p = pages[n].data;
1547         if(!aliasNbPages.empty)
1548           p = p.replace(aliasNbPages,to!string(nb));
1549         _newobj();
1550         auto p2 = compress ? (cast(string) .compress(p)) : p;
1551 
1552         _out("<<"~filter~"/Length "~to!string(p2.length)~">>");
1553         _putstream(p2);
1554         _out("endobj");
1555       }
1556       // Pages root
1557       offsets[1] = buffer.data.length;
1558       _out("1 0 obj");
1559       _out("<</Type /Pages");
1560       auto kids = "/Kids [";
1561       for(auto i=0;i<nb;i++)
1562         kids ~= to!string(3+2*i)~" 0 R ";
1563       _out(kids~"]");
1564       _out("/Count "~to!string(nb));
1565       _out(format("/MediaBox [0 0 %.2F %.2F]",wPt,hPt));
1566       _out(">>");
1567       _out("endobj");
1568     }
1569 
1570     //---------------------------------
1571 
1572     void _putfonts()
1573     {
1574       auto nf = n;
1575       foreach(diff; diffs)
1576       {
1577         // Encodings
1578         _newobj();
1579         _out("<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ["~diff~"]>>");
1580         _out("endobj");
1581       }/*
1582       foreach(file,info ; FontFiles)
1583       {
1584         // Font file embedding
1585         _newobj();
1586         FontFiles[file].n = n;
1587         auto font = readFile(fontpath~file);
1588 
1589         bool compressed = file[$-2..$]==".z");
1590         if(!compressed && info.length2 != 0))
1591           font = font[6,info.length1 ~ font[6+info.length1+6..info.length2];
1592         _out("<</Length "~to!string(font.length));
1593         if(compressed)
1594           _out("/Filter /FlateDecode");
1595         _out("/Length1 "~to!string(info.length2);
1596         if(info.length2 != 0)
1597           _out("/Length2 "~to!string(info.length2)~" /Length3 0");
1598         _out(">>");
1599         _putstream(font);
1600         _out("endobj");
1601       }*/
1602       foreach(ref font; fonts)
1603       {
1604         // Font objects
1605         font.n = n+1;
1606         auto type = font.type;
1607         auto name = font.name;
1608         if (type=="Core")
1609         {
1610           // Core font
1611           _newobj();
1612           _out("<</Type /Font");
1613           _out("/BaseFont /"~name);
1614           _out("/Subtype /Type1");
1615           if(name!="Symbol" && name!="ZapfDingbats")
1616             _out("/Encoding /WinAnsiEncoding");
1617           _out(">>");
1618           _out("endobj");
1619         }
1620         else if(type=="Type1" || type=="TrueType")
1621         {
1622           // Additional Type1 or TrueType/OpenType font
1623           _newobj();
1624           _out("<</Type /Font");
1625           _out("/BaseFont /"~name);
1626           _out("/Subtype /"~type);
1627           _out("/FirstChar 32 /LastChar 255");
1628           _out("/Widths "~to!string(n+1)~" 0 R");
1629           _out("/FontDescriptor "~to!string(n+2)~" 0 R");
1630           if(font.diffn != ulong.max)
1631             _out("/Encoding "~to!string(nf+font.diffn)~" 0 R");
1632           else
1633             _out("/Encoding /WinAnsiEncoding");
1634           _out(">>");
1635           _out("endobj");
1636           // Widths
1637           _newobj();
1638           auto s = appender!string; s.put("[");
1639           for(auto i=32;i<=255;i++)
1640           {
1641             s.put(to!string(font.cw[i]));
1642             s.put(" ");
1643           }
1644           s.put("]");
1645           _out(s.data);
1646           _out("endobj");
1647           // Descriptor
1648           _newobj();
1649           s= appender!string; s.put("<</Type /FontDescriptor /FontName /"); s.put(name);
1650           foreach(k,v; font.desc)
1651             s.put(" /"~k~" "~v);
1652           if (!font.file.empty)
1653             s.put(" /FontFile"~(type=="Type1" ? "" : "2")~" "~to!string(FontFiles[font.file].n)~" 0 R");
1654           _out(s.data~">>");
1655           _out("endobj");
1656         }
1657         else
1658         {
1659           throw new Exception("Unsupported font type: "~type);
1660         }
1661       }
1662     }
1663 
1664     //---------------------------------
1665 
1666     void _putimages()
1667     {
1668       foreach(ref info; images)
1669       {
1670         _putimage(info);
1671         info.data = info.data.init;
1672         info.smask = null;
1673       }
1674     }
1675 
1676     //---------------------------------
1677 
1678     void _putimage(ref ImageInfo info)
1679     {
1680       _newobj();
1681       info.n = n;
1682       _out("<</Type /XObject");
1683       _out("/Subtype /Image");
1684       _out("/Width "~to!string(info.w));
1685       _out("/Height "~to!string(info.h));
1686       if(info.cs=="Indexed")
1687         _out("/ColorSpace [/Indexed /DeviceRGB "~to!string(info.pal.length/3-1)~" "~to!string(n+1)~" 0 R]");
1688       else
1689       {
1690         _out("/ColorSpace /"~info.cs);
1691         if(info.cs=="DeviceCMYK")
1692           _out("/Decode [1 0 1 0 1 0 1 0]");
1693       }
1694       _out("/BitsPerComponent "~to!string(info.bpc));
1695       if(!info.f.empty)
1696         _out("/Filter /"~info.f);
1697       if(!info.dp.empty)
1698         _out("/DecodeParms <<"~info.dp~">>");
1699 
1700       if (!info.trns.empty)
1701       {
1702         auto trns = "";
1703         foreach (i; info.trns)
1704           trns ~= to!string(i)~" "~to!string(i)~" ";
1705         _out("/Mask ["~trns~"]");
1706 	    }
1707 
1708       if (!info.smask.empty)
1709         _out("/SMask "~to!string(n+1)~" 0 R");
1710 
1711       _out("/Length "~to!string(info.data.length)~">>");
1712       _putstream(cast(string) info.data);
1713       _out("endobj");
1714 
1715       // Soft mask
1716       if (!info.smask.empty)
1717       {
1718         auto dp = "/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns "~to!string(info.w);
1719         auto smask = ImageInfo(0,0,info.w,info.h,"DeviceGray", 8, info.f, dp, null,null,info.smask);
1720         _putimage(smask);
1721       }
1722 
1723       // Palette
1724       if(info.cs=="Indexed")
1725       {
1726         auto filter = (compress) ? "/Filter /FlateDecode " : "";
1727         string pal = (compress) ? cast(string) .compress(info.pal) : info.pal;
1728         _newobj();
1729         _out("<<"~filter~"/Length "~to!string(pal.length)~">>");
1730         _putstream(pal);
1731         _out("endobj");
1732       }
1733     }
1734 
1735     //---------------------------------
1736 
1737     void _putxobjectdict()
1738     {
1739       foreach(image; images)
1740         _out("/I"~to!string(image.i)~" "~to!string(image.n)~" 0 R");
1741     }
1742 
1743     //---------------------------------
1744 
1745     void _putresourcedict()
1746     {
1747       _out("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]");
1748       _out("/Font <<");
1749       foreach(font; fonts)
1750         _out("/F"~to!string(font.i)~" "~to!string(font.n)~" 0 R");
1751       _out(">>");
1752       _out("/XObject <<");
1753       _putxobjectdict();
1754       _out(">>");
1755     }
1756 
1757     //---------------------------------
1758 
1759     void _putresources()
1760     {
1761       _putfonts();
1762       _putimages();
1763       // Resource dictionary
1764       offsets[2] = buffer.data.length;
1765       _out("2 0 obj");
1766       _out("<<");
1767       _putresourcedict();
1768       _out(">>");
1769       _out("endobj");
1770     }
1771 
1772     //---------------------------------
1773 
1774     void _putinfo()
1775     {
1776       _out("/Producer "~_textstring("FPDF (for D) "~to!string(fpdfVersion)));
1777       if(!title.empty)
1778         _out("/Title "~_textstring(title));
1779       if(!subject.empty)
1780         _out("/Subject "~_textstring(subject));
1781       if(!author.empty)
1782         _out("/Author "~_textstring(author));
1783       if(!keywords.empty)
1784         _out("/Keywords "~_textstring(keywords.join(",")));
1785       if(!creator.empty)
1786         _out("/Creator "~_textstring(creator));
1787 
1788       auto time = Clock.currTime.toISOString().replace("T","");
1789       auto dot = indexOf(time, ".");
1790       if (dot == -1) dot = time.length;
1791       _out("/CreationDate "~_textstring("D:"~time[0..dot]));
1792     }
1793 
1794     //---------------------------------
1795 
1796     void _putcatalog()
1797     {
1798       _out("/Type /Catalog");
1799       _out("/Pages 1 0 R");
1800       if(zoomMode=="fullpage")
1801         _out("/OpenAction [3 0 R /Fit]");
1802       else if(zoomMode=="fullwidth")
1803         _out("/OpenAction [3 0 R /FitH null]");
1804       else if(zoomMode=="real")
1805         _out("/OpenAction [3 0 R /XYZ null null 1]");
1806       else if(zoomMode == "custom")
1807         _out("/OpenAction [3 0 R /XYZ null null "~format("%.2F",zoom/100)~"]");
1808       if(layoutMode=="single")
1809         _out("/PageLayout /SinglePage");
1810       else if(layoutMode=="continuous")
1811         _out("/PageLayout /OneColumn");
1812       else if(layoutMode=="two")
1813         _out("/PageLayout /TwoColumnLeft");
1814     }
1815 
1816     //---------------------------------
1817 
1818     void _putheader()
1819     {
1820       _out("%PDF-"~to!string(pdfVersion));
1821     }
1822 
1823     //---------------------------------
1824 
1825     void _puttrailer()
1826     {
1827       _out("/Size "~to!string(n+1));
1828       _out("/Root "~to!string(n)~" 0 R");
1829       _out("/Info "~to!string(n-1)~" 0 R");
1830     }
1831 
1832     //---------------------------------
1833 
1834     void _enddoc()
1835     {
1836       _putheader();
1837       _putpages();
1838       _putresources();
1839       // Info
1840       _newobj();
1841       _out("<<");
1842       _putinfo();
1843       _out(">>");
1844       _out("endobj");
1845       // Catalog
1846       _newobj();
1847       _out("<<");
1848       _putcatalog();
1849       _out(">>");
1850       _out("endobj");
1851       // Cross-ref
1852       auto o = buffer.data.length;
1853       _out("xref");
1854       _out("0 "~to!string(n+1));
1855       _out("0000000000 65535 f ");
1856       for(auto i=1;i<=n;i++)
1857         _out(format("%010d 00000 n ",offsets[i]));
1858       // Trailer
1859       _out("trailer");
1860       _out("<<");
1861       _puttrailer();
1862       _out(">>");
1863       _out("startxref");
1864       _out(to!string(o));
1865       _out("%%EOF");
1866       state = 3;
1867     }
1868 
1869     //---------------------------------
1870 }
1871 
1872 //----------------------------------------------------------------------------
1873 // An addon to help construct tables.
1874 
1875 struct FpdfTable
1876 //----------------------------------------------------------------------------
1877 {
1878   Fpdf fpdf;
1879   float[] widths;
1880   string[] aligns;
1881 
1882   void SetWidths(float[] w)
1883   {
1884     //Set the array of column widths
1885     widths = w;
1886   }
1887 
1888   void SetAligns(string[] a)
1889   {
1890     //Set the array of column alignments
1891     aligns = a;
1892   }
1893 
1894   void Row(string[] data, float lh)
1895   {
1896     assert(data.length == widths.length);
1897     assert(data.length == aligns.length);
1898     //Calculate the height of the row
1899     ulong nb=0;
1900 
1901     for(size_t i=0;i<data.length;++i)
1902       nb=max(nb,NbLines(widths[i],data[i]));
1903     auto h = lh * nb;
1904 
1905     //Issue a page break first if needed
1906     CheckPageBreak(h);
1907 
1908     //Draw the cells of the row
1909     for(size_t i=0; i<data.length;++i)
1910     {
1911         auto w=widths[i];
1912         auto a= (i<aligns.length) ? aligns[i] : "L";
1913         //Save the current position
1914         auto x=fpdf.GetX();
1915         auto y=fpdf.GetY();
1916         //Draw the border
1917         fpdf.Rect(x,y,w,h);
1918         //Print the text
1919         fpdf.MultiCell(w,lh,data[i],"0",a);
1920         //Put the position to the right of the cell
1921         fpdf.SetXY(x+w,y);
1922     }
1923     //Go to the next line
1924     fpdf.Ln(h);
1925   }
1926 
1927   void CheckPageBreak(float h)
1928   {
1929     //If the height h would cause an overflow, add a new page immediately
1930     if (fpdf.GetY()+h > fpdf.PageBreakTrigger)
1931       fpdf.AddPage(fpdf.CurOrientation);
1932   }
1933 
1934   ulong NbLines(float w, string txt)
1935   {
1936     //Computes the number of lines a MultiCell of width w will take
1937     auto cw = fpdf.CurrentFont.cw;
1938     if(w==0)
1939       w = fpdf.w-fpdf.rMargin-fpdf.x;
1940     auto wmax=(w-2*fpdf.cMargin)*1000/fpdf.FontSize;
1941     auto s=txt.replace("\r","");
1942     auto nb=s.length;
1943 
1944     if (nb>0 && s[$-1]=='\n')
1945       --nb;
1946 
1947     auto sep=-1;
1948     auto i=0;
1949     auto j=0;
1950     auto l=0;
1951     auto nl=1;
1952 
1953     while(i<nb)
1954     {
1955       auto c= s[i];
1956       if(c=='\n')
1957       {
1958         ++i;
1959         sep=-1;
1960         j=i;
1961         l=0;
1962         nl++;
1963         continue;
1964       }
1965       if(c==' ')
1966         sep=i;
1967       l += cw[c];
1968       if (l> wmax)
1969       {
1970         if(sep==-1)
1971         {
1972           if(i==j)
1973             i++;
1974         }
1975         else
1976           i=sep+1;
1977         sep=-1;
1978         j=i;
1979         l=0;
1980         nl++;
1981       }
1982       else
1983         ++i;
1984     }
1985     return nl;
1986   }
1987 }