1<?php
2/**
3 * Based on the FPDF class by Olivier Plathey (http://www.fpdf.org/).
4 *
5 * Minimal conversion to PHP 5 by Maintainable Software
6 * (http://maintainable.com).
7 *
8 * Copyright 2001-2003 Olivier Plathey <olivier@fpdf.org>
9 * Copyright 2003-2017 Horde LLC (http://www.horde.org/)
10 *
11 * @author   Olivier Plathey <olivier@fpdf.org>
12 * @author   Marko Djukic <marko@oblo.com>
13 * @author   Jan Schneider <jan@horde.org>
14 * @license  http://www.horde.org/licenses/lgpl21
15 * @category Horde
16 * @package  Pdf
17 */
18
19/**
20 * The Horde_Pdf_Writer class provides a PHP-only implementation of a PDF
21 * library. No external libs or PHP extensions are required.
22 *
23 * @category Horde
24 * @package  Pdf
25 */
26class Horde_Pdf_Writer
27{
28    /**
29     * Current page number.
30     *
31     * @var integer
32     */
33    protected $_page = 0;
34
35    /**
36     * Current object number.
37     *
38     * @var integer
39     */
40    protected $_n = 2;
41
42    /**
43     * Array of object offsets.
44     *
45     * @var array
46     */
47    protected $_offsets = array();
48
49    /**
50     * Buffer holding in-memory Pdf.
51     *
52     * @var string
53     */
54    protected $_buffer = '';
55
56    /**
57     * Buffer length, including already flushed content.
58     *
59     * @var integer
60     */
61    protected $_buflen = 0;
62
63    /**
64     * Whether the buffer has been flushed already.
65     *
66     * @var boolean
67     */
68    protected $_flushed = false;
69
70    /**
71     * Array containing the pages.
72     *
73     * @var array
74     */
75    protected $_pages = array();
76
77    /**
78     * Current document state.<pre>
79     *   0 - initial state
80     *   1 - document opened
81     *   2 - page opened
82     *   3 - document closed
83     * </pre>
84     *
85     * @var integer
86     */
87    protected $_state = 0;
88
89    /**
90     * Flag indicating if PDF file is to be compressed or not.
91     *
92     * @var boolean
93     */
94    protected $_compress;
95
96    /**
97     * The default page orientation.
98     *
99     * @var string
100     */
101    protected $_default_orientation;
102
103    /**
104     * The current page orientation.
105     *
106     * @var string
107     */
108    protected $_current_orientation;
109
110    /**
111     * Array indicating orientation changes.
112     *
113     * @var array
114     */
115    protected $_orientation_changes = array();
116
117    /**
118     * Current width of page format in points.
119     *
120     * @var float
121     */
122    public $fwPt;
123
124    /**
125     * Current height of page format in points.
126     *
127     * @var float
128     */
129    public $fhPt;
130
131    /**
132     * Current width of page format in user units.
133     *
134     * @var float
135     */
136    public $fw;
137
138    /**
139     * Current height of page format in user units.
140     *
141     * @var float
142     */
143    public $fh;
144
145    /**
146     * Current width of page in points.
147     *
148     * @var float
149     */
150    public $wPt;
151
152    /**
153     * Current height of page in points.
154     *
155     * @var float
156     */
157    public $hPt;
158
159    /**
160     * Current width of page in user units
161     *
162     * @var float
163     */
164    public $w;
165
166    /**
167     * Current height of page in user units
168     *
169     * @var float
170     */
171    public $h;
172
173    /**
174     * Scale factor (number of points in user units).
175     *
176     * @var float
177     */
178    protected $_scale;
179
180    /**
181     * Left page margin size.
182     *
183     * @var float
184     */
185    protected $_left_margin;
186
187    /**
188     * Top page margin size.
189     *
190     * @var float
191     */
192    protected $_top_margin;
193
194    /**
195     * Right page margin size.
196     *
197     * @var float
198     */
199    protected $_right_margin;
200
201    /**
202     * Break page margin size, the bottom margin which triggers a page break.
203     *
204     * @var float
205     */
206    protected $_break_margin;
207
208    /**
209     * Cell margin size.
210     *
211     * @var float
212     */
213    protected $_cell_margin;
214
215    /**
216     * The current horizontal position for cell positioning.
217     * Value is set in user units and is calculated from the top left corner
218     * as origin.
219     *
220     * @var float
221     */
222    public $x;
223
224    /**
225     * The current vertical position for cell positioning.
226     * Value is set in user units and is calculated from the top left corner
227     * as origin.
228     *
229     * @var float
230     */
231    public $y;
232
233    /**
234     * The height of the last cell printed.
235     *
236     * @var float
237     */
238    protected $_last_height;
239
240    /**
241     * Line width in user units.
242     *
243     * @var float
244     */
245    protected $_line_width;
246
247    /**
248     * An array of standard font names.
249     *
250     * @var array
251     */
252    protected $_core_fonts = array('courier'      => 'Courier',
253                                   'courierB'     => 'Courier-Bold',
254                                   'courierI'     => 'Courier-Oblique',
255                                   'courierBI'    => 'Courier-BoldOblique',
256                                   'helvetica'    => 'Helvetica',
257                                   'helveticaB'   => 'Helvetica-Bold',
258                                   'helveticaI'   => 'Helvetica-Oblique',
259                                   'helveticaBI'  => 'Helvetica-BoldOblique',
260                                   'times'        => 'Times-Roman',
261                                   'timesB'       => 'Times-Bold',
262                                   'timesI'       => 'Times-Italic',
263                                   'timesBI'      => 'Times-BoldItalic',
264                                   'symbol'       => 'Symbol',
265                                   'zapfdingbats' => 'ZapfDingbats');
266
267    /**
268     * An array of used fonts.
269     *
270     * @var array
271     */
272    protected $_fonts = array();
273
274    /**
275     * An array of font files.
276     *
277     * @var array
278     */
279    protected $_font_files = array();
280
281    /**
282     * Widths of specific font files
283     *
284     * @var array
285     */
286    protected static $_font_widths = array();
287
288    /**
289     * An array of encoding differences.
290     *
291     * @var array
292     */
293    protected $_diffs = array();
294
295    /**
296     * An array of used images.
297     *
298     * @var array
299     */
300    protected $_images = array();
301
302    /**
303     * An array of links in pages.
304     *
305     * @var array
306     */
307    protected $_page_links;
308
309    /**
310     * An array of internal links.
311     *
312     * @var array
313     */
314    protected $_links = array();
315
316    /**
317     * Current font family.
318     *
319     * @var string
320     */
321    protected $_font_family = '';
322
323    /**
324     * Current font style.
325     *
326     * @var string
327     */
328    protected $_font_style = '';
329
330    /**
331     * Underlining flag.
332     *
333     * @var boolean
334     */
335    protected $_underline = false;
336
337    /**
338     * An array containing current font info.
339     *
340     * @var array
341     */
342    protected $_current_font;
343
344    /**
345     * Current font size in points.
346     *
347     * @var float
348     */
349    protected $_font_size_pt = 12;
350
351    /**
352     * Current font size in user units.
353     *
354     * @var float
355     */
356    protected $_font_size = 12;
357
358    /**
359     * Commands for filling color.
360     *
361     * @var string
362     */
363    protected $_fill_color = '0 g';
364
365    /**
366     * Commands for text color.
367     *
368     * @var string
369     */
370    protected $_text_color = '0 g';
371
372    /**
373     * Whether text color is different from fill color.
374     *
375     * @var boolean
376     */
377    protected $_color_flag = false;
378
379    /**
380     * Commands for drawing color.
381     *
382     * @var string
383     */
384    protected $_draw_color = '0 G';
385
386    /**
387     * Word spacing.
388     *
389     * @var integer
390     */
391    protected $_word_spacing = 0;
392
393    /**
394     * Automatic page breaking.
395     *
396     * @var boolean
397     */
398    protected $_auto_page_break;
399
400    /**
401     * Threshold used to trigger page breaks.
402     *
403     * @var float
404     */
405    protected $_page_break_trigger;
406
407    /**
408     * Flag set when processing footer.
409     *
410     * @var boolean
411     */
412    protected $_in_footer = false;
413
414    /**
415     * Zoom display mode.
416     *
417     * @var string
418     */
419    protected $_zoom_mode;
420
421    /**
422     * Layout display mode.
423     *
424     * @var string
425     */
426    protected $_layout_mode;
427
428    /**
429     * An array containing the document info, consisting of:
430     *   - title
431     *   - subject
432     *   - author
433     *   - keywords
434     *   - creator
435     *
436     * @var array
437     */
438    protected $_info = array();
439
440    /**
441     * Alias for total number of pages.
442     *
443     * @var string
444     */
445    protected $_alias_nb_pages = '{nb}';
446
447    /**
448     * Constructor
449     *
450     * It allows to set up the page format, the orientation and the units of
451     * measurement used in all the methods (except for the font sizes).
452     *
453     * Example:
454     * <code>
455     * $pdf = new Horde_Pdf_Writer(array('orientation' => 'P',
456     *                                   'unit'   => 'mm',
457     *                                   'format' => 'A4'));
458     * </code>
459     *
460     * @param array $params  A hash with parameters for the created PDF object.
461     *                       Possible parameters are:
462     *                       - orientation - Default page orientation. Possible
463     *                         values are (case insensitive):
464     *                         - P or Portrait (default)
465     *                         - L or Landscape
466     *                       - unit - User measure units. Possible values
467     *                         values are:
468     *                         - pt: point
469     *                         - mm: millimeter (default)
470     *                         - cm: centimeter
471     *                         - in: inch
472     *                         A point equals 1/72 of inch, that is to say
473     *                         about 0.35 mm (an inch being 2.54 cm). This is a
474     *                         very common unit in typography; font sizes are
475     *                         expressed in that unit.
476     *                       - format - The format used for pages. It can be
477     *                         either one of the following values (case
478     *                         insensitive):
479     *                         - A3
480     *                         - A4 (default)
481     *                         - A5
482     *                         - Letter
483     *                         - Legal
484     *                         or a custom format in the form of a two-element
485     *                         array containing the width and the height
486     *                         (expressed in the unit given by the unit
487     *                         parameter).
488     */
489    public function __construct($params = array())
490    {
491        /* Default parameters. */
492        $defaults = array('orientation' => 'P', 'unit' => 'mm', 'format' => 'A4');
493        $params = array_merge($defaults, $params);
494
495        /* Scale factor. */
496        if ($params['unit'] == 'pt') {
497            $this->_scale = 1;
498        } elseif ($params['unit'] == 'mm') {
499            $this->_scale = 72 / 25.4;
500        } elseif ($params['unit'] == 'cm') {
501            $this->_scale = 72 / 2.54;
502        } elseif ($params['unit'] == 'in') {
503            $this->_scale = 72;
504        } else {
505            throw new Horde_Pdf_Exception(sprintf('Incorrect units: %s', $params['unit']));
506        }
507        /* Page format. */
508        if (is_string($params['format'])) {
509            $params['format'] = Horde_String::lower($params['format']);
510            if ($params['format'] == 'a3') {
511                $params['format'] = array(841.89, 1190.55);
512            } elseif ($params['format'] == 'a4') {
513                $params['format'] = array(595.28, 841.89);
514            } elseif ($params['format'] == 'a5') {
515                $params['format'] = array(420.94, 595.28);
516            } elseif ($params['format'] == 'letter') {
517                $params['format'] = array(612, 792);
518            } elseif ($params['format'] == 'legal') {
519                $params['format'] = array(612, 1008);
520            } else {
521                throw new Horde_Pdf_Exception(sprintf('Unknown page format: %s', $params['format']));
522            }
523            $this->fwPt = $params['format'][0];
524            $this->fhPt = $params['format'][1];
525        } else {
526            $this->fwPt = $params['format'][0] * $this->_scale;
527            $this->fhPt = $params['format'][1] * $this->_scale;
528        }
529        $this->fw = $this->fwPt / $this->_scale;
530        $this->fh = $this->fhPt / $this->_scale;
531
532        /* Page orientation. */
533        $params['orientation'] = Horde_String::lower($params['orientation']);
534        if ($params['orientation'] == 'p' || $params['orientation'] == 'portrait') {
535            $this->_default_orientation = 'P';
536            $this->wPt = $this->fwPt;
537            $this->hPt = $this->fhPt;
538        } elseif ($params['orientation'] == 'l' || $params['orientation'] == 'landscape') {
539            $this->_default_orientation = 'L';
540            $this->wPt = $this->fhPt;
541            $this->hPt = $this->fwPt;
542        } else {
543            throw new Horde_Pdf_Exception(sprintf('Incorrect orientation: %s', $params['orientation']));
544        }
545        $this->_current_orientation = $this->_default_orientation;
546        $this->w = $this->wPt / $this->_scale;
547        $this->h = $this->hPt / $this->_scale;
548
549        /* Page margins (1 cm) */
550        $margin = 28.35 / $this->_scale;
551        $this->setMargins($margin, $margin);
552
553        /* Interior cell margin (1 mm) */
554        $this->_cell_margin = $margin / 10;
555
556        /* Line width (0.2 mm) */
557        $this->_line_width = .567 / $this->_scale;
558
559        /* Automatic page break */
560        $this->setAutoPageBreak(true, 2 * $margin);
561
562        /* Full width display mode */
563        $this->setDisplayMode('fullwidth');
564
565        /* Compression */
566        $this->setCompression(true);
567    }
568
569    /**
570     * Defines the left, top and right margins.
571     *
572     * By default, they equal 1 cm. Call this method to change them.
573     *
574     * @param float $left   Left margin.
575     * @param float $top    Top margin.
576     * @param float $right  Right margin. If not specified default to the value
577     *                      of the left one.
578     *
579     * @see setAutoPageBreak()
580     * @see setLeftMargin()
581     * @see setRightMargin()
582     * @see setTopMargin()
583     */
584    public function setMargins($left, $top, $right = null)
585    {
586        /* Set left and top margins. */
587        $this->_left_margin  = $left;
588        $this->_top_margin   = $top;
589        /* If no right margin set default to same as left. */
590        $this->_right_margin = (is_null($right) ? $left : $right);
591    }
592
593    /**
594     * Defines the left margin.
595     *
596     * The method can be called before creating the first page.  If the
597     * current abscissa gets out of page, it is brought back to the margin.
598     *
599     * @param float $margin  The margin.
600     *
601     * @see setAutoPageBreak()
602     * @see setMargins()
603     * @see setRightMargin()
604     * @see setTopMargin()
605     */
606    public function setLeftMargin($margin)
607    {
608        $this->_left_margin = $margin;
609        /* If there is a current page and the current X position is less than
610         * margin set the X position to the margin value. */
611        if ($this->_page > 0 && $this->x < $margin) {
612            $this->x = $margin;
613        }
614    }
615
616    /**
617     * Defines the top margin.
618     *
619     * The method can be called before creating the first page.
620     *
621     * @param float $margin  The margin.
622     */
623    public function setTopMargin($margin)
624    {
625        $this->_top_margin = $margin;
626    }
627
628    /**
629     * Defines the right margin.
630     *
631     * The method can be called before creating the first page.
632     *
633     * @param float $margin  The margin.
634     */
635    public function setRightMargin($margin)
636    {
637        $this->_right_margin = $margin;
638    }
639
640    /**
641     * Returns the actual page width.
642     *
643     * @return float  The page width.
644     */
645    public function getPageWidth()
646    {
647        return ($this->w - $this->_right_margin - $this->_left_margin);
648    }
649
650    /**
651     * Returns the actual page height.
652     *
653     * @return float  The page height.
654     */
655    public function getPageHeight()
656    {
657        return ($this->h - $this->_top_margin - $this->_break_margin);
658    }
659
660    /**
661     * Enables or disables the automatic page breaking mode.
662     *
663     * When enabling, the second parameter is the distance from the bottom of
664     * the page that defines the triggering limit. By default, the mode is on
665     * and the margin is 2 cm.
666     *
667     * @param boolean $auto  Boolean indicating if mode should be on or off.
668     * @param float $margin  Distance from the bottom of the page.
669     */
670    public function setAutoPageBreak($auto, $margin = 0)
671    {
672        $this->_auto_page_break    = $auto;
673        $this->_break_margin       = $margin;
674        $this->_page_break_trigger = $this->h - $margin;
675    }
676
677    /**
678     * Defines the way the document is to be displayed by the viewer.
679     *
680     * The zoom level can be set: pages can be displayed entirely on screen,
681     * occupy the full width of the window, use real size, be scaled by a
682     * specific zooming factor or use viewer default (configured in the
683     * Preferences menu of Acrobat). The page layout can be specified too:
684     * single at once, continuous display, two columns or viewer default.  By
685     * default, documents use the full width mode with continuous display.
686     *
687     * @param mixed $zoom    The zoom to use. It can be one of the following
688     *                       string values:
689     *                         - fullpage: entire page on screen
690     *                         - fullwidth: maximum width of window
691     *                         - real: uses real size (100% zoom)
692     *                         - default: uses viewer default mode
693     *                       or a number indicating the zooming factor.
694     * @param string layout  The page layout. Possible values are:
695     *                         - single: one page at once
696     *                         - continuous: pages in continuously
697     *                         - two: two pages on two columns
698     *                         - default: uses viewer default mode
699     *                       Default value is continuous.
700     */
701    public function setDisplayMode($zoom, $layout = 'continuous')
702    {
703        $zoom = Horde_String::lower($zoom);
704        if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real'
705            || $zoom == 'default' || !is_string($zoom)) {
706            $this->_zoom_mode = $zoom;
707        } elseif ($zoom == 'zoom') {
708            $this->_zoom_mode = $layout;
709        } else {
710            throw new Horde_Pdf_Exception(sprintf('Incorrect zoom display mode: %s', $zoom));
711        }
712
713        $layout = Horde_String::lower($layout);
714        if ($layout == 'single' || $layout == 'continuous' || $layout == 'two'
715            || $layout == 'default') {
716            $this->_layout_mode = $layout;
717        } elseif ($zoom != 'zoom') {
718            throw new Horde_Pdf_Exception(sprintf('Incorrect layout display mode: %s', $layout));
719        }
720    }
721
722    /**
723     * Activates or deactivates page compression.
724     *
725     * When activated, the internal representation of each page is compressed,
726     * which leads to a compression ratio of about 2 for the resulting
727     * document. Compression is on by default.
728     *
729     * Note: the {@link http://www.php.net/zlib/ zlib extension} is required
730     * for this feature. If not present, compression will be turned off.
731     *
732     * @param boolean $compress  Boolean indicating if compression must be
733     *                           enabled or not.
734     */
735    public function setCompression($compress)
736    {
737        /* If no gzcompress function is available then default to false. */
738        $this->_compress = (function_exists('gzcompress') ? $compress : false);
739    }
740
741    /**
742     * Set the info to a document.
743     *
744     * Possible info settings are:
745     *   - title
746     *   - subject
747     *   - author
748     *   - keywords
749     *   - creator
750     *
751     * @param array|string $info  If passed as an array then the complete hash
752     *                            containing the info to be inserted into the
753     *                            document. Otherwise the name of setting to be
754     *                            set.
755     * @param string $value       The value of the setting.
756     */
757    public function setInfo($info, $value = '')
758    {
759        if (is_array($info)) {
760            $this->_info = $info;
761        } else {
762            $this->_info[$info] = $value;
763        }
764    }
765
766    /**
767     * Defines an alias for the total number of pages.
768     *
769     * It will be substituted as the document is closed.
770     *
771     * Example:
772     * <code>
773     * class My_Pdf extends Horde_Pdf_Writer {
774     *     function footer()
775     *     {
776     *         // Go to 1.5 cm from bottom
777     *         $this->setY(-15);
778     *         // Select Arial italic 8
779     *         $this->setFont('Arial', 'I', 8);
780     *         // Print current and total page numbers
781     *         $this->cell(0, 10, 'Page ' . $this->getPageNo() . '/{nb}', 0,
782     *                     0, 'C');
783     *     }
784     * }
785     * $pdf = new My_Pdf();
786     * $pdf->aliasNbPages();
787     * </code>
788     *
789     * @param string $alias  The alias.
790     *
791     * @see getPageNo()
792     * @see footer()
793     */
794    public function aliasNbPages($alias = '{nb}')
795    {
796        $this->_alias_nb_pages = $alias;
797    }
798
799    /**
800     * This method begins the generation of the PDF document; it must be
801     * called before any output commands.
802     *
803     * No page is created by this method, therefore it is necessary to call
804     * {@link addPage()}.
805     *
806     * @see addPage()
807     * @see close()
808     */
809    public function open()
810    {
811        $this->_beginDoc();
812    }
813
814    /**
815     * Terminates the PDF document.
816     *
817     * If the document contains no page, {@link addPage()} is called to
818     * prevent from getting an invalid document.
819     *
820     * @see open()
821     */
822    public function close()
823    {
824        // Terminate document
825        if ($this->_page == 0) {
826            $this->addPage();
827        }
828
829        // Page footer
830        $this->_in_footer = true;
831        $this->x = $this->_left_margin;
832        $this->footer();
833        $this->_in_footer = false;
834
835        // Close page and document
836        $this->_endPage();
837        $this->_endDoc();
838    }
839
840    /**
841     * Adds a new page to the document.
842     *
843     * If a page is already present, the {@link footer()} method is called
844     * first to output the footer. Then the page is added, the current
845     * position set to the top-left corner according to the left and top
846     * margins, and {@link header()} is called to display the header.
847     *
848     * The font which was set before calling is automatically restored. There
849     * is no need to call {@link setFont()} again if you want to continue with
850     * the same font. The same is true for colors and line width.  The origin
851     * of the coordinate system is at the top-left corner and increasing
852     * ordinates go downwards.
853     *
854     * @param string $orientation  Page orientation. Possible values
855     *                             are (case insensitive):
856     *                               - P or Portrait
857     *                               - L or Landscape
858     *                             The default value is the one passed to the
859     *                             constructor.
860     *
861     * @see header()
862     * @see footer()
863     * @see setMargins()
864     */
865    public function addPage($orientation = '')
866    {
867        /* For good measure make sure this is called. */
868        $this->_beginDoc();
869
870        /* Save style settings so that they are not overridden by
871         * footer() or header(). */
872        $lw = $this->_line_width;
873        $dc = $this->_draw_color;
874        $fc = $this->_fill_color;
875        $tc = $this->_text_color;
876        $cf = $this->_color_flag;
877        $font_family = $this->_font_family;
878        $font_style  = $this->_font_style . ($this->_underline ? 'U' : '');
879        $font_size   = $this->_font_size_pt;
880
881        if ($this->_page > 0) {
882            /* Page footer. */
883            $this->_in_footer = true;
884            $this->x = $this->_left_margin;
885            $this->footer();
886            $this->_in_footer = false;
887
888            /* Close page. */
889            $this->_endPage();
890        }
891
892        /* Start new page. */
893        $this->_beginPage($orientation);
894
895        /* Set line cap style to square. */
896        $this->_out('2 J');
897
898        /* Set line width. */
899        $this->_line_width = $lw;
900        $this->_out(sprintf('%.2F w', $lw * $this->_scale));
901
902        /* Force the setting of the font. Each new page requires a new
903         * call. */
904        if ($font_family) {
905            $this->setFont($font_family, $font_style, $font_size, true);
906        }
907
908        /* Restore styles. */
909        if ($this->_fill_color != $fc) {
910            $this->_fill_color = $fc;
911            $this->_out($this->_fill_color);
912        }
913        if ($this->_draw_color != $dc) {
914            $this->_draw_color = $dc;
915            $this->_out($this->_draw_color);
916        }
917        $this->_text_color = $tc;
918        $this->_color_flag = $cf;
919
920        /* Page header. */
921        $this->header();
922
923        /* Restore styles. */
924        if ($this->_line_width != $lw) {
925            $this->_line_width = $lw;
926            $this->_out(sprintf('%.2F w', $lw * $this->_scale));
927        }
928        $this->setFont($font_family, $font_style, $font_size);
929        if ($this->_fill_color != $fc) {
930            $this->_fill_color = $fc;
931            $this->_out($this->_fill_color);
932        }
933        if ($this->_draw_color != $dc) {
934            $this->_draw_color = $dc;
935            $this->_out($this->_draw_color);
936        }
937        $this->_text_color = $tc;
938        $this->_color_flag = $cf;
939    }
940
941    /**
942     * This method is used to render the page header.
943     *
944     * It is automatically called by {@link addPage()} and should not be
945     * called directly by the application. The implementation in Horde_Pdf_Writer is
946     * empty, so you have to subclass it and override the method if you want a
947     * specific processing.
948     *
949     * Example:
950     * <code>
951     * class My_Pdf extends Horde_Pdf_Writer {
952     *     function header()
953     *     {
954     *         // Select Arial bold 15
955     *         $this->setFont('Arial', 'B', 15);
956     *         // Move to the right
957     *         $this->cell(80);
958     *         // Framed title
959     *         $this->cell(30, 10, 'Title', 1, 0, 'C');
960     *         // Line break
961     *         $this->newLine(20);
962     *     }
963     * }
964     * </code>
965     *
966     * @see footer()
967     */
968    public function header()
969    {
970        /* To be implemented in your own inherited class. */
971    }
972
973    /**
974     * This method is used to render the page footer.
975     *
976     * It is automatically called by {@link addPage()} and {@link close()} and
977     * should not be called directly by the application. The implementation in
978     * Horde_Pdf_Writer is empty, so you have to subclass it and override the method
979     * if you want a specific processing.
980     *
981     * Example:
982     * <code>
983     * class My_Pdf extends Horde_Pdf_Writer {
984     *    function footer()
985     *    {
986     *        // Go to 1.5 cm from bottom
987     *        $this->setY(-15);
988     *        // Select Arial italic 8
989     *        $this->setFont('Arial', 'I', 8);
990     *        // Print centered page number
991     *        $this->cell(0, 10, 'Page ' . $this->getPageNo(), 0, 0, 'C');
992     *    }
993     * }
994     * </code>
995     *
996     * @see header()
997     */
998    public function footer()
999    {
1000        /* To be implemented in your own inherited class. */
1001    }
1002
1003    /**
1004     * Returns the current page number.
1005     *
1006     * @return integer
1007     *
1008     * @see aliasNbPages()
1009     */
1010    public function getPageNo()
1011    {
1012        return $this->_page;
1013    }
1014
1015    /**
1016     * Sets the fill color.
1017     *
1018     * Depending on the colorspace called, the number of color component
1019     * parameters required can be either 1, 3 or 4. The method can be called
1020     * before the first page is created and the color is retained from page to
1021     * page.
1022     *
1023     * @param string $cs  Indicates the colorspace which can be either 'rgb',
1024     *                    'hex', 'cmyk', or 'gray'. Defaults to 'rgb'.
1025     * @param float $c1   First color component, floating point value between 0
1026     *                    and 1. Required for gray, rgb and cmyk.
1027     * @param float $c2   Second color component, floating point value
1028     *                    between 0 and 1. Required for rgb and cmyk.
1029     * @param float $c3   Third color component, floating point value between 0
1030     *                    and 1. Required for rgb and cmyk.
1031     * @param float $c4   Fourth color component, floating point value
1032     *                    between 0 and 1. Required for cmyk.
1033     *
1034     * @see setTextColor()
1035     * @see setDrawColor()
1036     * @see rect()
1037     * @see cell()
1038     * @see multiCell()
1039     */
1040    public function setFillColor($cs = 'rgb', $c1 = 0, $c2 = 0, $c3 = 0, $c4 = 0)
1041    {
1042        $cs = Horde_String::lower($cs);
1043
1044        // convert hex to rgb
1045        if ($cs == 'hex') {
1046            $cs = 'rgb';
1047            list($c1, $c2, $c3) = $this->_hexToRgb($c1);
1048        }
1049
1050        if ($cs == 'rgb') {
1051            $this->_fill_color = sprintf('%.3F %.3F %.3F rg', $c1, $c2, $c3);
1052        } elseif ($cs == 'cmyk') {
1053            $this->_fill_color = sprintf('%.3F %.3F %.3F %.3F k', $c1, $c2, $c3, $c4);
1054        } else {
1055            $this->_fill_color = sprintf('%.3F g', $c1);
1056        }
1057        if ($this->_page > 0) {
1058            $this->_out($this->_fill_color);
1059        }
1060        $this->_color_flag = $this->_fill_color != $this->_text_color;
1061    }
1062
1063    /**
1064     * Get the fill color
1065     *
1066     * @return  string
1067     */
1068    public function getFillColor()
1069    {
1070        return $this->_fill_color;
1071    }
1072
1073    /**
1074     * Sets the text color.
1075     *
1076     * Depending on the colorspace called, the number of color component
1077     * parameters required can be either 1, 3 or 4. The method can be called
1078     * before the first page is created and the color is retained from page to
1079     * page.
1080     *
1081     * @param string $cs  Indicates the colorspace which can be either 'rgb',
1082     *                    'hex', 'cmyk' or 'gray'. Defaults to 'rgb'.
1083     * @param float $c1   First color component, floating point value between 0
1084     *                    and 1. Required for gray, rgb and cmyk.
1085     * @param float $c2   Second color component, floating point value
1086     *                    between 0 and 1. Required for rgb and cmyk.
1087     * @param float $c3   Third color component, floating point value between 0
1088     *                    and 1. Required for rgb and cmyk.
1089     * @param float $c4   Fourth color component, floating point value
1090     *                    between 0 and 1. Required for cmyk.
1091     *
1092     * @see setFillColor()
1093     * @see setDrawColor()
1094     * @see rect()
1095     * @see cell()
1096     * @see multiCell()
1097     */
1098    public function setTextColor($cs = 'rgb', $c1 = 0, $c2 = 0, $c3 = 0, $c4 = 0)
1099    {
1100        $cs = Horde_String::lower($cs);
1101
1102        // convert hex to rgb
1103        if ($cs == 'hex') {
1104            $cs = 'rgb';
1105            list($c1, $c2, $c3) = $this->_hexToRgb($c1);
1106        }
1107
1108        if ($cs == 'rgb') {
1109            $this->_text_color = sprintf('%.3F %.3F %.3F rg', $c1, $c2, $c3);
1110        } elseif ($cs == 'cmyk') {
1111            $this->_text_color = sprintf('%.3F %.3F %.3F %.3F k', $c1, $c2, $c3, $c4);
1112        } else {
1113            $this->_text_color = sprintf('%.3F g', $c1);
1114        }
1115
1116        $this->_color_flag = $this->_fill_color != $this->_text_color;
1117    }
1118
1119    /**
1120     * Get the text color
1121     *
1122     * @return  string
1123     */
1124    public function getTextColor()
1125    {
1126        return $this->_text_color;
1127    }
1128
1129    /**
1130     * Sets the draw color, used when drawing lines.
1131     *
1132     * Depending on the colorspace called, the number of color component
1133     * parameters required can be either 1, 3 or 4. The method can be called
1134     * before the first page is created and the color is retained from page to
1135     * page.
1136     *
1137     * @param string $cs  Indicates the colorspace which can be either 'rgb',
1138     *                    'hex', 'cmyk' or 'gray'. Defaults to 'rgb'.
1139     * @param float $c1   First color component, floating point value between 0
1140     *                    and 1. Required for gray, rgb and cmyk.
1141     * @param float $c2   Second color component, floating point value
1142     *                    between 0 and 1. Required for rgb and cmyk.
1143     * @param float $c3   Third color component, floating point value between 0
1144     *                    and 1. Required for rgb and cmyk.
1145     * @param float $c4   Fourth color component, floating point value
1146     *                    between 0 and 1. Required for cmyk.
1147     *
1148     * @see setFillColor()
1149     * @see line()
1150     * @see rect()
1151     * @see cell()
1152     * @see multiCell()
1153     */
1154    public function setDrawColor($cs = 'rgb', $c1 = 0, $c2 = 0, $c3 = 0, $c4 = 0)
1155    {
1156        $cs = Horde_String::lower($cs);
1157
1158        // convert hex to rgb
1159        if ($cs == 'hex') {
1160            $cs = 'rgb';
1161            list($c1, $c2, $c3) = $this->_hexToRgb($c1);
1162        }
1163
1164        if ($cs == 'rgb') {
1165            $this->_draw_color = sprintf('%.3F %.3F %.3F RG', $c1, $c2, $c3);
1166        } elseif ($cs == 'cmyk') {
1167            $this->_draw_color = sprintf('%.3F %.3F %.3F %.3F K', $c1, $c2, $c3, $c4);
1168        } else {
1169            $this->_draw_color = sprintf('%.3F G', $c1);
1170        }
1171        if ($this->_page > 0) {
1172            $this->_out($this->_draw_color);
1173        }
1174    }
1175
1176    /**
1177     * Get the draw color
1178     *
1179     * @return  string
1180     */
1181    public function getDrawColor()
1182    {
1183        return $this->_draw_color;
1184    }
1185
1186    /**
1187     * Returns the length of a text string. A font must be selected.
1188     *
1189     * @param string $text  The text whose length is to be computed.
1190     * @param boolean $pt   Whether the width should be returned in points or
1191     *                      user units.
1192     *
1193     * @return float
1194     */
1195    public function getStringWidth($text, $pt = false)
1196    {
1197        $text = (string)$text;
1198        $width = 0;
1199        $length = strlen($text);
1200        for ($i = 0; $i < $length; $i++) {
1201            $width += $this->_current_font['cw'][$text[$i]];
1202        }
1203
1204        /* Adjust for word spacing. */
1205        $width += $this->_word_spacing * substr_count($text, ' ') * $this->_current_font['cw'][' '];
1206
1207        if ($pt) {
1208            return $width * $this->_font_size_pt / 1000;
1209        } else {
1210            return $width * $this->_font_size / 1000;
1211        }
1212    }
1213
1214    /**
1215     * Defines the line width.
1216     *
1217     * By default, the value equals 0.2 mm. The method can be called before
1218     * the first page is created and the value is retained from page to page.
1219     *
1220     * @param float $width  The width.
1221     *
1222     * @see line()
1223     * @see rect()
1224     * @see cell()
1225     * @see multiCell()
1226     */
1227    public function setLineWidth($width)
1228    {
1229        $this->_line_width = $width;
1230        if ($this->_page > 0) {
1231            $this->_out(sprintf('%.2F w', $width * $this->_scale));
1232        }
1233    }
1234
1235    /**
1236     * P (portrait) or L (landscape)
1237     *
1238     * @return  string
1239     */
1240    public function getDefaultOrientation()
1241    {
1242        return $this->_default_orientation;
1243    }
1244
1245    /**
1246     * @return  integer
1247     */
1248    public function getScale()
1249    {
1250        return $this->_scale;
1251    }
1252
1253    /**
1254     * @return  float
1255     */
1256    public function getFormatHeight()
1257    {
1258        return $this->_default_orientation == 'P' ? $this->fhPt : $this->fwPt;
1259    }
1260
1261    /**
1262     * @return  float
1263     */
1264    public function getFormatWidth()
1265    {
1266        return $this->_default_orientation == 'P' ? $this->fwPt : $this->fhPt;
1267    }
1268
1269    /**
1270     * Draws a line between two points.
1271     *
1272     * All coordinates can be negative to provide values from the right or
1273     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
1274     *
1275     * @param float $x1  Abscissa of first point.
1276     * @param float $y1  Ordinate of first point.
1277     * @param float $x2  Abscissa of second point.
1278     * @param float $y2  Ordinate of second point.
1279     *
1280     * @see setLineWidth()
1281     * @see setDrawColor()
1282     */
1283    public function line($x1, $y1, $x2, $y2)
1284    {
1285        if ($x1 < 0) {
1286            $x1 += $this->w;
1287        }
1288        if ($y1 < 0) {
1289            $y1 += $this->h;
1290        }
1291        if ($x2 < 0) {
1292            $x2 += $this->w;
1293        }
1294        if ($y2 < 0) {
1295            $y2 += $this->h;
1296        }
1297
1298        $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1 * $this->_scale, ($this->h - $y1) * $this->_scale, $x2 * $this->_scale, ($this->h - $y2) * $this->_scale));
1299    }
1300
1301    /**
1302     * Outputs a rectangle.
1303     *
1304     * It can be drawn (border only), filled (with no border) or both.
1305     *
1306     * All coordinates can be negative to provide values from the right or
1307     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
1308     *
1309     * @param float $x       Abscissa of upper-left corner.
1310     * @param float $y       Ordinate of upper-left corner.
1311     * @param float $width   Width.
1312     * @param float $height  Height.
1313     * @param float $style   Style of rendering. Possible values are:
1314     *                         - D or empty string: draw (default)
1315     *                         - F: fill
1316     *                         - DF or FD: draw and fill
1317     *
1318     * @see setLineWidth()
1319     * @see setDrawColor()
1320     * @see setFillColor()
1321     */
1322    public function rect($x, $y, $width, $height, $style = '')
1323    {
1324        if ($x < 0) {
1325            $x += $this->w;
1326        }
1327        if ($y < 0) {
1328            $y += $this->h;
1329        }
1330
1331        $style = Horde_String::upper($style);
1332        if ($style == 'F') {
1333            $op = 'f';
1334        } elseif ($style == 'FD' || $style == 'DF') {
1335            $op = 'B';
1336        } else {
1337            $op = 'S';
1338        }
1339
1340        $x      = $this->_toPt($x);
1341        $y      = $this->_toPt($y);
1342        $width  = $this->_toPt($width);
1343        $height = $this->_toPt($height);
1344
1345        $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s', $x, $this->hPt - $y, $width, -$height, $op));
1346    }
1347
1348    /**
1349     * Outputs a circle. It can be drawn (border only), filled (with no
1350     * border) or both.
1351     *
1352     * All coordinates can be negative to provide values from the right or
1353     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
1354     *
1355     * @param float $x       Abscissa of the center of the circle.
1356     * @param float $y       Ordinate of the center of the circle.
1357     * @param float $r       Circle radius.
1358     * @param string $style  Style of rendering. Possible values are:
1359     *                         - D or empty string: draw (default)
1360     *                         - F: fill
1361     *                         - DF or FD: draw and fill
1362     */
1363    public function circle($x, $y, $r, $style = '')
1364    {
1365        if ($x < 0) {
1366            $x += $this->w;
1367        }
1368        if ($y < 0) {
1369            $y += $this->h;
1370        }
1371
1372        $style = Horde_String::lower($style);
1373        if ($style == 'f') {
1374            $op = 'f';      // Style is fill only.
1375        } elseif ($style == 'fd' || $style == 'df') {
1376            $op = 'B';      // Style is fill and stroke.
1377        } else {
1378            $op = 'S';      // Style is stroke only.
1379        }
1380
1381        $x = $this->_toPt($x);
1382        $y = $this->_toPt($y);
1383        $r = $this->_toPt($r);
1384
1385        /* Invert the y scale. */
1386        $y = $this->hPt - $y;
1387        /* Length of the Bezier control. */
1388        $b = $r * 0.552;
1389
1390        /* Move from the given origin and set the current point
1391         * to the start of the first Bezier curve. */
1392        $c = sprintf('%.2F %.2F m', $x - $r, $y);
1393        $x = $x - $r;
1394        /* First circle quarter. */
1395        $c .= sprintf(' %.2F %.2F %.2F %.2F %.2F %.2F c',
1396                      $x, $y + $b,           // First control point.
1397                      $x + $r - $b, $y + $r, // Second control point.
1398                      $x + $r, $y + $r);     // Final point.
1399        /* Set x/y to the final point. */
1400        $x = $x + $r;
1401        $y = $y + $r;
1402        /* Second circle quarter. */
1403        $c .= sprintf(' %.2F %.2F %.2F %.2F %.2F %.2F c',
1404                      $x + $b, $y,
1405                      $x + $r, $y - $r + $b,
1406                      $x + $r, $y - $r);
1407        /* Set x/y to the final point. */
1408        $x = $x + $r;
1409        $y = $y - $r;
1410        /* Third circle quarter. */
1411        $c .= sprintf(' %.2F %.2F %.2F %.2F %.2F %.2F c',
1412                      $x, $y - $b,
1413                      $x - $r + $b, $y - $r,
1414                      $x - $r, $y - $r);
1415        /* Set x/y to the final point. */
1416        $x = $x - $r;
1417        $y = $y - $r;
1418        /* Fourth circle quarter. */
1419        $c .= sprintf(' %.2F %.2F %.2F %.2F %.2F %.2F c %s',
1420                      $x - $b, $y,
1421                      $x - $r, $y + $r - $b,
1422                      $x - $r, $y + $r,
1423                      $op);
1424        /* Output the whole string. */
1425        $this->_out($c);
1426    }
1427
1428    /**
1429     * Imports a TrueType or Type1 font and makes it available. It is
1430     * necessary to generate a font definition file first with the
1431     * makefont.php utility.
1432     * The location of the definition file (and the font file itself when
1433     * embedding) must be found at the full path name included.
1434     *
1435     * Example:
1436     * <code>
1437     * $pdf->addFont('Comic', 'I');
1438     * is equivalent to:
1439     * $pdf->addFont('Comic', 'I', 'comici.php');
1440     * </code>
1441     *
1442     * @param string $family  Font family. The name can be chosen arbitrarily.
1443     *                        If it is a standard family name, it will
1444     *                        override the corresponding font.
1445     * @param string $style   Font style. Possible values are (case
1446     *                        insensitive):
1447     *                          - empty string: regular (default)
1448     *                          - B: bold
1449     *                          - I: italic
1450     *                          - BI or IB: bold italic
1451     * @param string $file    The font definition file. By default, the name is
1452     *                        built from the family and style, in lower case
1453     *                        with no space.
1454     *
1455     * @see setFont()
1456     * @todo Fonts use a class instead of a definition file
1457     */
1458    public function addFont($family, $style = '', $file = '')
1459    {
1460        $family = Horde_String::lower($family);
1461        if ($family == 'arial') {
1462            $family = 'helvetica';
1463        }
1464
1465        $style = Horde_String::upper($style);
1466        if ($style == 'IB') {
1467            $style = 'BI';
1468        }
1469        if (isset($this->_fonts[$family . $style])) {
1470            throw new Horde_Pdf_Exception(sprintf('Font already added: %s %s', $family, $style));
1471        }
1472        if ($file == '') {
1473            $file = str_replace(' ', '', $family) . Horde_String::lower($style) . '.php';
1474        }
1475        include $file;
1476        if (!isset($name)) {
1477            throw new Horde_Pdf_Exception('Could not include font definition file');
1478        }
1479        $i = count($this->_fonts) + 1;
1480        $this->_fonts[$family . $style] = array('i' => $i, 'type' => $type, 'name' => $name, 'desc' => $desc, 'up' => $up, 'ut' => $ut, 'cw' => $cw, 'enc' => $enc, 'file' => $file);
1481        if ($diff) {
1482            /* Search existing encodings. */
1483            $d = 0;
1484            $nb = count($this->_diffs);
1485            for ($i = 1; $i <= $nb; $i++) {
1486                if ($this->_diffs[$i] == $diff) {
1487                    $d = $i;
1488                    break;
1489                }
1490            }
1491            if ($d == 0) {
1492                $d = $nb + 1;
1493                $this->_diffs[$d] = $diff;
1494            }
1495            $this->_fonts[$family . $style]['diff'] = $d;
1496        }
1497        if ($file) {
1498            if ($type == 'TrueType') {
1499                $this->_font_files[$file] = array('length1' => $originalsize);
1500            } else {
1501                $this->_font_files[$file] = array('length1' => $size1, 'length2' => $size2);
1502            }
1503        }
1504    }
1505
1506    /**
1507     * Sets the font used to print character strings.
1508     *
1509     * It is mandatory to call this method at least once before printing text
1510     * or the resulting document would not be valid. The font can be either a
1511     * standard one or a font added via the {@link addFont()} method. Standard
1512     * fonts use Windows encoding cp1252 (Western Europe).
1513     *
1514     * The method can be called before the first page is created and the font
1515     * is retained from page to page.
1516     *
1517     * If you just wish to change the current font size, it is simpler to call
1518     * {@link setFontSize()}.
1519     *
1520     * @param string $family  Family font. It can be either a name defined by
1521     *                        {@link addFont()} or one of the standard families
1522     *                        (case insensitive):
1523     *                          - Courier (fixed-width)
1524     *                          - Helvetica or Arial (sans serif)
1525     *                          - Times (serif)
1526     *                          - Symbol (symbolic)
1527     *                          - ZapfDingbats (symbolic)
1528     *                        It is also possible to pass an empty string. In
1529     *                        that case, the current family is retained.
1530     * @param string $style   Font style. Possible values are (case
1531     *                        insensitive):
1532     *                          - empty string: regular
1533     *                          - B: bold
1534     *                          - I: italic
1535     *                          - U: underline
1536     *                        or any combination. Bold and italic styles do not
1537     *                        apply to Symbol and ZapfDingbats.
1538     * @param integer $size   Font size in points. The default value is the
1539     *                        current size. If no size has been specified since
1540     *                        the beginning of the document, the value taken
1541     *                        is 12.
1542     * @param boolean $force  Force the setting of the font. Each new page will
1543     *                        require a new call to {@link setFont()} and
1544     *                        setting this to true will make sure that the
1545     *                        checks for same font calls will be skipped.
1546     *
1547     * @see addFont()
1548     * @see setFontSize()
1549     * @see cell()
1550     * @see multiCell()
1551     * @see write()
1552     */
1553    public function setFont($family, $style = '', $size = null, $force = false)
1554    {
1555        $family = Horde_String::lower($family);
1556        if (empty($family)) {
1557            $family = $this->_font_family;
1558        }
1559        if ($family == 'arial') {
1560            /* Use helvetica instead of arial. */
1561            $family = 'helvetica';
1562        } elseif ($family == 'symbol' || $family == 'zapfdingbats') {
1563            /* These two fonts do not have styles available. */
1564            $style = '';
1565        }
1566
1567        $style = Horde_String::upper($style);
1568
1569        /* Underline is handled separately, if specified in the style var
1570         * remove it from the style and set the underline flag. */
1571        if (strpos($style, 'U') !== false) {
1572            $this->_underline = true;
1573            $style = str_replace('U', '', $style);
1574        } else {
1575            $this->_underline = false;
1576        }
1577
1578        if ($style == 'IB') {
1579            $style = 'BI';
1580        }
1581
1582        /* If no size specified, use current size. */
1583        if (is_null($size)) {
1584            $size = $this->_font_size_pt;
1585        }
1586
1587        /* If font requested is already the current font and no force setting
1588         * of the font is requested (eg. when adding a new page) don't bother
1589         * with the rest of the function and simply return. */
1590        if ($this->_font_family == $family && $this->_font_style == $style &&
1591            $this->_font_size_pt == $size && !$force) {
1592            return;
1593        }
1594
1595        /* Set the font key. */
1596        $fontkey = $family . $style;
1597
1598        /* Test if already cached. */
1599        if (!isset($this->_fonts[$fontkey])) {
1600            /* Get the character width definition file. */
1601            $font_widths = self::_getFontFile($fontkey);
1602
1603            $i = count($this->_fonts) + 1;
1604            $this->_fonts[$fontkey] = array(
1605                'i'    => $i,
1606                'type' => 'core',
1607                'name' => $this->_core_fonts[$fontkey],
1608                'up'   => -100,
1609                'ut'   => 50,
1610                'cw'   => $font_widths[$fontkey]);
1611        }
1612
1613        /* Store font information as current font. */
1614        $this->_font_family  = $family;
1615        $this->_font_style   = $style;
1616        $this->_font_size_pt = $size;
1617        $this->_font_size    = $size / $this->_scale;
1618        $this->_current_font = $this->_fonts[$fontkey];
1619
1620        /* Output font information if at least one page has been defined. */
1621        if ($this->_page > 0) {
1622            $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->_current_font['i'], $this->_font_size_pt));
1623        }
1624    }
1625
1626    /**
1627     * Defines the size of the current font.
1628     *
1629     * @param float $size  The size (in points).
1630     *
1631     * @see setFont()
1632     */
1633    public function setFontSize($size)
1634    {
1635        /* If the font size is already the current font size, just return. */
1636        if ($this->_font_size_pt == $size) {
1637            return;
1638        }
1639        /* Set the current font size, both in points and scaled to user
1640         * units. */
1641        $this->_font_size_pt = $size;
1642        $this->_font_size = $size / $this->_scale;
1643
1644        /* Output font information if at least one page has been defined. */
1645        if ($this->_page > 0) {
1646            $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->_current_font['i'], $this->_font_size_pt));
1647        }
1648    }
1649
1650    /**
1651     * Defines the style of the current font.
1652     *
1653     * @param string $style  The font style.
1654     *
1655     * @see setFont()
1656     */
1657    public function setFontStyle($style)
1658    {
1659        $this->setFont($this->_font_family, $style);
1660    }
1661
1662    /**
1663     * Creates a new internal link and returns its identifier.
1664     *
1665     * An internal link is a clickable area which directs to another place
1666     * within the document.
1667     *
1668     * The identifier can then be passed to {@link cell()}, {@link()} write,
1669     * {@link image()} or {@link link()}. The destination is defined with
1670     * {@link setLink()}.
1671     *
1672     * @see cell()
1673     * @see write()
1674     * @see image()
1675     * @see link()
1676     * @see setLink()
1677     */
1678    public function addLink()
1679    {
1680        $n = count($this->_links) + 1;
1681        $this->_links[$n] = array(0, 0);
1682        return $n;
1683    }
1684
1685    /**
1686     * Defines the page and position a link points to.
1687     *
1688     * @param integer $link  The link identifier returned by {@link addLink()}.
1689     * @param float $y       Ordinate of target position; -1 indicates the
1690     *                       current position. The default value is 0 (top of
1691     *                       page).
1692     * @param integer $page  Number of target page; -1 indicates the current
1693     *                       page.
1694     *
1695     * @see addLink()
1696     */
1697    public function setLink($link, $y = 0, $page = -1)
1698    {
1699        if ($y == -1) {
1700            $y = $this->y;
1701        }
1702        if ($page == -1) {
1703            $page = $this->_page;
1704        }
1705        $this->_links[$link] = array($page, $y);
1706    }
1707
1708    /**
1709     * Puts a link on a rectangular area of the page.
1710     *
1711     * Text or image links are generally put via {@link cell()}, {@link
1712     * write()} or {@link image()}, but this method can be useful for instance
1713     * to define a clickable area inside an image.
1714     *
1715     * All coordinates can be negative to provide values from the right or
1716     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
1717     *
1718     * @param float $x       Abscissa of the upper-left corner of the
1719     *                       rectangle.
1720     * @param float $y       Ordinate of the upper-left corner of the
1721     *                       rectangle.
1722     * @param float $width   Width of the rectangle.
1723     * @param float $height  Height of the rectangle.
1724     * @param mixed $link    URL or identifier returned by {@link addLink()}.
1725     *
1726     * @see addLink()
1727     * @see cell()
1728     * @see write()
1729     * @see image()
1730     */
1731    public function link($x, $y, $width, $height, $link)
1732    {
1733        if ($x < 0) {
1734            $x += $this->w;
1735        }
1736        if ($y < 0) {
1737            $y += $this->h;
1738        }
1739
1740        /* Set up the coordinates with correct scaling in pt. */
1741        $x      = $this->_toPt($x);
1742        $y      = $this->hPt - $this->_toPt($y);
1743        $width  = $this->_toPt($width);
1744        $height = $this->_toPt($height);
1745
1746        /* Save link to page links array. */
1747        $this->_link($x, $y, $width, $height, $link);
1748    }
1749
1750    /**
1751     * Prints a character string.
1752     *
1753     * The origin is on the left of the first character, on the baseline. This
1754     * method allows to place a string precisely on the page, but it is
1755     * usually easier to use {@link cell()}, {@link multiCell()} or {@link
1756     * write()} which are the standard methods to print text.
1757     *
1758     * All coordinates can be negative to provide values from the right or
1759     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
1760     *
1761     * @param float $x      Abscissa of the origin.
1762     * @param float $y      Ordinate of the origin.
1763     * @param string $text  String to print.
1764     *
1765     * @see setFont()
1766     * @see cell()
1767     * @see multiCell()
1768     * @see write()
1769     */
1770    public function text($x, $y, $text)
1771    {
1772        if ($x < 0) {
1773            $x += $this->w;
1774        }
1775        if ($y < 0) {
1776            $y += $this->h;
1777        }
1778
1779        /* Scale coordinates into points and set correct Y position. */
1780        $x = $this->_toPt($x);
1781        $y = $this->hPt - $this->_toPt($y);
1782
1783        /* Escape any potentially harmful characters. */
1784        $text = $this->_escape($text);
1785
1786        $out = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x, $y, $text);
1787        if ($this->_underline && $text != '') {
1788            $out .= ' ' . $this->_doUnderline($x, $y, $text);
1789        }
1790        if ($this->_color_flag) {
1791            $out = sprintf('q %s %s Q', $this->_text_color, $out);
1792        }
1793        $this->_out($out);
1794    }
1795
1796    /**
1797     * Whenever a page break condition is met, the method is called, and the
1798     * break is issued or not depending on the returned value. The default
1799     * implementation returns a value according to the mode selected by
1800     * {@link setAutoPageBreak()}.
1801     * This method is called automatically and should not be called directly
1802     * by the application.
1803     *
1804     * @return boolean
1805     *
1806     * @see setAutoPageBreak()
1807     */
1808    public function acceptPageBreak()
1809    {
1810        return $this->_auto_page_break;
1811    }
1812
1813    /**
1814     * Prints a cell (rectangular area) with optional borders, background
1815     * color and character string.
1816     *
1817     * The upper-left corner of the cell corresponds to the current
1818     * position. The text can be aligned or centered. After the call, the
1819     * current position moves to the right or to the next line. It is possible
1820     * to put a link on the text.  If automatic page breaking is enabled and
1821     * the cell goes beyond the limit, a page break is done before outputting.
1822     *
1823     * @param float $width   Cell width. If 0, the cell extends up to the right
1824     *                       margin.
1825     * @param float $height  Cell height.
1826     * @param string $text   String to print.
1827     * @param mixed $border  Indicates if borders must be drawn around the
1828     *                       cell. The value can be either a number:
1829     *                         - 0: no border (default)
1830     *                         - 1: frame
1831     *                       or a string containing some or all of the
1832     *                       following characters (in any order):
1833     *                         - L: left
1834     *                         - T: top
1835     *                         - R: right
1836     *                         - B: bottom
1837     * @param integer $ln    Indicates where the current position should go
1838     *                       after the call. Possible values are:
1839     *                         - 0: to the right (default)
1840     *                         - 1: to the beginning of the next line
1841     *                         - 2: below
1842     *                       Putting 1 is equivalent to putting 0 and calling
1843     *                       {@link newLine()} just after.
1844     * @param string $align  Allows to center or align the text. Possible
1845     *                       values are:
1846     *                         - L or empty string: left (default)
1847     *                         - C: center
1848     *                         - R: right
1849     * @param integer $fill  Indicates if the cell fill type. Possible values
1850     *                       are:
1851     *                         - 0: transparent (default)
1852     *                         - 1: painted
1853     * @param string $link   URL or identifier returned by {@link addLink()}.
1854     *
1855     * @see setFont()
1856     * @see setDrawColor()
1857     * @see setFillColor()
1858     * @see setLineWidth()
1859     * @see addLink()
1860     * @see newLine()
1861     * @see multiCell()
1862     * @see write()
1863     * @see setAutoPageBreak()
1864     */
1865    public function cell($width, $height = 0, $text = '', $border = 0, $ln = 0,
1866                  $align = '', $fill = 0, $link = '')
1867    {
1868        $k = $this->_scale;
1869        if ($this->y + $height > $this->_page_break_trigger &&
1870            !$this->_in_footer && $this->acceptPageBreak()) {
1871            $x = $this->x;
1872            $ws = $this->_word_spacing;
1873            if ($ws > 0) {
1874                $this->_word_spacing = 0;
1875                $this->_out('0 Tw');
1876            }
1877            $this->addPage($this->_current_orientation);
1878            $this->x = $x;
1879            if ($ws > 0) {
1880                $this->_word_spacing = $ws;
1881                $this->_out(sprintf('%.3F Tw', $ws * $k));
1882            }
1883        }
1884        if ($width == 0) {
1885            $width = $this->w - $this->_right_margin - $this->x;
1886        }
1887        $s = '';
1888        if ($fill == 1 || $border == 1) {
1889            if ($fill == 1) {
1890                $op = ($border == 1) ? 'B' : 'f';
1891            } else {
1892                $op = 'S';
1893            }
1894            $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ($this->h - $this->y) * $k, $width * $k, -$height * $k, $op);
1895        }
1896        if (is_string($border)) {
1897            if (strpos($border, 'L') !== false) {
1898                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $this->x * $k, ($this->h - $this->y) * $k, $this->x * $k, ($this->h - ($this->y + $height)) * $k);
1899            }
1900            if (strpos($border, 'T') !== false) {
1901                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $this->x * $k, ($this->h - $this->y) * $k, ($this->x + $width) * $k, ($this->h - $this->y) * $k);
1902            }
1903            if (strpos($border, 'R') !== false) {
1904                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($this->x + $width) * $k, ($this->h - $this->y) * $k, ($this->x + $width) * $k, ($this->h - ($this->y + $height)) * $k);
1905            }
1906            if (strpos($border, 'B') !== false) {
1907                $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $this->x * $k, ($this->h - ($this->y + $height)) * $k, ($this->x + $width) * $k, ($this->h - ($this->y + $height)) * $k);
1908            }
1909        }
1910        if ($text != '') {
1911            if ($align == 'R') {
1912                $dx = $width - $this->_cell_margin - $this->getStringWidth($text);
1913            } elseif ($align == 'C') {
1914                $dx = ($width - $this->getStringWidth($text)) / 2;
1915            } else {
1916                $dx = $this->_cell_margin;
1917            }
1918            if ($this->_color_flag) {
1919                $s .= 'q ' . $this->_text_color . ' ';
1920            }
1921            $text = str_replace(')', '\\)', str_replace('(', '\\(', str_replace('\\', '\\\\', $text)));
1922            $test2 = ((.5 * $height) + (.3 * $this->_font_size));
1923            $test1 = $this->fhPt - (($this->y + $test2) * $k);
1924            $x = ($this->x + $dx) * $k;
1925            $y = ($this->h - ($this->y + .5 * $height + .3 * $this->_font_size)) * $k;
1926            $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', $x, $y, $text);
1927            if ($this->_underline) {
1928                $s .= ' ' . $this->_doUnderline($x, $y, $text);
1929            }
1930            if ($this->_color_flag) {
1931                $s .= ' Q';
1932            }
1933            if ($link) {
1934                $this->link($this->x + $dx, $this->y + .5 * $height- .5 * $this->_font_size, $this->getStringWidth($text), $this->_font_size, $link);
1935            }
1936        }
1937        if ($s) {
1938            $this->_out($s);
1939        }
1940        $this->_last_height = $height;
1941        if ($ln > 0) {
1942            // Go to next line.
1943            $this->y += $height;
1944            if ($ln == 1) {
1945                $this->x = $this->_left_margin;
1946            }
1947        } else {
1948            $this->x += $width;
1949        }
1950    }
1951
1952    /**
1953     * This method allows printing text with line breaks.
1954     *
1955     * They can be automatic (as soon as the text reaches the right border of
1956     * the cell) or explicit (via the \n character). As many cells as
1957     * necessary are output, one below the other. Text can be aligned,
1958     * centered or justified. The cell block can be framed and the background
1959     * painted.
1960     *
1961     * @param float $width   Width of cells. If 0, they extend up to the right
1962     *                       margin of the page.
1963     * @param float $height  Height of cells.
1964     * @param string $text   String to print.
1965     * @param mixed $border  Indicates if borders must be drawn around the cell
1966     *                       block. The value can be either a number:
1967     *                         - 0: no border (default)
1968     *                         - 1: frame
1969     *                       or a string containing some or all of the
1970     *                       following characters (in any order):
1971     *                         - L: left
1972     *                         - T: top
1973     *                         - R: right
1974     *                         - B: bottom
1975     * @param string $align  Sets the text alignment. Possible values are:
1976     *                         - L: left alignment
1977     *                         - C: center
1978     *                         - R: right alignment
1979     *                         - J: justification (default value)
1980     * @param integer $fill  Indicates if the cell background must:
1981     *                         - 0: transparent (default)
1982     *                         - 1: painted
1983     *
1984     * @see setFont()
1985     * @see setDrawColor()
1986     * @see setFillColor()
1987     * @see setLineWidth()
1988     * @see cell()
1989     * @see write()
1990     * @see setAutoPageBreak()
1991     */
1992    public function multiCell($width, $height, $text, $border = 0, $align = 'J',
1993                       $fill = 0)
1994    {
1995        $cw = $this->_current_font['cw'];
1996        if ($width == 0) {
1997            $width = $this->w - $this->_right_margin - $this->x;
1998        }
1999        $wmax = ($width-2 * $this->_cell_margin) * 1000 / $this->_font_size;
2000        $s = str_replace("\r", '', $text);
2001        $nb = strlen($s);
2002        if ($nb > 0 && $s[$nb-1] == "\n") {
2003            $nb--;
2004        }
2005        $b = 0;
2006        if ($border) {
2007            if ($border == 1) {
2008                $border = 'LTRB';
2009                $b = 'LRT';
2010                $b2 = 'LR';
2011            } else {
2012                $b2 = '';
2013                if (strpos($border, 'L') !== false) {
2014                    $b2 .= 'L';
2015                }
2016                if (strpos($border, 'R') !== false) {
2017                    $b2 .= 'R';
2018                }
2019                $b = (strpos($border, 'T') !== false) ? $b2 . 'T' : $b2;
2020            }
2021        }
2022        $sep = -1;
2023        $i   = 0;
2024        $j   = 0;
2025        $l   = 0;
2026        $ns  = 0;
2027        $nl  = 1;
2028        while ($i < $nb) {
2029            // Get next character.
2030            $c = $s[$i];
2031            if ($c == "\n") {
2032                // Explicit line break.
2033                if ($this->_word_spacing > 0) {
2034                    $this->_word_spacing = 0;
2035                    $this->_out('0 Tw');
2036                }
2037                $this->cell($width, $height, substr($s, $j, $i-$j), $b, 2, $align, $fill);
2038                $i++;
2039                $sep = -1;
2040                $j = $i;
2041                $l = 0;
2042                $ns = 0;
2043                $nl++;
2044                if ($border && $nl == 2) {
2045                    $b = $b2;
2046                }
2047                continue;
2048            }
2049            if ($c == ' ') {
2050                $sep = $i;
2051                $ls = $l;
2052                $ns++;
2053            }
2054            $l += $cw[$c];
2055            if ($l > $wmax) {
2056                // Automatic line break.
2057                if ($sep == -1) {
2058                    if ($i == $j) {
2059                        $i++;
2060                    }
2061                    if ($this->_word_spacing > 0) {
2062                        $this->_word_spacing = 0;
2063                        $this->_out('0 Tw');
2064                    }
2065                    $this->cell($width, $height, substr($s, $j, $i - $j), $b, 2, $align, $fill);
2066                } else {
2067                    if ($align == 'J') {
2068                        $this->_word_spacing = ($ns>1) ? ($wmax - $ls)/1000 * $this->_font_size / ($ns - 1) : 0;
2069                        $this->_out(sprintf('%.3F Tw', $this->_word_spacing * $this->_scale));
2070                    }
2071                    $this->cell($width, $height, substr($s, $j, $sep - $j), $b, 2, $align, $fill);
2072                    $i = $sep + 1;
2073                }
2074                $sep = -1;
2075                $j = $i;
2076                $l = 0;
2077                $ns = 0;
2078                $nl++;
2079                if ($border && $nl == 2) {
2080                    $b = $b2;
2081                }
2082            } else {
2083                $i++;
2084            }
2085        }
2086        // Last chunk.
2087        if ($this->_word_spacing > 0) {
2088            $this->_word_spacing = 0;
2089            $this->_out('0 Tw');
2090        }
2091        if ($border && strpos($border, 'B') !== false) {
2092            $b .= 'B';
2093        }
2094        $this->cell($width, $height, substr($s, $j, $i), $b, 2, $align, $fill);
2095        $this->x = $this->_left_margin;
2096    }
2097
2098    /**
2099     * This method prints text from the current position.
2100     *
2101     * When the right margin is reached (or the \n character is met) a line
2102     * break occurs and text continues from the left margin. Upon method exit,
2103     * the current position is left just at the end of the text.
2104     *
2105     * It is possible to put a link on the text.
2106     *
2107     * Example:
2108     * <code>
2109     * // Begin with regular font
2110     * $pdf->setFont('Arial', '', 14);
2111     * $pdf->write(5, 'Visit ');
2112     * // Then put a blue underlined link
2113     * $pdf->setTextColor(0, 0, 255);
2114     * $pdf->setFont('', 'U');
2115     * $pdf->write(5, 'www.fpdf.org', 'http://www.fpdf.org');
2116     * </code>
2117     *
2118     * @param float $height  Line height.
2119     * @param string $text   String to print.
2120     * @param mixed $link    URL or identifier returned by {@link addLink()}.
2121     *
2122     * @see setFont()
2123     * @see addLink()
2124     * @see multiCell()
2125     * @see setAutoPageBreak()
2126     */
2127    public function write($height, $text, $link = '')
2128    {
2129        $cw = $this->_current_font['cw'];
2130        $width = $this->w - $this->_right_margin - $this->x;
2131        $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
2132        $s = str_replace("\r", '', $text);
2133        $nb = strlen($s);
2134        $sep = -1;
2135        $i = 0;
2136        $j = 0;
2137        $l = 0;
2138        $nl = 1;
2139        while ($i < $nb) {
2140            // Get next character.
2141            $c = $s[$i];
2142            if ($c == "\n") {
2143                // Explicit line break.
2144                $this->cell($width, $height, substr($s, $j, $i - $j), 0, 2, '', 0, $link);
2145                $i++;
2146                $sep = -1;
2147                $j = $i;
2148                $l = 0;
2149                if ($nl == 1) {
2150                    $this->x = $this->_left_margin;
2151                    $width = $this->w - $this->_right_margin - $this->x;
2152                    $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
2153                }
2154                $nl++;
2155                continue;
2156            }
2157            if ($c == ' ') {
2158                $sep = $i;
2159                $ls = $l;
2160            }
2161            $l += (isset($cw[$c]) ? $cw[$c] : 0);
2162            if ($l > $wmax) {
2163                // Automatic line break.
2164                if ($sep == -1) {
2165                    if ($this->x > $this->_left_margin) {
2166                        // Move to next line.
2167                        $this->x = $this->_left_margin;
2168                        $this->y += $height;
2169                        $width = $this->w - $this->_right_margin - $this->x;
2170                        $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
2171                        $i++;
2172                        $nl++;
2173                        continue;
2174                    }
2175                    if ($i == $j) {
2176                        $i++;
2177                    }
2178                    $this->cell($width, $height, substr($s, $j, $i - $j), 0, 2, '', 0, $link);
2179                } else {
2180                    $this->cell($width, $height, substr($s, $j, $sep - $j), 0, 2, '', 0, $link);
2181                    $i = $sep + 1;
2182                }
2183                $sep = -1;
2184                $j = $i;
2185                $l = 0;
2186                if ($nl == 1) {
2187                    $this->x = $this->_left_margin;
2188                    $width = $this->w - $this->_right_margin - $this->x;
2189                    $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size;
2190                }
2191                $nl++;
2192            } else {
2193                $i++;
2194            }
2195        }
2196        // Last chunk.
2197        if ($i != $j) {
2198            $this->cell($l / 1000 * $this->_font_size, $height, substr($s, $j, $i), 0, 0, '', 0, $link);
2199        }
2200    }
2201
2202    /**
2203     * Writes text at an angle.
2204     *
2205     * All coordinates can be negative to provide values from the right or
2206     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
2207     *
2208     * @param integer $x         X coordinate.
2209     * @param integer $y         Y coordinate.
2210     * @param string $text       Text to write.
2211     * @param float $text_angle  Angle to rotate (Eg. 90 = bottom to top).
2212     * @param float $font_angle  Rotate characters as well as text.
2213     *
2214     * @see setFont()
2215     */
2216    public function writeRotated($x, $y, $text, $text_angle, $font_angle = 0)
2217    {
2218        if ($x < 0) {
2219            $x += $this->w;
2220        }
2221        if ($y < 0) {
2222            $y += $this->h;
2223        }
2224
2225        // Escape text.
2226        $text = $this->_escape($text);
2227
2228        $font_angle += 90 + $text_angle;
2229        $text_angle *= M_PI / 180;
2230        $font_angle *= M_PI / 180;
2231
2232        $text_dx = cos($text_angle);
2233        $text_dy = sin($text_angle);
2234        $font_dx = cos($font_angle);
2235        $font_dy = sin($font_angle);
2236
2237        $s= sprintf('BT %.2F %.2F %.2F %.2F %.2F %.2F Tm (%s) Tj ET',
2238                    $text_dx, $text_dy, $font_dx, $font_dy,
2239                    $x * $this->_scale, ($this->h-$y) * $this->_scale, $text);
2240
2241        if ($this->_draw_color) {
2242            $s = 'q ' . $this->_draw_color . ' ' . $s . ' Q';
2243        }
2244        $this->_out($s);
2245    }
2246
2247    /**
2248     * Prints an image in the page.
2249     *
2250     * The upper-left corner and at least one of the dimensions must be
2251     * specified; the height or the width can be calculated automatically in
2252     * order to keep the image proportions. Supported formats are JPEG and
2253     * PNG.
2254     *
2255     * All coordinates can be negative to provide values from the right or
2256     * bottom edge of the page (since File_Pdf 0.2.0, Horde 3.2).
2257     *
2258     * For JPEG, all flavors are allowed:
2259     *   - gray scales
2260     *   - true colors (24 bits)
2261     *   - CMYK (32 bits)
2262     *
2263     * For PNG, are allowed:
2264     *   - gray scales on at most 8 bits (256 levels)
2265     *   - indexed colors
2266     *   - true colors (24 bits)
2267     * but are not supported:
2268     *   - Interlacing
2269     *   - Alpha channel
2270     *
2271     * If a transparent color is defined, it will be taken into account (but
2272     * will be only interpreted by Acrobat 4 and above).
2273     * The format can be specified explicitly or inferred from the file
2274     * extension.
2275     * It is possible to put a link on the image.
2276     *
2277     * Remark: if an image is used several times, only one copy will be
2278     * embedded in the file.
2279     *
2280     * @param string $file   Name of the file containing the image.
2281     * @param float $x       Abscissa of the upper-left corner.
2282     * @param float $y       Ordinate of the upper-left corner.
2283     * @param float $width   Width of the image in the page. If equal to zero,
2284     *                       it is automatically calculated to keep the
2285     *                       original proportions.
2286     * @param float $height  Height of the image in the page. If not specified
2287     *                       or equal to zero, it is automatically calculated
2288     *                       to keep the original proportions.
2289     * @param string $type   Image format. Possible values are (case
2290     *                       insensitive): JPG, JPEG, PNG. If not specified,
2291     *                       the type is inferred from the file extension.
2292     * @param mixed $link    URL or identifier returned by {@link addLink()}.
2293     *
2294     * @see addLink()
2295     */
2296    public function image($file, $x, $y, $width = 0, $height = 0, $type = '',
2297                   $link = '')
2298    {
2299        if ($x < 0) {
2300            $x += $this->w;
2301        }
2302        if ($y < 0) {
2303            $y += $this->h;
2304        }
2305
2306        if (!isset($this->_images[$file])) {
2307            // First use of image, get some file info.
2308            if ($type == '') {
2309                $pos = strrpos($file, '.');
2310                if ($pos === false) {
2311                    throw new Horde_Pdf_Exception(sprintf('Image file has no extension and no type was specified: %s', $file));
2312                }
2313                $type = substr($file, $pos + 1);
2314            }
2315
2316            $mqr = function_exists("get_magic_quotes_runtime") ? @get_magic_quotes_runtime() : 0;
2317            if ($mqr) { set_magic_quotes_runtime(0); }
2318
2319            $type = Horde_String::lower($type);
2320            if ($type == 'jpg' || $type == 'jpeg') {
2321                $info = $this->_parseJPG($file);
2322            } elseif ($type == 'png') {
2323                $info = $this->_parsePNG($file);
2324            } else {
2325                throw new Horde_Pdf_Exception(sprintf('Unsupported image file type: %s', $type));
2326            }
2327
2328            if ($mqr) { set_magic_quotes_runtime($mqr); }
2329
2330            $info['i'] = count($this->_images) + 1;
2331            $this->_images[$file] = $info;
2332        } else {
2333            $info = $this->_images[$file];
2334        }
2335
2336        // Make sure all vars are converted to pt scale.
2337        $x      = $this->_toPt($x);
2338        $y      = $this->hPt - $this->_toPt($y);
2339        $width  = $this->_toPt($width);
2340        $height = $this->_toPt($height);
2341
2342        // If not specified do automatic width and height calculations.
2343        if (empty($width) && empty($height)) {
2344            $width = $info['w'];
2345            $height = $info['h'];
2346        } elseif (empty($width)) {
2347            $width = $height * $info['w'] / $info['h'];
2348        } elseif (empty($height)) {
2349            $height = $width * $info['h'] / $info['w'];
2350        }
2351
2352        $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $width, $height, $x, $y - $height, $info['i']));
2353
2354        // Set any link if requested.
2355        if ($link) {
2356            $this->_link($x, $y, $width, $height, $link);
2357        }
2358    }
2359
2360    /**
2361     * Performs a line break.
2362     *
2363     * The current abscissa goes back to the left margin and the ordinate
2364     * increases by the amount passed in parameter.
2365     *
2366     * @param float $height  The height of the break. By default, the value
2367     *                       equals the height of the last printed cell.
2368     *
2369     * @see cell()
2370     */
2371    public function newLine($height = '')
2372    {
2373        $this->x = $this->_left_margin;
2374        if (is_string($height)) {
2375            $this->y += $this->_last_height;
2376        } else {
2377            $this->y += $height;
2378        }
2379    }
2380
2381    /**
2382     * Get the current page
2383     *
2384     * @return  integer
2385     */
2386    public function getPage()
2387    {
2388        return $this->_page;
2389    }
2390
2391    /**
2392     * Set the current page
2393     * @param   integer $page
2394     */
2395    public function setPage($page)
2396    {
2397        $this->_page = $page;
2398    }
2399
2400    /**
2401     * Returns the abscissa of the current position in user units.
2402     *
2403     * @return float
2404     *
2405     * @see setX()
2406     * @see getY()
2407     * @see setY()
2408     */
2409    public function getX()
2410    {
2411        return $this->x;
2412    }
2413
2414    /**
2415     * Defines the abscissa of the current position.
2416     *
2417     * If the passed value is negative, it is relative to the right of the
2418     * page.
2419     *
2420     * @param float $x  The value of the abscissa.
2421     *
2422     * @see getX()
2423     * @see getY()
2424     * @see setY()
2425     * @see setXY()
2426     */
2427    public function setX($x)
2428    {
2429        if ($x >= 0) {
2430            // Absolute value.
2431            $this->x = $x;
2432        } else {
2433            // Negative, so relative to right edge of the page.
2434            $this->x = $this->w + $x;
2435        }
2436    }
2437
2438    /**
2439     * Returns the ordinate of the current position in user units.
2440     *
2441     * @return float
2442     *
2443     * @see setY()
2444     * @see getX()
2445     * @see setX()
2446     */
2447    public function getY()
2448    {
2449        return $this->y;
2450    }
2451
2452    /**
2453     * Defines the ordinate of the current position.
2454     *
2455     * If the passed value is negative, it is relative to the bottom of the
2456     * page.
2457     *
2458     * @param float $y  The value of the ordinate.
2459     *
2460     * @see getX()
2461     * @see getY()
2462     * @see setY()
2463     * @see setXY()
2464     */
2465    public function setY($y)
2466    {
2467        if ($y >= 0) {
2468            // Absolute value.
2469            $this->y = $y;
2470        } else {
2471            // Negative, so relative to bottom edge of the page.
2472            $this->y = $this->h + $y;
2473        }
2474    }
2475
2476    /**
2477     * Defines the abscissa and ordinate of the current position.
2478     *
2479     * If the passed values are negative, they are relative respectively to
2480     * the right and bottom of the page.
2481     *
2482     * @param float $x  The value of the abscissa.
2483     * @param float $y  The value of the ordinate.
2484     *
2485     * @see setX()
2486     * @see setY()
2487     */
2488    public function setXY($x, $y)
2489    {
2490        $this->setY($y);
2491        $this->setX($x);
2492    }
2493
2494    /**
2495     * Returns the current buffer content and resets the buffer.
2496     *
2497     * Use this method when creating large files to avoid memory problems.
2498     * This method doesn't work in combination with the save() method,
2499     * use getOutput() at the end. Calling this method doubles the
2500     * memory usage during the call.
2501     *
2502     * @see getOutput()
2503     */
2504    public function flush()
2505    {
2506        // Make sure we have the file header.
2507        $this->_beginDoc();
2508
2509        $buffer = $this->_buffer;
2510        $this->_buffer = '';
2511        $this->_flushed = true;
2512        $this->_buflen += strlen($buffer);
2513
2514        return $buffer;
2515    }
2516
2517    /**
2518     * Returns the raw Pdf file.
2519     *
2520     * @see flush()
2521     */
2522    public function getOutput()
2523    {
2524        // Check whether file has been closed.
2525        if ($this->_state < 3) {
2526            $this->close();
2527        }
2528
2529        return $this->_buffer;
2530    }
2531
2532    /**
2533     * Saves the PDF file on the filesystem.
2534     *
2535     * @param string $filename  The filename for the output file.
2536     */
2537    public function save($filename = 'unknown.pdf')
2538    {
2539        // Check whether the buffer has been flushed already.
2540        if ($this->_flushed) {
2541            throw new Horde_Pdf_Exception('The buffer has been flushed already, don\'t use save() in combination with flush().');
2542        }
2543
2544        // Check whether file has been closed.
2545        if ($this->_state < 3) {
2546            $this->close();
2547        }
2548
2549        $f = fopen($filename, 'wb');
2550        if (!$f) {
2551            throw new Horde_Pdf_Exception(sprintf('Unable to save Pdf file: %s', $filename));
2552        }
2553        fwrite($f, $this->_buffer, strlen($this->_buffer));
2554        fclose($f);
2555    }
2556
2557    /**
2558     * Scale a value.
2559     *
2560     * @param  integer  $val  Value
2561     * @return integer        Value multiplied by scale
2562     */
2563    protected function _toPt($val)
2564    {
2565        return $val * $this->_scale;
2566    }
2567
2568    /**
2569     * Load information about a font from its key name.
2570     *
2571     * @param  string  $fontkey  Font name key
2572     * @return array             Array of all font widths, including this font.
2573     */
2574    protected static function _getFontFile($fontkey)
2575    {
2576        if (!isset(self::$_font_widths[$fontkey])) {
2577            $fontClass = 'Horde_Pdf_Font_' . Horde_String::ucfirst(Horde_String::lower($fontkey));
2578            if (!class_exists($fontClass)) {
2579                throw new Horde_Pdf_Exception(sprintf('Could not include font metric class: %s', $fontClass));
2580            }
2581
2582            $font = new $fontClass;
2583
2584            self::$_font_widths = array_merge(self::$_font_widths, $font->getWidths());
2585            if (!isset(self::$_font_widths[$fontkey])) {
2586                throw new Horde_Pdf_Exception(sprintf('Could not include font metric class: %s', $fontClass));
2587            }
2588        }
2589
2590        return self::$_font_widths;
2591    }
2592
2593    /**
2594     * Save link to page links array.
2595     *
2596     * @param  integer  $x       X-coordinate
2597     * @param  integer  $y       Y-coordinate
2598     * @param  integer  $width   Width
2599     * @param  integer  $height  Height
2600     * @param  string   $link    Link
2601     * @return void
2602     */
2603    protected function _link($x, $y, $width, $height, $link)
2604    {
2605        $this->_page_links[$this->_page][] = array($x, $y, $width, $height, $link);
2606    }
2607
2608    /**
2609     * Begin the PDF document.
2610     *
2611     * @return void
2612     */
2613    protected function _beginDoc()
2614    {
2615        // Start document, but only if not yet started.
2616        if ($this->_state < 1) {
2617            $this->_state = 1;
2618            $this->_out('%PDF-1.3');
2619        }
2620    }
2621
2622    /**
2623     * Write the PDF pages.
2624     *
2625     * @return void
2626     */
2627    protected function _putPages()
2628    {
2629        $nb = $this->_page;
2630        if (!empty($this->_alias_nb_pages)) {
2631            // Replace number of pages.
2632            for ($n = 1; $n <= $nb; $n++) {
2633                $this->_pages[$n] = str_replace($this->_alias_nb_pages, $nb, $this->_pages[$n]);
2634            }
2635        }
2636        if ($this->_default_orientation == 'P') {
2637            $wPt = $this->fwPt;
2638            $hPt = $this->fhPt;
2639        } else {
2640            $wPt = $this->fhPt;
2641            $hPt = $this->fwPt;
2642        }
2643        $filter = ($this->_compress) ? '/Filter /FlateDecode ' : '';
2644        for ($n = 1; $n <= $nb; $n++) {
2645            // Page
2646            $this->_newobj();
2647            $this->_out('<</Type /Page');
2648            $this->_out('/Parent 1 0 R');
2649            if (isset($this->_orientation_changes[$n])) {
2650                $this->_out(sprintf('/MediaBox [0 0 %.2F %.2F]', $hPt, $wPt));
2651            }
2652            $this->_out('/Resources 2 0 R');
2653            if (isset($this->_page_links[$n])) {
2654                // Links
2655                $annots = '/Annots [';
2656                foreach ($this->_page_links[$n] as $pl) {
2657                    $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]);
2658                    $annots .= '<</Type /Annot /Subtype /Link /Rect [' . $rect . '] /Border [0 0 0] ';
2659                    if (is_string($pl[4])) {
2660                        $annots .= '/A <</S /URI /URI ' . $this->_textString($pl[4]) . '>>>>';
2661                    } else {
2662                        $l = $this->_links[$pl[4]];
2663                        $height = isset($this->_orientation_changes[$l[0]]) ? $wPt : $hPt;
2664                        $annots .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>', 1 + 2 * $l[0], $height - $l[1] * $this->_scale);
2665                    }
2666                }
2667                $this->_out($annots . ']');
2668            }
2669            $this->_out('/Contents ' . ($this->_n + 1) . ' 0 R>>');
2670            $this->_out('endobj');
2671            // Page content
2672            $p = ($this->_compress) ? gzcompress($this->_pages[$n]) : $this->_pages[$n];
2673            $this->_newobj();
2674            $this->_out('<<' . $filter . '/Length ' . strlen($p) . '>>');
2675            $this->_putStream($p);
2676            $this->_out('endobj');
2677        }
2678        // Pages root
2679        $this->_offsets[1] = $this->_buflen + strlen($this->_buffer);
2680        $this->_out('1 0 obj');
2681        $this->_out('<</Type /Pages');
2682        $kids = '/Kids [';
2683        for ($i = 0; $i < $nb; $i++) {
2684            $kids .= (3 + 2 * $i) . ' 0 R ';
2685        }
2686        $this->_out($kids . ']');
2687        $this->_out('/Count ' . $nb);
2688        $this->_out(sprintf('/MediaBox [0 0 %.2F %.2F]', $wPt, $hPt));
2689        $this->_out('>>');
2690        $this->_out('endobj');
2691    }
2692
2693    /**
2694     * Write the PDF fonts.
2695     *
2696     * @return void
2697     */
2698    protected function _putFonts()
2699    {
2700        $nf = $this->_n;
2701        foreach ($this->_diffs as $diff) {
2702            // Encodings
2703            $this->_newobj();
2704            $this->_out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' . $diff . ']>>');
2705            $this->_out('endobj');
2706        }
2707
2708        $mqr = function_exists("get_magic_quotes_runtime") ? @get_magic_quotes_runtime() : 0;
2709        if ($mqr) { set_magic_quotes_runtime(0); }
2710
2711        foreach ($this->_font_files as $file => $info) {
2712            // Font file embedding.
2713            $this->_newobj();
2714            $this->_font_files[$file]['n'] = $this->_n;
2715            $size = filesize($file);
2716            if (!$size) {
2717                throw new Horde_Pdf_Exception('Font file not found');
2718            }
2719            $this->_out('<</Length ' . $size);
2720            if (substr($file, -2) == '.z') {
2721                $this->_out('/Filter /FlateDecode');
2722            }
2723            $this->_out('/Length1 ' . $info['length1']);
2724            if (isset($info['length2'])) {
2725                $this->_out('/Length2 ' . $info['length2'] . ' /Length3 0');
2726            }
2727            $this->_out('>>');
2728            $f = fopen($file, 'rb');
2729            $this->_putStream(fread($f, $size));
2730            fclose($f);
2731            $this->_out('endobj');
2732        }
2733
2734        if ($mqr) { set_magic_quotes_runtime($mqr); }
2735
2736        foreach ($this->_fonts as $k => $font) {
2737            // Font objects
2738            $this->_newobj();
2739            $this->_fonts[$k]['n'] = $this->_n;
2740            $name = $font['name'];
2741            $this->_out('<</Type /Font');
2742            $this->_out('/BaseFont /' . $name);
2743            if ($font['type'] == 'core') {
2744                // Standard font.
2745                $this->_out('/Subtype /Type1');
2746                if ($name != 'Symbol' && $name != 'ZapfDingbats') {
2747                    $this->_out('/Encoding /WinAnsiEncoding');
2748                }
2749            } else {
2750                // Additional font.
2751                $this->_out('/Subtype /' . $font['type']);
2752                $this->_out('/FirstChar 32');
2753                $this->_out('/LastChar 255');
2754                $this->_out('/Widths ' . ($this->_n + 1) . ' 0 R');
2755                $this->_out('/FontDescriptor ' . ($this->_n + 2) . ' 0 R');
2756                if ($font['enc']) {
2757                    if (isset($font['diff'])) {
2758                        $this->_out('/Encoding ' . ($nf + $font['diff']) . ' 0 R');
2759                    } else {
2760                        $this->_out('/Encoding /WinAnsiEncoding');
2761                    }
2762                }
2763            }
2764            $this->_out('>>');
2765            $this->_out('endobj');
2766            if ($font['type'] != 'core') {
2767                // Widths.
2768                $this->_newobj();
2769                $cw = $font['cw'];
2770                $s = '[';
2771                for ($i = 32; $i <= 255; $i++) {
2772                    $s .= $cw[chr($i)] . ' ';
2773                }
2774                $this->_out($s . ']');
2775                $this->_out('endobj');
2776                // Descriptor.
2777                $this->_newobj();
2778                $s = '<</Type /FontDescriptor /FontName /' . $name;
2779                foreach ($font['desc'] as $k => $v) {
2780                    $s .= ' /' . $k . ' ' . $v;
2781                }
2782                $file = $font['file'];
2783                if ($file) {
2784                    $s .= ' /FontFile' . ($font['type'] == 'Type1' ? '' : '2') . ' ' . $this->_font_files[$file]['n'] . ' 0 R';
2785                }
2786                $this->_out($s . '>>');
2787                $this->_out('endobj');
2788            }
2789        }
2790    }
2791
2792    /**
2793     * Write the PDF images.
2794     *
2795     * @return void
2796     */
2797    protected function _putImages()
2798    {
2799        $filter = ($this->_compress) ? '/Filter /FlateDecode ' : '';
2800        foreach ($this->_images as $file => $info) {
2801            $this->_newobj();
2802            $this->_images[$file]['n'] = $this->_n;
2803            $this->_out('<</Type /XObject');
2804            $this->_out('/Subtype /Image');
2805            $this->_out('/Width ' . $info['w']);
2806            $this->_out('/Height ' . $info['h']);
2807            if ($info['cs'] == 'Indexed') {
2808                $this->_out('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal'])/3 - 1) . ' ' . ($this->_n + 1) . ' 0 R]');
2809            } else {
2810                $this->_out('/ColorSpace /' . $info['cs']);
2811                if ($info['cs'] == 'DeviceCMYK') {
2812                    $this->_out('/Decode [1 0 1 0 1 0 1 0]');
2813                }
2814            }
2815            $this->_out('/BitsPerComponent ' . $info['bpc']);
2816            $this->_out('/Filter /' . $info['f']);
2817            if (isset($info['parms'])) {
2818                $this->_out($info['parms']);
2819            }
2820            if (isset($info['trns']) && is_array($info['trns'])) {
2821                $trns = '';
2822                $i_max = count($info['trns']);
2823                for ($i = 0; $i < $i_max; $i++) {
2824                    $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' ';
2825                }
2826                $this->_out('/Mask [' . $trns . ']');
2827            }
2828            $this->_out('/Length ' . strlen($info['data']) . '>>');
2829            $this->_putStream($info['data']);
2830            $this->_out('endobj');
2831
2832            // Palette.
2833            if ($info['cs'] == 'Indexed') {
2834                $this->_newobj();
2835                $pal = ($this->_compress) ? gzcompress($info['pal']) : $info['pal'];
2836                $this->_out('<<' . $filter . '/Length ' . strlen($pal) . '>>');
2837                $this->_putStream($pal);
2838                $this->_out('endobj');
2839            }
2840        }
2841    }
2842
2843    /**
2844     * Write the PDF resources.
2845     *
2846     * @return void
2847     */
2848    protected function _putResources()
2849    {
2850        $this->_putFonts();
2851        $this->_putImages();
2852        // Resource dictionary
2853        $this->_offsets[2] = $this->_buflen + strlen($this->_buffer);
2854        $this->_out('2 0 obj');
2855        $this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
2856        $this->_out('/Font <<');
2857        foreach ($this->_fonts as $font) {
2858            $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R');
2859        }
2860        $this->_out('>>');
2861        if (count($this->_images)) {
2862            $this->_out('/XObject <<');
2863            foreach ($this->_images as $image) {
2864                $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R');
2865            }
2866            $this->_out('>>');
2867        }
2868        $this->_out('>>');
2869        $this->_out('endobj');
2870    }
2871
2872    /**
2873     * Write the PDF information.
2874     *
2875     * @return void
2876     */
2877    protected function _putInfo()
2878    {
2879        $this->_out('/Producer ' . $this->_textString('Horde PDF'));
2880        if (!empty($this->_info['title'])) {
2881            $this->_out('/Title ' . $this->_textString($this->_info['title']));
2882        }
2883        if (!empty($this->_info['subject'])) {
2884            $this->_out('/Subject ' . $this->_textString($this->_info['subject']));
2885        }
2886        if (!empty($this->_info['author'])) {
2887            $this->_out('/Author ' . $this->_textString($this->_info['author']));
2888        }
2889        if (!empty($this->keywords)) {
2890            $this->_out('/Keywords ' . $this->_textString($this->keywords));
2891        }
2892        if (!empty($this->creator)) {
2893            $this->_out('/Creator ' . $this->_textString($this->creator));
2894        }
2895        if (!isset($this->_info['CreationDate'])) {
2896            $this->_info['CreationDate'] = 'D:' . date('YmdHis', time());
2897        }
2898        $this->_out('/CreationDate ' . $this->_textString($this->_info['CreationDate']));
2899    }
2900
2901    /**
2902     * Write the PDF catalog.
2903     *
2904     * @return void
2905     */
2906    protected function _putCatalog()
2907    {
2908        $this->_out('/Type /Catalog');
2909        $this->_out('/Pages 1 0 R');
2910        if ($this->_zoom_mode == 'fullpage') {
2911            $this->_out('/OpenAction [3 0 R /Fit]');
2912        } elseif ($this->_zoom_mode == 'fullwidth') {
2913            $this->_out('/OpenAction [3 0 R /FitH null]');
2914        } elseif ($this->_zoom_mode == 'real') {
2915            $this->_out('/OpenAction [3 0 R /XYZ null null 1]');
2916        } elseif (!is_string($this->_zoom_mode)) {
2917            $this->_out('/OpenAction [3 0 R /XYZ null null ' . ($this->_zoom_mode / 100) . ']');
2918        }
2919        if ($this->_layout_mode == 'single') {
2920            $this->_out('/PageLayout /SinglePage');
2921        } elseif ($this->_layout_mode == 'continuous') {
2922            $this->_out('/PageLayout /OneColumn');
2923        } elseif ($this->_layout_mode == 'two') {
2924            $this->_out('/PageLayout /TwoColumnLeft');
2925        }
2926    }
2927
2928    /**
2929     * Write the PDF trailer.
2930     *
2931     * @return void
2932     */
2933    protected function _putTrailer()
2934    {
2935        $this->_out('/Size ' . ($this->_n + 1));
2936        $this->_out('/Root ' . $this->_n . ' 0 R');
2937        $this->_out('/Info ' . ($this->_n - 1) . ' 0 R');
2938    }
2939
2940    /**
2941     * End the PDF document
2942     *
2943     * @return void
2944     */
2945    protected function _endDoc()
2946    {
2947        $this->_putPages();
2948        $this->_putResources();
2949        // Info
2950        $this->_newobj();
2951        $this->_out('<<');
2952        $this->_putInfo();
2953        $this->_out('>>');
2954        $this->_out('endobj');
2955        // Catalog
2956        $this->_newobj();
2957        $this->_out('<<');
2958        $this->_putCatalog();
2959        $this->_out('>>');
2960        $this->_out('endobj');
2961        // Cross-ref
2962        $o = $this->_buflen + strlen($this->_buffer);
2963        $this->_out('xref');
2964        $this->_out('0 ' . ($this->_n + 1));
2965        $this->_out('0000000000 65535 f ');
2966        for ($i = 1; $i <= $this->_n; $i++) {
2967            $this->_out(sprintf('%010d 00000 n ', $this->_offsets[$i]));
2968        }
2969        // Trailer
2970        $this->_out('trailer');
2971        $this->_out('<<');
2972        $this->_putTrailer();
2973        $this->_out('>>');
2974        $this->_out('startxref');
2975        $this->_out($o);
2976        $this->_out('%%EOF');
2977        $this->_state = 3;
2978    }
2979
2980    /**
2981     * Begin a new page.
2982     *
2983     * @param  string  $orientation  Orientation code
2984     * @return void
2985     */
2986    protected function _beginPage($orientation)
2987    {
2988        $this->_page++;
2989
2990        // only assign page contents if it is new
2991        if (!isset($this->_pages[$this->_page])) {
2992            $this->_pages[$this->_page] = '';
2993        }
2994
2995        $this->_state = 2;
2996        $this->x = $this->_left_margin;
2997        $this->y = $this->_top_margin;
2998        $this->_last_height = 0;
2999        // Page orientation
3000        if (!$orientation) {
3001            $orientation = $this->_default_orientation;
3002        } else {
3003            $orientation = Horde_String::upper($orientation[0]);
3004            if ($orientation != $this->_default_orientation) {
3005                $this->_orientation_changes[$this->_page] = true;
3006            }
3007        }
3008        if ($orientation != $this->_current_orientation) {
3009            // Change orientation
3010            if ($orientation == 'P') {
3011                $this->wPt = $this->fwPt;
3012                $this->hPt = $this->fhPt;
3013                $this->w   = $this->fw;
3014                $this->h   = $this->fh;
3015            } else {
3016                $this->wPt = $this->fhPt;
3017                $this->hPt = $this->fwPt;
3018                $this->w   = $this->fh;
3019                $this->h   = $this->fw;
3020            }
3021            $this->_page_break_trigger = $this->h - $this->_break_margin;
3022            $this->_current_orientation = $orientation;
3023        }
3024    }
3025
3026    /**
3027     * Set the end of page contents.
3028     *
3029     * @return void
3030     */
3031    protected function _endPage()
3032    {
3033        $this->_state = 1;
3034    }
3035
3036    /**
3037     * Begin a new object.
3038     *
3039     * @return void
3040     */
3041    protected function _newobj()
3042    {
3043        $this->_n++;
3044        $this->_offsets[$this->_n] = $this->_buflen + strlen($this->_buffer);
3045        $this->_out($this->_n . ' 0 obj');
3046    }
3047
3048    /**
3049     * Underline a block of text.
3050     *
3051     * @param  integer  $x     X-coordinate
3052     * @param  integer  $y     Y-coordinate
3053     * @param  string   $text  Text to underline
3054     * @return string          Underlined string
3055     */
3056    protected function _doUnderline($x, $y, $text)
3057    {
3058        // Set the rectangle width according to text width.
3059        $width  = $this->getStringWidth($text, true);
3060
3061        /* Set rectangle position and height, using underline position and
3062         * thickness settings scaled by the font size. */
3063        $y = $y + ($this->_current_font['up'] * $this->_font_size_pt / 1000);
3064        $height = -$this->_current_font['ut'] * $this->_font_size_pt / 1000;
3065
3066        return sprintf('%.2F %.2F %.2F %.2F re f', $x, $y, $width, $height);
3067    }
3068
3069    /**
3070     * Extract info from a JPEG file.
3071     *
3072     * @param  string  $file  Filename of JPEG image
3073     * @return array         Assoc. array of info
3074     */
3075    protected function _parseJPG($file)
3076    {
3077        // Extract info from a JPEG file.
3078        $img = @getimagesize($file);
3079        if (!$img) {
3080            throw new Horde_Pdf_Exception(sprintf('Missing or incorrect image file: %s', $file));
3081        }
3082        if ($img[2] != 2) {
3083            throw new Horde_Pdf_Exception(sprintf('Not a JPEG file: %s', $file));
3084        }
3085        if (!isset($img['channels']) || $img['channels'] == 3) {
3086            $colspace = 'DeviceRGB';
3087        } elseif ($img['channels'] == 4) {
3088            $colspace = 'DeviceCMYK';
3089        } else {
3090            $colspace = 'DeviceGray';
3091        }
3092        $bpc = isset($img['bits']) ? $img['bits'] : 8;
3093
3094        // Read whole file.
3095        $f = fopen($file, 'rb');
3096        $data = fread($f, filesize($file));
3097        fclose($f);
3098
3099        return array('w' => $img[0], 'h' => $img[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data);
3100    }
3101
3102    /**
3103     * Extract info from a PNG file.
3104     *
3105     * @param  string  $file  Filename of PNG image
3106     * @return array          Assoc. array of info
3107     */
3108    protected function _parsePNG($file)
3109    {
3110        // Extract info from a PNG file.
3111        $f = fopen($file, 'rb');
3112        if (!$f) {
3113            throw new Horde_Pdf_Exception(sprintf('Unable to open image file: %s', $file));
3114        }
3115
3116        // Check signature.
3117        if (fread($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) {
3118            throw new Horde_Pdf_Exception(sprintf('Not a PNG file: %s', $file));
3119        }
3120
3121        // Read header chunk.
3122        fread($f, 4);
3123        if (fread($f, 4) != 'IHDR') {
3124            throw new Horde_Pdf_Exception(sprintf('Incorrect PNG file: %s', $file));
3125        }
3126        $width = $this->_freadInt($f);
3127        $height = $this->_freadInt($f);
3128        $bpc = ord(fread($f, 1));
3129        if ($bpc > 8) {
3130            throw new Horde_Pdf_Exception(sprintf('16-bit depth not supported: %s', $file));
3131        }
3132        $ct = ord(fread($f, 1));
3133        if ($ct == 0) {
3134            $colspace = 'DeviceGray';
3135        } elseif ($ct == 2) {
3136            $colspace = 'DeviceRGB';
3137        } elseif ($ct == 3) {
3138            $colspace = 'Indexed';
3139        } else {
3140            throw new Horde_Pdf_Exception(sprintf('Alpha channel not supported: %s', $file));
3141        }
3142        if (ord(fread($f, 1)) != 0) {
3143            throw new Horde_Pdf_Exception(sprintf('Unknown compression method: %s', $file));
3144        }
3145        if (ord(fread($f, 1)) != 0) {
3146            throw new Horde_Pdf_Exception(sprintf('Unknown filter method: %s', $file));
3147        }
3148        if (ord(fread($f, 1)) != 0) {
3149            throw new Horde_Pdf_Exception(sprintf('Interlacing not supported: %s', $file));
3150        }
3151        fread($f, 4);
3152        $parms = '/DecodeParms <</Predictor 15 /Colors ' . ($ct == 2 ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $width . '>>';
3153        // Scan chunks looking for palette, transparency and image data.
3154        $pal = '';
3155        $trns = '';
3156        $data = '';
3157        do {
3158            $n = $this->_freadInt($f);
3159            $type = fread($f, 4);
3160            if ($type == 'PLTE') {
3161                // Read palette
3162                $pal = fread($f, $n);
3163                fread($f, 4);
3164            } elseif ($type == 'tRNS') {
3165                // Read transparency info
3166                $t = fread($f, $n);
3167                if ($ct == 0) {
3168                    $trns = array(ord(substr($t, 1, 1)));
3169                } elseif ($ct == 2) {
3170                    $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1)));
3171                } else {
3172                    $pos = strpos($t, chr(0));
3173                    if (is_int($pos)) {
3174                        $trns = array($pos);
3175                    }
3176                }
3177                fread($f, 4);
3178            } elseif ($type == 'IDAT') {
3179                // Read image data block
3180                $data .= fread($f, $n);
3181                fread($f, 4);
3182            } elseif ($type == 'IEND') {
3183                break;
3184            } else {
3185                fread($f, $n + 4);
3186            }
3187        } while ($n);
3188
3189        if ($colspace == 'Indexed' && empty($pal)) {
3190            throw new Horde_Pdf_Exception(sprintf('Missing palette in: %s', $file));
3191        }
3192        fclose($f);
3193
3194        return array('w' => $width, 'h' => $height, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'parms' => $parms, 'pal' => $pal, 'trns' => $trns, 'data' => $data);
3195    }
3196
3197    /**
3198     * Read a 4-byte integer from stream.
3199     *
3200     * @param  resource  $f  Stream resource
3201     * @return integer       Byte
3202     */
3203    protected function _freadInt($f)
3204    {
3205        $i  = ord(fread($f, 1)) << 24;
3206        $i += ord(fread($f, 1)) << 16;
3207        $i += ord(fread($f, 1)) << 8;
3208        $i += ord(fread($f, 1));
3209        return $i;
3210    }
3211
3212    /**
3213     * Format a text string by escaping and wrapping in parentheses.
3214     *
3215     * @param  string  $s  String to format.
3216     * @param  string      Formatted string.
3217     * @return string
3218     */
3219    protected function _textString($s)
3220    {
3221        return '(' . $this->_escape($s) . ')';
3222    }
3223
3224    /**
3225     * Escape parentheses and forward slash.
3226     *
3227     * @param  string  $s  String to escape.
3228     * @return string      Escaped string.
3229     */
3230    protected function _escape($s)
3231    {
3232        return str_replace(array('\\', ')', '('),
3233                           array('\\\\', '\\)', '\\('),
3234                           $s);
3235    }
3236
3237    /**
3238     * Add a line to the document wrapped in 'stream' and 'endstream'.
3239     *
3240     * @param  string  $s  Line to add.
3241     * @return void
3242     */
3243    protected function _putStream($s)
3244    {
3245        $this->_out('stream');
3246        $this->_out($s);
3247        $this->_out('endstream');
3248    }
3249
3250    /**
3251     * Add a line to the document.
3252     *
3253     * @param  string  $s  Line to add.
3254     * @return void
3255     */
3256    protected function _out($s)
3257    {
3258        if ($this->_state == 2) {
3259            $this->_pages[$this->_page] .= $s . "\n";
3260        } else {
3261            $this->_buffer .= $s . "\n";
3262        }
3263    }
3264
3265    /**
3266     * Convert hex-based color to RGB
3267     */
3268    protected function _hexToRgb($hex)
3269    {
3270        if (substr($hex, 0, 1) == '#') { $hex = substr($hex, 1); }
3271
3272        if (strlen($hex) == 6) {
3273            list($r, $g, $b) = array(substr($hex, 0, 2),
3274                                     substr($hex, 2, 2),
3275                                     substr($hex, 4, 2));
3276        } elseif (strlen($hex) == 3) {
3277            list($r, $g, $b) = array(substr($hex, 0, 1).substr($hex, 0, 1),
3278                                     substr($hex, 1, 1).substr($hex, 1, 1),
3279                                     substr($hex, 2, 1).substr($hex, 2, 1));
3280        }
3281        $r = hexdec($r)/255;
3282        $g = hexdec($g)/255;
3283        $b = hexdec($b)/255;
3284
3285        return array($r, $g, $b);
3286    }
3287}
3288