source: trunk/filemanager/tp/dompdf/lib/class.pdf.php @ 7673

Revision 7673, 145.5 KB checked in by douglasz, 11 years ago (diff)

Ticket #3236 - Correcoes para Performance: Function Within Loop Declaration.

Line 
1<?php
2  /**
3   * Cpdf
4   *
5   * http://www.ros.co.nz/pdf
6   *
7   * A PHP class to provide the basic functionality to create a pdf document without
8   * any requirement for additional modules.
9   *
10   * Note that they companion class CezPdf can be used to extend this class and dramatically
11   * simplify the creation of documents.
12   *
13   * Extended by Orion Richardson to support Unicode / UTF-8 characters using
14   * TCPDF and others as a guide.
15   *
16   * IMPORTANT NOTE
17   * there is no warranty, implied or otherwise with this software.
18   *
19   * LICENCE
20   * This code has been placed in the Public Domain for all to enjoy.
21   *
22   * @author       Wayne Munro <pdf@ros.co.nz>
23   * @contributor  Orion Richardson <orionr@yahoo.com>
24   * @contributor  Helmut Tischer <htischer@weihenstephan.org>
25   * @version  009
26   * @package  Cpdf
27   *
28   * Changes
29   * @contributor Helmut Tischer <htischer@weihenstephan.org>
30   * @version 0.5.1.htischer.20090507
31   * - On multiple identical png and jpg images, put only one copy into the pdf file and refer to it.
32   *   This reduces file size and rendering time.
33   * - Allow font metrics cache to be a different folder as the font metrics. This allows a read only installation.
34   * - Allow adding images directly from a gd object. This increases performance by avoiding temporary files.
35   * - On png image files remove alpa channel to allow display of typical png files in pdf.
36   * - On addImage avoid temporary file. Todo: Duplicate Image (currently not used)
37   * - Add a check function, whether image is already cached, This avoids double creation by caller which saves
38   *   CPU time and memory.
39   * @contributor Helmut Tischer <htischer@weihenstephan.org>
40   * @version dompdf_trunk_with_helmut_mods.20090524
41   * - Allow temp and fontcache folders to be passed in by class creator
42   * @version dompdf_trunk_with_helmut_mods.20090528
43   * - typo 'decent' instead of 'descent' at various locations made getFontDescender worthless
44   */
45class  Cpdf {
46
47
48  /**
49   * the current number of pdf objects in the document
50   */
51  public  $numObj = 0;
52
53  /**
54   * this array contains all of the pdf objects, ready for final assembly
55   */
56  public  $objects =  array();
57
58  /**
59   * the objectId (number within the objects array) of the document catalog
60   */
61  public  $catalogId;
62
63  /**
64   * array carrying information about the fonts that the system currently knows about
65   * used to ensure that a font is not loaded twice, among other things
66   */
67  public  $fonts = array();
68
69  /**
70   * a record of the current font
71   */
72  public  $currentFont = '';
73
74  /**
75   * the current base font
76   */
77  public  $currentBaseFont = '';
78
79  /**
80   * the number of the current font within the font array
81   */
82  public  $currentFontNum = 0;
83
84  /**
85   *
86   */
87  public  $currentNode;
88
89  /**
90   * object number of the current page
91   */
92  public  $currentPage;
93
94  /**
95   * object number of the currently active contents block
96   */
97  public  $currentContents;
98
99  /**
100   * number of fonts within the system
101   */
102  public  $numFonts = 0;
103
104  /**
105   * Number of graphic state resources used
106   */
107  private  $numStates =  0;
108
109
110  /**
111   * current colour for fill operations, defaults to inactive value, all three components should be between 0 and 1 inclusive when active
112   */
113  public  $currentColour = array('r'=>-1, 'g'=>-1, 'b'=>-1);
114
115  /**
116   * current colour for stroke operations (lines etc.)
117   */
118  public  $currentStrokeColour = array('r'=>-1, 'g'=>-1, 'b'=>-1);
119
120  /**
121   * current style that lines are drawn in
122   */
123  public  $currentLineStyle = '';
124
125  /**
126   * current line transparency (partial graphics state)
127   */
128  public $currentLineTransparency = array("mode" => "Normal", "opacity" => 1.0);
129 
130  /**
131   * current fill transparency (partial graphics state)
132   */
133  public $currentFillTransparency = array("mode" => "Normal", "opacity" => 1.0);
134 
135  /**
136   * an array which is used to save the state of the document, mainly the colours and styles
137   * it is used to temporarily change to another state, the change back to what it was before
138   */
139  public  $stateStack =  array();
140
141  /**
142   * number of elements within the state stack
143   */
144  public  $nStateStack =  0;
145
146  /**
147   * number of page objects within the document
148   */
149  public  $numPages = 0;
150
151  /**
152   * object Id storage stack
153   */
154  public  $stack = array();
155
156  /**
157   * number of elements within the object Id storage stack
158   */
159  public  $nStack = 0;
160
161  /**
162   * an array which contains information about the objects which are not firmly attached to pages
163   * these have been added with the addObject function
164   */
165  public  $looseObjects = array();
166
167  /**
168   * array contains infomation about how the loose objects are to be added to the document
169   */
170  public  $addLooseObjects = array();
171
172  /**
173   * the objectId of the information object for the document
174   * this contains authorship, title etc.
175   */
176  public  $infoObject = 0;
177
178  /**
179   * number of images being tracked within the document
180   */
181  public  $numImages = 0;
182
183  /**
184   * an array containing options about the document
185   * it defaults to turning on the compression of the objects
186   */
187  public  $options = array('compression'=>1);
188
189  /**
190   * the objectId of the first page of the document
191   */
192  public  $firstPageId;
193
194  /**
195   * used to track the last used value of the inter-word spacing, this is so that it is known
196   * when the spacing is changed.
197   */
198  public  $wordSpaceAdjust = 0;
199
200  /**
201   * the object Id of the procset object
202   */
203  public  $procsetObjectId;
204
205  /**
206   * store the information about the relationship between font families
207   * this used so that the code knows which font is the bold version of another font, etc.
208   * the value of this array is initialised in the constuctor function.
209   */
210  public  $fontFamilies =  array();
211 
212  /**
213   * folder for php serialized formats of font metrics files.
214   * If empty string, use same folder as original metrics files.
215   * This can be passed in from class creator.
216   * If this folder does not exist or is not writable, Cpdf will be **much** slower.
217   * Because of potential trouble with php safe mode, folder cannot be created at runtime.
218   */
219  public  $fontcache = '';
220 
221  /**
222   * temporary folder.
223   * If empty string, will attempty system tmp folder.
224   * This can be passed in from class creator.
225   * Only used for conversion of gd images to jpeg images.
226   */
227  public  $tmp = '';
228
229  /**
230   * track if the current font is bolded or italicised
231   */
232  public  $currentTextState =  '';
233
234  /**
235   * messages are stored here during processing, these can be selected afterwards to give some useful debug information
236   */
237  public  $messages = '';
238
239  /**
240   * the ancryption array for the document encryption is stored here
241   */
242  public  $arc4 = '';
243
244  /**
245   * the object Id of the encryption information
246   */
247  public  $arc4_objnum = 0;
248
249  /**
250   * the file identifier, used to uniquely identify a pdf document
251   */
252  public  $fileIdentifier = '';
253
254  /**
255   * a flag to say if a document is to be encrypted or not
256   */
257  public  $encrypted = 0;
258
259  /**
260   * the ancryption key for the encryption of all the document content (structure is not encrypted)
261   */
262  public  $encryptionKey = '';
263
264  /**
265   * array which forms a stack to keep track of nested callback functions
266   */
267  public  $callback =  array();
268
269  /**
270   * the number of callback functions in the callback array
271   */
272  public  $nCallback =  0;
273
274  /**
275   * store label->id pairs for named destinations, these will be used to replace internal links
276   * done this way so that destinations can be defined after the location that links to them
277   */
278  public  $destinations =  array();
279
280  /**
281   * store the stack for the transaction commands, each item in here is a record of the values of all the
282   * publiciables within the class, so that the user can rollback at will (from each 'start' command)
283   * note that this includes the objects array, so these can be large.
284   */
285  public  $checkpoint =  '';
286
287  /* Table of Image origin filenames and image labels which were already added with o_image().
288   * Allows to merge identical images
289   */
290  public  $imagelist = array();
291
292  /**
293   * whether the text passed in should be treated as Unicode or just local character set.
294   */
295  public  $isUnicode = false;
296
297  /**
298   * class constructor
299   * this will start a new document
300   * @var array array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero.
301   * @var boolean whether text will be treated as Unicode or not.
302   */
303  function  Cpdf ($pageSize = array(0, 0, 612, 792), $isUnicode = false, $fontcache = '', $tmp = '') {
304
305    $this->isUnicode = $isUnicode;
306
307    $this->fontcache = $fontcache;
308
309    $this->tmp = $tmp;
310
311    $this->newDocument($pageSize);
312
313
314    // also initialize the font families that are known about already
315    $this->setFontFamily('init');
316
317    //  $this->fileIdentifier = md5('xxxxxxxx'.time());
318
319
320  }
321
322
323  /**
324   * Document object methods (internal use only)
325   *
326   * There is about one object method for each type of object in the pdf document
327   * Each function has the same call list ($id,$action,$options).
328   * $id = the object ID of the object, or what it is to be if it is being created
329   * $action = a string specifying the action to be performed, though ALL must support:
330   *           'new' - create the object with the id $id
331   *           'out' - produce the output for the pdf object
332   * $options = optional, a string or array containing the various parameters for the object
333   *
334   * These, in conjunction with the output function are the ONLY way for output to be produced
335   * within the pdf 'file'.
336   */
337
338  /**
339   *destination object, used to specify the location for the user to jump to, presently on opening
340   */
341  function  o_destination($id, $action, $options = '') {
342
343    if  ($action != 'new') {
344
345      $o = & $this->objects[$id];
346    }
347
348    switch ($action) {
349
350    case  'new':
351
352      $this->objects[$id] = array('t'=>'destination', 'info'=>array());
353
354      $tmp =  '';
355
356      switch  ($options['type']) {
357
358      case  'XYZ':
359
360      case  'FitR':
361
362        $tmp =   ' '.$options['p3'].$tmp;
363
364      case  'FitH':
365
366      case  'FitV':
367
368      case  'FitBH':
369
370      case  'FitBV':
371
372        $tmp =   ' '.$options['p1'].' '.$options['p2'].$tmp;
373
374      case  'Fit':
375
376      case  'FitB':
377
378        $tmp =   $options['type'].$tmp;
379
380        $this->objects[$id]['info']['string'] = $tmp;
381
382        $this->objects[$id]['info']['page'] = $options['page'];
383      }
384
385      break;
386
387    case  'out':
388
389      $tmp =  $o['info'];
390
391      $res = "\n".$id." 0 obj\n".'['.$tmp['page'].' 0 R /'.$tmp['string']."]\nendobj";
392
393      return  $res;
394
395      break;
396    }
397  }
398
399
400  /**
401   * set the viewer preferences
402   */
403  function  o_viewerPreferences($id, $action, $options = '') {
404
405    if  ($action != 'new') {
406
407      $o = & $this->objects[$id];
408    }
409
410    switch  ($action) {
411
412    case  'new':
413
414      $this->objects[$id] = array('t'=>'viewerPreferences', 'info'=>array());
415
416      break;
417
418    case  'add':
419
420      foreach($options as  $k=>$v) {
421
422        switch  ($k) {
423
424        case  'HideToolbar':
425
426        case  'HideMenubar':
427
428        case  'HideWindowUI':
429
430        case  'FitWindow':
431
432        case  'CenterWindow':
433
434        case  'NonFullScreenPageMode':
435
436        case  'Direction':
437
438          $o['info'][$k] = $v;
439
440          break;
441        }
442      }
443
444      break;
445
446    case  'out':
447
448
449      $res = "\n".$id." 0 obj\n".'<< ';
450
451      foreach($o['info'] as  $k=>$v) {
452
453        $res.= "\n/".$k.' '.$v;
454      }
455
456      $res.= "\n>>\n";
457
458      return  $res;
459
460      break;
461    }
462  }
463
464
465  /**
466   * define the document catalog, the overall controller for the document
467   */
468  function  o_catalog($id, $action, $options = '') {
469
470    if  ($action != 'new') {
471
472      $o = & $this->objects[$id];
473    }
474
475    switch  ($action) {
476
477    case  'new':
478
479      $this->objects[$id] = array('t'=>'catalog', 'info'=>array());
480
481      $this->catalogId = $id;
482
483      break;
484
485    case  'outlines':
486
487    case  'pages':
488
489    case  'openHere':
490
491      $o['info'][$action] = $options;
492
493      break;
494
495    case  'viewerPreferences':
496
497      if  (!isset($o['info']['viewerPreferences'])) {
498
499        $this->numObj++;
500
501        $this->o_viewerPreferences($this->numObj, 'new');
502
503        $o['info']['viewerPreferences'] = $this->numObj;
504      }
505
506      $vp =  $o['info']['viewerPreferences'];
507
508      $this->o_viewerPreferences($vp, 'add', $options);
509
510      break;
511
512    case  'out':
513
514      $res = "\n".$id." 0 obj\n".'<< /Type /Catalog';
515
516      foreach($o['info'] as  $k=>$v) {
517
518        switch ($k) {
519
520        case  'outlines':
521
522          $res.= "\n".'/Outlines '.$v.' 0 R';
523
524          break;
525
526        case  'pages':
527
528          $res.= "\n".'/Pages '.$v.' 0 R';
529
530          break;
531
532        case  'viewerPreferences':
533
534          $res.= "\n".'/ViewerPreferences '.$o['info']['viewerPreferences'].' 0 R';
535
536          break;
537
538        case  'openHere':
539
540          $res.= "\n".'/OpenAction '.$o['info']['openHere'].' 0 R';
541
542          break;
543        }
544      }
545
546      $res.= " >>\nendobj";
547
548      return  $res;
549
550      break;
551    }
552  }
553
554
555  /**
556   * object which is a parent to the pages in the document
557   */
558  function  o_pages($id, $action, $options = '') {
559
560    if  ($action != 'new') {
561
562      $o = & $this->objects[$id];
563    }
564
565    switch  ($action) {
566
567    case  'new':
568
569      $this->objects[$id] = array('t'=>'pages', 'info'=>array());
570
571      $this->o_catalog($this->catalogId, 'pages', $id);
572
573      break;
574
575    case  'page':
576
577      if  (!is_array($options)) {
578
579        // then it will just be the id of the new page
580        $o['info']['pages'][] = $options;
581      } else {
582
583        // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative
584        // and pos is either 'before' or 'after', saying where this page will fit.
585        if  (isset($options['id']) &&  isset($options['rid']) &&  isset($options['pos'])) {
586
587          $i =  array_search($options['rid'], $o['info']['pages']);
588
589          if  (isset($o['info']['pages'][$i]) &&  $o['info']['pages'][$i] == $options['rid']) {
590
591            // then there is a match
592            // make a space
593            switch  ($options['pos']) {
594
595            case  'before':
596
597              $k =  $i;
598
599              break;
600
601            case  'after':
602
603              $k = $i+1;
604
605              break;
606
607            default:
608
609              $k = -1;
610
611              break;
612            }
613
614            if  ($k >= 0) {
615
616              for  ($j = count($o['info']['pages']) -1;$j >= $k;$j--) {
617
618                $o['info']['pages'][$j+1] = $o['info']['pages'][$j];
619              }
620
621              $o['info']['pages'][$k] = $options['id'];
622            }
623          }
624        }
625      }
626
627      break;
628
629    case  'procset':
630
631      $o['info']['procset'] = $options;
632
633      break;
634
635    case  'mediaBox':
636
637      $o['info']['mediaBox'] = $options;
638      // which should be an array of 4 numbers
639      break;
640
641    case  'font':
642
643      $o['info']['fonts'][] = array('objNum'=>$options['objNum'], 'fontNum'=>$options['fontNum']);
644
645      break;
646
647
648    case  'extGState':
649
650      $o['info']['extGStates'][] =  array('objNum' => $options['objNum'],  'stateNum' => $options['stateNum']);
651
652      break;
653
654
655    case  'xObject':
656
657      $o['info']['xObjects'][] = array('objNum'=>$options['objNum'], 'label'=>$options['label']);
658
659      break;
660
661    case  'out':
662
663      if  (count($o['info']['pages'])) {
664
665        $res = "\n".$id." 0 obj\n<< /Type /Pages\n/Kids [";
666
667        foreach($o['info']['pages'] as  $k=>$v) {
668
669          $res.= $v." 0 R\n";
670        }
671
672        $res.= "]\n/Count ".count($this->objects[$id]['info']['pages']);
673
674
675        if  ( (isset($o['info']['fonts']) &&  count($o['info']['fonts'])) ||
676              isset($o['info']['procset']) ||
677              (isset($o['info']['extGStates']) &&  count($o['info']['extGStates']))) {
678
679
680          $res.= "\n/Resources <<";
681
682          if  (isset($o['info']['procset'])) {
683
684            $res.= "\n/ProcSet ".$o['info']['procset']." 0 R";
685          }
686
687          if  (isset($o['info']['fonts']) &&  count($o['info']['fonts'])) {
688
689            $res.= "\n/Font << ";
690
691            foreach($o['info']['fonts'] as  $finfo) {
692
693              $res.= "\n/F".$finfo['fontNum']." ".$finfo['objNum']." 0 R";
694            }
695
696            $res.= "\n>>";
697          }
698
699          if  (isset($o['info']['xObjects']) &&  count($o['info']['xObjects'])) {
700
701            $res.= "\n/XObject << ";
702
703            foreach($o['info']['xObjects'] as  $finfo) {
704
705              $res.= "\n/".$finfo['label']." ".$finfo['objNum']." 0 R";
706            }
707
708            $res.= "\n>>";
709          }
710
711          if  ( isset($o['info']['extGStates']) &&  count($o['info']['extGStates'])) {
712
713            $res.=  "\n/ExtGState << ";
714
715            foreach ($o['info']['extGStates'] as  $gstate) {
716
717              $res.=  "\n/GS" . $gstate['stateNum'] . " " . $gstate['objNum'] . " 0 R";
718            }
719
720            $res.=  "\n>>";
721          }
722
723
724          $res.= "\n>>";
725
726          if  (isset($o['info']['mediaBox'])) {
727
728            $tmp = $o['info']['mediaBox'];
729
730            $res.= "\n/MediaBox [".sprintf('%.3F', $tmp[0]) .' '.sprintf('%.3F', $tmp[1]) .' '.sprintf('%.3F', $tmp[2]) .' '.sprintf('%.3F', $tmp[3]) .']';
731          }
732        }
733
734        $res.= "\n >>\nendobj";
735      } else {
736
737        $res = "\n".$id." 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj";
738      }
739
740      return  $res;
741
742      break;
743    }
744  }
745
746
747  /**
748   * define the outlines in the doc, empty for now
749   */
750  function  o_outlines($id, $action, $options = '') {
751
752    if  ($action != 'new') {
753
754      $o = & $this->objects[$id];
755    }
756
757    switch  ($action) {
758
759    case  'new':
760
761      $this->objects[$id] = array('t'=>'outlines', 'info'=>array('outlines'=>array()));
762
763      $this->o_catalog($this->catalogId, 'outlines', $id);
764
765      break;
766
767    case  'outline':
768
769      $o['info']['outlines'][] = $options;
770
771      break;
772
773    case  'out':
774
775      if  (count($o['info']['outlines'])) {
776
777        $res = "\n".$id." 0 obj\n<< /Type /Outlines /Kids [";
778
779        foreach($o['info']['outlines'] as  $k=>$v) {
780
781          $res.= $v." 0 R ";
782        }
783
784        $res.= "] /Count ".count($o['info']['outlines']) ." >>\nendobj";
785      } else {
786
787        $res = "\n".$id." 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj";
788      }
789
790      return  $res;
791
792      break;
793    }
794  }
795
796
797  /**
798   * an object to hold the font description
799   */
800  function  o_font($id, $action, $options = '') {
801
802    if  ($action != 'new') {
803
804      $o = & $this->objects[$id];
805    }
806
807    switch  ($action) {
808
809    case  'new':
810
811      $this->objects[$id] =  array('t' => 'font', 'info' => array('name' => $options['name'], 'fontFileName' => $options['fontFileName'], 'SubType' => 'Type1'));
812
813      $fontNum =  $this->numFonts;
814
815      $this->objects[$id]['info']['fontNum'] =  $fontNum;
816
817      // deal with the encoding and the differences
818      if  (isset($options['differences'])) {
819
820        // then we'll need an encoding dictionary
821        $this->numObj++;
822
823        $this->o_fontEncoding($this->numObj, 'new', $options);
824
825        $this->objects[$id]['info']['encodingDictionary'] =  $this->numObj;
826      } else  if  (isset($options['encoding'])) {
827
828        // we can specify encoding here
829        switch ($options['encoding']) {
830
831        case  'WinAnsiEncoding':
832
833        case  'MacRomanEncoding':
834
835        case  'MacExpertEncoding':
836
837          $this->objects[$id]['info']['encoding'] =  $options['encoding'];
838
839          break;
840
841        case  'none':
842
843          break;
844
845        default:
846
847          $this->objects[$id]['info']['encoding'] =  'WinAnsiEncoding';
848
849          break;
850        }
851      } else {
852
853        $this->objects[$id]['info']['encoding'] =  'WinAnsiEncoding';
854      }
855
856      if ($this->isUnicode) {
857
858        // For Unicode fonts, we need to incorporate font data into
859        // sub-sections that are linked from the primary font section.
860        // Look at o_fontGIDtoCID and o_fontDescendentCID functions
861        // for more informaiton.
862        //
863        // All of this code is adapted from the excellent changes made to
864        // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/)
865
866        $toUnicodeId = ++$this->numObj;
867        $this->o_contents($toUnicodeId, 'new', 'raw');
868        $this->objects[$id]['info']['toUnicode'] = $toUnicodeId;
869       
870        $stream =  "/CIDInit /ProcSet findresource begin\n";
871        $stream.=  "12 dict begin\n";
872        $stream.=  "begincmap\n";
873        $stream.=  "/CIDSystemInfo\n";
874        $stream.=  "<</Registry (Adobe)\n";
875        $stream.=  "/Ordering (UCS)\n";
876        $stream.=  "/Supplement 0\n";
877        $stream.=  ">> def\n";
878        $stream.=  "/CMapName /Adobe-Identity-UCS def\n";
879        $stream.=  "/CMapType 2 def\n";
880        $stream.=  "1 begincodespacerange\n";
881        $stream.=  "<0000> <FFFF>\n";
882        $stream.=  "endcodespacerange\n";
883        $stream.=  "1 beginbfrange\n";
884        $stream.=  "<0000> <FFFF> <0000>\n";
885        $stream.=  "endbfrange\n";
886        $stream.=  "endcmap\n";
887        $stream.=  "CMapName currentdict /CMap defineresource pop\n";
888        $stream.=  "end\n";
889        $stream.=  "end\n";
890
891        $res =   "<</Length " . mb_strlen($stream) . " >>\n";
892        $res .=  "stream\n" . $stream . "endstream";
893
894        $this->objects[$toUnicodeId]['c'] = $res;
895
896        $cidFontId = ++$this->numObj;
897        $this->o_fontDescendentCID($cidFontId, 'new', $options);
898        $this->objects[$id]['info']['cidFont'] = $cidFontId;
899      }
900     
901      // also tell the pages node about the new font
902      $this->o_pages($this->currentNode, 'font', array('fontNum' => $fontNum, 'objNum' => $id));
903
904      break;
905
906
907    case  'add':
908
909      foreach ($options as  $k => $v) {
910
911        switch  ($k) {
912
913        case  'BaseFont':
914
915          $o['info']['name'] =  $v;
916
917          break;
918
919        case  'FirstChar':
920
921        case  'LastChar':
922
923        case  'Widths':
924
925        case  'FontDescriptor':
926
927        case  'SubType':
928
929          $this->addMessage('o_font '.$k." : ".$v);
930
931          $o['info'][$k] =  $v;
932
933          break;
934        }
935      }
936
937      // pass values down to descendent font
938      if (isset($o['info']['cidFont'])) {
939
940        $this->o_fontDescendentCID($o['info']['cidFont'], 'add', $options);
941      }
942       
943      break;
944
945
946    case  'out':
947
948      if ($this->isUnicode) {
949
950        // For Unicode fonts, we need to incorporate font data into
951        // sub-sections that are linked from the primary font section.
952        // Look at o_fontGIDtoCID and o_fontDescendentCID functions
953        // for more informaiton.
954        //
955        // All of this code is adapted from the excellent changes made to
956        // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/)
957
958        $res =  "\n".$id." 0 obj\n<</Type /Font\n/Subtype /Type0\n";
959        $res.=  "/BaseFont /".$o['info']['name']."\n";
960
961        // The horizontal identity mapping for 2-byte CIDs; may be used
962        // with CIDFonts using any Registry, Ordering, and Supplement values.
963        $res.=  "/Encoding /Identity-H\n";
964        $res.=  "/DescendantFonts [".$o['info']['cidFont']." 0 R]\n";
965        $res.=  "/ToUnicode ".$o['info']['toUnicode']." 0 R\n";
966        $res.=  ">>\n";
967        $res.=  "endobj";
968
969      } else {
970      $res =  "\n".$id." 0 obj\n<< /Type /Font\n/Subtype /".$o['info']['SubType']."\n";
971
972      $res.=  "/Name /F".$o['info']['fontNum']."\n";
973
974      $res.=  "/BaseFont /".$o['info']['name']."\n";
975
976      if  (isset($o['info']['encodingDictionary'])) {
977
978        // then place a reference to the dictionary
979        $res.=  "/Encoding ".$o['info']['encodingDictionary']." 0 R\n";
980      } else  if  (isset($o['info']['encoding'])) {
981
982        // use the specified encoding
983        $res.=  "/Encoding /".$o['info']['encoding']."\n";
984      }
985
986      if  (isset($o['info']['FirstChar'])) {
987
988        $res.=  "/FirstChar ".$o['info']['FirstChar']."\n";
989      }
990
991      if  (isset($o['info']['LastChar'])) {
992
993        $res.=  "/LastChar ".$o['info']['LastChar']."\n";
994      }
995
996      if  (isset($o['info']['Widths'])) {
997
998        $res.=  "/Widths ".$o['info']['Widths']." 0 R\n";
999      }
1000
1001      if  (isset($o['info']['FontDescriptor'])) {
1002
1003        $res.=  "/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n";
1004      }
1005
1006        $res.=  ">>\n";
1007        $res.=  "endobj";
1008       
1009      }
1010
1011      return  $res;
1012
1013      break;
1014    }
1015  }
1016
1017
1018  /**
1019   * a font descriptor, needed for including additional fonts
1020   */
1021  function  o_fontDescriptor($id, $action, $options = '') {
1022
1023    if  ($action != 'new') {
1024
1025      $o = & $this->objects[$id];
1026    }
1027
1028    switch  ($action) {
1029
1030    case  'new':
1031
1032      $this->objects[$id] = array('t'=>'fontDescriptor', 'info'=>$options);
1033
1034      break;
1035
1036    case  'out':
1037
1038      $res = "\n".$id." 0 obj\n<< /Type /FontDescriptor\n";
1039
1040      foreach ($o['info'] as  $label => $value) {
1041
1042        switch  ($label) {
1043
1044        case  'Ascent':
1045
1046        case  'CapHeight':
1047
1048        case  'Descent':
1049
1050        case  'Flags':
1051
1052        case  'ItalicAngle':
1053
1054        case  'StemV':
1055
1056        case  'AvgWidth':
1057
1058        case  'Leading':
1059
1060        case  'MaxWidth':
1061
1062        case  'MissingWidth':
1063
1064        case  'StemH':
1065
1066        case  'XHeight':
1067
1068        case  'CharSet':
1069
1070          if  (mb_strlen($value)) {
1071
1072            $res.= '/'.$label.' '.$value."\n";
1073          }
1074
1075          break;
1076
1077        case  'FontFile':
1078
1079        case  'FontFile2':
1080
1081        case  'FontFile3':
1082
1083          $res.= '/'.$label.' '.$value." 0 R\n";
1084
1085          break;
1086
1087        case  'FontBBox':
1088
1089          $res.= '/'.$label.' ['.$value[0].' '.$value[1].' '.$value[2].' '.$value[3]."]\n";
1090
1091          break;
1092
1093        case  'FontName':
1094
1095          $res.= '/'.$label.' /'.$value."\n";
1096
1097          break;
1098        }
1099      }
1100
1101      $res.= ">>\nendobj";
1102
1103      return  $res;
1104
1105      break;
1106    }
1107  }
1108
1109
1110  /**
1111   * the font encoding
1112   */
1113  function  o_fontEncoding($id, $action, $options = '') {
1114
1115    if  ($action != 'new') {
1116
1117      $o = & $this->objects[$id];
1118    }
1119
1120    switch  ($action) {
1121
1122    case  'new':
1123
1124      // the options array should contain 'differences' and maybe 'encoding'
1125      $this->objects[$id] = array('t'=>'fontEncoding', 'info'=>$options);
1126
1127      break;
1128
1129    case  'out':
1130
1131      $res = "\n".$id." 0 obj\n<< /Type /Encoding\n";
1132
1133      if  (!isset($o['info']['encoding'])) {
1134
1135        $o['info']['encoding'] = 'WinAnsiEncoding';
1136      }
1137
1138      if  ($o['info']['encoding'] != 'none') {
1139
1140        $res.= "/BaseEncoding /".$o['info']['encoding']."\n";
1141      }
1142
1143      $res.= "/Differences \n[";
1144
1145      $onum = -100;
1146
1147      foreach($o['info']['differences'] as  $num=>$label) {
1148
1149        if  ($num != $onum+1) {
1150
1151          // we cannot make use of consecutive numbering
1152          $res.=  "\n".$num." /".$label;
1153        } else {
1154
1155          $res.=  " /".$label;
1156        }
1157
1158        $onum = $num;
1159      }
1160
1161      $res.= "\n]\n>>\nendobj";
1162
1163      return  $res;
1164
1165      break;
1166    }
1167  }
1168
1169
1170  /**
1171   * a descendent cid font,  needed for unicode fonts
1172   */
1173  function  o_fontDescendentCID($id, $action, $options = '') {
1174
1175    if  ($action != 'new') {
1176
1177      $o = & $this->objects[$id];
1178    }
1179
1180    switch  ($action) {
1181
1182    case  'new':
1183
1184      $this->objects[$id] =  array('t'=>'fontDescendentCID', 'info'=>$options);
1185
1186      // we need a CID system info section
1187      $cidSystemInfoId = ++$this->numObj;
1188      $this->o_contents($cidSystemInfoId, 'new', 'raw');
1189      $this->objects[$id]['info']['cidSystemInfo'] = $cidSystemInfoId;
1190      $res=   "<</Registry (Adobe)\n"; // A string identifying an issuer of character collections
1191      $res.=  "/Ordering (UCS)\n"; // A string that uniquely names a character collection issued by a specific registry
1192      $res.=  "/Supplement 0\n"; // The supplement number of the character collection.
1193      $res.=  ">>";
1194      $this->objects[$cidSystemInfoId]['c'] = $res;
1195
1196      // and a CID to GID map
1197      $cidToGidMapId = ++$this->numObj;
1198      $this->o_fontGIDtoCIDMap($cidToGidMapId, 'new', $options);
1199      $this->objects[$id]['info']['cidToGidMap'] = $cidToGidMapId;
1200     
1201      break;
1202
1203    case  'add':
1204
1205      foreach ($options as  $k => $v) {
1206        switch  ($k) {
1207        case  'BaseFont':
1208          $o['info']['name'] =  $v;
1209          break;
1210
1211        case  'FirstChar':
1212        case  'LastChar':
1213        case  'MissingWidth':
1214        case  'FontDescriptor':
1215        case  'SubType':
1216          $this->addMessage('o_fontDescendentCID '.$k." : ".$v);
1217          $o['info'][$k] =  $v;
1218          break;
1219        }
1220      }
1221
1222      // pass values down to cid to gid map
1223      $this->o_fontGIDtoCIDMap($o['info']['cidToGidMap'], 'add', $options);
1224     
1225      break;
1226
1227    case  'out':
1228
1229      $res =  "\n".$id." 0 obj\n";
1230      $res.=  "<</Type /Font\n";
1231      $res.=  "/Subtype /CIDFontType2\n";
1232      $res.=  "/BaseFont /".$o['info']['name']."\n";
1233      $res.=  "/CIDSystemInfo ".$o['info']['cidSystemInfo']." 0 R\n";
1234//      if  (isset($o['info']['FirstChar'])) {
1235//
1236//        $res.=  "/FirstChar ".$o['info']['FirstChar']."\n";
1237//      }
1238
1239//      if  (isset($o['info']['LastChar'])) {
1240//
1241//        $res.=  "/LastChar ".$o['info']['LastChar']."\n";
1242//      }
1243      if  (isset($o['info']['FontDescriptor'])) {
1244
1245        $res.=  "/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n";
1246      }
1247
1248      if  (isset($o['info']['MissingWidth'])) {
1249        $res.=  "/DW ".$o['info']['MissingWidth']."\n";
1250      }
1251
1252      if  (isset($o['info']['fontFileName']) && isset($this->fonts[$o['info']['fontFileName']]['CIDWidths'])) {
1253        $cid_widths = &$this->fonts[$o['info']['fontFileName']]['CIDWidths'];
1254        $w = '';
1255        foreach ($cid_widths as $cid => $width) {
1256          $w .= $cid.' ['.$width.'] ';
1257        }
1258        $res.=  "/W [".$w."]\n";
1259      }
1260
1261      $res.=  "/CIDToGIDMap ".$o['info']['cidToGidMap']." 0 R\n";
1262      $res.=  ">>\n";
1263      $res.=  "endobj";
1264
1265      return  $res;
1266
1267      break;
1268    }
1269  }
1270 
1271
1272  /**
1273   * a font glyph to character map,  needed for unicode fonts
1274   */
1275  function  o_fontGIDtoCIDMap($id, $action, $options = '') {
1276
1277    if  ($action != 'new') {
1278
1279      $o = & $this->objects[$id];
1280    }
1281
1282    switch  ($action) {
1283
1284    case  'new':
1285
1286      $this->objects[$id] =  array('t'=>'fontGIDtoCIDMap', 'info'=>$options);
1287
1288      break;
1289
1290    case  'out':
1291       
1292      $res = "\n".$id." 0 obj\n";
1293      $tmp = $this->fonts[$o['info']['fontFileName']]['CIDtoGID'] = base64_decode($this->fonts[$o['info']['fontFileName']]['CIDtoGID']);
1294      $compressed = isset($this->fonts[$o['info']['fontFileName']]['CIDtoGID_Compressed']) &&
1295                    $this->fonts[$o['info']['fontFileName']]['CIDtoGID_Compressed'];
1296
1297      if  (!$compressed && isset($o['raw'])) {
1298
1299        $res.= $tmp;
1300      } else {
1301
1302        $res.=  "<<";
1303
1304        if  (!$compressed && function_exists('gzcompress') &&  $this->options['compression']) {
1305
1306          // then implement ZLIB based compression on this content stream
1307          $compressed = true;
1308
1309          $tmp =  gzcompress($tmp,  6);
1310        }
1311        if ($compressed) {
1312
1313          $res.= "\n/Filter /FlateDecode";
1314        }
1315
1316        $res.= "\n/Length ".mb_strlen($tmp) .">>\nstream\n".$tmp."\nendstream";
1317      }
1318
1319      $res.= "\nendobj";
1320
1321      return  $res;
1322
1323      break;
1324    }
1325  }
1326 
1327
1328  /**
1329   * the document procset, solves some problems with printing to old PS printers
1330   */
1331  function  o_procset($id, $action, $options = '') {
1332
1333    if  ($action != 'new') {
1334
1335      $o = & $this->objects[$id];
1336    }
1337
1338    switch  ($action) {
1339
1340    case  'new':
1341
1342      $this->objects[$id] = array('t'=>'procset', 'info'=>array('PDF'=>1, 'Text'=>1));
1343
1344      $this->o_pages($this->currentNode, 'procset', $id);
1345
1346      $this->procsetObjectId = $id;
1347
1348      break;
1349
1350    case  'add':
1351
1352      // this is to add new items to the procset list, despite the fact that this is considered
1353      // obselete, the items are required for printing to some postscript printers
1354      switch  ($options) {
1355
1356      case  'ImageB':
1357
1358      case  'ImageC':
1359
1360      case  'ImageI':
1361
1362        $o['info'][$options] = 1;
1363
1364        break;
1365      }
1366
1367      break;
1368
1369    case  'out':
1370
1371      $res = "\n".$id." 0 obj\n[";
1372
1373      foreach ($o['info'] as  $label=>$val) {
1374
1375        $res.= '/'.$label.' ';
1376      }
1377
1378      $res.= "]\nendobj";
1379
1380      return  $res;
1381
1382      break;
1383    }
1384  }
1385
1386
1387  /**
1388   * define the document information
1389   */
1390  function  o_info($id, $action, $options = '') {
1391
1392    if  ($action != 'new') {
1393
1394      $o = & $this->objects[$id];
1395    }
1396
1397    switch  ($action) {
1398
1399    case  'new':
1400
1401      $this->infoObject = $id;
1402
1403      $date = 'D:'.@date('Ymd');
1404
1405      $this->objects[$id] = array('t'=>'info', 'info'=>array('Creator'=>'R and OS php pdf writer, http://www.ros.co.nz', 'CreationDate'=>$date));
1406
1407      break;
1408
1409    case  'Title':
1410
1411    case  'Author':
1412
1413    case  'Subject':
1414
1415    case  'Keywords':
1416
1417    case  'Creator':
1418
1419    case  'Producer':
1420
1421    case  'CreationDate':
1422
1423    case  'ModDate':
1424
1425    case  'Trapped':
1426
1427      $o['info'][$action] = $options;
1428
1429      break;
1430
1431    case  'out':
1432
1433      if  ($this->encrypted) {
1434
1435        $this->encryptInit($id);
1436      }
1437
1438      $res = "\n".$id." 0 obj\n<<\n";
1439
1440      foreach ($o['info'] as  $k=>$v) {
1441
1442        $res.= '/'.$k.' (';
1443
1444        // dates must be outputted as-is, without Unicode transformations
1445        $raw = ($k == 'CreationDate' || $k == 'ModDate');
1446        $c = $v;
1447
1448        if  ($this->encrypted) {
1449
1450          $c = $this->ARC4($c);
1451        }
1452
1453        $res.= ($raw) ? $c : $this->filterText($c);
1454
1455        $res.= ")\n";
1456      }
1457
1458      $res.= ">>\nendobj";
1459
1460      return  $res;
1461
1462      break;
1463    }
1464  }
1465
1466
1467  /**
1468   * an action object, used to link to URLS initially
1469   */
1470  function  o_action($id, $action, $options = '') {
1471
1472    if  ($action != 'new') {
1473
1474      $o = & $this->objects[$id];
1475    }
1476
1477    switch  ($action) {
1478
1479    case  'new':
1480
1481      if  (is_array($options)) {
1482
1483        $this->objects[$id] = array('t'=>'action', 'info'=>$options, 'type'=>$options['type']);
1484      } else {
1485
1486        // then assume a URI action
1487        $this->objects[$id] = array('t'=>'action', 'info'=>$options, 'type'=>'URI');
1488      }
1489
1490      break;
1491
1492    case  'out':
1493
1494      if  ($this->encrypted) {
1495
1496        $this->encryptInit($id);
1497      }
1498
1499      $res = "\n".$id." 0 obj\n<< /Type /Action";
1500
1501      switch ($o['type']) {
1502
1503      case  'ilink':
1504
1505        // there will be an 'label' setting, this is the name of the destination
1506        $res.= "\n/S /GoTo\n/D ".$this->destinations[(string)$o['info']['label']]." 0 R";
1507
1508        break;
1509
1510      case  'URI':
1511
1512        $res.= "\n/S /URI\n/URI (";
1513
1514        if  ($this->encrypted) {
1515
1516          $res.= $this->filterText($this->ARC4($o['info']));
1517        } else {
1518
1519          $res.= $this->filterText($o['info']);
1520        }
1521
1522        $res.= ")";
1523
1524        break;
1525      }
1526
1527      $res.= "\n>>\nendobj";
1528
1529      return  $res;
1530
1531      break;
1532    }
1533  }
1534
1535
1536  /**
1537   * an annotation object, this will add an annotation to the current page.
1538   * initially will support just link annotations
1539   */
1540  function  o_annotation($id, $action, $options = '') {
1541
1542    if  ($action != 'new') {
1543
1544      $o = & $this->objects[$id];
1545    }
1546
1547    switch  ($action) {
1548
1549    case  'new':
1550
1551      // add the annotation to the current page
1552      $pageId =  $this->currentPage;
1553
1554      $this->o_page($pageId, 'annot', $id);
1555
1556      // and add the action object which is going to be required
1557      switch ($options['type']) {
1558
1559      case  'link':
1560
1561        $this->objects[$id] = array('t'=>'annotation', 'info'=>$options);
1562
1563        $this->numObj++;
1564
1565        $this->o_action($this->numObj, 'new', $options['url']);
1566
1567        $this->objects[$id]['info']['actionId'] = $this->numObj;
1568
1569        break;
1570
1571      case  'ilink':
1572
1573        // this is to a named internal link
1574        $label =  $options['label'];
1575
1576        $this->objects[$id] = array('t'=>'annotation', 'info'=>$options);
1577
1578        $this->numObj++;
1579
1580        $this->o_action($this->numObj, 'new', array('type'=>'ilink', 'label'=>$label));
1581
1582        $this->objects[$id]['info']['actionId'] = $this->numObj;
1583
1584        break;
1585      }
1586
1587      break;
1588
1589    case  'out':
1590
1591      $res = "\n".$id." 0 obj\n<< /Type /Annot";
1592
1593      switch ($o['info']['type']) {
1594
1595      case  'link':
1596
1597      case  'ilink':
1598
1599        $res.=  "\n/Subtype /Link";
1600
1601        break;
1602      }
1603
1604      $res.= "\n/A ".$o['info']['actionId']." 0 R";
1605
1606      $res.= "\n/Border [0 0 0]";
1607
1608      $res.= "\n/H /I";
1609
1610      $res.= "\n/Rect [ ";
1611
1612      foreach($o['info']['rect'] as  $v) {
1613
1614        $res.=  sprintf("%.4F ", $v);
1615      }
1616
1617      $res.= "]";
1618
1619      $res.= "\n>>\nendobj";
1620
1621      return  $res;
1622
1623      break;
1624    }
1625  }
1626
1627
1628  /**
1629   * a page object, it also creates a contents object to hold its contents
1630   */
1631  function  o_page($id, $action, $options = '') {
1632
1633    if  ($action != 'new') {
1634
1635      $o = & $this->objects[$id];
1636    }
1637
1638    switch  ($action) {
1639
1640    case  'new':
1641
1642      $this->numPages++;
1643
1644      $this->objects[$id] = array('t'=>'page', 'info'=>array('parent'=>$this->currentNode, 'pageNum'=>$this->numPages));
1645
1646      if  (is_array($options)) {
1647
1648        // then this must be a page insertion, array shoudl contain 'rid','pos'=[before|after]
1649        $options['id'] = $id;
1650
1651        $this->o_pages($this->currentNode, 'page', $options);
1652      } else {
1653
1654        $this->o_pages($this->currentNode, 'page', $id);
1655      }
1656
1657      $this->currentPage = $id;
1658
1659      //make a contents object to go with this page
1660      $this->numObj++;
1661
1662      $this->o_contents($this->numObj, 'new', $id);
1663
1664      $this->currentContents = $this->numObj;
1665
1666      $this->objects[$id]['info']['contents'] = array();
1667
1668      $this->objects[$id]['info']['contents'][] = $this->numObj;
1669
1670      $match =  ($this->numPages%2 ?  'odd' :  'even');
1671
1672      foreach($this->addLooseObjects as  $oId=>$target) {
1673
1674        if  ($target == 'all' ||  $match == $target) {
1675
1676          $this->objects[$id]['info']['contents'][] = $oId;
1677        }
1678      }
1679
1680      break;
1681
1682    case  'content':
1683
1684      $o['info']['contents'][] = $options;
1685
1686      break;
1687
1688    case  'annot':
1689
1690      // add an annotation to this page
1691      if  (!isset($o['info']['annot'])) {
1692
1693        $o['info']['annot'] = array();
1694      }
1695
1696      // $options should contain the id of the annotation dictionary
1697      $o['info']['annot'][] = $options;
1698
1699      break;
1700
1701    case  'out':
1702
1703      $res = "\n".$id." 0 obj\n<< /Type /Page";
1704
1705      $res.= "\n/Parent ".$o['info']['parent']." 0 R";
1706
1707      if  (isset($o['info']['annot'])) {
1708
1709        $res.= "\n/Annots [";
1710
1711        foreach($o['info']['annot'] as  $aId) {
1712
1713          $res.= " ".$aId." 0 R";
1714        }
1715
1716        $res.= " ]";
1717      }
1718
1719      $count =  count($o['info']['contents']);
1720
1721      if  ($count == 1) {
1722
1723        $res.= "\n/Contents ".$o['info']['contents'][0]." 0 R";
1724      } else  if  ($count>1) {
1725
1726        $res.= "\n/Contents [\n";
1727
1728        // reverse the page contents so added objects are below normal content
1729        //foreach (array_reverse($o['info']['contents']) as  $cId) {
1730
1731        // Back to normal now that I've got transparency working --Benj
1732        foreach ($o['info']['contents'] as  $cId) {
1733          $res.= $cId." 0 R\n";
1734        }
1735
1736        $res.= "]";
1737      }
1738
1739      $res.= "\n>>\nendobj";
1740
1741      return  $res;
1742
1743      break;
1744    }
1745  }
1746
1747
1748  /**
1749   * the contents objects hold all of the content which appears on pages
1750   */
1751  function  o_contents($id, $action, $options = '') {
1752
1753    if  ($action != 'new') {
1754
1755      $o = & $this->objects[$id];
1756    }
1757
1758    switch  ($action) {
1759
1760    case  'new':
1761
1762      $this->objects[$id] = array('t'=>'contents', 'c'=>'', 'info'=>array());
1763
1764      if  (mb_strlen($options) &&  intval($options)) {
1765
1766        // then this contents is the primary for a page
1767        $this->objects[$id]['onPage'] = $options;
1768      } else  if  ($options == 'raw') {
1769
1770        // then this page contains some other type of system object
1771        $this->objects[$id]['raw'] = 1;
1772      }
1773
1774      break;
1775
1776    case  'add':
1777
1778      // add more options to the decleration
1779      foreach ($options as  $k=>$v) {
1780
1781        $o['info'][$k] = $v;
1782      }
1783
1784    case  'out':
1785      $tmp = $o['c'];
1786
1787      $res =  "\n".$id." 0 obj\n";
1788
1789      if  (isset($this->objects[$id]['raw'])) {
1790
1791        $res.= $tmp;
1792      } else {
1793
1794        $res.=  "<<";
1795
1796        if  (function_exists('gzcompress') &&  $this->options['compression']) {
1797
1798          // then implement ZLIB based compression on this content stream
1799          $res.= " /Filter /FlateDecode";
1800
1801          $tmp =  gzcompress($tmp,  6);
1802        }
1803
1804        if  ($this->encrypted) {
1805
1806          $this->encryptInit($id);
1807
1808          $tmp =  $this->ARC4($tmp);
1809        }
1810
1811        foreach($o['info'] as  $k=>$v) {
1812
1813          $res.=  "\n/".$k.' '.$v;
1814        }
1815
1816        $res.= "\n/Length ".mb_strlen($tmp) ." >>\nstream\n".$tmp."\nendstream";
1817      }
1818
1819      $res.= "\nendobj";
1820
1821      return  $res;
1822
1823      break;
1824    }
1825  }
1826
1827
1828  /**
1829   * an image object, will be an XObject in the document, includes description and data
1830   */
1831  function  o_image($id, $action, $options = '') {
1832
1833    if  ($action != 'new') {
1834      $o = & $this->objects[$id];
1835    }
1836
1837    switch ($action) {
1838
1839    case  'new':
1840
1841      // make the new object
1842      $this->objects[$id] = array('t'=>'image', 'data'=>&$options['data'], 'info'=>array());
1843
1844      $this->objects[$id]['info']['Type'] = '/XObject';
1845
1846      $this->objects[$id]['info']['Subtype'] = '/Image';
1847
1848      $this->objects[$id]['info']['Width'] = $options['iw'];
1849
1850      $this->objects[$id]['info']['Height'] = $options['ih'];
1851
1852      if  (!isset($options['type']) ||  $options['type'] == 'jpg') {
1853
1854        if  (!isset($options['channels'])) {
1855
1856          $options['channels'] = 3;
1857        }
1858
1859        switch ($options['channels']) {
1860
1861        case  1:
1862
1863          $this->objects[$id]['info']['ColorSpace'] = '/DeviceGray';
1864
1865          break;
1866
1867        default:
1868
1869          $this->objects[$id]['info']['ColorSpace'] = '/DeviceRGB';
1870
1871          break;
1872        }
1873
1874        $this->objects[$id]['info']['Filter'] = '/DCTDecode';
1875
1876        $this->objects[$id]['info']['BitsPerComponent'] = 8;
1877      } else  if  ($options['type'] == 'png') {
1878       
1879        $this->objects[$id]['info']['Filter'] = '/FlateDecode';
1880
1881        $this->objects[$id]['info']['DecodeParms'] = '<< /Predictor 15 /Colors '.$options['ncolor'].' /Columns '.$options['iw'].' /BitsPerComponent '.$options['bitsPerComponent'].'>>';
1882        if  (mb_strlen($options['pdata'])) {
1883
1884          $tmp =  ' [ /Indexed /DeviceRGB '.(mb_strlen($options['pdata']) /3-1) .' ';
1885
1886          $this->numObj++;
1887
1888          $this->o_contents($this->numObj, 'new');
1889
1890          $this->objects[$this->numObj]['c'] = $options['pdata'];
1891
1892          $tmp.= $this->numObj.' 0 R';
1893
1894          $tmp.= ' ]';
1895
1896          $this->objects[$id]['info']['ColorSpace'] =  $tmp;
1897
1898          if  (isset($options['transparency'])) {
1899
1900            switch ($options['transparency']['type']) {
1901
1902            case  'indexed':
1903
1904              $tmp = ' [ '.$options['transparency']['data'].' '.$options['transparency']['data'].'] ';
1905
1906              $this->objects[$id]['info']['Mask'] =  $tmp;
1907
1908              break;
1909
1910            case 'color-key':
1911              $tmp = ' [ '.
1912                $options['transparency']['r'] . ' ' . $options['transparency']['r'] .
1913                $options['transparency']['g'] . ' ' . $options['transparency']['g'] .
1914                $options['transparency']['b'] . ' ' . $options['transparency']['b'] .
1915                ' ] ';
1916              $this->objects[$id]['info']['Mask'] = $tmp;
1917              pre_r($tmp);
1918              break;
1919             
1920            }
1921          }
1922        } else {
1923
1924          if  (isset($options['transparency'])) {
1925
1926            switch ($options['transparency']['type']) {
1927
1928            case  'indexed':
1929
1930              $tmp = ' [ '.$options['transparency']['data'].' '.$options['transparency']['data'].'] ';
1931
1932              $this->objects[$id]['info']['Mask'] =  $tmp;
1933
1934              break;
1935
1936            case 'color-key':
1937              $tmp = ' [ '.
1938                $options['transparency']['r'] . ' ' . $options['transparency']['r'] . ' ' .
1939                $options['transparency']['g'] . ' ' . $options['transparency']['g'] . ' ' .
1940                $options['transparency']['b'] . ' ' . $options['transparency']['b'] .
1941                ' ] ';
1942              $this->objects[$id]['info']['Mask'] = $tmp;
1943              break;
1944             
1945            }
1946          }
1947          $this->objects[$id]['info']['ColorSpace'] = '/'.$options['color'];
1948        }
1949
1950        $this->objects[$id]['info']['BitsPerComponent'] = $options['bitsPerComponent'];
1951      }
1952
1953      // assign it a place in the named resource dictionary as an external object, according to
1954      // the label passed in with it.
1955      $this->o_pages($this->currentNode, 'xObject', array('label'=>$options['label'], 'objNum'=>$id));
1956
1957      // also make sure that we have the right procset object for it.
1958      $this->o_procset($this->procsetObjectId, 'add', 'ImageC');
1959
1960      break;
1961
1962    case  'out':
1963
1964      $tmp = &$o['data'];
1965
1966      $res =  "\n".$id." 0 obj\n<<";
1967
1968      foreach($o['info'] as  $k=>$v) {
1969
1970        $res.= "\n/".$k.' '.$v;
1971      }
1972
1973      if  ($this->encrypted) {
1974
1975        $this->encryptInit($id);
1976
1977        $tmp =  $this->ARC4($tmp);
1978      }
1979
1980      $res.= "\n/Length ".mb_strlen($tmp, '8bit') .">>\nstream\n".$tmp."\nendstream\nendobj";
1981
1982      return  $res;
1983
1984      break;
1985    }
1986  }
1987
1988
1989  /**
1990   * graphics state object
1991   */
1992  function  o_extGState($id,  $action,  $options = "") {
1993
1994    static  $valid_params =  array("LW",  "LC",  "LC",  "LJ",  "ML",
1995                                   "D",  "RI",  "OP",  "op",  "OPM",
1996                                   "Font",  "BG",  "BG2",  "UCR",
1997                                   "TR",  "TR2",  "HT",  "FL",
1998                                   "SM",  "SA",  "BM",  "SMask",
1999                                   "CA",  "ca",  "AIS",  "TK");
2000
2001    if  ( $action !=  "new") {
2002      $o = & $this->objects[$id];
2003    }
2004
2005    switch  ($action) {
2006
2007    case  "new":
2008      $this->objects[$id] =  array('t' => 'extGState',  'info' => $options);
2009
2010      // Tell the pages about the new resource
2011      $this->numStates++;
2012      $this->o_pages($this->currentNode,  'extGState',  array("objNum" => $id,  "stateNum" => $this->numStates));
2013      break;
2014
2015
2016    case  "out":
2017      $res =
2018        "\n" . $id . " 0 obj\n".
2019        "<< /Type /ExtGState\n";
2020
2021      foreach ($o["info"] as  $parameter => $value) {
2022        if  ( !in_array($parameter,  $valid_params))
2023          continue;
2024        $res.=  "/$parameter $value\n";
2025      }
2026
2027      $res.=
2028        ">>\n".
2029        "endobj";
2030
2031      return  $res;
2032    }
2033  }
2034
2035
2036  /**
2037   * encryption object.
2038   */
2039  function  o_encryption($id, $action, $options = '') {
2040
2041    if  ($action != 'new') {
2042
2043      $o = & $this->objects[$id];
2044    }
2045
2046    switch ($action) {
2047
2048    case  'new':
2049
2050      // make the new object
2051      $this->objects[$id] = array('t'=>'encryption', 'info'=>$options);
2052
2053      $this->arc4_objnum = $id;
2054
2055      // figure out the additional paramaters required
2056      $pad =  chr(0x28) .chr(0xBF) .chr(0x4E) .chr(0x5E) .chr(0x4E) .chr(0x75) .chr(0x8A) .chr(0x41) .chr(0x64) .chr(0x00) .chr(0x4E) .chr(0x56) .chr(0xFF) .chr(0xFA) .chr(0x01) .chr(0x08) .chr(0x2E) .chr(0x2E) .chr(0x00) .chr(0xB6) .chr(0xD0) .chr(0x68) .chr(0x3E) .chr(0x80) .chr(0x2F) .chr(0x0C) .chr(0xA9) .chr(0xFE) .chr(0x64) .chr(0x53) .chr(0x69) .chr(0x7A);
2057
2058      $len =  mb_strlen($options['owner']);
2059
2060      if  ($len>32) {
2061
2062        $owner =  substr($options['owner'], 0, 32);
2063      } else  if  ($len<32) {
2064
2065        $owner =  $options['owner'].substr($pad, 0, 32-$len);
2066      } else {
2067
2068        $owner =  $options['owner'];
2069      }
2070
2071      $len =  mb_strlen($options['user']);
2072
2073      if  ($len>32) {
2074
2075        $user =  substr($options['user'], 0, 32);
2076      } else  if  ($len<32) {
2077
2078        $user =  $options['user'].substr($pad, 0, 32-$len);
2079      } else {
2080
2081        $user =  $options['user'];
2082      }
2083
2084      $tmp =  $this->md5_16($owner);
2085
2086      $okey =  substr($tmp, 0, 5);
2087
2088      $this->ARC4_init($okey);
2089
2090      $ovalue = $this->ARC4($user);
2091
2092      $this->objects[$id]['info']['O'] = $ovalue;
2093
2094      // now make the u value, phew.
2095      $tmp =  $this->md5_16($user.$ovalue.chr($options['p']) .chr(255) .chr(255) .chr(255) .$this->fileIdentifier);
2096
2097      $ukey =  substr($tmp, 0, 5);
2098
2099
2100      $this->ARC4_init($ukey);
2101
2102      $this->encryptionKey =  $ukey;
2103
2104      $this->encrypted = 1;
2105
2106      $uvalue = $this->ARC4($pad);
2107
2108
2109      $this->objects[$id]['info']['U'] = $uvalue;
2110
2111      $this->encryptionKey = $ukey;
2112
2113
2114      // initialize the arc4 array
2115      break;
2116
2117    case  'out':
2118
2119      $res =  "\n".$id." 0 obj\n<<";
2120
2121      $res.= "\n/Filter /Standard";
2122
2123      $res.= "\n/V 1";
2124
2125      $res.= "\n/R 2";
2126
2127      $res.= "\n/O (".$this->filterText($o['info']['O']) .')';
2128
2129      $res.= "\n/U (".$this->filterText($o['info']['U']) .')';
2130
2131      // and the p-value needs to be converted to account for the twos-complement approach
2132      $o['info']['p'] =  (($o['info']['p']^255) +1) *-1;
2133
2134      $res.= "\n/P ".($o['info']['p']);
2135
2136      $res.= "\n>>\nendobj";
2137
2138
2139      return  $res;
2140
2141      break;
2142    }
2143  }
2144
2145
2146  /**
2147   * ARC4 functions
2148   * A series of function to implement ARC4 encoding in PHP
2149   */
2150
2151  /**
2152   * calculate the 16 byte version of the 128 bit md5 digest of the string
2153   */
2154  function  md5_16($string) {
2155
2156    $tmp =  md5($string);
2157
2158    $out = '';
2159
2160    for  ($i = 0;$i <= 30;$i = $i+2) {
2161
2162      $out.= chr(hexdec(substr($tmp, $i, 2)));
2163    }
2164
2165    return  $out;
2166  }
2167
2168
2169  /**
2170   * initialize the encryption for processing a particular object
2171   */
2172  function  encryptInit($id) {
2173
2174    $tmp =  $this->encryptionKey;
2175
2176    $hex =  dechex($id);
2177
2178    if  (mb_strlen($hex) <6) {
2179
2180      $hex =  substr('000000', 0, 6-mb_strlen($hex)) .$hex;
2181    }
2182
2183    $tmp.=  chr(hexdec(substr($hex, 4, 2))) .chr(hexdec(substr($hex, 2, 2))) .chr(hexdec(substr($hex, 0, 2))) .chr(0) .chr(0);
2184
2185    $key =  $this->md5_16($tmp);
2186
2187    $this->ARC4_init(substr($key, 0, 10));
2188  }
2189
2190
2191  /**
2192   * initialize the ARC4 encryption
2193   */
2194  function  ARC4_init($key = '') {
2195
2196    $this->arc4 =  '';
2197
2198    // setup the control array
2199    if  (mb_strlen($key) == 0) {
2200
2201      return;
2202    }
2203
2204    $k =  '';
2205
2206    while (mb_strlen($k) <256) {
2207
2208      $k.= $key;
2209    }
2210
2211    $k = substr($k, 0, 256);
2212
2213    for  ($i = 0;$i<256;++$i) {
2214
2215      $this->arc4.=  chr($i);
2216    }
2217
2218    $j = 0;
2219
2220    for  ($i = 0;$i<256;++$i) {
2221
2222      $t =  $this->arc4[$i];
2223
2224      $j =  ($j + ord($t)  + ord($k[$i])) %256;
2225
2226      $this->arc4[$i] = $this->arc4[$j];
2227
2228      $this->arc4[$j] = $t;
2229    }
2230  }
2231
2232
2233  /**
2234   * ARC4 encrypt a text string
2235   */
2236  function  ARC4($text) {
2237
2238    $len = mb_strlen($text);
2239
2240    $a = 0;
2241
2242    $b = 0;
2243
2244    $c =  $this->arc4;
2245
2246    $out = '';
2247
2248    for  ($i = 0;$i<$len;++$i) {
2249
2250      $a =  ($a+1) %256;
2251
2252      $t =  $c[$a];
2253
2254      $b =  ($b+ord($t)) %256;
2255
2256      $c[$a] = $c[$b];
2257
2258      $c[$b] = $t;
2259
2260      $k =  ord($c[(ord($c[$a]) +ord($c[$b])) %256]);
2261
2262      $out.= chr(ord($text[$i])  ^ $k);
2263    }
2264
2265
2266    return  $out;
2267  }
2268
2269
2270  /**
2271   * functions which can be called to adjust or add to the document
2272   */
2273
2274  /**
2275   * add a link in the document to an external URL
2276   */
2277  function  addLink($url, $x0, $y0, $x1, $y1) {
2278
2279    $this->numObj++;
2280
2281    $info =  array('type'=>'link', 'url'=>$url, 'rect'=>array($x0, $y0, $x1, $y1));
2282
2283    $this->o_annotation($this->numObj, 'new', $info);
2284  }
2285
2286
2287  /**
2288   * add a link in the document to an internal destination (ie. within the document)
2289   */
2290  function  addInternalLink($label, $x0, $y0, $x1, $y1) {
2291
2292    $this->numObj++;
2293
2294    $info =  array('type'=>'ilink', 'label'=>$label, 'rect'=>array($x0, $y0, $x1, $y1));
2295
2296    $this->o_annotation($this->numObj, 'new', $info);
2297  }
2298
2299
2300  /**
2301   * set the encryption of the document
2302   * can be used to turn it on and/or set the passwords which it will have.
2303   * also the functions that the user will have are set here, such as print, modify, add
2304   */
2305  function  setEncryption($userPass = '', $ownerPass = '', $pc = array()) {
2306
2307    $p = bindec(11000000);
2308
2309
2310    $options =  array(
2311                      'print'=>4, 'modify'=>8, 'copy'=>16, 'add'=>32);
2312
2313    foreach($pc as  $k=>$v) {
2314
2315      if  ($v &&  isset($options[$k])) {
2316
2317        $p+= $options[$k];
2318      } else  if  (isset($options[$v])) {
2319
2320        $p+= $options[$v];
2321      }
2322    }
2323
2324    // implement encryption on the document
2325    if  ($this->arc4_objnum ==  0) {
2326
2327      // then the block does not exist already, add it.
2328      $this->numObj++;
2329
2330      if  (mb_strlen($ownerPass) == 0) {
2331
2332        $ownerPass = $userPass;
2333      }
2334
2335      $this->o_encryption($this->numObj, 'new', array('user'=>$userPass, 'owner'=>$ownerPass, 'p'=>$p));
2336    }
2337  }
2338
2339
2340  /**
2341   * should be used for internal checks, not implemented as yet
2342   */
2343  function  checkAllHere() {
2344  }
2345
2346
2347  /**
2348   * return the pdf stream as a string returned from the function
2349   */
2350  function  output($debug = 0) {
2351
2352
2353    if  ($debug) {
2354      // turn compression off
2355      $this->options['compression'] = 0;
2356    }
2357
2358
2359    if  ($this->arc4_objnum) {
2360
2361      $this->ARC4_init($this->encryptionKey);
2362    }
2363
2364
2365    $this->checkAllHere();
2366
2367
2368    $xref = array();
2369
2370    $content = '%PDF-1.3';
2371    $pos = mb_strlen($content);
2372
2373    foreach($this->objects as  $k=>$v) {
2374
2375      $tmp = 'o_'.$v['t'];
2376
2377      $cont = $this->$tmp($k, 'out');
2378
2379      $content.= $cont;
2380
2381      $xref[] = $pos;
2382
2383      $pos+= mb_strlen($cont);
2384    }
2385
2386    $content.= "\nxref\n0 ".(count($xref) +1) ."\n0000000000 65535 f \n";
2387
2388    foreach($xref as  $p) {
2389
2390      $content.= str_pad($p,  10,  "0",  STR_PAD_LEFT)  . " 00000 n \n";
2391    }
2392
2393    $content.= "trailer\n<<\n/Size ".(count($xref) +1) ."\n/Root 1 0 R\n/Info ".$this->infoObject." 0 R\n";
2394
2395    // if encryption has been applied to this document then add the marker for this dictionary
2396    if  ($this->arc4_objnum > 0) {
2397
2398      $content.=  "/Encrypt ".$this->arc4_objnum." 0 R\n";
2399    }
2400
2401    if  (mb_strlen($this->fileIdentifier)) {
2402
2403      $content.=  "/ID[<".$this->fileIdentifier."><".$this->fileIdentifier.">]\n";
2404    }
2405
2406    $content.=  ">>\nstartxref\n".$pos."\n%%EOF\n";
2407
2408    return  $content;
2409  }
2410
2411
2412  /**
2413   * intialize a new document
2414   * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum
2415   * this function is called automatically by the constructor function
2416   *
2417   * @access private
2418   */
2419  function  newDocument($pageSize = array(0, 0, 612, 792)) {
2420
2421    $this->numObj = 0;
2422
2423    $this->objects =  array();
2424
2425
2426    $this->numObj++;
2427
2428    $this->o_catalog($this->numObj, 'new');
2429
2430
2431    $this->numObj++;
2432
2433    $this->o_outlines($this->numObj, 'new');
2434
2435
2436    $this->numObj++;
2437
2438    $this->o_pages($this->numObj, 'new');
2439
2440
2441    $this->o_pages($this->numObj, 'mediaBox', $pageSize);
2442
2443    $this->currentNode =  3;
2444
2445
2446    $this->numObj++;
2447
2448    $this->o_procset($this->numObj, 'new');
2449
2450
2451    $this->numObj++;
2452
2453    $this->o_info($this->numObj, 'new');
2454
2455
2456    $this->numObj++;
2457
2458    $this->o_page($this->numObj, 'new');
2459
2460
2461    // need to store the first page id as there is no way to get it to the user during
2462    // startup
2463    $this->firstPageId =  $this->currentContents;
2464  }
2465
2466
2467  /**
2468   * open the font file and return a php structure containing it.
2469   * first check if this one has been done before and saved in a form more suited to php
2470   * note that if a php serialized version does not exist it will try and make one, but will
2471   * require write access to the directory to do it... it is MUCH faster to have these serialized
2472   * files.
2473   *
2474   * @access private
2475   */
2476  function  openFont($font) {
2477
2478    // assume that $font contains the path and file but not the extension
2479    $pos = strrpos($font, '/');
2480
2481    if  ($pos === false) {
2482
2483      $dir =  './';
2484
2485      $name =  $font;
2486    } else {
2487
2488      $dir = substr($font, 0, $pos+1);
2489
2490      $name = substr($font, $pos+1);
2491    }
2492   
2493    $fontcache = $this->fontcache;
2494    if ($fontcache == '') {
2495        $fontcache = $dir;
2496    }
2497   
2498    //$name       filename without folder and extension of font metrics
2499    //$dir                folder of font metrics
2500    //$fontcache  folder of runtime created php serialized version of font metrics.
2501    //            If this is not given, the same folder as the font metrics will be used.
2502    //            Storing and reusing serialized versions improves speed much
2503   
2504    $this->addMessage('openFont: '.$font.' - '.$name);
2505
2506    $metrics_name = $name . (($this->isUnicode) ? '.ufm' : '.afm');
2507    $cache_name = 'php_' . $metrics_name;
2508    $this->addMessage('metrics: '.$metrics_name.', cache: '.$cache_name);
2509    if  (file_exists($fontcache . $cache_name)) {
2510
2511      $this->addMessage('openFont: php file exists ' . $fontcache . $cache_name);
2512
2513      $tmp =  file_get_contents($fontcache . $cache_name);
2514
2515      eval($tmp);
2516
2517      if  (!isset($this->fonts[$font]['_version_']) ||  $this->fonts[$font]['_version_']<1) {
2518
2519        // if the font file is old, then clear it out and prepare for re-creation
2520        $this->addMessage('openFont: clear out, make way for new version.');
2521
2522        unset($this->fonts[$font]);
2523      }
2524    }
2525
2526    if  (!isset($this->fonts[$font]) &&  file_exists($dir . $metrics_name)) {
2527
2528      // then rebuild the php_<font>.afm file from the <font>.afm file
2529      $this->addMessage('openFont: build php file from ' . $dir . $metrics_name);
2530
2531      $data =  array();
2532      $cidtogid = '';
2533      if ($this->isUnicode) {
2534        $cidtogid = str_pad('', 256*256*2, "\x00");
2535      }
2536
2537      $file =  file($dir . $metrics_name);
2538
2539      foreach ($file as  $rowA) {
2540
2541        $row = trim($rowA);
2542
2543        $pos = strpos($row, ' ');
2544
2545        if  ($pos) {
2546
2547          // then there must be some keyword
2548          $key =  substr($row, 0, $pos);
2549
2550          switch  ($key) {
2551
2552          case  'FontName':
2553
2554          case  'FullName':
2555
2556          case  'FamilyName':
2557
2558          case  'Weight':
2559
2560          case  'ItalicAngle':
2561
2562          case  'IsFixedPitch':
2563
2564          case  'CharacterSet':
2565
2566          case  'UnderlinePosition':
2567
2568          case  'UnderlineThickness':
2569
2570          case  'Version':
2571
2572          case  'EncodingScheme':
2573
2574          case  'CapHeight':
2575
2576          case  'XHeight':
2577
2578          case  'Ascender':
2579
2580          case  'Descender':
2581
2582          case  'StdHW':
2583
2584          case  'StdVW':
2585
2586          case  'StartCharMetrics':
2587
2588          case  'FontHeightOffset': // OAR - Added so we can offset the height calculation of a Windows font.  Otherwise it's too big.
2589
2590            $data[$key] = trim(substr($row, $pos));
2591
2592            break;
2593
2594          case  'FontBBox':
2595
2596            $data[$key] = explode(' ', trim(substr($row, $pos)));
2597
2598            break;
2599
2600          case  'C': // Found in AFM files
2601
2602            //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ;
2603            $bits = explode(';', trim($row));
2604
2605            $dtmp = array();
2606
2607            foreach($bits as  $bit) {
2608
2609              $bits2 =  explode(' ', trim($bit));
2610
2611              if  (mb_strlen($bits2[0])) {
2612
2613                if  (count($bits2) >2) {
2614
2615                  $dtmp[$bits2[0]] = array();
2616
2617                  $bits2_count = count($bits2);
2618                  for  ($i = 1;$i<$bits2_count;++$i) {
2619
2620                    $dtmp[$bits2[0]][] = $bits2[$i];
2621                  }
2622                } else  if  (count($bits2) == 2) {
2623
2624                  $dtmp[$bits2[0]] = $bits2[1];
2625                }
2626              }
2627            }
2628
2629            $cc = (int)$dtmp['C'];
2630            if  ($cc >= 0) {
2631
2632              $data['C'][$dtmp['C']] = $dtmp;
2633
2634              $data['C'][$dtmp['N']] = $dtmp;
2635            } else {
2636
2637              $data['C'][$dtmp['N']] = $dtmp;
2638            }
2639
2640            if  (!isset($data['MissingWidth']) && $cc == -1 && $dtmp['N'] == '.notdef') {
2641
2642              $data['MissingWidth'] = $width;
2643            }
2644           
2645            break;
2646
2647          case  'U': // Found in UFM files
2648            if ($this->isUnicode) {
2649              // U 827 ; WX 0 ; N squaresubnosp ; G 675 ;
2650              $bits = explode(';', trim($row));
2651             
2652              $dtmp = array();
2653
2654              foreach($bits as  $bit) {
2655
2656                $bits2 =  explode(' ', trim($bit));
2657
2658                if  (mb_strlen($bits2[0])) {
2659
2660                  if  (count($bits2) >2) {
2661
2662                    $dtmp[$bits2[0]] = array();
2663
2664                    $bits2_count = count($bits2);
2665                    for  ($i = 1;$i<$bits2_count;++$i) {
2666
2667                      $dtmp[$bits2[0]][] = $bits2[$i];
2668                    }
2669                  } else  if  (count($bits2) == 2) {
2670
2671                    $dtmp[$bits2[0]] = $bits2[1];
2672                  }
2673                }
2674              }
2675
2676              $cc = (int)$dtmp['U'];
2677              $glyph = $dtmp['G'];
2678              $width = $dtmp['WX'];
2679              if  ($cc >= 0) {
2680                // Set values in CID to GID map
2681                if ($cc >= 0 && $cc < 0xFFFF && $glyph) {
2682                  $cidtogid{$cc*2} = chr($glyph >> 8);
2683                  $cidtogid{$cc*2 + 1} = chr($glyph & 0xFF);
2684                }
2685
2686                $data['C'][$dtmp['U']] = $dtmp;
2687
2688                $data['C'][$dtmp['N']] = $dtmp;
2689              } else {
2690
2691                $data['C'][$dtmp['N']] = $dtmp;
2692              }
2693             
2694              if  (!isset($data['MissingWidth']) && $cc == -1 && $dtmp['N'] == '.notdef') {
2695
2696                $data['MissingWidth'] = $width;
2697              }
2698            }
2699
2700            break;
2701
2702          case  'KPX':
2703
2704            //KPX Adieresis yacute -40
2705            $bits = explode(' ', trim($row));
2706
2707            $data['KPX'][$bits[1]][$bits[2]] = $bits[3];
2708
2709            break;
2710          }
2711        }
2712      }
2713
2714      //    echo $cidtogid; die("CIDtoGID Displayed!");
2715
2716      if  (function_exists('gzcompress') &&  $this->options['compression']) {
2717
2718        // then implement ZLIB based compression on CIDtoGID string
2719        $data['CIDtoGID_Compressed'] = true;
2720
2721        $cidtogid =  gzcompress($cidtogid,  6);
2722      }
2723      $data['CIDtoGID'] = base64_encode($cidtogid);
2724     
2725      $data['_version_'] = 1;
2726
2727      $this->fonts[$font] = $data;
2728
2729      //Because of potential trouble with php safe mode, expect that the folder already exists.
2730      //If not existing, this will hit performance because of missing cached results.
2731      if ( is_dir(substr($fontcache,0,-1)) ) {
2732        file_put_contents($fontcache . $cache_name,  '$this->fonts[$font]=' . var_export($data,  true)  . ';');
2733      }
2734    }
2735   
2736    if  (!isset($this->fonts[$font])) {
2737      $this->addMessage("openFont: no font file found for $font.  Do you need to run load_font.php?");
2738      //echo 'Font not Found '.$font;
2739    }
2740
2741    //pre_r($this->messages);
2742  }
2743
2744
2745  /**
2746   * if the font is not loaded then load it and make the required object
2747   * else just make it the current font
2748   * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding'
2749   * note that encoding='none' will need to be used for symbolic fonts
2750   * and 'differences' => an array of mappings between numbers 0->255 and character names.
2751   *
2752   */
2753  function  selectFont($fontName, $encoding =  '', $set =  true) {
2754
2755    $ext = substr($fontName, -4);
2756    if  ($ext == '.afm' || $ext == '.ufm') {
2757      $fontName = substr($fontName, 0, mb_strlen($fontName)-4);
2758    }
2759
2760    if  (!isset($this->fonts[$fontName])) {
2761      $this->addMessage("selectFont: selecting - $fontName - $encoding, $set");
2762
2763      // load the file
2764      $this->openFont($fontName);
2765
2766      if  (isset($this->fonts[$fontName])) {
2767
2768        $this->numObj++;
2769
2770        $this->numFonts++;
2771
2772        //$this->numFonts = md5($fontName);
2773        $pos =  strrpos($fontName, '/');
2774
2775        //      $dir = substr($fontName,0,$pos+1);
2776        $name =  substr($fontName, $pos+1);
2777
2778        $options =  array('name' => $name, 'fontFileName' => $fontName);
2779
2780        if  (is_array($encoding)) {
2781
2782          // then encoding and differences might be set
2783          if  (isset($encoding['encoding'])) {
2784
2785            $options['encoding'] =  $encoding['encoding'];
2786          }
2787
2788          if  (isset($encoding['differences'])) {
2789
2790            $options['differences'] =  $encoding['differences'];
2791          }
2792        } else  if  (mb_strlen($encoding)) {
2793
2794          // then perhaps only the encoding has been set
2795          $options['encoding'] =  $encoding;
2796        }
2797
2798
2799        $fontObj =  $this->numObj;
2800
2801        $this->o_font($this->numObj, 'new', $options);
2802
2803        $this->fonts[$fontName]['fontNum'] =  $this->numFonts;
2804
2805
2806        // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there
2807        // should be for all non-basic fonts), then load it into an object and put the
2808        // references into the font object
2809        $basefile =  $fontName;
2810        if  (file_exists($basefile.'.pfb')) {
2811
2812          $fbtype =  'pfb';
2813        } else  if  (file_exists($basefile.'.ttf')) {
2814
2815          $fbtype =  'ttf';
2816        } else {
2817
2818          $fbtype =  '';
2819        }
2820
2821        $fbfile =  $basefile.'.'.$fbtype;
2822
2823
2824        //      $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb';
2825        //      $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf';
2826        $this->addMessage('selectFont: checking for - '.$fbfile);
2827
2828
2829        // OAR - I don't understand this old check
2830        // if  (substr($fontName, -4) ==  '.afm' &&  strlen($fbtype)) {
2831        if  (mb_strlen($fbtype)) {
2832          $adobeFontName =  $this->fonts[$fontName]['FontName'];
2833
2834          //        $fontObj = $this->numObj;
2835          $this->addMessage('selectFont: adding font file - '.$fbfile.' - '.$adobeFontName);
2836
2837          // find the array of font widths, and put that into an object.
2838          $firstChar =  -1;
2839
2840          $lastChar =  0;
2841
2842          $widths =  array();
2843          $cid_widths = array();
2844
2845          foreach ($this->fonts[$fontName]['C'] as  $num => $d) {
2846
2847            if  (intval($num) >0 ||  $num ==  '0') {
2848
2849              if (!$this->isUnicode) {
2850                // With Unicode, widths array isn't used
2851                if  ($lastChar>0 &&  $num>$lastChar+1) {
2852
2853                  for ($i =  $lastChar+1;$i<$num;++$i) {
2854
2855                    $widths[] =  0;
2856                  }
2857                }
2858              }
2859
2860              $widths[] =  $d['WX'];
2861
2862              if ($this->isUnicode) {
2863                $cid_widths[$num] =  $d['WX'];
2864              }
2865
2866              if  ($firstChar ==  -1) {
2867                $firstChar =  $num;
2868              }
2869
2870              $lastChar =  $num;
2871            }
2872          }
2873
2874          // also need to adjust the widths for the differences array
2875          if  (isset($options['differences'])) {
2876
2877            foreach($options['differences'] as  $charNum => $charName) {
2878
2879              if  ($charNum > $lastChar) {
2880
2881                if (!$this->isUnicode) {
2882                  // With Unicode, widths array isn't used
2883                  for ($i =  $lastChar + 1; $i <=  $charNum; ++$i) {
2884
2885                    $widths[] =  0;
2886                  }
2887                }
2888
2889                $lastChar =  $charNum;
2890              }
2891
2892              if  (isset($this->fonts[$fontName]['C'][$charName])) {
2893
2894                $widths[$charNum-$firstChar] =  $this->fonts[$fontName]['C'][$charName]['WX'];
2895                if ($this->isUnicode) {
2896                  $cid_widths[$charName] =  $this->fonts[$fontName]['C'][$charName]['WX'];
2897                }
2898              }
2899            }
2900          }
2901
2902          if ($this->isUnicode) {
2903            $this->fonts[$fontName]['CIDWidths'] = $cid_widths;
2904          }
2905
2906          $this->addMessage('selectFont: FirstChar = '.$firstChar);
2907
2908          $this->addMessage('selectFont: LastChar = '.$lastChar);
2909
2910          $widthid = -1;
2911
2912          if (!$this->isUnicode) {
2913            // With Unicode, widths array isn't used
2914
2915            $this->numObj++;
2916
2917            $this->o_contents($this->numObj, 'new', 'raw');
2918
2919            $this->objects[$this->numObj]['c'].=  '[';
2920
2921            foreach($widths as  $width) {
2922              $this->objects[$this->numObj]['c'].=  ' '.$width;
2923            }
2924
2925            $this->objects[$this->numObj]['c'].=  ' ]';
2926
2927            $widthid =  $this->numObj;
2928          }
2929
2930          $missing_width = 500;
2931          $stemV = 70;
2932
2933          if (isset($this->fonts[$fontName]['MissingWidth'])) {
2934
2935            $missing_width =  $this->fonts[$fontName]['MissingWidth'];
2936          }
2937          if (isset($this->fonts[$fontName]['StdVW'])) {
2938
2939            $stemV = $this->fonts[$fontName]['StdVW'];
2940          } elseif (isset($this->fonts[$fontName]['Weight']) && preg_match('!(bold|black)!i', $this->fonts[$fontName]['Weight'])) {
2941
2942            $stemV = 120;
2943          }
2944
2945          // load the pfb file, and put that into an object too.
2946          // note that pdf supports only binary format type 1 font files, though there is a
2947          // simple utility to convert them from pfa to pfb.
2948          $data =  file_get_contents($fbfile);
2949
2950
2951          // create the font descriptor
2952          $this->numObj++;
2953
2954          $fontDescriptorId =  $this->numObj;
2955
2956          $this->numObj++;
2957
2958          $pfbid =  $this->numObj;
2959
2960          // determine flags (more than a little flakey, hopefully will not matter much)
2961          $flags =  0;
2962
2963          if  ($this->fonts[$fontName]['ItalicAngle'] !=  0) {
2964            $flags+=  pow(2, 6);
2965          }
2966
2967          if  ($this->fonts[$fontName]['IsFixedPitch'] ==  'true') {
2968            $flags+=  1;
2969          }
2970
2971          $flags+=  pow(2, 5); // assume non-sybolic
2972
2973          $list =  array('Ascent' => 'Ascender', 'CapHeight' => 'CapHeight', 'MissingWidth' => 'MissingWidth', 'Descent' => 'Descender', 'FontBBox' => 'FontBBox', 'ItalicAngle' => 'ItalicAngle');
2974
2975          $fdopt =  array('Flags' => $flags, 'FontName' => $adobeFontName, 'StemV' => $stemV);
2976
2977          foreach($list as  $k => $v) {
2978
2979            if  (isset($this->fonts[$fontName][$v])) {
2980
2981              $fdopt[$k] =  $this->fonts[$fontName][$v];
2982            }
2983          }
2984
2985
2986          if  ($fbtype ==  'pfb') {
2987
2988            $fdopt['FontFile'] =  $pfbid;
2989          } else  if  ($fbtype ==  'ttf') {
2990
2991            $fdopt['FontFile2'] =  $pfbid;
2992          }
2993
2994          $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt);
2995
2996
2997          // embed the font program
2998          $this->o_contents($this->numObj, 'new');
2999
3000          $this->objects[$pfbid]['c'].=  $data;
3001
3002          // determine the cruicial lengths within this file
3003          if  ($fbtype ==  'pfb') {
3004
3005            $l1 =  strpos($data, 'eexec') +6;
3006
3007            $l2 =  strpos($data, '00000000') -$l1;
3008
3009            $l3 =  mb_strlen($data) -$l2-$l1;
3010
3011            $this->o_contents($this->numObj, 'add', array('Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3));
3012          } else  if  ($fbtype ==  'ttf') {
3013
3014            $l1 =  mb_strlen($data);
3015
3016            $this->o_contents($this->numObj, 'add', array('Length1' => $l1));
3017          }
3018
3019
3020
3021          // tell the font object about all this new stuff
3022          $tmp =  array('BaseFont' => $adobeFontName, 'MissingWidth' => $missing_width, 'Widths' => $widthid, 'FirstChar' => $firstChar, 'LastChar' => $lastChar, 'FontDescriptor' => $fontDescriptorId);
3023
3024          if  ($fbtype ==  'ttf') {
3025
3026            $tmp['SubType'] =  'TrueType';
3027          }
3028
3029          $this->addMessage('adding extra info to font.('.$fontObj.')');
3030
3031          foreach($tmp as  $fk => $fv) {
3032
3033            $this->addMessage($fk." : ".$fv);
3034          }
3035
3036          $this->o_font($fontObj, 'add', $tmp);
3037        } else {
3038
3039          $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts');
3040        }
3041
3042
3043
3044        // also set the differences here, note that this means that these will take effect only the
3045        //first time that a font is selected, else they are ignored
3046        if  (isset($options['differences'])) {
3047
3048          $this->fonts[$fontName]['differences'] =  $options['differences'];
3049        }
3050      }
3051    }
3052
3053    if  ($set &&  isset($this->fonts[$fontName])) {
3054
3055      // so if for some reason the font was not set in the last one then it will not be selected
3056      $this->currentBaseFont =  $fontName;
3057
3058      // the next lines mean that if a new font is selected, then the current text state will be
3059      // applied to it as well.
3060      $this->currentFont =  $this->currentBaseFont;
3061
3062      $this->currentFontNum =  $this->fonts[$this->currentFont]['fontNum'];
3063
3064      //$this->setCurrentFont();
3065
3066    }
3067
3068    return  $this->currentFontNum;
3069
3070    //return $this->numObj;
3071
3072  }
3073
3074
3075  /**
3076   * sets up the current font, based on the font families, and the current text state
3077   * note that this system is quite flexible, a bold-italic font can be completely different to a
3078   * italic-bold font, and even bold-bold will have to be defined within the family to have meaning
3079   * This function is to be called whenever the currentTextState is changed, it will update
3080   * the currentFont setting to whatever the appropriatte family one is.
3081   * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont
3082   * This function will change the currentFont to whatever it should be, but will not change the
3083   * currentBaseFont.
3084   *
3085   * @access private
3086   */
3087  function  setCurrentFont() {
3088
3089    //   if (strlen($this->currentBaseFont) == 0){
3090    //     // then assume an initial font
3091    //     $this->selectFont('./fonts/Helvetica.afm');
3092    //   }
3093    //   $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1);
3094    //   if (strlen($this->currentTextState)
3095    //     && isset($this->fontFamilies[$cf])
3096    //       && isset($this->fontFamilies[$cf][$this->currentTextState])){
3097    //     // then we are in some state or another
3098    //     // and this font has a family, and the current setting exists within it
3099    //     // select the font, then return it
3100    //     $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState];
3101    //     $this->selectFont($nf,'',0);
3102    //     $this->currentFont = $nf;
3103    //     $this->currentFontNum = $this->fonts[$nf]['fontNum'];
3104    //   } else {
3105    //     // the this font must not have the right family member for the current state
3106    //     // simply assume the base font
3107    $this->currentFont =  $this->currentBaseFont;
3108
3109    $this->currentFontNum =  $this->fonts[$this->currentFont]['fontNum'];
3110
3111    //  }
3112
3113  }
3114
3115
3116  /**
3117   * function for the user to find out what the ID is of the first page that was created during
3118   * startup - useful if they wish to add something to it later.
3119   */
3120  function  getFirstPageId() {
3121
3122    return  $this->firstPageId;
3123  }
3124
3125
3126  /**
3127   * add content to the currently active object
3128   *
3129   * @access private
3130   */
3131  function  addContent($content) {
3132
3133    $this->objects[$this->currentContents]['c'].=  $content;
3134  }
3135
3136
3137  /**
3138   * sets the colour for fill operations
3139   */
3140  function  setColor($r, $g, $b, $force =  0) {
3141
3142    if  ($r >=  0 &&  ($force ||  $r !=  $this->currentColour['r'] ||  $g !=  $this->currentColour['g'] ||  $b !=  $this->currentColour['b'])) {
3143
3144      $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $r) .' '.sprintf('%.3F', $g) .' '.sprintf('%.3F', $b) .' rg';
3145
3146      $this->currentColour =  array('r' => $r, 'g' => $g, 'b' => $b);
3147    }
3148  }
3149
3150
3151  /**
3152   * sets the colour for stroke operations
3153   */
3154  function  setStrokeColor($r, $g, $b, $force =  0) {
3155
3156    if  ($r >=  0 &&  ($force ||  $r !=  $this->currentStrokeColour['r'] ||  $g !=  $this->currentStrokeColour['g'] ||  $b !=  $this->currentStrokeColour['b'])) {
3157
3158      $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $r) .' '.sprintf('%.3F', $g) .' '.sprintf('%.3F', $b) .' RG';
3159
3160      $this->currentStrokeColour =  array('r' => $r, 'g' => $g, 'b' => $b);
3161    }
3162  }
3163
3164
3165  /**
3166   * Set the graphics state for compositions
3167   */
3168  function  setGraphicsState($parameters) {
3169
3170    // Create a new graphics state object
3171    // FIXME: should actually keep track of states that have already been created...
3172    $this->numObj++;
3173
3174    $this->o_extGState($this->numObj,  'new',  $parameters);
3175
3176    $this->objects[ $this->currentContents ]['c'].=  "\n/GS" . $this->numStates . " gs";
3177  }
3178
3179
3180  /**
3181   * Set current blend mode & opacity for lines.
3182   *
3183   * Valid blend modes are:
3184   *
3185   * Normal, Multiply, Screen, Overlay, Darken, Lighten,
3186   * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
3187   * Exclusion
3188   *
3189   * @param string $mode the blend mode to use
3190   * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
3191   */
3192  function setLineTransparency($mode, $opacity) {
3193    static $blend_modes = array("Normal", "Multiply", "Screen",
3194                                "Overlay", "Darken", "Lighten",
3195                                "ColorDogde", "ColorBurn", "HardLight",
3196                                "SoftLight", "Difference", "Exclusion");
3197
3198    if ( !in_array($mode, $blend_modes) )
3199      $mode = "Normal";
3200   
3201    // Only create a new graphics state if required
3202    if ( $mode == $this->currentLineTransparency["mode"]  &&
3203         $opacity == $this->currentLineTransparency["opacity"] )
3204      return;
3205
3206    $options = array("BM" => "/$mode",
3207                     "CA" => (float)$opacity);
3208
3209    $this->setGraphicsState($options);
3210  }
3211 
3212  /**
3213   * Set current blend mode & opacity for filled objects.
3214   *
3215   * Valid blend modes are:
3216   *
3217   * Normal, Multiply, Screen, Overlay, Darken, Lighten,
3218   * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
3219   * Exclusion
3220   *
3221   * @param string $mode the blend mode to use
3222   * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
3223   */
3224  function setFillTransparency($mode, $opacity) {
3225    static $blend_modes = array("Normal", "Multiply", "Screen",
3226                                "Overlay", "Darken", "Lighten",
3227                                "ColorDogde", "ColorBurn", "HardLight",
3228                                "SoftLight", "Difference", "Exclusion");
3229
3230    if ( !in_array($mode, $blend_modes) )
3231      $mode = "Normal";
3232
3233    if ( $mode == $this->currentFillTransparency["mode"]  &&
3234         $opacity == $this->currentFillTransparency["opacity"] )
3235      return;
3236
3237    $options = array("BM" => "/$mode",
3238                     "ca" => (float)$opacity);
3239   
3240    $this->setGraphicsState($options);
3241  }
3242
3243  /**
3244   * draw a line from one set of coordinates to another
3245   */
3246  function  line($x1, $y1, $x2, $y2) {
3247
3248    $this->objects[$this->currentContents]['c'] .=
3249      "\n".sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1) .' m '.sprintf('%.3F', $x2) .' '.sprintf('%.3F', $y2) .' l S';
3250  }
3251
3252
3253  /**
3254   * draw a bezier curve based on 4 control points
3255   */
3256  function  curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) {
3257
3258    // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points
3259    // as the control points for the curve.
3260    $this->objects[$this->currentContents]['c'] .=
3261      "\n".sprintf('%.3F', $x0) .' '.sprintf('%.3F', $y0) .' m '.sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1);
3262
3263    $this->objects[$this->currentContents]['c'] .=
3264      ' '.sprintf('%.3F', $x2) .' '.sprintf('%.3F', $y2) .' '.sprintf('%.3F', $x3) .' '.sprintf('%.3F', $y3) .' c S';
3265  }
3266
3267
3268  /**
3269   * draw a part of an ellipse
3270   */
3271  function  partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 =  0, $angle =  0, $nSeg =  8) {
3272
3273    $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, 0);
3274  }
3275
3276
3277  /**
3278   * draw a filled ellipse
3279   */
3280  function  filledEllipse($x0, $y0, $r1, $r2 =  0, $angle =  0, $nSeg =  8, $astart =  0, $afinish =  360) {
3281
3282    return  $this->ellipse($x0, $y0, $r1, $r2 =  0, $angle, $nSeg, $astart, $afinish, 1, 1);
3283  }
3284
3285
3286  /**
3287   * draw an ellipse
3288   * note that the part and filled ellipse are just special cases of this function
3289   *
3290   * draws an ellipse in the current line style
3291   * centered at $x0,$y0, radii $r1,$r2
3292   * if $r2 is not set, then a circle is drawn
3293   * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a
3294   * pretty crappy shape at 2, as we are approximating with bezier curves.
3295   */
3296  function  ellipse($x0, $y0, $r1, $r2 =  0, $angle =  0, $nSeg =  8, $astart =  0, $afinish =  360, $close =  1, $fill =  0) {
3297
3298    if  ($r1 ==  0) {
3299      return;
3300    }
3301
3302    if  ($r2 ==  0) {
3303      $r2 =  $r1;
3304    }
3305
3306    if  ($nSeg < 2) {
3307      $nSeg =  2;
3308    }
3309
3310
3311    $astart =  deg2rad((float)$astart);
3312
3313    $afinish =  deg2rad((float)$afinish);
3314
3315    $totalAngle = $afinish-$astart;
3316
3317
3318    $dt =  $totalAngle/$nSeg;
3319
3320    $dtm =  $dt/3;
3321
3322
3323    if  ($angle !=  0) {
3324
3325      $a =  -1*deg2rad((float)$angle);
3326
3327      $tmp  =  "\n q ";
3328      $tmp .=  sprintf('%.3F', cos($a)) .' '.sprintf('%.3F', (-1.0*sin($a))) .' '.sprintf('%.3F', sin($a)) .' '.sprintf('%.3F', cos($a)) .' ';
3329      $tmp .=  sprintf('%.3F', $x0) .' '.sprintf('%.3F', $y0) .' cm';
3330
3331      $this->objects[$this->currentContents]['c'].=  $tmp;
3332
3333      $x0 =  0;
3334      $y0 =  0;
3335    }
3336
3337
3338    $t1 =  $astart;
3339    $a0 =  $x0 + $r1*cos($t1);
3340    $b0 =  $y0 + $r2*sin($t1);
3341    $c0 =  -$r1 * sin($t1);
3342    $d0 =  $r2 * cos($t1);
3343
3344
3345    $this->objects[$this->currentContents]['c'] .=  "\n".sprintf('%.3F', $a0) .' '.sprintf('%.3F', $b0) .' m ';
3346
3347    for  ($i = 1; $i <=  $nSeg; ++$i) {
3348
3349      // draw this bit of the total curve
3350      $t1 =  $i * $dt + $astart;
3351
3352      $a1 =  $x0 + $r1 * cos($t1);
3353
3354      $b1 =  $y0 + $r2 * sin($t1);
3355
3356      $c1 = -$r1 * sin($t1);
3357
3358      $d1 =  $r2 * cos($t1);
3359
3360      $this->objects[$this->currentContents]['c']
3361        .=  "\n".sprintf('%.3F', ($a0+$c0*$dtm)) .' '.sprintf('%.3F', ($b0 + $d0 * $dtm));
3362
3363      $this->objects[$this->currentContents]['c'] .=
3364        ' '.sprintf('%.3F', ($a1-$c1*$dtm)) .' '.sprintf('%.3F', ($b1-$d1*$dtm)) .' '.sprintf('%.3F', $a1) .' '.sprintf('%.3F', $b1) .' c';
3365
3366      $a0 =  $a1;
3367
3368      $b0 =  $b1;
3369
3370      $c0 =  $c1;
3371
3372      $d0 =  $d1;
3373    }
3374
3375    if  ($fill) {
3376      $this->objects[$this->currentContents]['c'].=  ' f';
3377
3378    } else if ($close) {
3379
3380        $this->objects[$this->currentContents]['c'].=  ' s';
3381        // small 's' signifies closing the path as well
3382
3383    } else {
3384
3385      $this->objects[$this->currentContents]['c'].=  ' S';
3386
3387    }
3388
3389    if  ($angle !=  0) {
3390      $this->objects[$this->currentContents]['c'].=  ' Q';
3391    }
3392
3393  }
3394
3395
3396  /**
3397   * this sets the line drawing style.
3398   * width, is the thickness of the line in user units
3399   * cap is the type of cap to put on the line, values can be 'butt','round','square'
3400   *    where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the
3401   *    end of the line.
3402   * join can be 'miter', 'round', 'bevel'
3403   * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the
3404   *   on and off dashes.
3405   *   (2) represents 2 on, 2 off, 2 on , 2 off ...
3406   *   (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
3407   * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts.
3408   */
3409  function  setLineStyle($width =  1, $cap =  '', $join =  '', $dash =  '', $phase =  0) {
3410
3411
3412    // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day
3413    $string =  '';
3414
3415    if  ($width>0) {
3416
3417      $string.=  $width.' w';
3418    }
3419
3420    $ca =  array('butt' => 0, 'round' => 1, 'square' => 2);
3421
3422    if  (isset($ca[$cap])) {
3423
3424      $string.=  ' '.$ca[$cap].' J';
3425    }
3426
3427    $ja =  array('miter' => 0, 'round' => 1, 'bevel' => 2);
3428
3429    if  (isset($ja[$join])) {
3430
3431      $string.=  ' '.$ja[$join].' j';
3432    }
3433
3434    if  (is_array($dash)) {
3435
3436      $string.=  ' [';
3437
3438      foreach ($dash as  $len) {
3439
3440        $string.=  ' '.$len;
3441      }
3442
3443      $string.=  ' ] '.$phase.' d';
3444    }
3445
3446    $this->currentLineStyle =  $string;
3447
3448    $this->objects[$this->currentContents]['c'].=  "\n".$string;
3449  }
3450
3451
3452
3453  /**
3454   * draw a polygon, the syntax for this is similar to the GD polygon command
3455   */
3456  function  polygon($p, $np, $f =  0) {
3457
3458    $this->objects[$this->currentContents]['c'].=  "\n";
3459
3460    $this->objects[$this->currentContents]['c'].=  sprintf('%.3F', $p[0]) .' '.sprintf('%.3F', $p[1]) .' m ';
3461
3462    for  ($i =  2; $i < $np * 2; $i =  $i + 2) {
3463
3464      $this->objects[$this->currentContents]['c'].=  sprintf('%.3F', $p[$i]) .' '.sprintf('%.3F', $p[$i+1]) .' l ';
3465    }
3466
3467    if  ($f ==  1) {
3468
3469      $this->objects[$this->currentContents]['c'].=  ' f';
3470    } else {
3471
3472      $this->objects[$this->currentContents]['c'].=  ' S';
3473    }
3474  }
3475
3476
3477  /**
3478   * a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
3479   * the coordinates of the upper-right corner
3480   */
3481  function  filledRectangle($x1, $y1, $width, $height) {
3482
3483    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1) .' '.sprintf('%.3F', $width) .' '.sprintf('%.3F', $height) .' re f';
3484  }
3485
3486
3487  /**
3488   * draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
3489   * the coordinates of the upper-right corner
3490   */
3491  function  rectangle($x1, $y1, $width, $height) {
3492
3493    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1) .' '.sprintf('%.3F', $width) .' '.sprintf('%.3F', $height) .' re S';
3494  }
3495
3496
3497  /**
3498   * add a new page to the document
3499   * this also makes the new page the current active object
3500   */
3501  function  newPage($insert =  0, $id =  0, $pos =  'after') {
3502
3503
3504    // if there is a state saved, then go up the stack closing them
3505    // then on the new page, re-open them with the right setings
3506
3507    if  ($this->nStateStack) {
3508
3509      for  ($i =  $this->nStateStack;$i >=  1;$i--) {
3510
3511        $this->restoreState($i);
3512      }
3513    }
3514
3515
3516    $this->numObj++;
3517
3518    if  ($insert) {
3519
3520      // the id from the ezPdf class is the od of the contents of the page, not the page object itself
3521      // query that object to find the parent
3522      $rid =  $this->objects[$id]['onPage'];
3523
3524      $opt =  array('rid' => $rid, 'pos' => $pos);
3525
3526      $this->o_page($this->numObj, 'new', $opt);
3527    } else {
3528
3529      $this->o_page($this->numObj, 'new');
3530    }
3531
3532    // if there is a stack saved, then put that onto the page
3533    if  ($this->nStateStack) {
3534
3535      for  ($i =  1;$i <=  $this->nStateStack;++$i) {
3536
3537        $this->saveState($i);
3538      }
3539    }
3540
3541    // and if there has been a stroke or fill colour set, then transfer them
3542    if  ($this->currentColour['r'] >=  0) {
3543
3544      $this->setColor($this->currentColour['r'], $this->currentColour['g'], $this->currentColour['b'], 1);
3545    }
3546
3547    if  ($this->currentStrokeColour['r'] >=  0) {
3548
3549      $this->setStrokeColor($this->currentStrokeColour['r'], $this->currentStrokeColour['g'], $this->currentStrokeColour['b'], 1);
3550    }
3551
3552
3553    // if there is a line style set, then put this in too
3554    if  (mb_strlen($this->currentLineStyle)) {
3555
3556      $this->objects[$this->currentContents]['c'].=  "\n".$this->currentLineStyle;
3557    }
3558
3559
3560    // the call to the o_page object set currentContents to the present page, so this can be returned as the page id
3561    return  $this->currentContents;
3562  }
3563
3564
3565  /**
3566   * output the pdf code, streaming it to the browser
3567   * the relevant headers are set so that hopefully the browser will recognise it
3568   */
3569  function  stream($options =  '') {
3570
3571    // setting the options allows the adjustment of the headers
3572    // values at the moment are:
3573    // 'Content-Disposition' => 'filename'  - sets the filename, though not too sure how well this will
3574    //        work as in my trial the browser seems to use the filename of the php file with .pdf on the end
3575    // 'Accept-Ranges' => 1 or 0 - if this is not set to 1, then this header is not included, off by default
3576    //    this header seems to have caused some problems despite tha fact that it is supposed to solve
3577    //    them, so I am leaving it off by default.
3578    // 'compress' = > 1 or 0 - apply content stream compression, this is on (1) by default
3579    // 'Attachment' => 1 or 0 - if 1, force the browser to open a download dialog
3580    if  (!is_array($options)) {
3581
3582      $options =  array();
3583    }
3584
3585    if  ( headers_sent())
3586      die("Unable to stream pdf: headers already sent");
3587
3588
3589    if  ( isset($options['compress']) &&  $options['compress'] ==  0) {
3590
3591      $tmp =  ltrim($this->output(1));
3592    } else {
3593
3594      $tmp =  ltrim($this->output());
3595    }
3596
3597
3598    header("Cache-Control: private");
3599
3600    header("Content-type: application/pdf");
3601
3602    //FIXME: I don't know that this is sufficient for determining content length (i.e. what about transport compression?)
3603    //header("Content-Length: " . mb_strlen($tmp));
3604    $fileName =  (isset($options['Content-Disposition']) ?  $options['Content-Disposition'] :  'file.pdf');
3605
3606    if  ( !isset($options["Attachment"]))
3607      $options["Attachment"] =  true;
3608
3609
3610    $attachment =  $options["Attachment"] ?  "attachment" :  "inline";
3611
3612
3613    header("Content-Disposition: $attachment; filename=\"$fileName\"");
3614
3615
3616    if  (isset($options['Accept-Ranges']) &&  $options['Accept-Ranges'] ==  1) {
3617      //FIXME: Is this the correct value ... spec says 1#range-unit
3618      header("Accept-Ranges: " . mb_strlen($tmp));
3619    }
3620
3621    echo  $tmp;
3622
3623    flush();
3624  }
3625
3626
3627  /**
3628   * return the height in units of the current font in the given size
3629   */
3630  function  getFontHeight($size) {
3631
3632    if  (!$this->numFonts) {
3633      $this->selectFont('./fonts/Helvetica');
3634    }
3635   
3636    // for the current font, and the given size, what is the height of the font in user units
3637    $h =  $this->fonts[$this->currentFont]['FontBBox'][3]-$this->fonts[$this->currentFont]['FontBBox'][1];
3638
3639    // have to adjust by a font offset for Windows fonts.  unfortunately it looks like
3640    // the bounding box calculations are wrong and I don't know why.
3641    if (isset($this->fonts[$this->currentFont]['FontHeightOffset'])) {
3642
3643      // For CourierNew from Windows this needs to be -646 to match the
3644      // Adobe native Courier font.
3645      //
3646      // For FreeMono from GNU this needs to be -337 to match the
3647      // Courier font.
3648      //
3649      // Both have been added manually to the .afm and .ufm files.
3650      $h += (int)$this->fonts[$this->currentFont]['FontHeightOffset'];
3651    }
3652
3653    return  $size*$h/1000;
3654  }
3655
3656
3657  /**
3658   * return the font descender, this will normally return a negative number
3659   * if you add this number to the baseline, you get the level of the bottom of the font
3660   * it is in the pdf user units
3661   */
3662  function  getFontDescender($size) {
3663
3664    // note that this will most likely return a negative value
3665    if  (!$this->numFonts) {
3666      $this->selectFont('./fonts/Helvetica');
3667    }
3668
3669    //$h = $this->fonts[$this->currentFont]['FontBBox'][1];
3670    $h = $this->fonts[$this->currentFont]['Descender'];
3671
3672    return  $size*$h/1000;
3673  }
3674
3675
3676  /**
3677   * filter the text, this is applied to all text just before being inserted into the pdf document
3678   * it escapes the various things that need to be escaped, and so on
3679   *
3680   * @access private
3681   */
3682  function  filterText($text, $bom = true) {
3683    if ($this->isUnicode) {
3684      $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
3685      $text =  $this->utf8toUtf16BE($text, $bom);
3686    } else {
3687      if (in_array('Windows-1252', mb_list_encodings())) {
3688        $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8');
3689      } else {
3690        $text = mb_convert_encoding($text, 'iso-8859-1', 'UTF-8');
3691      }
3692      $text = html_entity_decode($text, ENT_QUOTES);
3693    }
3694
3695    // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290)
3696    $text = strtr($text, array(')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r'));
3697    return  $text;
3698  }
3699
3700  /**
3701   * return array containing codepoints (UTF-8 character values) for the
3702   * string passed in.
3703   *
3704   * based on the excellent TCPDF code by Nicola Asuni and the
3705   * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
3706   *
3707   * @access private
3708   * @author Orion Richardson
3709   * @since January 5, 2008
3710   * @param string $text UTF-8 string to process
3711   * @return array UTF-8 codepoints array for the string
3712   */
3713  function  utf8toCodePointsArray(&$text) {
3714    $length = mb_strlen($text,'8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040
3715    $unicode = array(); // array containing unicode values
3716    $bytes = array(); // array containing single character byte sequences
3717    $numbytes = 1; // number of octetc needed to represent the UTF-8 character
3718   
3719    for ($i = 0; $i < $length; ++$i) {
3720      $c = ord($text{$i}); // get one string character at time
3721      if (count($bytes) == 0) { // get starting octect
3722        if ($c <= 0x7F) {
3723          $unicode[] = $c; // use the character "as is" because is ASCII
3724          $numbytes = 1;
3725        } elseif (($c >> 0x05) == 0x06) { // 2 bytes character (0x06 = 110 BIN)
3726          $bytes[] = ($c - 0xC0) << 0x06;
3727          $numbytes = 2;
3728        } elseif (($c >> 0x04) == 0x0E) { // 3 bytes character (0x0E = 1110 BIN)
3729          $bytes[] = ($c - 0xE0) << 0x0C;
3730          $numbytes = 3;
3731        } elseif (($c >> 0x03) == 0x1E) { // 4 bytes character (0x1E = 11110 BIN)
3732          $bytes[] = ($c - 0xF0) << 0x12;
3733          $numbytes = 4;
3734        } else {
3735          // use replacement character for other invalid sequences
3736          $unicode[] = 0xFFFD;
3737          $bytes = array();
3738          $numbytes = 1;
3739        }
3740      } elseif (($c >> 0x06) == 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN
3741        $bytes[] = $c - 0x80;
3742        if (count($bytes) == $numbytes) {
3743          // compose UTF-8 bytes to a single unicode value
3744          $c = $bytes[0];
3745          for ($j = 1; $j < $numbytes; ++$j) {
3746            $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06));
3747          }
3748          if ((($c >= 0xD800) AND ($c <= 0xDFFF)) OR ($c >= 0x10FFFF)) {
3749            // The definition of UTF-8 prohibits encoding character numbers between
3750            // U+D800 and U+DFFF, which are reserved for use with the UTF-16
3751            // encoding form (as surrogate pairs) and do not directly represent
3752            // characters.
3753            $unicode[] = 0xFFFD; // use replacement character
3754          } else {
3755            $unicode[] = $c; // add char to array
3756          }
3757          // reset data for next char
3758          $bytes = array();
3759          $numbytes = 1;
3760        }
3761      } else {
3762        // use replacement character for other invalid sequences
3763        $unicode[] = 0xFFFD;
3764        $bytes = array();
3765        $numbytes = 1;
3766      }
3767    }
3768    return $unicode;
3769  }
3770
3771  /**
3772   * convert UTF-8 to UTF-16 with an additional byte order marker
3773   * at the front if required.
3774   *
3775   * based on the excellent TCPDF code by Nicola Asuni and the
3776   * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
3777   *
3778   * @access private
3779   * @author Orion Richardson
3780   * @since January 5, 2008
3781   * @param string $text UTF-8 string to process
3782   * @param boolean $bom whether to add the byte order marker
3783   * @return string UTF-16 result string
3784   */
3785  function  utf8toUtf16BE(&$text, $bom = true) {
3786    if (!$this->isUnicode) return $text;
3787    $out = $bom ? "\xFE\xFF" : '';
3788   
3789    $unicode = $this->utf8toCodePointsArray($text);
3790    foreach ($unicode as $c) {
3791      if ($c == 0xFFFD) {
3792        $out .= "\xFF\xFD"; // replacement character
3793      } elseif ($c < 0x10000) {
3794        $out .= chr($c >> 0x08);
3795        $out .= chr($c & 0xFF);
3796       } else {
3797        $c -= 0x10000;
3798        $w1 = 0xD800 | ($c >> 0x10);
3799        $w2 = 0xDC00 | ($c & 0x3FF);
3800        $out .= chr($w1 >> 0x08);
3801        $out .= chr($w1 & 0xFF);
3802        $out .= chr($w2 >> 0x08);
3803        $out .= chr($w2 & 0xFF);
3804      }
3805    }
3806    return $out;
3807  }
3808
3809
3810  /**
3811   * given a start position and information about how text is to be laid out, calculate where
3812   * on the page the text will end
3813   *
3814   * @access private
3815   */
3816  function  PRVTgetTextPosition($x, $y, $angle, $size, $wa, $text) {
3817
3818    // given this information return an array containing x and y for the end position as elements 0 and 1
3819    $w =  $this->getTextWidth($size, $text);
3820
3821    // need to adjust for the number of spaces in this text
3822    $words =  explode(' ', $text);
3823
3824    $nspaces =  count($words) -1;
3825
3826    $w+=  $wa*$nspaces;
3827
3828    $a =  deg2rad((float)$angle);
3829
3830    return  array(cos($a) *$w+$x, -sin($a) *$w+$y);
3831  }
3832
3833
3834  /**
3835   * wrapper function for PRVTcheckTextDirective1
3836   *
3837   * @access private
3838   */
3839  function  PRVTcheckTextDirective(&$text, $i, &$f) {
3840
3841    return  0;
3842
3843    $x =  0;
3844
3845    $y =  0;
3846
3847    return  $this->PRVTcheckTextDirective1($text, $i, $f, 0, $x, $y);
3848  }
3849
3850
3851  /**
3852   * checks if the text stream contains a control directive
3853   * if so then makes some changes and returns the number of characters involved in the directive
3854   * this has been re-worked to include everything neccesary to fins the current writing point, so that
3855   * the location can be sent to the callback function if required
3856   * if the directive does not require a font change, then $f should be set to 0
3857   *
3858   * @access private
3859   */
3860  function  PRVTcheckTextDirective1(&$text, $i, &$f, $final, &$x, &$y, $size =  0, $angle =  0, $wordSpaceAdjust =  0) {
3861
3862    return  0;
3863
3864    $directive =  0;
3865
3866    $j =  $i;
3867
3868    if  ($text[$j] ==  '<') {
3869
3870      ++$j;
3871
3872      switch ($text[$j]) {
3873
3874      case  '/':
3875
3876        ++$j;
3877
3878        if  (mb_strlen($text) <=  $j) {
3879
3880          return  $directive;
3881        }
3882
3883        switch ($text[$j]) {
3884
3885        case  'b':
3886
3887        case  'i':
3888
3889          ++$j;
3890
3891          if  ($text[$j] ==  '>') {
3892
3893            $p =  mb_strrpos($this->currentTextState, $text[$j-1]);
3894
3895            if  ($p !==  false) {
3896
3897              // then there is one to remove
3898              $this->currentTextState =  mb_substr($this->currentTextState, 0, $p) .substr($this->currentTextState, $p+1);
3899            }
3900
3901            $directive =  $j-$i+1;
3902          }
3903
3904          break;
3905
3906        case  'c':
3907
3908          // this this might be a callback function
3909          ++$j;
3910
3911          $k =  mb_strpos($text, '>', $j);
3912
3913          if  ($k !==  false &&  $text[$j] ==  ':') {
3914
3915            // then this will be treated as a callback directive
3916            $directive =  $k-$i+1;
3917
3918            $f =  0;
3919
3920            // split the remainder on colons to get the function name and the paramater
3921            $tmp =  mb_substr($text, $j+1, $k-$j-1);
3922
3923            $b1 =  mb_strpos($tmp, ':');
3924
3925            if  ($b1 !==  false) {
3926
3927              $func =  mb_substr($tmp, 0, $b1);
3928
3929              $parm =  mb_substr($tmp, $b1+1);
3930            } else {
3931
3932              $func =  $tmp;
3933
3934              $parm =  '';
3935            }
3936
3937            if  (!isset($func) ||  !mb_strlen(trim($func))) {
3938
3939              $directive =  0;
3940            } else {
3941
3942              // only call the function if this is the final call
3943              if  ($final) {
3944
3945                // need to assess the text position, calculate the text width to this point
3946                // can use getTextWidth to find the text width I think
3947                $tmp =  $this->PRVTgetTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, mb_substr($text, 0, $i));
3948
3949                $info =  array('x' => $tmp[0], 'y' => $tmp[1], 'angle' => $angle, 'status' => 'end', 'p' => $parm, 'nCallback' => $this->nCallback);
3950
3951                $x =  $tmp[0];
3952
3953                $y =  $tmp[1];
3954
3955                $ret =  $this->$func($info);
3956
3957                if  (is_array($ret)) {
3958
3959                  // then the return from the callback function could set the position, to start with, later will do font colour, and font
3960                  foreach($ret as  $rk => $rv) {
3961
3962                    switch ($rk) {
3963
3964                    case  'x':
3965
3966                    case  'y':
3967
3968                      $$rk =  $rv;
3969
3970                      break;
3971                    }
3972                  }
3973                }
3974
3975                // also remove from to the stack
3976                // for simplicity, just take from the end, fix this another day
3977                $this->nCallback--;
3978
3979                if  ($this->nCallback<0) {
3980
3981                  $this->nCallBack =  0;
3982                }
3983              }
3984            }
3985          }
3986
3987          break;
3988        }
3989
3990        break;
3991
3992      case  'b':
3993
3994      case  'i':
3995
3996        ++$j;
3997
3998        if  ($text[$j] ==  '>') {
3999
4000          $this->currentTextState.=  $text[$j-1];
4001
4002          $directive =  $j-$i+1;
4003        }
4004
4005        break;
4006
4007      case  'C':
4008
4009        $noClose =  1;
4010
4011      case  'c':
4012
4013        // this this might be a callback function
4014        ++$j;
4015
4016        $k =  mb_strpos($text, '>', $j);
4017
4018        if  ($k !==  false &&  $text[$j] ==  ':') {
4019
4020          // then this will be treated as a callback directive
4021          $directive =  $k-$i+1;
4022
4023          $f =  0;
4024
4025          // split the remainder on colons to get the function name and the paramater
4026          //          $bits = explode(':',substr($text,$j+1,$k-$j-1));
4027          $tmp =  mb_substr($text, $j+1, $k-$j-1);
4028
4029          $b1 =  mb_strpos($tmp, ':');
4030
4031          if  ($b1 !==  false) {
4032
4033            $func =  mb_substr($tmp, 0, $b1);
4034
4035            $parm =  mb_substr($tmp, $b1+1);
4036          } else {
4037
4038            $func =  $tmp;
4039
4040            $parm =  '';
4041          }
4042
4043          if  (!isset($func) ||  !mb_strlen(trim($func))) {
4044
4045            $directive =  0;
4046          } else {
4047
4048            // only call the function if this is the final call, ie, the one actually doing printing, not measurement
4049            if  ($final) {
4050
4051              // need to assess the text position, calculate the text width to this point
4052              // can use getTextWidth to find the text width I think
4053              // also add the text height and descender
4054              $tmp =  $this->PRVTgetTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, mb_substr($text, 0, $i));
4055
4056              $info =  array('x' => $tmp[0], 'y' => $tmp[1], 'angle' => $angle, 'status' => 'start', 'p' => $parm, 'f' => $func, 'height' => $this->getFontHeight($size), 'descender' => $this->getFontDescender($size));
4057
4058              $x =  $tmp[0];
4059
4060              $y =  $tmp[1];
4061
4062              if  (!isset($noClose) ||  !$noClose) {
4063
4064                // only add to the stack if this is a small 'c', therefore is a start-stop pair
4065                $this->nCallback++;
4066
4067                $info['nCallback'] =  $this->nCallback;
4068
4069                $this->callback[$this->nCallback] =  $info;
4070              }
4071
4072              $ret =  $this->$func($info);
4073
4074              if  (is_array($ret)) {
4075
4076                // then the return from the callback function could set the position, to start with, later will do font colour, and font
4077                foreach($ret as  $rk => $rv) {
4078
4079                  switch ($rk) {
4080
4081                  case  'x':
4082
4083                  case  'y':
4084
4085                    $$rk =  $rv;
4086
4087                    break;
4088                  }
4089                }
4090              }
4091            }
4092          }
4093        }
4094
4095        break;
4096      }
4097    }
4098
4099    return  $directive;
4100  }
4101
4102
4103  /**
4104   * add text to the document, at a specified location, size and angle on the page
4105   */
4106  function  addText($x, $y, $size, $text, $angle =  0, $wordSpaceAdjust =  0) {
4107    if  (!$this->numFonts) {
4108      $this->selectFont('./fonts/Helvetica');
4109    }
4110
4111
4112    // if there are any open callbacks, then they should be called, to show the start of the line
4113    if  ($this->nCallback>0) {
4114
4115      for  ($i =  $this->nCallback;$i>0;$i--) {
4116
4117        // call each function
4118        $info =  array('x' => $x,
4119                       'y' => $y,
4120                       'angle' => $angle,
4121                       'status' => 'sol',
4122                       'p' => $this->callback[$i]['p'],
4123                       'nCallback' => $this->callback[$i]['nCallback'],
4124                       'height' => $this->callback[$i]['height'],
4125                       'descender' => $this->callback[$i]['descender']);
4126
4127        $func =  $this->callback[$i]['f'];
4128
4129        $this->$func($info);
4130      }
4131    }
4132
4133    if  ($angle ==  0) {
4134
4135      $this->objects[$this->currentContents]['c'].=  "\n".'BT '.sprintf('%.3F', $x) .' '.sprintf('%.3F', $y) .' Td';
4136
4137    } else {
4138
4139      $a =  deg2rad((float)$angle);
4140
4141      $tmp =  "\n".'BT ';
4142
4143      $tmp.=  sprintf('%.3F', cos($a)) .' '.sprintf('%.3F', (-1.0*sin($a))) .' '.sprintf('%.3F', sin($a)) .' '.sprintf('%.3F', cos($a)) .' ';
4144
4145      $tmp.=  sprintf('%.3F', $x) .' '.sprintf('%.3F', $y) .' Tm';
4146
4147      $this->objects[$this->currentContents]['c'].=  $tmp;
4148    }
4149
4150    if  ($wordSpaceAdjust !=  0 ||  $wordSpaceAdjust !=  $this->wordSpaceAdjust) {
4151
4152      $this->wordSpaceAdjust =  $wordSpaceAdjust;
4153
4154      $this->objects[$this->currentContents]['c'].=  ' '.sprintf('%.3F', $wordSpaceAdjust) .' Tw';
4155    }
4156
4157    $len =  mb_strlen($text);
4158
4159    $start =  0;
4160
4161    /*
4162     for ($i = 0;$i<$len;++$i){
4163     $f = 1;
4164     $directive = 0; //$this->PRVTcheckTextDirective($text,$i,$f);
4165     if ($directive){
4166     // then we should write what we need to
4167     if ($i>$start){
4168     $part = mb_substr($text,$start,$i-$start);
4169     $this->objects[$this->currentContents]['c'] .= ' /F'.$this->currentFontNum.' '.sprintf('%.1F',$size).' Tf ';
4170     $this->objects[$this->currentContents]['c'] .= ' ('.$this->filterText($part, false).') Tj';
4171     }
4172     if ($f){
4173     // then there was nothing drastic done here, restore the contents
4174     $this->setCurrentFont();
4175     } else {
4176     $this->objects[$this->currentContents]['c'] .= ' ET';
4177     $f = 1;
4178     $xp = $x;
4179     $yp = $y;
4180     $directive = 0; //$this->PRVTcheckTextDirective1($text,$i,$f,1,$xp,$yp,$size,$angle,$wordSpaceAdjust);
4181
4182     // restart the text object
4183     if ($angle == 0){
4184     $this->objects[$this->currentContents]['c'] .= "\n".'BT '.sprintf('%.3F',$xp).' '.sprintf('%.3F',$yp).' Td';
4185     } else {
4186     $a = deg2rad((float)$angle);
4187     $tmp = "\n".'BT ';
4188     $tmp .= sprintf('%.3F',cos($a)).' '.sprintf('%.3F',(-1.0*sin($a))).' '.sprintf('%.3F',sin($a)).' '.sprintf('%.3F',cos($a)).' ';
4189     $tmp .= sprintf('%.3F',$xp).' '.sprintf('%.3F',$yp).' Tm';
4190     $this->objects[$this->currentContents]['c'] .= $tmp;
4191     }
4192     if ($wordSpaceAdjust != 0 || $wordSpaceAdjust != $this->wordSpaceAdjust){
4193     $this->wordSpaceAdjust = $wordSpaceAdjust;
4194     $this->objects[$this->currentContents]['c'] .= ' '.sprintf('%.3F',$wordSpaceAdjust).' Tw';
4195     }
4196     }
4197     // and move the writing point to the next piece of text
4198     $i = $i+$directive-1;
4199     $start = $i+1;
4200     }
4201
4202     }
4203    */
4204    if  ($start < $len) {
4205
4206      $part =  $text; // OAR - Don't need this anymore, given that $start always equals zero.  substr($text, $start);
4207
4208      $this->objects[$this->currentContents]['c'].=  ' /F'.$this->currentFontNum.' '.sprintf('%.1F', $size) .' Tf ';
4209
4210      $this->objects[$this->currentContents]['c'].=  ' ('.$this->filterText($part, false) .') Tj';
4211    }
4212
4213    $this->objects[$this->currentContents]['c'].=  ' ET';
4214
4215
4216    // if there are any open callbacks, then they should be called, to show the end of the line
4217    if  ($this->nCallback>0) {
4218
4219      for  ($i =  $this->nCallback;$i>0;$i--) {
4220
4221        // call each function
4222        $tmp =  $this->PRVTgetTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text);
4223
4224        $info =  array('x' => $tmp[0], 'y' => $tmp[1], 'angle' => $angle, 'status' => 'eol', 'p' => $this->callback[$i]['p'], 'nCallback' => $this->callback[$i]['nCallback'], 'height' => $this->callback[$i]['height'], 'descender' => $this->callback[$i]['descender']);
4225
4226        $func =  $this->callback[$i]['f'];
4227
4228        $this->$func($info);
4229      }
4230    }
4231  }
4232
4233
4234  /**
4235   * calculate how wide a given text string will be on a page, at a given size.
4236   * this can be called externally, but is alse used by the other class functions
4237   */
4238  function  getTextWidth($size, $text, $spacing =  0) {
4239
4240    // this function should not change any of the settings, though it will need to
4241    // track any directives which change during calculation, so copy them at the start
4242    // and put them back at the end.
4243    $store_currentTextState =  $this->currentTextState;
4244
4245
4246    if  (!$this->numFonts) {
4247      $this->selectFont('./fonts/Helvetica');
4248    }
4249
4250
4251    // converts a number or a float to a string so it can get the width
4252    $text =  "$text";
4253
4254    // hmm, this is where it all starts to get tricky - use the font information to
4255    // calculate the width of each character, add them up and convert to user units
4256    $w =  0;
4257
4258    $cf =  $this->currentFont;
4259
4260    $space_scale =  1000 / $size;
4261
4262    if ( $this->isUnicode) {
4263      // for Unicode, use the code points array to calculate width rather
4264      // than just the string itself
4265      $unicode =  $this->utf8toCodePointsArray($text);
4266
4267      foreach ($unicode as $char) {
4268        // check if we have to replace character
4269      if  ( isset($this->fonts[$cf]['differences'][$char])) {
4270          $char =  $this->fonts[$cf]['differences'][$char];
4271        }
4272        // add the character width
4273        if  ( isset($this->fonts[$cf]['C'][$char]['WX'])) {
4274          $w+=  $this->fonts[$cf]['C'][$char]['WX'];
4275        }
4276        // add additional padding for space
4277        if  ( $char ==  32) {  // Space
4278          $w+=  $spacing * $space_scale;
4279        }
4280      }
4281
4282    } else {
4283
4284      $len =  mb_strlen($text);
4285
4286      for  ($i =  0; $i < $len; ++$i) {
4287        $char =  ord($text{$i});
4288        // check if we have to replace character
4289        if  ( isset($this->fonts[$cf]['differences'][$char])) {
4290          $char =  $this->fonts[$cf]['differences'][$char];
4291        }
4292        // add the character width
4293        if  ( isset($this->fonts[$cf]['C'][$char]['WX'])) {
4294        $w+=  $this->fonts[$cf]['C'][$char]['WX'];
4295        }
4296        // add additional padding for space
4297        if  ( $char ==  32) {  // Space
4298        $w+=  $spacing * $space_scale;
4299    }
4300      }
4301    }
4302
4303
4304    $this->currentTextState =  $store_currentTextState;
4305
4306    $this->setCurrentFont();
4307
4308
4309    return  $w*$size/1000;
4310  }
4311
4312
4313  /**
4314   * do a part of the calculation for sorting out the justification of the text
4315   *
4316   * @access private
4317   */
4318  function  PRVTadjustWrapText($text, $actual, $width, &$x, &$adjust, $justification) {
4319
4320    switch  ($justification) {
4321
4322    case  'left':
4323
4324      return;
4325
4326      break;
4327
4328    case  'right':
4329
4330      $x+=  $width-$actual;
4331
4332      break;
4333
4334    case  'center':
4335
4336    case  'centre':
4337
4338      $x+=  ($width-$actual) /2;
4339
4340      break;
4341
4342    case  'full':
4343
4344      // count the number of words
4345      $words =  explode(' ', $text);
4346
4347      $nspaces =  count($words) -1;
4348
4349      if  ($nspaces>0) {
4350
4351        $adjust =  ($width-$actual) /$nspaces;
4352      } else {
4353
4354        $adjust =  0;
4355      }
4356
4357      break;
4358    }
4359  }
4360
4361
4362  /**
4363   * add text to the page, but ensure that it fits within a certain width
4364   * if it does not fit then put in as much as possible, splitting at word boundaries
4365   * and return the remainder.
4366   * justification and angle can also be specified for the text
4367   */
4368  function  addTextWrap($x, $y, $width, $size, $text, $justification =  'left', $angle =  0, $test =  0) {
4369        // TODO - need to support Unicode
4370    if ($this->isUnicode) {
4371        die("addTextWrap does not support Unicode yet!");
4372    }
4373
4374    // this will display the text, and if it goes beyond the width $width, will backtrack to the
4375    // previous space or hyphen, and return the remainder of the text.
4376
4377    // $justification can be set to 'left','right','center','centre','full'
4378
4379    // need to store the initial text state, as this will change during the width calculation
4380    // but will need to be re-set before printing, so that the chars work out right
4381    $store_currentTextState =  $this->currentTextState;
4382
4383
4384    if  (!$this->numFonts) {
4385      $this->selectFont('./fonts/Helvetica');
4386    }
4387
4388    if  ($width <=  0) {
4389
4390      // error, pretend it printed ok, otherwise risking a loop
4391      return  '';
4392    }
4393
4394    $w =  0;
4395
4396    $break =  0;
4397
4398    $breakWidth =  0;
4399
4400    $len =  mb_strlen($text);
4401
4402    $cf =  $this->currentFont;
4403
4404    $tw =  $width/$size*1000;
4405
4406    for  ($i =  0;$i<$len;++$i) {
4407
4408      $f =  1;
4409
4410      $directive =  0;
4411      //$this->PRVTcheckTextDirective($text,$i,$f);
4412      if  ($directive) {
4413
4414        if  ($f) {
4415
4416          $this->setCurrentFont();
4417
4418          $cf =  $this->currentFont;
4419        }
4420
4421        $i =  $i+$directive-1;
4422      } else {
4423
4424        $cOrd =  ord($text[$i]);
4425
4426        if  (isset($this->fonts[$cf]['differences'][$cOrd])) {
4427
4428          // then this character is being replaced by another
4429          $cOrd2 =  $this->fonts[$cf]['differences'][$cOrd];
4430        } else {
4431
4432          $cOrd2 =  $cOrd;
4433        }
4434
4435
4436        if  (isset($this->fonts[$cf]['C'][$cOrd2]['WX'])) {
4437
4438          $w+=  $this->fonts[$cf]['C'][$cOrd2]['WX'];
4439        }
4440
4441        if  ($w>$tw) {
4442
4443          // then we need to truncate this line
4444          if  ($break>0) {
4445
4446            // then we have somewhere that we can split :)
4447            if  ($text[$break] ==  ' ') {
4448
4449              $tmp =  mb_substr($text, 0, $break);
4450            } else {
4451
4452              $tmp =  mb_substr($text, 0, $break+1);
4453            }
4454
4455            $adjust =  0;
4456
4457            $this->PRVTadjustWrapText($tmp, $breakWidth, $width, $x, $adjust, $justification);
4458
4459
4460            // reset the text state
4461            $this->currentTextState =  $store_currentTextState;
4462
4463            $this->setCurrentFont();
4464
4465            if  (!$test) {
4466
4467              $this->addText($x, $y, $size, $tmp, $angle, $adjust);
4468            }
4469
4470            return  mb_substr($text, $break+1);
4471          } else {
4472
4473            // just split before the current character
4474            $tmp =  mb_substr($text, 0, $i);
4475
4476            $adjust =  0;
4477
4478            $ctmp =  ord($text[$i]);
4479
4480            if  (isset($this->fonts[$cf]['differences'][$ctmp])) {
4481
4482              $ctmp =  $this->fonts[$cf]['differences'][$ctmp];
4483            }
4484
4485            $tmpw =  ($w-$this->fonts[$cf]['C'][$ctmp]['WX']) *$size/1000;
4486
4487            $this->PRVTadjustWrapText($tmp, $tmpw, $width, $x, $adjust, $justification);
4488
4489            // reset the text state
4490            $this->currentTextState =  $store_currentTextState;
4491
4492            $this->setCurrentFont();
4493
4494            if  (!$test) {
4495
4496              $this->addText($x, $y, $size, $tmp, $angle, $adjust);
4497            }
4498
4499            return  mb_substr($text, $i);
4500          }
4501        }
4502
4503        if  ($text[$i] ==  '-') {
4504
4505          $break =  $i;
4506
4507          $breakWidth =  $w*$size/1000;
4508        }
4509
4510        if  ($text[$i] ==  ' ') {
4511
4512          $break =  $i;
4513
4514          $ctmp =  ord($text[$i]);
4515
4516          if  (isset($this->fonts[$cf]['differences'][$ctmp])) {
4517
4518            $ctmp =  $this->fonts[$cf]['differences'][$ctmp];
4519          }
4520
4521          $breakWidth =  ($w-$this->fonts[$cf]['C'][$ctmp]['WX']) *$size/1000;
4522        }
4523      }
4524    }
4525
4526    // then there was no need to break this line
4527    if  ($justification ==  'full') {
4528
4529      $justification =  'left';
4530    }
4531
4532    $adjust =  0;
4533
4534    $tmpw =  $w*$size/1000;
4535
4536    $this->PRVTadjustWrapText($text, $tmpw, $width, $x, $adjust, $justification);
4537
4538    // reset the text state
4539    $this->currentTextState =  $store_currentTextState;
4540
4541    $this->setCurrentFont();
4542
4543    if  (!$test) {
4544
4545      $this->addText($x, $y, $size, $text, $angle, $adjust, $angle);
4546    }
4547
4548    return  '';
4549  }
4550
4551
4552  /**
4553   * this will be called at a new page to return the state to what it was on the
4554   * end of the previous page, before the stack was closed down
4555   * This is to get around not being able to have open 'q' across pages
4556   *
4557   */
4558  function  saveState($pageEnd =  0) {
4559
4560    if  ($pageEnd) {
4561
4562      // this will be called at a new page to return the state to what it was on the
4563      // end of the previous page, before the stack was closed down
4564      // This is to get around not being able to have open 'q' across pages
4565      $opt =  $this->stateStack[$pageEnd];
4566      // ok to use this as stack starts numbering at 1
4567      $this->setColor($opt['col']['r'], $opt['col']['g'], $opt['col']['b'], 1);
4568
4569      $this->setStrokeColor($opt['str']['r'], $opt['str']['g'], $opt['str']['b'], 1);
4570
4571      $this->objects[$this->currentContents]['c'].=  "\n".$opt['lin'];
4572
4573      //    $this->currentLineStyle = $opt['lin'];
4574
4575    } else {
4576
4577      $this->nStateStack++;
4578
4579      $this->stateStack[$this->nStateStack] =  array(
4580                                                     'col' => $this->currentColour, 'str' => $this->currentStrokeColour, 'lin' => $this->currentLineStyle);
4581    }
4582
4583    $this->objects[$this->currentContents]['c'].=  "\nq";
4584  }
4585
4586
4587  /**
4588   * restore a previously saved state
4589   */
4590  function  restoreState($pageEnd =  0) {
4591
4592    if  (!$pageEnd) {
4593
4594      $n =  $this->nStateStack;
4595
4596      $this->currentColour =  $this->stateStack[$n]['col'];
4597
4598      $this->currentStrokeColour =  $this->stateStack[$n]['str'];
4599
4600      $this->objects[$this->currentContents]['c'].=  "\n".$this->stateStack[$n]['lin'];
4601
4602      $this->currentLineStyle =  $this->stateStack[$n]['lin'];
4603
4604      unset($this->stateStack[$n]);
4605
4606      $this->nStateStack--;
4607    }
4608
4609    $this->objects[$this->currentContents]['c'].=  "\nQ";
4610  }
4611
4612
4613  /**
4614   * make a loose object, the output will go into this object, until it is closed, then will revert to
4615   * the current one.
4616   * this object will not appear until it is included within a page.
4617   * the function will return the object number
4618   */
4619  function  openObject() {
4620
4621    $this->nStack++;
4622
4623    $this->stack[$this->nStack] =  array('c' => $this->currentContents, 'p' => $this->currentPage);
4624
4625    // add a new object of the content type, to hold the data flow
4626    $this->numObj++;
4627
4628    $this->o_contents($this->numObj, 'new');
4629
4630    $this->currentContents =  $this->numObj;
4631
4632    $this->looseObjects[$this->numObj] =  1;
4633
4634
4635    return  $this->numObj;
4636  }
4637
4638
4639  /**
4640   * open an existing object for editing
4641   */
4642  function  reopenObject($id) {
4643
4644    $this->nStack++;
4645
4646    $this->stack[$this->nStack] =  array('c' => $this->currentContents, 'p' => $this->currentPage);
4647
4648    $this->currentContents =  $id;
4649
4650    // also if this object is the primary contents for a page, then set the current page to its parent
4651    if  (isset($this->objects[$id]['onPage'])) {
4652
4653      $this->currentPage =  $this->objects[$id]['onPage'];
4654    }
4655  }
4656
4657
4658  /**
4659   * close an object
4660   */
4661  function  closeObject() {
4662
4663    // close the object, as long as there was one open in the first place, which will be indicated by
4664    // an objectId on the stack.
4665    if  ($this->nStack>0) {
4666
4667      $this->currentContents =  $this->stack[$this->nStack]['c'];
4668
4669      $this->currentPage =  $this->stack[$this->nStack]['p'];
4670
4671      $this->nStack--;
4672
4673      // easier to probably not worry about removing the old entries, they will be overwritten
4674      // if there are new ones.
4675
4676    }
4677  }
4678
4679
4680  /**
4681   * stop an object from appearing on pages from this point on
4682   */
4683  function  stopObject($id) {
4684
4685    // if an object has been appearing on pages up to now, then stop it, this page will
4686    // be the last one that could contian it.
4687    if  (isset($this->addLooseObjects[$id])) {
4688
4689      $this->addLooseObjects[$id] =  '';
4690    }
4691  }
4692
4693
4694  /**
4695   * after an object has been created, it wil only show if it has been added, using this function.
4696   */
4697  function  addObject($id, $options =  'add') {
4698
4699    // add the specified object to the page
4700    if  (isset($this->looseObjects[$id]) &&  $this->currentContents !=  $id) {
4701
4702      // then it is a valid object, and it is not being added to itself
4703      switch ($options) {
4704
4705      case  'all':
4706
4707        // then this object is to be added to this page (done in the next block) and
4708        // all future new pages.
4709        $this->addLooseObjects[$id] =  'all';
4710
4711      case  'add':
4712
4713        if  (isset($this->objects[$this->currentContents]['onPage'])) {
4714
4715          // then the destination contents is the primary for the page
4716          // (though this object is actually added to that page)
4717          $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id);
4718        }
4719
4720        break;
4721
4722      case  'even':
4723
4724        $this->addLooseObjects[$id] =  'even';
4725
4726        $pageObjectId =  $this->objects[$this->currentContents]['onPage'];
4727
4728        if  ($this->objects[$pageObjectId]['info']['pageNum']%2 ==  0) {
4729
4730          $this->addObject($id);
4731          // hacky huh :)
4732
4733        }
4734
4735        break;
4736
4737      case  'odd':
4738
4739        $this->addLooseObjects[$id] =  'odd';
4740
4741        $pageObjectId =  $this->objects[$this->currentContents]['onPage'];
4742
4743        if  ($this->objects[$pageObjectId]['info']['pageNum']%2 ==  1) {
4744
4745          $this->addObject($id);
4746          // hacky huh :)
4747
4748        }
4749
4750        break;
4751
4752      case  'next':
4753
4754        $this->addLooseObjects[$id] =  'all';
4755
4756        break;
4757
4758      case  'nexteven':
4759
4760        $this->addLooseObjects[$id] =  'even';
4761
4762        break;
4763
4764      case  'nextodd':
4765
4766        $this->addLooseObjects[$id] =  'odd';
4767
4768        break;
4769      }
4770    }
4771  }
4772
4773
4774  /**
4775   * return a storable representation of a specific object
4776   */
4777  function  serializeObject($id) {
4778    if  ( array_key_exists($id,  $this->objects))
4779      return  var_export($this->objects[$id],  true);
4780
4781
4782    return  null;
4783  }
4784
4785
4786  /**
4787   * restore an object from its stored representation.  returns its new object id.
4788   */
4789  function  restoreSerializedObject($obj) {
4790
4791
4792    $obj_id =  $this->openObject();
4793
4794    eval('$this->objects[$obj_id] = ' . $obj . ';');
4795
4796    $this->closeObject();
4797
4798    return  $obj_id;
4799  }
4800
4801
4802
4803  /**
4804   * add content to the documents info object
4805   */
4806  function  addInfo($label, $value =  0) {
4807
4808    // this will only work if the label is one of the valid ones.
4809    // modify this so that arrays can be passed as well.
4810    // if $label is an array then assume that it is key => value pairs
4811    // else assume that they are both scalar, anything else will probably error
4812    if  (is_array($label)) {
4813
4814      foreach ($label as  $l => $v) {
4815
4816        $this->o_info($this->infoObject, $l, $v);
4817      }
4818    } else {
4819
4820      $this->o_info($this->infoObject, $label, $value);
4821    }
4822  }
4823
4824
4825  /**
4826   * set the viewer preferences of the document, it is up to the browser to obey these.
4827   */
4828  function  setPreferences($label, $value =  0) {
4829
4830    // this will only work if the label is one of the valid ones.
4831    if  (is_array($label)) {
4832
4833      foreach ($label as  $l => $v) {
4834
4835        $this->o_catalog($this->catalogId, 'viewerPreferences', array($l => $v));
4836      }
4837    } else {
4838
4839      $this->o_catalog($this->catalogId, 'viewerPreferences', array($label => $value));
4840    }
4841  }
4842
4843
4844  /**
4845   * extract an integer from a position in a byte stream
4846   *
4847   * @access private
4848   */
4849  function  PRVT_getBytes(&$data, $pos, $num) {
4850
4851    // return the integer represented by $num bytes from $pos within $data
4852    $ret =  0;
4853
4854    for  ($i =  0;$i<$num;++$i) {
4855
4856      $ret =  $ret*256;
4857
4858      $ret+=  ord($data[$pos+$i]);
4859    }
4860
4861    return  $ret;
4862  }
4863
4864
4865  /**
4866   * add a PNG image into the document, from a GD object
4867   * this should work with remote files
4868   */
4869  function addImagePng($file, $x, $y, $w =  0, $h =  0, &$img) {
4870    //if already cached, need not to read again
4871        if ( isset($this->imagelist[$file]) ) {
4872          $data = null;
4873        } else {
4874          // Example for transparency handling on new image. Retain for current image
4875      // $tIndex = imagecolortransparent($img);
4876      // if ($tIndex > 0) {
4877      //   $tColor    = imagecolorsforindex($img, $tIndex);
4878      //   $new_tIndex    = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']);
4879      //   imagefill($new_img, 0, 0, $new_tIndex);
4880      //   imagecolortransparent($new_img, $new_tIndex);
4881      // }
4882          // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn
4883          //imagealphablending($img, true);
4884          //default, but explicitely set to ensure pdf compatibility
4885      imagesavealpha($img, false);
4886     
4887      $error =  0;
4888      //DEBUG_IMG_TEMP
4889      //debugpng
4890      if (DEBUGPNG) print '[addImagePng '.$file.']';
4891
4892      ob_start();
4893      @imagepng($img);
4894      //$data = ob_get_contents(); ob_end_clean();
4895      $data = ob_get_clean();
4896
4897      if ($data == '') {
4898        $error = 1;
4899        $errormsg = 'trouble writing file from GD';
4900        //DEBUG_IMG_TEMP
4901        //debugpng
4902        if (DEBUGPNG) print 'trouble writing file from GD';
4903          }
4904
4905      if  ($error) {
4906        $this->addMessage('PNG error - ('.$file.') '.$errormsg);
4907        return;
4908      }
4909    }  //End isset($this->imagelist[$file]) (png Duplicate removal)
4910
4911    $this->addPngFromBuf($file, $x, $y, $w, $h, $data);
4912  }
4913
4914
4915  /**
4916   * add a PNG image into the document, from a file
4917   * this should work with remote files
4918   */
4919  function  addPngFromFile($file, $x, $y, $w =  0, $h =  0) {
4920    //if already cached, need not to read again
4921        if ( isset($this->imagelist[$file]) ) {
4922          $img = null;
4923        } else {
4924      //png files typically contain an alpha channel.
4925      //pdf file format or class.pdf does not support alpha blending.
4926      //on alpha blended images, more transparent areas have a color near black.
4927      //This appears in the result on not storing the alpha channel.
4928      //Correct would be the box background image or its parent when transparent.
4929      //But this would make the image dependent on the background.
4930      //Therefore create an image with white background and copy in
4931      //A more natural background than black is white.
4932      //Therefore create an empty image with white background and merge the
4933      //image in with alpha blending.
4934      $imgtmp = @imagecreatefrompng($file);
4935      if (!$imgtmp) {
4936        return;
4937      }
4938      $sx = imagesx($imgtmp);
4939      $sy = imagesy($imgtmp);
4940      $img = imagecreatetruecolor($sx,$sy);
4941      imagealphablending($img, true);
4942          $ti = imagecolortransparent($imgtmp);
4943          if ($ti >= 0) {
4944            $tc = imagecolorsforindex($imgtmp,$ti);
4945        $ti = imagecolorallocate($img,$tc['red'],$tc['green'],$tc['blue']);
4946        imagefill($img,0,0,$ti);
4947        imagecolortransparent($img, $ti);
4948      } else {
4949        imagefill($img,1,1,imagecolorallocate($img,255,255,255));
4950      }
4951      imagecopy($img,$imgtmp,0,0,0,0,$sx,$sy);
4952      imagedestroy($imgtmp);
4953    }
4954    $this->addImagePng($file, $x, $y, $w, $h, $img);
4955  }
4956
4957
4958  /**
4959   * add a PNG image into the document, from a memory buffer of the file
4960   */
4961  function  addPngFromBuf($file, $x, $y, $w =  0, $h =  0, &$data) {
4962
4963        if ( isset($this->imagelist[$file]) ) {
4964      //debugpng
4965      //if (DEBUGPNG) print '[addPngFromBuf Duplicate '.$file.']';
4966          $data = null;
4967      $info['width'] = $this->imagelist[$file]['w'];
4968      $info['height'] = $this->imagelist[$file]['h'];
4969      $label = $this->imagelist[$file]['label'];
4970
4971        } else {
4972     
4973      if ($data == null) {
4974        $this->addMessage('addPngFromBuf error - ('.$imgname.') data not present!');
4975        return;
4976      }
4977      //debugpng
4978      //if (DEBUGPNG) print '[addPngFromBuf file='.$file.']';
4979    $error =  0;
4980
4981    if  (!$error) {
4982
4983      $header =  chr(137) .chr(80) .chr(78) .chr(71) .chr(13) .chr(10) .chr(26) .chr(10);
4984
4985      if  (mb_substr($data, 0, 8, '8bit') !=  $header) {
4986
4987        $error =  1;
4988
4989        //debugpng
4990        if (DEBUGPNG) print '[addPngFromFile this file does not have a valid header '.$file.']';
4991
4992        $errormsg =  'this file does not have a valid header';
4993      }
4994    }
4995
4996
4997    if  (!$error) {
4998
4999      // set pointer
5000      $p =  8;
5001
5002      $len =  mb_strlen($data, '8bit');
5003
5004      // cycle through the file, identifying chunks
5005      $haveHeader =  0;
5006
5007      $info =  array();
5008
5009      $idata =  '';
5010
5011      $pdata =  '';
5012
5013      while  ($p < $len) {
5014
5015        $chunkLen =  $this->PRVT_getBytes($data, $p, 4);
5016
5017        $chunkType =  mb_substr($data, $p+4, 4, '8bit');
5018
5019        //      echo $chunkType.' - '.$chunkLen.'<br>';
5020
5021        switch ($chunkType) {
5022
5023        case  'IHDR':
5024
5025          // this is where all the file information comes from
5026          $info['width'] =  $this->PRVT_getBytes($data, $p+8, 4);
5027
5028          $info['height'] =  $this->PRVT_getBytes($data, $p+12, 4);
5029
5030          $info['bitDepth'] =  ord($data[$p+16]);
5031
5032          $info['colorType'] =  ord($data[$p+17]);
5033
5034          $info['compressionMethod'] =  ord($data[$p+18]);
5035
5036          $info['filterMethod'] =  ord($data[$p+19]);
5037
5038          $info['interlaceMethod'] =  ord($data[$p+20]);
5039
5040          //print_r($info);
5041          $haveHeader =  1;
5042
5043          if  ($info['compressionMethod'] !=  0) {
5044
5045            $error =  1;
5046
5047            //debugpng
5048            if (DEBUGPNG) print '[addPngFromFile unsupported compression method '.$file.']';
5049
5050            $errormsg =  'unsupported compression method';
5051          }
5052
5053          if  ($info['filterMethod'] !=  0) {
5054
5055            $error =  1;
5056
5057            //debugpng
5058            if (DEBUGPNG) print '[addPngFromFile unsupported filter method '.$file.']';
5059
5060            $errormsg =  'unsupported filter method';
5061          }
5062
5063          break;
5064
5065        case  'PLTE':
5066
5067          $pdata.=  mb_substr($data, $p+8, $chunkLen, '8bit');
5068
5069          break;
5070
5071        case  'IDAT':
5072
5073          $idata.=  mb_substr($data, $p+8, $chunkLen, '8bit');
5074
5075          break;
5076
5077        case  'tRNS':
5078
5079          //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk
5080          //print "tRNS found, color type = ".$info['colorType']."\n";
5081          $transparency =  array();
5082
5083          if  ($info['colorType'] ==  3) {
5084            // indexed color, rbg
5085            /* corresponding to entries in the plte chunk
5086             Alpha for palette index 0: 1 byte
5087             Alpha for palette index 1: 1 byte
5088             ...etc...
5089            */
5090            // there will be one entry for each palette entry. up until the last non-opaque entry.
5091            // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent)
5092            $transparency['type'] =  'indexed';
5093
5094            $numPalette =  mb_strlen($pdata, '8bit') /3;
5095
5096            $trans =  0;
5097
5098            for  ($i =  $chunkLen;$i >=  0;$i--) {
5099
5100              if  (ord($data[$p+8+$i]) ==  0) {
5101
5102                $trans =  $i;
5103              }
5104            }
5105
5106            $transparency['data'] =  $trans;
5107          } elseif ($info['colorType'] ==  0) {
5108            // grayscale
5109            /* corresponding to entries in the plte chunk
5110             Gray: 2 bytes, range 0 .. (2^bitdepth)-1
5111            */
5112            //            $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale
5113            $transparency['type'] =  'indexed';
5114
5115            $transparency['data'] =  ord($data[$p+8+1]);
5116          } elseif ($info['colorType'] ==  2) {
5117            // truecolor
5118            /* corresponding to entries in the plte chunk
5119             Red: 2 bytes, range 0 .. (2^bitdepth)-1
5120             Green: 2 bytes, range 0 .. (2^bitdepth)-1
5121             Blue: 2 bytes, range 0 .. (2^bitdepth)-1
5122            */
5123            $transparency['r'] =  $this->PRVT_getBytes($data, $p+8, 2);
5124            // r from truecolor
5125            $transparency['g'] =  $this->PRVT_getBytes($data, $p+10, 2);
5126            // g from truecolor
5127            $transparency['b'] =  $this->PRVT_getBytes($data, $p+12, 2);
5128            // b from truecolor
5129
5130            $transparency['type'] = 'color-key';
5131           
5132          } else {
5133
5134            //unsupported transparency type
5135
5136            //debugpng
5137            if (DEBUGPNG) print '[addPngFromFile unsupported transparency type '.$file.']';
5138          }
5139
5140          // KS End new code
5141          break;
5142
5143        default:
5144
5145          break;
5146        }
5147
5148
5149        $p+=  $chunkLen+12;
5150      }
5151
5152
5153      if (!$haveHeader) {
5154
5155        $error =  1;
5156
5157        //debugpng
5158        if (DEBUGPNG) print '[addPngFromFile information header is missing '.$file.']';
5159
5160        $errormsg =  'information header is missing';
5161      }
5162
5163      if  (isset($info['interlaceMethod']) &&  $info['interlaceMethod']) {
5164
5165        $error =  1;
5166
5167        //debugpng
5168        if (DEBUGPNG) print '[addPngFromFile no support for interlaced images in pdf '.$file.']';
5169
5170        $errormsg =  'There appears to be no support for interlaced images in pdf.';
5171      }
5172    }
5173
5174
5175    if  (!$error &&  $info['bitDepth'] > 8) {
5176
5177      $error =  1;
5178
5179      //debugpng
5180      if (DEBUGPNG) print '[addPngFromFile bit depth of 8 or less is supported '.$file.']';
5181
5182      $errormsg =  'only bit depth of 8 or less is supported';
5183    }
5184
5185
5186    if  (!$error) {
5187
5188      if  ($info['colorType'] !=  2 &&  $info['colorType'] !=  0 &&  $info['colorType'] !=  3) {
5189
5190        $error =  1;
5191
5192        //debugpng
5193        if (DEBUGPNG) print '[addPngFromFile alpha channel not supported: '.$info['colorType'].' '.$file.']';
5194
5195        $errormsg =  'transparancey alpha channel not supported, transparency only supported for palette images.';
5196      } else {
5197
5198        switch  ($info['colorType']) {
5199
5200        case  3:
5201
5202          $color =  'DeviceRGB';
5203
5204          $ncolor =  1;
5205
5206          break;
5207
5208        case  2:
5209
5210          $color =  'DeviceRGB';
5211
5212          $ncolor =  3;
5213
5214          break;
5215
5216        case  0:
5217
5218          $color =  'DeviceGray';
5219
5220          $ncolor =  1;
5221
5222          break;
5223        }
5224      }
5225    }
5226
5227    if  ($error) {
5228
5229      $this->addMessage('PNG error - ('.$file.') '.$errormsg);
5230
5231      return;
5232    }
5233
5234      //print_r($info);
5235      // so this image is ok... add it in.
5236      $this->numImages++;
5237
5238      $im =  $this->numImages;
5239
5240      $label =  'I'.$im;
5241
5242      $this->numObj++;
5243
5244      //  $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width']));
5245      $options =  array('label' => $label, 'data' => $idata, 'bitsPerComponent' => $info['bitDepth'], 'pdata' => $pdata, 'iw' => $info['width'], 'ih' => $info['height'], 'type' => 'png', 'color' => $color, 'ncolor' => $ncolor);
5246
5247      if  (isset($transparency)) {
5248
5249        $options['transparency'] =  $transparency;
5250      }
5251
5252      $this->o_image($this->numObj, 'new', $options);
5253
5254      $this->imagelist[$file] = array('label' =>$label, 'w' => $info['width'], 'h' => $info['height']);
5255    }
5256
5257    if  ($w <=  0 && $h <=  0) {
5258      $w =  $info['width'];
5259      $h =  $info['height'];
5260    }
5261
5262    if  ($w <=  0) {
5263
5264      $w =  $h/$info['height']*$info['width'];
5265    }
5266
5267    if  ($h <=  0) {
5268
5269      $h =  $w*$info['height']/$info['width'];
5270    }
5271
5272    $this->objects[$this->currentContents]['c'].=  "\nq";
5273
5274    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $w) ." 0 0 ".sprintf('%.3F', $h) ." ".sprintf('%.3F', $x) ." ".sprintf('%.3F', $y) ." cm";
5275
5276    $this->objects[$this->currentContents]['c'].=  "\n/".$label.' Do';
5277
5278    $this->objects[$this->currentContents]['c'].=  "\nQ";
5279  }
5280
5281
5282  /**
5283   * add a JPEG image into the document, from a file
5284   */
5285  function  addJpegFromFile($img, $x, $y, $w =  0, $h =  0) {
5286
5287    // attempt to add a jpeg image straight from a file, using no GD commands
5288    // note that this function is unable to operate on a remote file.
5289
5290    if  (!file_exists($img)) {
5291
5292      return;
5293    }
5294
5295        if ( isset($this->imagelist[$img]) ) {
5296          $data = null;
5297      $imageWidth = $this->imagelist[$img]['w'];
5298      $imageHeight = $this->imagelist[$img]['h'];
5299      $channels =  $this->imagelist[$img]['c'];
5300        } else {
5301
5302      $tmp =  getimagesize($img);
5303
5304      $imageWidth =  $tmp[0];
5305
5306      $imageHeight =  $tmp[1];
5307
5308
5309      if  (isset($tmp['channels'])) {
5310
5311        $channels =  $tmp['channels'];
5312      } else {
5313
5314        $channels =  3;
5315      }
5316
5317
5318      //$fp = fopen($img,'rb');
5319
5320      $data =  file_get_contents($img);
5321
5322      //fread($fp,filesize($img));
5323
5324      //fclose($fp);
5325    }
5326
5327    if  ($w <=  0 &&  $h <=  0) {
5328
5329      $w =  $imageWidth;
5330    }
5331
5332    if  ($w ==  0) {
5333
5334      $w =  $h/$imageHeight*$imageWidth;
5335    }
5336
5337    if  ($h ==  0) {
5338
5339      $h =  $w*$imageHeight/$imageWidth;
5340    }
5341
5342    $this->addJpegImage_common($data, $x, $y, $w, $h, $imageWidth, $imageHeight, $channels, $img);
5343  }
5344
5345
5346  /**
5347   * add an image into the document, from a GD object
5348   * this function is not all that reliable, and I would probably encourage people to use
5349   * the file based functions
5350   */
5351  function  addImage(&$img, $x, $y, $w =  0, $h =  0, $quality =  75) {
5352
5353    /* Todo:
5354     * Pass in original filename as $imgname
5355     * If already cached like image_iscached(), allow empty $img
5356     * How to get w  and h in this case?
5357     * Then caller can check with image_iscached() whether generation of image is needed.
5358     *
5359     * But anyway, this function is not used!
5360     */
5361    $imgname = tempnam($this->tmp, "cpdf_img_").'.jpeg';
5362
5363    // add a new image into the current location, as an external object
5364    // add the image at $x,$y, and with width and height as defined by $w & $h
5365
5366    // note that this will only work with full colour images and makes them jpg images for display
5367    // later versions could present lossless image formats if there is interest.
5368
5369    // there seems to be some problem here in that images that have quality set above 75 do not appear
5370    // not too sure why this is, but in the meantime I have restricted this to 75.
5371    if  ($quality>75) {
5372
5373      $quality =  75;
5374    }
5375
5376
5377    // if the width or height are set to zero, then set the other one based on keeping the image
5378    // height/width ratio the same, if they are both zero, then give up :)
5379    $imageWidth =  imagesx($img);
5380
5381    $imageHeight =  imagesy($img);
5382
5383
5384    if  ($w <=  0 &&  $h <=  0) {
5385
5386      return;
5387    }
5388
5389    if  ($w ==  0) {
5390
5391      $w =  $h/$imageHeight*$imageWidth;
5392    }
5393
5394    if  ($h ==  0) {
5395
5396      $h =  $w*$imageHeight/$imageWidth;
5397    }
5398
5399    // gotta get the data out of the img..
5400    ob_start();
5401    imagejpeg($img, '', $quality);
5402    //$data = ob_get_contents(); ob_end_clean();
5403    $data = ob_get_clean();
5404
5405    $this->addJpegImage_common($data, $x, $y, $w, $h, $imageWidth, $imageHeight, $imgname);
5406  }
5407
5408
5409  /* Check if image already added to pdf image directory.
5410   * If yes, need not to create again (pass empty data)
5411   */
5412  function  image_iscached($imgname) {
5413    return isset($this->imagelist[$imgname]);
5414  }
5415
5416
5417  /**
5418   * common code used by the two JPEG adding functions
5419   *
5420   * @access private
5421   */
5422  function  addJpegImage_common(&$data, $x, $y, $w =  0, $h =  0, $imageWidth, $imageHeight, $channels =  3, $imgname) {
5423
5424    if ( isset($this->imagelist[$imgname]) ) {
5425      $label = $this->imagelist[$imgname]['label'];
5426      //debugpng
5427      //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']';
5428
5429    } else {
5430
5431      if ($data == null) {
5432        $this->addMessage('addJpegImage_common error - ('.$imgname.') data not present!');
5433        return;
5434      }
5435
5436      // note that this function is not to be called externally
5437      // it is just the common code between the GD and the file options
5438      $this->numImages++;
5439
5440      $im =  $this->numImages;
5441
5442      $label =  'I'.$im;
5443
5444      $this->numObj++;
5445
5446      $this->o_image($this->numObj, 'new', array('label' => $label, 'data' => &$data, 'iw' => $imageWidth, 'ih' => $imageHeight, 'channels' => $channels));
5447
5448      $this->imagelist[$imgname] = array('label' =>$label, 'w' => $imageWidth, 'h' => $imageHeight, 'c'=> $channels );
5449    }
5450
5451
5452    $this->objects[$this->currentContents]['c'].=  "\nq";
5453
5454    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $w) ." 0 0 ".sprintf('%.3F', $h) ." ".sprintf('%.3F', $x) ." ".sprintf('%.3F', $y) ." cm";
5455
5456    $this->objects[$this->currentContents]['c'].=  "\n/".$label.' Do';
5457
5458    $this->objects[$this->currentContents]['c'].=  "\nQ";
5459  }
5460
5461
5462  /**
5463   * specify where the document should open when it first starts
5464   */
5465  function  openHere($style, $a =  0, $b =  0, $c =  0) {
5466
5467    // this function will open the document at a specified page, in a specified style
5468    // the values for style, and the required paramters are:
5469    // 'XYZ'  left, top, zoom
5470    // 'Fit'
5471    // 'FitH' top
5472    // 'FitV' left
5473    // 'FitR' left,bottom,right
5474    // 'FitB'
5475    // 'FitBH' top
5476    // 'FitBV' left
5477    $this->numObj++;
5478
5479    $this->o_destination($this->numObj, 'new', array('page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c));
5480
5481    $id =  $this->catalogId;
5482
5483    $this->o_catalog($id, 'openHere', $this->numObj);
5484  }
5485
5486
5487  /**
5488   * create a labelled destination within the document
5489   */
5490  function  addDestination($label, $style, $a =  0, $b =  0, $c =  0) {
5491
5492    // associates the given label with the destination, it is done this way so that a destination can be specified after
5493    // it has been linked to
5494    // styles are the same as the 'openHere' function
5495    $this->numObj++;
5496
5497    $this->o_destination($this->numObj, 'new', array('page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c));
5498
5499    $id =  $this->numObj;
5500
5501    // store the label->idf relationship, note that this means that labels can be used only once
5502    $this->destinations["$label"] =  $id;
5503  }
5504
5505
5506  /**
5507   * define font families, this is used to initialize the font families for the default fonts
5508   * and for the user to add new ones for their fonts. The default bahavious can be overridden should
5509   * that be desired.
5510   */
5511  function  setFontFamily($family, $options =  '') {
5512
5513    if  (!is_array($options)) {
5514
5515      if  ($family ==  'init') {
5516
5517        // set the known family groups
5518        // these font families will be used to enable bold and italic markers to be included
5519        // within text streams. html forms will be used... <b></b> <i></i>
5520        $this->fontFamilies['Helvetica.afm'] =
5521          array('b' => 'Helvetica-Bold.afm',
5522                'i' => 'Helvetica-Oblique.afm',
5523                'bi' => 'Helvetica-BoldOblique.afm',
5524                'ib' => 'Helvetica-BoldOblique.afm');
5525
5526        $this->fontFamilies['Courier.afm'] =
5527          array('b' => 'Courier-Bold.afm',
5528                'i' => 'Courier-Oblique.afm',
5529                'bi' => 'Courier-BoldOblique.afm',
5530                'ib' => 'Courier-BoldOblique.afm');
5531
5532        $this->fontFamilies['Times-Roman.afm'] =
5533          array('b' => 'Times-Bold.afm',
5534                'i' => 'Times-Italic.afm',
5535                'bi' => 'Times-BoldItalic.afm',
5536                'ib' => 'Times-BoldItalic.afm');
5537      }
5538    } else {
5539
5540      // the user is trying to set a font family
5541      // note that this can also be used to set the base ones to something else
5542      if  (mb_strlen($family)) {
5543
5544        $this->fontFamilies[$family] =  $options;
5545      }
5546    }
5547  }
5548
5549
5550  /**
5551   * used to add messages for use in debugging
5552   */
5553  function  addMessage($message) {
5554
5555    $this->messages.=  $message."\n";
5556  }
5557
5558
5559  /**
5560   * a few functions which should allow the document to be treated transactionally.
5561   */
5562  function  transaction($action) {
5563
5564    switch  ($action) {
5565
5566    case  'start':
5567
5568      // store all the data away into the checkpoint variable
5569      $data =  get_object_vars($this);
5570
5571      $this->checkpoint =  $data;
5572
5573      unset($data);
5574
5575      break;
5576
5577    case  'commit':
5578
5579      if  (is_array($this->checkpoint) &&  isset($this->checkpoint['checkpoint'])) {
5580
5581        $tmp =  $this->checkpoint['checkpoint'];
5582
5583        $this->checkpoint =  $tmp;
5584
5585        unset($tmp);
5586      } else {
5587
5588        $this->checkpoint =  '';
5589      }
5590
5591      break;
5592
5593    case  'rewind':
5594
5595      // do not destroy the current checkpoint, but move us back to the state then, so that we can try again
5596      if  (is_array($this->checkpoint)) {
5597
5598        // can only abort if were inside a checkpoint
5599        $tmp =  $this->checkpoint;
5600
5601        foreach ($tmp as  $k => $v) {
5602
5603          if  ($k !=  'checkpoint') {
5604
5605            $this->$k =  $v;
5606          }
5607        }
5608
5609        unset($tmp);
5610      }
5611
5612      break;
5613
5614    case  'abort':
5615
5616      if  (is_array($this->checkpoint)) {
5617
5618        // can only abort if were inside a checkpoint
5619        $tmp =  $this->checkpoint;
5620
5621        foreach ($tmp as  $k => $v) {
5622
5623          $this->$k =  $v;
5624        }
5625
5626        unset($tmp);
5627      }
5628
5629      break;
5630    }
5631  }
5632}
5633// end of class
5634
5635?>
Note: See TracBrowser for help on using the repository browser.