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

Revision 2000, 145.4 KB checked in by amuller, 14 years ago (diff)

Ticket #597 - Implementação do módulo gerenciador de arquivos

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                  for  ($i = 1;$i<count($bits2);$i++) {
2618
2619                    $dtmp[$bits2[0]][] = $bits2[$i];
2620                  }
2621                } else  if  (count($bits2) == 2) {
2622
2623                  $dtmp[$bits2[0]] = $bits2[1];
2624                }
2625              }
2626            }
2627
2628            $cc = (int)$dtmp['C'];
2629            if  ($cc >= 0) {
2630
2631              $data['C'][$dtmp['C']] = $dtmp;
2632
2633              $data['C'][$dtmp['N']] = $dtmp;
2634            } else {
2635
2636              $data['C'][$dtmp['N']] = $dtmp;
2637            }
2638
2639            if  (!isset($data['MissingWidth']) && $cc == -1 && $dtmp['N'] == '.notdef') {
2640
2641              $data['MissingWidth'] = $width;
2642            }
2643           
2644            break;
2645
2646          case  'U': // Found in UFM files
2647            if ($this->isUnicode) {
2648              // U 827 ; WX 0 ; N squaresubnosp ; G 675 ;
2649              $bits = explode(';', trim($row));
2650             
2651              $dtmp = array();
2652
2653              foreach($bits as  $bit) {
2654
2655                $bits2 =  explode(' ', trim($bit));
2656
2657                if  (mb_strlen($bits2[0])) {
2658
2659                  if  (count($bits2) >2) {
2660
2661                    $dtmp[$bits2[0]] = array();
2662
2663                    for  ($i = 1;$i<count($bits2);$i++) {
2664
2665                      $dtmp[$bits2[0]][] = $bits2[$i];
2666                    }
2667                  } else  if  (count($bits2) == 2) {
2668
2669                    $dtmp[$bits2[0]] = $bits2[1];
2670                  }
2671                }
2672              }
2673
2674              $cc = (int)$dtmp['U'];
2675              $glyph = $dtmp['G'];
2676              $width = $dtmp['WX'];
2677              if  ($cc >= 0) {
2678                // Set values in CID to GID map
2679                if ($cc >= 0 && $cc < 0xFFFF && $glyph) {
2680                  $cidtogid{$cc*2} = chr($glyph >> 8);
2681                  $cidtogid{$cc*2 + 1} = chr($glyph & 0xFF);
2682                }
2683
2684                $data['C'][$dtmp['U']] = $dtmp;
2685
2686                $data['C'][$dtmp['N']] = $dtmp;
2687              } else {
2688
2689                $data['C'][$dtmp['N']] = $dtmp;
2690              }
2691             
2692              if  (!isset($data['MissingWidth']) && $cc == -1 && $dtmp['N'] == '.notdef') {
2693
2694                $data['MissingWidth'] = $width;
2695              }
2696            }
2697
2698            break;
2699
2700          case  'KPX':
2701
2702            //KPX Adieresis yacute -40
2703            $bits = explode(' ', trim($row));
2704
2705            $data['KPX'][$bits[1]][$bits[2]] = $bits[3];
2706
2707            break;
2708          }
2709        }
2710      }
2711
2712      //    echo $cidtogid; die("CIDtoGID Displayed!");
2713
2714      if  (function_exists('gzcompress') &&  $this->options['compression']) {
2715
2716        // then implement ZLIB based compression on CIDtoGID string
2717        $data['CIDtoGID_Compressed'] = true;
2718
2719        $cidtogid =  gzcompress($cidtogid,  6);
2720      }
2721      $data['CIDtoGID'] = base64_encode($cidtogid);
2722     
2723      $data['_version_'] = 1;
2724
2725      $this->fonts[$font] = $data;
2726
2727      //Because of potential trouble with php safe mode, expect that the folder already exists.
2728      //If not existing, this will hit performance because of missing cached results.
2729      if ( is_dir(substr($fontcache,0,-1)) ) {
2730        file_put_contents($fontcache . $cache_name,  '$this->fonts[$font]=' . var_export($data,  true)  . ';');
2731      }
2732    }
2733   
2734    if  (!isset($this->fonts[$font])) {
2735      $this->addMessage("openFont: no font file found for $font.  Do you need to run load_font.php?");
2736      //echo 'Font not Found '.$font;
2737    }
2738
2739    //pre_r($this->messages);
2740  }
2741
2742
2743  /**
2744   * if the font is not loaded then load it and make the required object
2745   * else just make it the current font
2746   * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding'
2747   * note that encoding='none' will need to be used for symbolic fonts
2748   * and 'differences' => an array of mappings between numbers 0->255 and character names.
2749   *
2750   */
2751  function  selectFont($fontName, $encoding =  '', $set =  true) {
2752
2753    $ext = substr($fontName, -4);
2754    if  ($ext == '.afm' || $ext == '.ufm') {
2755      $fontName = substr($fontName, 0, mb_strlen($fontName)-4);
2756    }
2757
2758    if  (!isset($this->fonts[$fontName])) {
2759      $this->addMessage("selectFont: selecting - $fontName - $encoding, $set");
2760
2761      // load the file
2762      $this->openFont($fontName);
2763
2764      if  (isset($this->fonts[$fontName])) {
2765
2766        $this->numObj++;
2767
2768        $this->numFonts++;
2769
2770        //$this->numFonts = md5($fontName);
2771        $pos =  strrpos($fontName, '/');
2772
2773        //      $dir = substr($fontName,0,$pos+1);
2774        $name =  substr($fontName, $pos+1);
2775
2776        $options =  array('name' => $name, 'fontFileName' => $fontName);
2777
2778        if  (is_array($encoding)) {
2779
2780          // then encoding and differences might be set
2781          if  (isset($encoding['encoding'])) {
2782
2783            $options['encoding'] =  $encoding['encoding'];
2784          }
2785
2786          if  (isset($encoding['differences'])) {
2787
2788            $options['differences'] =  $encoding['differences'];
2789          }
2790        } else  if  (mb_strlen($encoding)) {
2791
2792          // then perhaps only the encoding has been set
2793          $options['encoding'] =  $encoding;
2794        }
2795
2796
2797        $fontObj =  $this->numObj;
2798
2799        $this->o_font($this->numObj, 'new', $options);
2800
2801        $this->fonts[$fontName]['fontNum'] =  $this->numFonts;
2802
2803
2804        // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there
2805        // should be for all non-basic fonts), then load it into an object and put the
2806        // references into the font object
2807        $basefile =  $fontName;
2808        if  (file_exists($basefile.'.pfb')) {
2809
2810          $fbtype =  'pfb';
2811        } else  if  (file_exists($basefile.'.ttf')) {
2812
2813          $fbtype =  'ttf';
2814        } else {
2815
2816          $fbtype =  '';
2817        }
2818
2819        $fbfile =  $basefile.'.'.$fbtype;
2820
2821
2822        //      $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb';
2823        //      $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf';
2824        $this->addMessage('selectFont: checking for - '.$fbfile);
2825
2826
2827        // OAR - I don't understand this old check
2828        // if  (substr($fontName, -4) ==  '.afm' &&  strlen($fbtype)) {
2829        if  (mb_strlen($fbtype)) {
2830          $adobeFontName =  $this->fonts[$fontName]['FontName'];
2831
2832          //        $fontObj = $this->numObj;
2833          $this->addMessage('selectFont: adding font file - '.$fbfile.' - '.$adobeFontName);
2834
2835          // find the array of font widths, and put that into an object.
2836          $firstChar =  -1;
2837
2838          $lastChar =  0;
2839
2840          $widths =  array();
2841          $cid_widths = array();
2842
2843          foreach ($this->fonts[$fontName]['C'] as  $num => $d) {
2844
2845            if  (intval($num) >0 ||  $num ==  '0') {
2846
2847              if (!$this->isUnicode) {
2848                // With Unicode, widths array isn't used
2849                if  ($lastChar>0 &&  $num>$lastChar+1) {
2850
2851                  for ($i =  $lastChar+1;$i<$num;$i++) {
2852
2853                    $widths[] =  0;
2854                  }
2855                }
2856              }
2857
2858              $widths[] =  $d['WX'];
2859
2860              if ($this->isUnicode) {
2861                $cid_widths[$num] =  $d['WX'];
2862              }
2863
2864              if  ($firstChar ==  -1) {
2865                $firstChar =  $num;
2866              }
2867
2868              $lastChar =  $num;
2869            }
2870          }
2871
2872          // also need to adjust the widths for the differences array
2873          if  (isset($options['differences'])) {
2874
2875            foreach($options['differences'] as  $charNum => $charName) {
2876
2877              if  ($charNum > $lastChar) {
2878
2879                if (!$this->isUnicode) {
2880                  // With Unicode, widths array isn't used
2881                  for ($i =  $lastChar + 1; $i <=  $charNum; $i++) {
2882
2883                    $widths[] =  0;
2884                  }
2885                }
2886
2887                $lastChar =  $charNum;
2888              }
2889
2890              if  (isset($this->fonts[$fontName]['C'][$charName])) {
2891
2892                $widths[$charNum-$firstChar] =  $this->fonts[$fontName]['C'][$charName]['WX'];
2893                if ($this->isUnicode) {
2894                  $cid_widths[$charName] =  $this->fonts[$fontName]['C'][$charName]['WX'];
2895                }
2896              }
2897            }
2898          }
2899
2900          if ($this->isUnicode) {
2901            $this->fonts[$fontName]['CIDWidths'] = $cid_widths;
2902          }
2903
2904          $this->addMessage('selectFont: FirstChar = '.$firstChar);
2905
2906          $this->addMessage('selectFont: LastChar = '.$lastChar);
2907
2908          $widthid = -1;
2909
2910          if (!$this->isUnicode) {
2911            // With Unicode, widths array isn't used
2912
2913            $this->numObj++;
2914
2915            $this->o_contents($this->numObj, 'new', 'raw');
2916
2917            $this->objects[$this->numObj]['c'].=  '[';
2918
2919            foreach($widths as  $width) {
2920              $this->objects[$this->numObj]['c'].=  ' '.$width;
2921            }
2922
2923            $this->objects[$this->numObj]['c'].=  ' ]';
2924
2925            $widthid =  $this->numObj;
2926          }
2927
2928          $missing_width = 500;
2929          $stemV = 70;
2930
2931          if (isset($this->fonts[$fontName]['MissingWidth'])) {
2932
2933            $missing_width =  $this->fonts[$fontName]['MissingWidth'];
2934          }
2935          if (isset($this->fonts[$fontName]['StdVW'])) {
2936
2937            $stemV = $this->fonts[$fontName]['StdVW'];
2938          } elseif (isset($this->fonts[$fontName]['Weight']) && preg_match('!(bold|black)!i', $this->fonts[$fontName]['Weight'])) {
2939
2940            $stemV = 120;
2941          }
2942
2943          // load the pfb file, and put that into an object too.
2944          // note that pdf supports only binary format type 1 font files, though there is a
2945          // simple utility to convert them from pfa to pfb.
2946          $data =  file_get_contents($fbfile);
2947
2948
2949          // create the font descriptor
2950          $this->numObj++;
2951
2952          $fontDescriptorId =  $this->numObj;
2953
2954          $this->numObj++;
2955
2956          $pfbid =  $this->numObj;
2957
2958          // determine flags (more than a little flakey, hopefully will not matter much)
2959          $flags =  0;
2960
2961          if  ($this->fonts[$fontName]['ItalicAngle'] !=  0) {
2962            $flags+=  pow(2, 6);
2963          }
2964
2965          if  ($this->fonts[$fontName]['IsFixedPitch'] ==  'true') {
2966            $flags+=  1;
2967          }
2968
2969          $flags+=  pow(2, 5); // assume non-sybolic
2970
2971          $list =  array('Ascent' => 'Ascender', 'CapHeight' => 'CapHeight', 'MissingWidth' => 'MissingWidth', 'Descent' => 'Descender', 'FontBBox' => 'FontBBox', 'ItalicAngle' => 'ItalicAngle');
2972
2973          $fdopt =  array('Flags' => $flags, 'FontName' => $adobeFontName, 'StemV' => $stemV);
2974
2975          foreach($list as  $k => $v) {
2976
2977            if  (isset($this->fonts[$fontName][$v])) {
2978
2979              $fdopt[$k] =  $this->fonts[$fontName][$v];
2980            }
2981          }
2982
2983
2984          if  ($fbtype ==  'pfb') {
2985
2986            $fdopt['FontFile'] =  $pfbid;
2987          } else  if  ($fbtype ==  'ttf') {
2988
2989            $fdopt['FontFile2'] =  $pfbid;
2990          }
2991
2992          $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt);
2993
2994
2995          // embed the font program
2996          $this->o_contents($this->numObj, 'new');
2997
2998          $this->objects[$pfbid]['c'].=  $data;
2999
3000          // determine the cruicial lengths within this file
3001          if  ($fbtype ==  'pfb') {
3002
3003            $l1 =  strpos($data, 'eexec') +6;
3004
3005            $l2 =  strpos($data, '00000000') -$l1;
3006
3007            $l3 =  mb_strlen($data) -$l2-$l1;
3008
3009            $this->o_contents($this->numObj, 'add', array('Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3));
3010          } else  if  ($fbtype ==  'ttf') {
3011
3012            $l1 =  mb_strlen($data);
3013
3014            $this->o_contents($this->numObj, 'add', array('Length1' => $l1));
3015          }
3016
3017
3018
3019          // tell the font object about all this new stuff
3020          $tmp =  array('BaseFont' => $adobeFontName, 'MissingWidth' => $missing_width, 'Widths' => $widthid, 'FirstChar' => $firstChar, 'LastChar' => $lastChar, 'FontDescriptor' => $fontDescriptorId);
3021
3022          if  ($fbtype ==  'ttf') {
3023
3024            $tmp['SubType'] =  'TrueType';
3025          }
3026
3027          $this->addMessage('adding extra info to font.('.$fontObj.')');
3028
3029          foreach($tmp as  $fk => $fv) {
3030
3031            $this->addMessage($fk." : ".$fv);
3032          }
3033
3034          $this->o_font($fontObj, 'add', $tmp);
3035        } else {
3036
3037          $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts');
3038        }
3039
3040
3041
3042        // also set the differences here, note that this means that these will take effect only the
3043        //first time that a font is selected, else they are ignored
3044        if  (isset($options['differences'])) {
3045
3046          $this->fonts[$fontName]['differences'] =  $options['differences'];
3047        }
3048      }
3049    }
3050
3051    if  ($set &&  isset($this->fonts[$fontName])) {
3052
3053      // so if for some reason the font was not set in the last one then it will not be selected
3054      $this->currentBaseFont =  $fontName;
3055
3056      // the next lines mean that if a new font is selected, then the current text state will be
3057      // applied to it as well.
3058      $this->currentFont =  $this->currentBaseFont;
3059
3060      $this->currentFontNum =  $this->fonts[$this->currentFont]['fontNum'];
3061
3062      //$this->setCurrentFont();
3063
3064    }
3065
3066    return  $this->currentFontNum;
3067
3068    //return $this->numObj;
3069
3070  }
3071
3072
3073  /**
3074   * sets up the current font, based on the font families, and the current text state
3075   * note that this system is quite flexible, a bold-italic font can be completely different to a
3076   * italic-bold font, and even bold-bold will have to be defined within the family to have meaning
3077   * This function is to be called whenever the currentTextState is changed, it will update
3078   * the currentFont setting to whatever the appropriatte family one is.
3079   * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont
3080   * This function will change the currentFont to whatever it should be, but will not change the
3081   * currentBaseFont.
3082   *
3083   * @access private
3084   */
3085  function  setCurrentFont() {
3086
3087    //   if (strlen($this->currentBaseFont) == 0){
3088    //     // then assume an initial font
3089    //     $this->selectFont('./fonts/Helvetica.afm');
3090    //   }
3091    //   $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1);
3092    //   if (strlen($this->currentTextState)
3093    //     && isset($this->fontFamilies[$cf])
3094    //       && isset($this->fontFamilies[$cf][$this->currentTextState])){
3095    //     // then we are in some state or another
3096    //     // and this font has a family, and the current setting exists within it
3097    //     // select the font, then return it
3098    //     $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState];
3099    //     $this->selectFont($nf,'',0);
3100    //     $this->currentFont = $nf;
3101    //     $this->currentFontNum = $this->fonts[$nf]['fontNum'];
3102    //   } else {
3103    //     // the this font must not have the right family member for the current state
3104    //     // simply assume the base font
3105    $this->currentFont =  $this->currentBaseFont;
3106
3107    $this->currentFontNum =  $this->fonts[$this->currentFont]['fontNum'];
3108
3109    //  }
3110
3111  }
3112
3113
3114  /**
3115   * function for the user to find out what the ID is of the first page that was created during
3116   * startup - useful if they wish to add something to it later.
3117   */
3118  function  getFirstPageId() {
3119
3120    return  $this->firstPageId;
3121  }
3122
3123
3124  /**
3125   * add content to the currently active object
3126   *
3127   * @access private
3128   */
3129  function  addContent($content) {
3130
3131    $this->objects[$this->currentContents]['c'].=  $content;
3132  }
3133
3134
3135  /**
3136   * sets the colour for fill operations
3137   */
3138  function  setColor($r, $g, $b, $force =  0) {
3139
3140    if  ($r >=  0 &&  ($force ||  $r !=  $this->currentColour['r'] ||  $g !=  $this->currentColour['g'] ||  $b !=  $this->currentColour['b'])) {
3141
3142      $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $r) .' '.sprintf('%.3F', $g) .' '.sprintf('%.3F', $b) .' rg';
3143
3144      $this->currentColour =  array('r' => $r, 'g' => $g, 'b' => $b);
3145    }
3146  }
3147
3148
3149  /**
3150   * sets the colour for stroke operations
3151   */
3152  function  setStrokeColor($r, $g, $b, $force =  0) {
3153
3154    if  ($r >=  0 &&  ($force ||  $r !=  $this->currentStrokeColour['r'] ||  $g !=  $this->currentStrokeColour['g'] ||  $b !=  $this->currentStrokeColour['b'])) {
3155
3156      $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $r) .' '.sprintf('%.3F', $g) .' '.sprintf('%.3F', $b) .' RG';
3157
3158      $this->currentStrokeColour =  array('r' => $r, 'g' => $g, 'b' => $b);
3159    }
3160  }
3161
3162
3163  /**
3164   * Set the graphics state for compositions
3165   */
3166  function  setGraphicsState($parameters) {
3167
3168    // Create a new graphics state object
3169    // FIXME: should actually keep track of states that have already been created...
3170    $this->numObj++;
3171
3172    $this->o_extGState($this->numObj,  'new',  $parameters);
3173
3174    $this->objects[ $this->currentContents ]['c'].=  "\n/GS" . $this->numStates . " gs";
3175  }
3176
3177
3178  /**
3179   * Set current blend mode & opacity for lines.
3180   *
3181   * Valid blend modes are:
3182   *
3183   * Normal, Multiply, Screen, Overlay, Darken, Lighten,
3184   * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
3185   * Exclusion
3186   *
3187   * @param string $mode the blend mode to use
3188   * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
3189   */
3190  function setLineTransparency($mode, $opacity) {
3191    static $blend_modes = array("Normal", "Multiply", "Screen",
3192                                "Overlay", "Darken", "Lighten",
3193                                "ColorDogde", "ColorBurn", "HardLight",
3194                                "SoftLight", "Difference", "Exclusion");
3195
3196    if ( !in_array($mode, $blend_modes) )
3197      $mode = "Normal";
3198   
3199    // Only create a new graphics state if required
3200    if ( $mode == $this->currentLineTransparency["mode"]  &&
3201         $opacity == $this->currentLineTransparency["opacity"] )
3202      return;
3203
3204    $options = array("BM" => "/$mode",
3205                     "CA" => (float)$opacity);
3206
3207    $this->setGraphicsState($options);
3208  }
3209 
3210  /**
3211   * Set current blend mode & opacity for filled objects.
3212   *
3213   * Valid blend modes are:
3214   *
3215   * Normal, Multiply, Screen, Overlay, Darken, Lighten,
3216   * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
3217   * Exclusion
3218   *
3219   * @param string $mode the blend mode to use
3220   * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
3221   */
3222  function setFillTransparency($mode, $opacity) {
3223    static $blend_modes = array("Normal", "Multiply", "Screen",
3224                                "Overlay", "Darken", "Lighten",
3225                                "ColorDogde", "ColorBurn", "HardLight",
3226                                "SoftLight", "Difference", "Exclusion");
3227
3228    if ( !in_array($mode, $blend_modes) )
3229      $mode = "Normal";
3230
3231    if ( $mode == $this->currentFillTransparency["mode"]  &&
3232         $opacity == $this->currentFillTransparency["opacity"] )
3233      return;
3234
3235    $options = array("BM" => "/$mode",
3236                     "ca" => (float)$opacity);
3237   
3238    $this->setGraphicsState($options);
3239  }
3240
3241  /**
3242   * draw a line from one set of coordinates to another
3243   */
3244  function  line($x1, $y1, $x2, $y2) {
3245
3246    $this->objects[$this->currentContents]['c'] .=
3247      "\n".sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1) .' m '.sprintf('%.3F', $x2) .' '.sprintf('%.3F', $y2) .' l S';
3248  }
3249
3250
3251  /**
3252   * draw a bezier curve based on 4 control points
3253   */
3254  function  curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) {
3255
3256    // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points
3257    // as the control points for the curve.
3258    $this->objects[$this->currentContents]['c'] .=
3259      "\n".sprintf('%.3F', $x0) .' '.sprintf('%.3F', $y0) .' m '.sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1);
3260
3261    $this->objects[$this->currentContents]['c'] .=
3262      ' '.sprintf('%.3F', $x2) .' '.sprintf('%.3F', $y2) .' '.sprintf('%.3F', $x3) .' '.sprintf('%.3F', $y3) .' c S';
3263  }
3264
3265
3266  /**
3267   * draw a part of an ellipse
3268   */
3269  function  partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 =  0, $angle =  0, $nSeg =  8) {
3270
3271    $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, 0);
3272  }
3273
3274
3275  /**
3276   * draw a filled ellipse
3277   */
3278  function  filledEllipse($x0, $y0, $r1, $r2 =  0, $angle =  0, $nSeg =  8, $astart =  0, $afinish =  360) {
3279
3280    return  $this->ellipse($x0, $y0, $r1, $r2 =  0, $angle, $nSeg, $astart, $afinish, 1, 1);
3281  }
3282
3283
3284  /**
3285   * draw an ellipse
3286   * note that the part and filled ellipse are just special cases of this function
3287   *
3288   * draws an ellipse in the current line style
3289   * centered at $x0,$y0, radii $r1,$r2
3290   * if $r2 is not set, then a circle is drawn
3291   * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a
3292   * pretty crappy shape at 2, as we are approximating with bezier curves.
3293   */
3294  function  ellipse($x0, $y0, $r1, $r2 =  0, $angle =  0, $nSeg =  8, $astart =  0, $afinish =  360, $close =  1, $fill =  0) {
3295
3296    if  ($r1 ==  0) {
3297      return;
3298    }
3299
3300    if  ($r2 ==  0) {
3301      $r2 =  $r1;
3302    }
3303
3304    if  ($nSeg < 2) {
3305      $nSeg =  2;
3306    }
3307
3308
3309    $astart =  deg2rad((float)$astart);
3310
3311    $afinish =  deg2rad((float)$afinish);
3312
3313    $totalAngle = $afinish-$astart;
3314
3315
3316    $dt =  $totalAngle/$nSeg;
3317
3318    $dtm =  $dt/3;
3319
3320
3321    if  ($angle !=  0) {
3322
3323      $a =  -1*deg2rad((float)$angle);
3324
3325      $tmp  =  "\n q ";
3326      $tmp .=  sprintf('%.3F', cos($a)) .' '.sprintf('%.3F', (-1.0*sin($a))) .' '.sprintf('%.3F', sin($a)) .' '.sprintf('%.3F', cos($a)) .' ';
3327      $tmp .=  sprintf('%.3F', $x0) .' '.sprintf('%.3F', $y0) .' cm';
3328
3329      $this->objects[$this->currentContents]['c'].=  $tmp;
3330
3331      $x0 =  0;
3332      $y0 =  0;
3333    }
3334
3335
3336    $t1 =  $astart;
3337    $a0 =  $x0 + $r1*cos($t1);
3338    $b0 =  $y0 + $r2*sin($t1);
3339    $c0 =  -$r1 * sin($t1);
3340    $d0 =  $r2 * cos($t1);
3341
3342
3343    $this->objects[$this->currentContents]['c'] .=  "\n".sprintf('%.3F', $a0) .' '.sprintf('%.3F', $b0) .' m ';
3344
3345    for  ($i = 1; $i <=  $nSeg; $i++) {
3346
3347      // draw this bit of the total curve
3348      $t1 =  $i * $dt + $astart;
3349
3350      $a1 =  $x0 + $r1 * cos($t1);
3351
3352      $b1 =  $y0 + $r2 * sin($t1);
3353
3354      $c1 = -$r1 * sin($t1);
3355
3356      $d1 =  $r2 * cos($t1);
3357
3358      $this->objects[$this->currentContents]['c']
3359        .=  "\n".sprintf('%.3F', ($a0+$c0*$dtm)) .' '.sprintf('%.3F', ($b0 + $d0 * $dtm));
3360
3361      $this->objects[$this->currentContents]['c'] .=
3362        ' '.sprintf('%.3F', ($a1-$c1*$dtm)) .' '.sprintf('%.3F', ($b1-$d1*$dtm)) .' '.sprintf('%.3F', $a1) .' '.sprintf('%.3F', $b1) .' c';
3363
3364      $a0 =  $a1;
3365
3366      $b0 =  $b1;
3367
3368      $c0 =  $c1;
3369
3370      $d0 =  $d1;
3371    }
3372
3373    if  ($fill) {
3374      $this->objects[$this->currentContents]['c'].=  ' f';
3375
3376    } else if ($close) {
3377
3378        $this->objects[$this->currentContents]['c'].=  ' s';
3379        // small 's' signifies closing the path as well
3380
3381    } else {
3382
3383      $this->objects[$this->currentContents]['c'].=  ' S';
3384
3385    }
3386
3387    if  ($angle !=  0) {
3388      $this->objects[$this->currentContents]['c'].=  ' Q';
3389    }
3390
3391  }
3392
3393
3394  /**
3395   * this sets the line drawing style.
3396   * width, is the thickness of the line in user units
3397   * cap is the type of cap to put on the line, values can be 'butt','round','square'
3398   *    where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the
3399   *    end of the line.
3400   * join can be 'miter', 'round', 'bevel'
3401   * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the
3402   *   on and off dashes.
3403   *   (2) represents 2 on, 2 off, 2 on , 2 off ...
3404   *   (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
3405   * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts.
3406   */
3407  function  setLineStyle($width =  1, $cap =  '', $join =  '', $dash =  '', $phase =  0) {
3408
3409
3410    // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day
3411    $string =  '';
3412
3413    if  ($width>0) {
3414
3415      $string.=  $width.' w';
3416    }
3417
3418    $ca =  array('butt' => 0, 'round' => 1, 'square' => 2);
3419
3420    if  (isset($ca[$cap])) {
3421
3422      $string.=  ' '.$ca[$cap].' J';
3423    }
3424
3425    $ja =  array('miter' => 0, 'round' => 1, 'bevel' => 2);
3426
3427    if  (isset($ja[$join])) {
3428
3429      $string.=  ' '.$ja[$join].' j';
3430    }
3431
3432    if  (is_array($dash)) {
3433
3434      $string.=  ' [';
3435
3436      foreach ($dash as  $len) {
3437
3438        $string.=  ' '.$len;
3439      }
3440
3441      $string.=  ' ] '.$phase.' d';
3442    }
3443
3444    $this->currentLineStyle =  $string;
3445
3446    $this->objects[$this->currentContents]['c'].=  "\n".$string;
3447  }
3448
3449
3450
3451  /**
3452   * draw a polygon, the syntax for this is similar to the GD polygon command
3453   */
3454  function  polygon($p, $np, $f =  0) {
3455
3456    $this->objects[$this->currentContents]['c'].=  "\n";
3457
3458    $this->objects[$this->currentContents]['c'].=  sprintf('%.3F', $p[0]) .' '.sprintf('%.3F', $p[1]) .' m ';
3459
3460    for  ($i =  2; $i < $np * 2; $i =  $i + 2) {
3461
3462      $this->objects[$this->currentContents]['c'].=  sprintf('%.3F', $p[$i]) .' '.sprintf('%.3F', $p[$i+1]) .' l ';
3463    }
3464
3465    if  ($f ==  1) {
3466
3467      $this->objects[$this->currentContents]['c'].=  ' f';
3468    } else {
3469
3470      $this->objects[$this->currentContents]['c'].=  ' S';
3471    }
3472  }
3473
3474
3475  /**
3476   * a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
3477   * the coordinates of the upper-right corner
3478   */
3479  function  filledRectangle($x1, $y1, $width, $height) {
3480
3481    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1) .' '.sprintf('%.3F', $width) .' '.sprintf('%.3F', $height) .' re f';
3482  }
3483
3484
3485  /**
3486   * draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
3487   * the coordinates of the upper-right corner
3488   */
3489  function  rectangle($x1, $y1, $width, $height) {
3490
3491    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $x1) .' '.sprintf('%.3F', $y1) .' '.sprintf('%.3F', $width) .' '.sprintf('%.3F', $height) .' re S';
3492  }
3493
3494
3495  /**
3496   * add a new page to the document
3497   * this also makes the new page the current active object
3498   */
3499  function  newPage($insert =  0, $id =  0, $pos =  'after') {
3500
3501
3502    // if there is a state saved, then go up the stack closing them
3503    // then on the new page, re-open them with the right setings
3504
3505    if  ($this->nStateStack) {
3506
3507      for  ($i =  $this->nStateStack;$i >=  1;$i--) {
3508
3509        $this->restoreState($i);
3510      }
3511    }
3512
3513
3514    $this->numObj++;
3515
3516    if  ($insert) {
3517
3518      // the id from the ezPdf class is the od of the contents of the page, not the page object itself
3519      // query that object to find the parent
3520      $rid =  $this->objects[$id]['onPage'];
3521
3522      $opt =  array('rid' => $rid, 'pos' => $pos);
3523
3524      $this->o_page($this->numObj, 'new', $opt);
3525    } else {
3526
3527      $this->o_page($this->numObj, 'new');
3528    }
3529
3530    // if there is a stack saved, then put that onto the page
3531    if  ($this->nStateStack) {
3532
3533      for  ($i =  1;$i <=  $this->nStateStack;$i++) {
3534
3535        $this->saveState($i);
3536      }
3537    }
3538
3539    // and if there has been a stroke or fill colour set, then transfer them
3540    if  ($this->currentColour['r'] >=  0) {
3541
3542      $this->setColor($this->currentColour['r'], $this->currentColour['g'], $this->currentColour['b'], 1);
3543    }
3544
3545    if  ($this->currentStrokeColour['r'] >=  0) {
3546
3547      $this->setStrokeColor($this->currentStrokeColour['r'], $this->currentStrokeColour['g'], $this->currentStrokeColour['b'], 1);
3548    }
3549
3550
3551    // if there is a line style set, then put this in too
3552    if  (mb_strlen($this->currentLineStyle)) {
3553
3554      $this->objects[$this->currentContents]['c'].=  "\n".$this->currentLineStyle;
3555    }
3556
3557
3558    // the call to the o_page object set currentContents to the present page, so this can be returned as the page id
3559    return  $this->currentContents;
3560  }
3561
3562
3563  /**
3564   * output the pdf code, streaming it to the browser
3565   * the relevant headers are set so that hopefully the browser will recognise it
3566   */
3567  function  stream($options =  '') {
3568
3569    // setting the options allows the adjustment of the headers
3570    // values at the moment are:
3571    // 'Content-Disposition' => 'filename'  - sets the filename, though not too sure how well this will
3572    //        work as in my trial the browser seems to use the filename of the php file with .pdf on the end
3573    // 'Accept-Ranges' => 1 or 0 - if this is not set to 1, then this header is not included, off by default
3574    //    this header seems to have caused some problems despite tha fact that it is supposed to solve
3575    //    them, so I am leaving it off by default.
3576    // 'compress' = > 1 or 0 - apply content stream compression, this is on (1) by default
3577    // 'Attachment' => 1 or 0 - if 1, force the browser to open a download dialog
3578    if  (!is_array($options)) {
3579
3580      $options =  array();
3581    }
3582
3583    if  ( headers_sent())
3584      die("Unable to stream pdf: headers already sent");
3585
3586
3587    if  ( isset($options['compress']) &&  $options['compress'] ==  0) {
3588
3589      $tmp =  ltrim($this->output(1));
3590    } else {
3591
3592      $tmp =  ltrim($this->output());
3593    }
3594
3595
3596    header("Cache-Control: private");
3597
3598    header("Content-type: application/pdf");
3599
3600    //FIXME: I don't know that this is sufficient for determining content length (i.e. what about transport compression?)
3601    //header("Content-Length: " . mb_strlen($tmp));
3602    $fileName =  (isset($options['Content-Disposition']) ?  $options['Content-Disposition'] :  'file.pdf');
3603
3604    if  ( !isset($options["Attachment"]))
3605      $options["Attachment"] =  true;
3606
3607
3608    $attachment =  $options["Attachment"] ?  "attachment" :  "inline";
3609
3610
3611    header("Content-Disposition: $attachment; filename=\"$fileName\"");
3612
3613
3614    if  (isset($options['Accept-Ranges']) &&  $options['Accept-Ranges'] ==  1) {
3615      //FIXME: Is this the correct value ... spec says 1#range-unit
3616      header("Accept-Ranges: " . mb_strlen($tmp));
3617    }
3618
3619    echo  $tmp;
3620
3621    flush();
3622  }
3623
3624
3625  /**
3626   * return the height in units of the current font in the given size
3627   */
3628  function  getFontHeight($size) {
3629
3630    if  (!$this->numFonts) {
3631      $this->selectFont('./fonts/Helvetica');
3632    }
3633   
3634    // for the current font, and the given size, what is the height of the font in user units
3635    $h =  $this->fonts[$this->currentFont]['FontBBox'][3]-$this->fonts[$this->currentFont]['FontBBox'][1];
3636
3637    // have to adjust by a font offset for Windows fonts.  unfortunately it looks like
3638    // the bounding box calculations are wrong and I don't know why.
3639    if (isset($this->fonts[$this->currentFont]['FontHeightOffset'])) {
3640
3641      // For CourierNew from Windows this needs to be -646 to match the
3642      // Adobe native Courier font.
3643      //
3644      // For FreeMono from GNU this needs to be -337 to match the
3645      // Courier font.
3646      //
3647      // Both have been added manually to the .afm and .ufm files.
3648      $h += (int)$this->fonts[$this->currentFont]['FontHeightOffset'];
3649    }
3650
3651    return  $size*$h/1000;
3652  }
3653
3654
3655  /**
3656   * return the font descender, this will normally return a negative number
3657   * if you add this number to the baseline, you get the level of the bottom of the font
3658   * it is in the pdf user units
3659   */
3660  function  getFontDescender($size) {
3661
3662    // note that this will most likely return a negative value
3663    if  (!$this->numFonts) {
3664      $this->selectFont('./fonts/Helvetica');
3665    }
3666
3667    //$h = $this->fonts[$this->currentFont]['FontBBox'][1];
3668    $h = $this->fonts[$this->currentFont]['Descender'];
3669
3670    return  $size*$h/1000;
3671  }
3672
3673
3674  /**
3675   * filter the text, this is applied to all text just before being inserted into the pdf document
3676   * it escapes the various things that need to be escaped, and so on
3677   *
3678   * @access private
3679   */
3680  function  filterText($text, $bom = true) {
3681    if ($this->isUnicode) {
3682      $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
3683      $text =  $this->utf8toUtf16BE($text, $bom);
3684    } else {
3685      if (in_array('Windows-1252', mb_list_encodings())) {
3686        $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8');
3687      } else {
3688        $text = mb_convert_encoding($text, 'iso-8859-1', 'UTF-8');
3689      }
3690      $text = html_entity_decode($text, ENT_QUOTES);
3691    }
3692
3693    // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290)
3694    $text = strtr($text, array(')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r'));
3695    return  $text;
3696  }
3697
3698  /**
3699   * return array containing codepoints (UTF-8 character values) for the
3700   * string passed in.
3701   *
3702   * based on the excellent TCPDF code by Nicola Asuni and the
3703   * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
3704   *
3705   * @access private
3706   * @author Orion Richardson
3707   * @since January 5, 2008
3708   * @param string $text UTF-8 string to process
3709   * @return array UTF-8 codepoints array for the string
3710   */
3711  function  utf8toCodePointsArray(&$text) {
3712    $length = mb_strlen($text,'8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040
3713    $unicode = array(); // array containing unicode values
3714    $bytes = array(); // array containing single character byte sequences
3715    $numbytes = 1; // number of octetc needed to represent the UTF-8 character
3716   
3717    for ($i = 0; $i < $length; $i++) {
3718      $c = ord($text{$i}); // get one string character at time
3719      if (count($bytes) == 0) { // get starting octect
3720        if ($c <= 0x7F) {
3721          $unicode[] = $c; // use the character "as is" because is ASCII
3722          $numbytes = 1;
3723        } elseif (($c >> 0x05) == 0x06) { // 2 bytes character (0x06 = 110 BIN)
3724          $bytes[] = ($c - 0xC0) << 0x06;
3725          $numbytes = 2;
3726        } elseif (($c >> 0x04) == 0x0E) { // 3 bytes character (0x0E = 1110 BIN)
3727          $bytes[] = ($c - 0xE0) << 0x0C;
3728          $numbytes = 3;
3729        } elseif (($c >> 0x03) == 0x1E) { // 4 bytes character (0x1E = 11110 BIN)
3730          $bytes[] = ($c - 0xF0) << 0x12;
3731          $numbytes = 4;
3732        } else {
3733          // use replacement character for other invalid sequences
3734          $unicode[] = 0xFFFD;
3735          $bytes = array();
3736          $numbytes = 1;
3737        }
3738      } elseif (($c >> 0x06) == 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN
3739        $bytes[] = $c - 0x80;
3740        if (count($bytes) == $numbytes) {
3741          // compose UTF-8 bytes to a single unicode value
3742          $c = $bytes[0];
3743          for ($j = 1; $j < $numbytes; $j++) {
3744            $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06));
3745          }
3746          if ((($c >= 0xD800) AND ($c <= 0xDFFF)) OR ($c >= 0x10FFFF)) {
3747            // The definition of UTF-8 prohibits encoding character numbers between
3748            // U+D800 and U+DFFF, which are reserved for use with the UTF-16
3749            // encoding form (as surrogate pairs) and do not directly represent
3750            // characters.
3751            $unicode[] = 0xFFFD; // use replacement character
3752          } else {
3753            $unicode[] = $c; // add char to array
3754          }
3755          // reset data for next char
3756          $bytes = array();
3757          $numbytes = 1;
3758        }
3759      } else {
3760        // use replacement character for other invalid sequences
3761        $unicode[] = 0xFFFD;
3762        $bytes = array();
3763        $numbytes = 1;
3764      }
3765    }
3766    return $unicode;
3767  }
3768
3769  /**
3770   * convert UTF-8 to UTF-16 with an additional byte order marker
3771   * at the front if required.
3772   *
3773   * based on the excellent TCPDF code by Nicola Asuni and the
3774   * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
3775   *
3776   * @access private
3777   * @author Orion Richardson
3778   * @since January 5, 2008
3779   * @param string $text UTF-8 string to process
3780   * @param boolean $bom whether to add the byte order marker
3781   * @return string UTF-16 result string
3782   */
3783  function  utf8toUtf16BE(&$text, $bom = true) {
3784    if (!$this->isUnicode) return $text;
3785    $out = $bom ? "\xFE\xFF" : '';
3786   
3787    $unicode = $this->utf8toCodePointsArray($text);
3788    foreach ($unicode as $c) {
3789      if ($c == 0xFFFD) {
3790        $out .= "\xFF\xFD"; // replacement character
3791      } elseif ($c < 0x10000) {
3792        $out .= chr($c >> 0x08);
3793        $out .= chr($c & 0xFF);
3794       } else {
3795        $c -= 0x10000;
3796        $w1 = 0xD800 | ($c >> 0x10);
3797        $w2 = 0xDC00 | ($c & 0x3FF);
3798        $out .= chr($w1 >> 0x08);
3799        $out .= chr($w1 & 0xFF);
3800        $out .= chr($w2 >> 0x08);
3801        $out .= chr($w2 & 0xFF);
3802      }
3803    }
3804    return $out;
3805  }
3806
3807
3808  /**
3809   * given a start position and information about how text is to be laid out, calculate where
3810   * on the page the text will end
3811   *
3812   * @access private
3813   */
3814  function  PRVTgetTextPosition($x, $y, $angle, $size, $wa, $text) {
3815
3816    // given this information return an array containing x and y for the end position as elements 0 and 1
3817    $w =  $this->getTextWidth($size, $text);
3818
3819    // need to adjust for the number of spaces in this text
3820    $words =  explode(' ', $text);
3821
3822    $nspaces =  count($words) -1;
3823
3824    $w+=  $wa*$nspaces;
3825
3826    $a =  deg2rad((float)$angle);
3827
3828    return  array(cos($a) *$w+$x, -sin($a) *$w+$y);
3829  }
3830
3831
3832  /**
3833   * wrapper function for PRVTcheckTextDirective1
3834   *
3835   * @access private
3836   */
3837  function  PRVTcheckTextDirective(&$text, $i, &$f) {
3838
3839    return  0;
3840
3841    $x =  0;
3842
3843    $y =  0;
3844
3845    return  $this->PRVTcheckTextDirective1($text, $i, $f, 0, $x, $y);
3846  }
3847
3848
3849  /**
3850   * checks if the text stream contains a control directive
3851   * if so then makes some changes and returns the number of characters involved in the directive
3852   * this has been re-worked to include everything neccesary to fins the current writing point, so that
3853   * the location can be sent to the callback function if required
3854   * if the directive does not require a font change, then $f should be set to 0
3855   *
3856   * @access private
3857   */
3858  function  PRVTcheckTextDirective1(&$text, $i, &$f, $final, &$x, &$y, $size =  0, $angle =  0, $wordSpaceAdjust =  0) {
3859
3860    return  0;
3861
3862    $directive =  0;
3863
3864    $j =  $i;
3865
3866    if  ($text[$j] ==  '<') {
3867
3868      $j++;
3869
3870      switch ($text[$j]) {
3871
3872      case  '/':
3873
3874        $j++;
3875
3876        if  (mb_strlen($text) <=  $j) {
3877
3878          return  $directive;
3879        }
3880
3881        switch ($text[$j]) {
3882
3883        case  'b':
3884
3885        case  'i':
3886
3887          $j++;
3888
3889          if  ($text[$j] ==  '>') {
3890
3891            $p =  mb_strrpos($this->currentTextState, $text[$j-1]);
3892
3893            if  ($p !==  false) {
3894
3895              // then there is one to remove
3896              $this->currentTextState =  mb_substr($this->currentTextState, 0, $p) .substr($this->currentTextState, $p+1);
3897            }
3898
3899            $directive =  $j-$i+1;
3900          }
3901
3902          break;
3903
3904        case  'c':
3905
3906          // this this might be a callback function
3907          $j++;
3908
3909          $k =  mb_strpos($text, '>', $j);
3910
3911          if  ($k !==  false &&  $text[$j] ==  ':') {
3912
3913            // then this will be treated as a callback directive
3914            $directive =  $k-$i+1;
3915
3916            $f =  0;
3917
3918            // split the remainder on colons to get the function name and the paramater
3919            $tmp =  mb_substr($text, $j+1, $k-$j-1);
3920
3921            $b1 =  mb_strpos($tmp, ':');
3922
3923            if  ($b1 !==  false) {
3924
3925              $func =  mb_substr($tmp, 0, $b1);
3926
3927              $parm =  mb_substr($tmp, $b1+1);
3928            } else {
3929
3930              $func =  $tmp;
3931
3932              $parm =  '';
3933            }
3934
3935            if  (!isset($func) ||  !mb_strlen(trim($func))) {
3936
3937              $directive =  0;
3938            } else {
3939
3940              // only call the function if this is the final call
3941              if  ($final) {
3942
3943                // need to assess the text position, calculate the text width to this point
3944                // can use getTextWidth to find the text width I think
3945                $tmp =  $this->PRVTgetTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, mb_substr($text, 0, $i));
3946
3947                $info =  array('x' => $tmp[0], 'y' => $tmp[1], 'angle' => $angle, 'status' => 'end', 'p' => $parm, 'nCallback' => $this->nCallback);
3948
3949                $x =  $tmp[0];
3950
3951                $y =  $tmp[1];
3952
3953                $ret =  $this->$func($info);
3954
3955                if  (is_array($ret)) {
3956
3957                  // then the return from the callback function could set the position, to start with, later will do font colour, and font
3958                  foreach($ret as  $rk => $rv) {
3959
3960                    switch ($rk) {
3961
3962                    case  'x':
3963
3964                    case  'y':
3965
3966                      $$rk =  $rv;
3967
3968                      break;
3969                    }
3970                  }
3971                }
3972
3973                // also remove from to the stack
3974                // for simplicity, just take from the end, fix this another day
3975                $this->nCallback--;
3976
3977                if  ($this->nCallback<0) {
3978
3979                  $this->nCallBack =  0;
3980                }
3981              }
3982            }
3983          }
3984
3985          break;
3986        }
3987
3988        break;
3989
3990      case  'b':
3991
3992      case  'i':
3993
3994        $j++;
3995
3996        if  ($text[$j] ==  '>') {
3997
3998          $this->currentTextState.=  $text[$j-1];
3999
4000          $directive =  $j-$i+1;
4001        }
4002
4003        break;
4004
4005      case  'C':
4006
4007        $noClose =  1;
4008
4009      case  'c':
4010
4011        // this this might be a callback function
4012        $j++;
4013
4014        $k =  mb_strpos($text, '>', $j);
4015
4016        if  ($k !==  false &&  $text[$j] ==  ':') {
4017
4018          // then this will be treated as a callback directive
4019          $directive =  $k-$i+1;
4020
4021          $f =  0;
4022
4023          // split the remainder on colons to get the function name and the paramater
4024          //          $bits = explode(':',substr($text,$j+1,$k-$j-1));
4025          $tmp =  mb_substr($text, $j+1, $k-$j-1);
4026
4027          $b1 =  mb_strpos($tmp, ':');
4028
4029          if  ($b1 !==  false) {
4030
4031            $func =  mb_substr($tmp, 0, $b1);
4032
4033            $parm =  mb_substr($tmp, $b1+1);
4034          } else {
4035
4036            $func =  $tmp;
4037
4038            $parm =  '';
4039          }
4040
4041          if  (!isset($func) ||  !mb_strlen(trim($func))) {
4042
4043            $directive =  0;
4044          } else {
4045
4046            // only call the function if this is the final call, ie, the one actually doing printing, not measurement
4047            if  ($final) {
4048
4049              // need to assess the text position, calculate the text width to this point
4050              // can use getTextWidth to find the text width I think
4051              // also add the text height and descender
4052              $tmp =  $this->PRVTgetTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, mb_substr($text, 0, $i));
4053
4054              $info =  array('x' => $tmp[0], 'y' => $tmp[1], 'angle' => $angle, 'status' => 'start', 'p' => $parm, 'f' => $func, 'height' => $this->getFontHeight($size), 'descender' => $this->getFontDescender($size));
4055
4056              $x =  $tmp[0];
4057
4058              $y =  $tmp[1];
4059
4060              if  (!isset($noClose) ||  !$noClose) {
4061
4062                // only add to the stack if this is a small 'c', therefore is a start-stop pair
4063                $this->nCallback++;
4064
4065                $info['nCallback'] =  $this->nCallback;
4066
4067                $this->callback[$this->nCallback] =  $info;
4068              }
4069
4070              $ret =  $this->$func($info);
4071
4072              if  (is_array($ret)) {
4073
4074                // then the return from the callback function could set the position, to start with, later will do font colour, and font
4075                foreach($ret as  $rk => $rv) {
4076
4077                  switch ($rk) {
4078
4079                  case  'x':
4080
4081                  case  'y':
4082
4083                    $$rk =  $rv;
4084
4085                    break;
4086                  }
4087                }
4088              }
4089            }
4090          }
4091        }
4092
4093        break;
4094      }
4095    }
4096
4097    return  $directive;
4098  }
4099
4100
4101  /**
4102   * add text to the document, at a specified location, size and angle on the page
4103   */
4104  function  addText($x, $y, $size, $text, $angle =  0, $wordSpaceAdjust =  0) {
4105    if  (!$this->numFonts) {
4106      $this->selectFont('./fonts/Helvetica');
4107    }
4108
4109
4110    // if there are any open callbacks, then they should be called, to show the start of the line
4111    if  ($this->nCallback>0) {
4112
4113      for  ($i =  $this->nCallback;$i>0;$i--) {
4114
4115        // call each function
4116        $info =  array('x' => $x,
4117                       'y' => $y,
4118                       'angle' => $angle,
4119                       'status' => 'sol',
4120                       'p' => $this->callback[$i]['p'],
4121                       'nCallback' => $this->callback[$i]['nCallback'],
4122                       'height' => $this->callback[$i]['height'],
4123                       'descender' => $this->callback[$i]['descender']);
4124
4125        $func =  $this->callback[$i]['f'];
4126
4127        $this->$func($info);
4128      }
4129    }
4130
4131    if  ($angle ==  0) {
4132
4133      $this->objects[$this->currentContents]['c'].=  "\n".'BT '.sprintf('%.3F', $x) .' '.sprintf('%.3F', $y) .' Td';
4134
4135    } else {
4136
4137      $a =  deg2rad((float)$angle);
4138
4139      $tmp =  "\n".'BT ';
4140
4141      $tmp.=  sprintf('%.3F', cos($a)) .' '.sprintf('%.3F', (-1.0*sin($a))) .' '.sprintf('%.3F', sin($a)) .' '.sprintf('%.3F', cos($a)) .' ';
4142
4143      $tmp.=  sprintf('%.3F', $x) .' '.sprintf('%.3F', $y) .' Tm';
4144
4145      $this->objects[$this->currentContents]['c'].=  $tmp;
4146    }
4147
4148    if  ($wordSpaceAdjust !=  0 ||  $wordSpaceAdjust !=  $this->wordSpaceAdjust) {
4149
4150      $this->wordSpaceAdjust =  $wordSpaceAdjust;
4151
4152      $this->objects[$this->currentContents]['c'].=  ' '.sprintf('%.3F', $wordSpaceAdjust) .' Tw';
4153    }
4154
4155    $len =  mb_strlen($text);
4156
4157    $start =  0;
4158
4159    /*
4160     for ($i = 0;$i<$len;$i++){
4161     $f = 1;
4162     $directive = 0; //$this->PRVTcheckTextDirective($text,$i,$f);
4163     if ($directive){
4164     // then we should write what we need to
4165     if ($i>$start){
4166     $part = mb_substr($text,$start,$i-$start);
4167     $this->objects[$this->currentContents]['c'] .= ' /F'.$this->currentFontNum.' '.sprintf('%.1F',$size).' Tf ';
4168     $this->objects[$this->currentContents]['c'] .= ' ('.$this->filterText($part, false).') Tj';
4169     }
4170     if ($f){
4171     // then there was nothing drastic done here, restore the contents
4172     $this->setCurrentFont();
4173     } else {
4174     $this->objects[$this->currentContents]['c'] .= ' ET';
4175     $f = 1;
4176     $xp = $x;
4177     $yp = $y;
4178     $directive = 0; //$this->PRVTcheckTextDirective1($text,$i,$f,1,$xp,$yp,$size,$angle,$wordSpaceAdjust);
4179
4180     // restart the text object
4181     if ($angle == 0){
4182     $this->objects[$this->currentContents]['c'] .= "\n".'BT '.sprintf('%.3F',$xp).' '.sprintf('%.3F',$yp).' Td';
4183     } else {
4184     $a = deg2rad((float)$angle);
4185     $tmp = "\n".'BT ';
4186     $tmp .= sprintf('%.3F',cos($a)).' '.sprintf('%.3F',(-1.0*sin($a))).' '.sprintf('%.3F',sin($a)).' '.sprintf('%.3F',cos($a)).' ';
4187     $tmp .= sprintf('%.3F',$xp).' '.sprintf('%.3F',$yp).' Tm';
4188     $this->objects[$this->currentContents]['c'] .= $tmp;
4189     }
4190     if ($wordSpaceAdjust != 0 || $wordSpaceAdjust != $this->wordSpaceAdjust){
4191     $this->wordSpaceAdjust = $wordSpaceAdjust;
4192     $this->objects[$this->currentContents]['c'] .= ' '.sprintf('%.3F',$wordSpaceAdjust).' Tw';
4193     }
4194     }
4195     // and move the writing point to the next piece of text
4196     $i = $i+$directive-1;
4197     $start = $i+1;
4198     }
4199
4200     }
4201    */
4202    if  ($start < $len) {
4203
4204      $part =  $text; // OAR - Don't need this anymore, given that $start always equals zero.  substr($text, $start);
4205
4206      $this->objects[$this->currentContents]['c'].=  ' /F'.$this->currentFontNum.' '.sprintf('%.1F', $size) .' Tf ';
4207
4208      $this->objects[$this->currentContents]['c'].=  ' ('.$this->filterText($part, false) .') Tj';
4209    }
4210
4211    $this->objects[$this->currentContents]['c'].=  ' ET';
4212
4213
4214    // if there are any open callbacks, then they should be called, to show the end of the line
4215    if  ($this->nCallback>0) {
4216
4217      for  ($i =  $this->nCallback;$i>0;$i--) {
4218
4219        // call each function
4220        $tmp =  $this->PRVTgetTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text);
4221
4222        $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']);
4223
4224        $func =  $this->callback[$i]['f'];
4225
4226        $this->$func($info);
4227      }
4228    }
4229  }
4230
4231
4232  /**
4233   * calculate how wide a given text string will be on a page, at a given size.
4234   * this can be called externally, but is alse used by the other class functions
4235   */
4236  function  getTextWidth($size, $text, $spacing =  0) {
4237
4238    // this function should not change any of the settings, though it will need to
4239    // track any directives which change during calculation, so copy them at the start
4240    // and put them back at the end.
4241    $store_currentTextState =  $this->currentTextState;
4242
4243
4244    if  (!$this->numFonts) {
4245      $this->selectFont('./fonts/Helvetica');
4246    }
4247
4248
4249    // converts a number or a float to a string so it can get the width
4250    $text =  "$text";
4251
4252    // hmm, this is where it all starts to get tricky - use the font information to
4253    // calculate the width of each character, add them up and convert to user units
4254    $w =  0;
4255
4256    $cf =  $this->currentFont;
4257
4258    $space_scale =  1000 / $size;
4259
4260    if ( $this->isUnicode) {
4261      // for Unicode, use the code points array to calculate width rather
4262      // than just the string itself
4263      $unicode =  $this->utf8toCodePointsArray($text);
4264
4265      foreach ($unicode as $char) {
4266        // check if we have to replace character
4267      if  ( isset($this->fonts[$cf]['differences'][$char])) {
4268          $char =  $this->fonts[$cf]['differences'][$char];
4269        }
4270        // add the character width
4271        if  ( isset($this->fonts[$cf]['C'][$char]['WX'])) {
4272          $w+=  $this->fonts[$cf]['C'][$char]['WX'];
4273        }
4274        // add additional padding for space
4275        if  ( $char ==  32) {  // Space
4276          $w+=  $spacing * $space_scale;
4277        }
4278      }
4279
4280    } else {
4281
4282      $len =  mb_strlen($text);
4283
4284      for  ($i =  0; $i < $len; $i++) {
4285        $char =  ord($text{$i});
4286        // check if we have to replace character
4287        if  ( isset($this->fonts[$cf]['differences'][$char])) {
4288          $char =  $this->fonts[$cf]['differences'][$char];
4289        }
4290        // add the character width
4291        if  ( isset($this->fonts[$cf]['C'][$char]['WX'])) {
4292        $w+=  $this->fonts[$cf]['C'][$char]['WX'];
4293        }
4294        // add additional padding for space
4295        if  ( $char ==  32) {  // Space
4296        $w+=  $spacing * $space_scale;
4297    }
4298      }
4299    }
4300
4301
4302    $this->currentTextState =  $store_currentTextState;
4303
4304    $this->setCurrentFont();
4305
4306
4307    return  $w*$size/1000;
4308  }
4309
4310
4311  /**
4312   * do a part of the calculation for sorting out the justification of the text
4313   *
4314   * @access private
4315   */
4316  function  PRVTadjustWrapText($text, $actual, $width, &$x, &$adjust, $justification) {
4317
4318    switch  ($justification) {
4319
4320    case  'left':
4321
4322      return;
4323
4324      break;
4325
4326    case  'right':
4327
4328      $x+=  $width-$actual;
4329
4330      break;
4331
4332    case  'center':
4333
4334    case  'centre':
4335
4336      $x+=  ($width-$actual) /2;
4337
4338      break;
4339
4340    case  'full':
4341
4342      // count the number of words
4343      $words =  explode(' ', $text);
4344
4345      $nspaces =  count($words) -1;
4346
4347      if  ($nspaces>0) {
4348
4349        $adjust =  ($width-$actual) /$nspaces;
4350      } else {
4351
4352        $adjust =  0;
4353      }
4354
4355      break;
4356    }
4357  }
4358
4359
4360  /**
4361   * add text to the page, but ensure that it fits within a certain width
4362   * if it does not fit then put in as much as possible, splitting at word boundaries
4363   * and return the remainder.
4364   * justification and angle can also be specified for the text
4365   */
4366  function  addTextWrap($x, $y, $width, $size, $text, $justification =  'left', $angle =  0, $test =  0) {
4367        // TODO - need to support Unicode
4368    if ($this->isUnicode) {
4369        die("addTextWrap does not support Unicode yet!");
4370    }
4371
4372    // this will display the text, and if it goes beyond the width $width, will backtrack to the
4373    // previous space or hyphen, and return the remainder of the text.
4374
4375    // $justification can be set to 'left','right','center','centre','full'
4376
4377    // need to store the initial text state, as this will change during the width calculation
4378    // but will need to be re-set before printing, so that the chars work out right
4379    $store_currentTextState =  $this->currentTextState;
4380
4381
4382    if  (!$this->numFonts) {
4383      $this->selectFont('./fonts/Helvetica');
4384    }
4385
4386    if  ($width <=  0) {
4387
4388      // error, pretend it printed ok, otherwise risking a loop
4389      return  '';
4390    }
4391
4392    $w =  0;
4393
4394    $break =  0;
4395
4396    $breakWidth =  0;
4397
4398    $len =  mb_strlen($text);
4399
4400    $cf =  $this->currentFont;
4401
4402    $tw =  $width/$size*1000;
4403
4404    for  ($i =  0;$i<$len;$i++) {
4405
4406      $f =  1;
4407
4408      $directive =  0;
4409      //$this->PRVTcheckTextDirective($text,$i,$f);
4410      if  ($directive) {
4411
4412        if  ($f) {
4413
4414          $this->setCurrentFont();
4415
4416          $cf =  $this->currentFont;
4417        }
4418
4419        $i =  $i+$directive-1;
4420      } else {
4421
4422        $cOrd =  ord($text[$i]);
4423
4424        if  (isset($this->fonts[$cf]['differences'][$cOrd])) {
4425
4426          // then this character is being replaced by another
4427          $cOrd2 =  $this->fonts[$cf]['differences'][$cOrd];
4428        } else {
4429
4430          $cOrd2 =  $cOrd;
4431        }
4432
4433
4434        if  (isset($this->fonts[$cf]['C'][$cOrd2]['WX'])) {
4435
4436          $w+=  $this->fonts[$cf]['C'][$cOrd2]['WX'];
4437        }
4438
4439        if  ($w>$tw) {
4440
4441          // then we need to truncate this line
4442          if  ($break>0) {
4443
4444            // then we have somewhere that we can split :)
4445            if  ($text[$break] ==  ' ') {
4446
4447              $tmp =  mb_substr($text, 0, $break);
4448            } else {
4449
4450              $tmp =  mb_substr($text, 0, $break+1);
4451            }
4452
4453            $adjust =  0;
4454
4455            $this->PRVTadjustWrapText($tmp, $breakWidth, $width, $x, $adjust, $justification);
4456
4457
4458            // reset the text state
4459            $this->currentTextState =  $store_currentTextState;
4460
4461            $this->setCurrentFont();
4462
4463            if  (!$test) {
4464
4465              $this->addText($x, $y, $size, $tmp, $angle, $adjust);
4466            }
4467
4468            return  mb_substr($text, $break+1);
4469          } else {
4470
4471            // just split before the current character
4472            $tmp =  mb_substr($text, 0, $i);
4473
4474            $adjust =  0;
4475
4476            $ctmp =  ord($text[$i]);
4477
4478            if  (isset($this->fonts[$cf]['differences'][$ctmp])) {
4479
4480              $ctmp =  $this->fonts[$cf]['differences'][$ctmp];
4481            }
4482
4483            $tmpw =  ($w-$this->fonts[$cf]['C'][$ctmp]['WX']) *$size/1000;
4484
4485            $this->PRVTadjustWrapText($tmp, $tmpw, $width, $x, $adjust, $justification);
4486
4487            // reset the text state
4488            $this->currentTextState =  $store_currentTextState;
4489
4490            $this->setCurrentFont();
4491
4492            if  (!$test) {
4493
4494              $this->addText($x, $y, $size, $tmp, $angle, $adjust);
4495            }
4496
4497            return  mb_substr($text, $i);
4498          }
4499        }
4500
4501        if  ($text[$i] ==  '-') {
4502
4503          $break =  $i;
4504
4505          $breakWidth =  $w*$size/1000;
4506        }
4507
4508        if  ($text[$i] ==  ' ') {
4509
4510          $break =  $i;
4511
4512          $ctmp =  ord($text[$i]);
4513
4514          if  (isset($this->fonts[$cf]['differences'][$ctmp])) {
4515
4516            $ctmp =  $this->fonts[$cf]['differences'][$ctmp];
4517          }
4518
4519          $breakWidth =  ($w-$this->fonts[$cf]['C'][$ctmp]['WX']) *$size/1000;
4520        }
4521      }
4522    }
4523
4524    // then there was no need to break this line
4525    if  ($justification ==  'full') {
4526
4527      $justification =  'left';
4528    }
4529
4530    $adjust =  0;
4531
4532    $tmpw =  $w*$size/1000;
4533
4534    $this->PRVTadjustWrapText($text, $tmpw, $width, $x, $adjust, $justification);
4535
4536    // reset the text state
4537    $this->currentTextState =  $store_currentTextState;
4538
4539    $this->setCurrentFont();
4540
4541    if  (!$test) {
4542
4543      $this->addText($x, $y, $size, $text, $angle, $adjust, $angle);
4544    }
4545
4546    return  '';
4547  }
4548
4549
4550  /**
4551   * this will be called at a new page to return the state to what it was on the
4552   * end of the previous page, before the stack was closed down
4553   * This is to get around not being able to have open 'q' across pages
4554   *
4555   */
4556  function  saveState($pageEnd =  0) {
4557
4558    if  ($pageEnd) {
4559
4560      // this will be called at a new page to return the state to what it was on the
4561      // end of the previous page, before the stack was closed down
4562      // This is to get around not being able to have open 'q' across pages
4563      $opt =  $this->stateStack[$pageEnd];
4564      // ok to use this as stack starts numbering at 1
4565      $this->setColor($opt['col']['r'], $opt['col']['g'], $opt['col']['b'], 1);
4566
4567      $this->setStrokeColor($opt['str']['r'], $opt['str']['g'], $opt['str']['b'], 1);
4568
4569      $this->objects[$this->currentContents]['c'].=  "\n".$opt['lin'];
4570
4571      //    $this->currentLineStyle = $opt['lin'];
4572
4573    } else {
4574
4575      $this->nStateStack++;
4576
4577      $this->stateStack[$this->nStateStack] =  array(
4578                                                     'col' => $this->currentColour, 'str' => $this->currentStrokeColour, 'lin' => $this->currentLineStyle);
4579    }
4580
4581    $this->objects[$this->currentContents]['c'].=  "\nq";
4582  }
4583
4584
4585  /**
4586   * restore a previously saved state
4587   */
4588  function  restoreState($pageEnd =  0) {
4589
4590    if  (!$pageEnd) {
4591
4592      $n =  $this->nStateStack;
4593
4594      $this->currentColour =  $this->stateStack[$n]['col'];
4595
4596      $this->currentStrokeColour =  $this->stateStack[$n]['str'];
4597
4598      $this->objects[$this->currentContents]['c'].=  "\n".$this->stateStack[$n]['lin'];
4599
4600      $this->currentLineStyle =  $this->stateStack[$n]['lin'];
4601
4602      unset($this->stateStack[$n]);
4603
4604      $this->nStateStack--;
4605    }
4606
4607    $this->objects[$this->currentContents]['c'].=  "\nQ";
4608  }
4609
4610
4611  /**
4612   * make a loose object, the output will go into this object, until it is closed, then will revert to
4613   * the current one.
4614   * this object will not appear until it is included within a page.
4615   * the function will return the object number
4616   */
4617  function  openObject() {
4618
4619    $this->nStack++;
4620
4621    $this->stack[$this->nStack] =  array('c' => $this->currentContents, 'p' => $this->currentPage);
4622
4623    // add a new object of the content type, to hold the data flow
4624    $this->numObj++;
4625
4626    $this->o_contents($this->numObj, 'new');
4627
4628    $this->currentContents =  $this->numObj;
4629
4630    $this->looseObjects[$this->numObj] =  1;
4631
4632
4633    return  $this->numObj;
4634  }
4635
4636
4637  /**
4638   * open an existing object for editing
4639   */
4640  function  reopenObject($id) {
4641
4642    $this->nStack++;
4643
4644    $this->stack[$this->nStack] =  array('c' => $this->currentContents, 'p' => $this->currentPage);
4645
4646    $this->currentContents =  $id;
4647
4648    // also if this object is the primary contents for a page, then set the current page to its parent
4649    if  (isset($this->objects[$id]['onPage'])) {
4650
4651      $this->currentPage =  $this->objects[$id]['onPage'];
4652    }
4653  }
4654
4655
4656  /**
4657   * close an object
4658   */
4659  function  closeObject() {
4660
4661    // close the object, as long as there was one open in the first place, which will be indicated by
4662    // an objectId on the stack.
4663    if  ($this->nStack>0) {
4664
4665      $this->currentContents =  $this->stack[$this->nStack]['c'];
4666
4667      $this->currentPage =  $this->stack[$this->nStack]['p'];
4668
4669      $this->nStack--;
4670
4671      // easier to probably not worry about removing the old entries, they will be overwritten
4672      // if there are new ones.
4673
4674    }
4675  }
4676
4677
4678  /**
4679   * stop an object from appearing on pages from this point on
4680   */
4681  function  stopObject($id) {
4682
4683    // if an object has been appearing on pages up to now, then stop it, this page will
4684    // be the last one that could contian it.
4685    if  (isset($this->addLooseObjects[$id])) {
4686
4687      $this->addLooseObjects[$id] =  '';
4688    }
4689  }
4690
4691
4692  /**
4693   * after an object has been created, it wil only show if it has been added, using this function.
4694   */
4695  function  addObject($id, $options =  'add') {
4696
4697    // add the specified object to the page
4698    if  (isset($this->looseObjects[$id]) &&  $this->currentContents !=  $id) {
4699
4700      // then it is a valid object, and it is not being added to itself
4701      switch ($options) {
4702
4703      case  'all':
4704
4705        // then this object is to be added to this page (done in the next block) and
4706        // all future new pages.
4707        $this->addLooseObjects[$id] =  'all';
4708
4709      case  'add':
4710
4711        if  (isset($this->objects[$this->currentContents]['onPage'])) {
4712
4713          // then the destination contents is the primary for the page
4714          // (though this object is actually added to that page)
4715          $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id);
4716        }
4717
4718        break;
4719
4720      case  'even':
4721
4722        $this->addLooseObjects[$id] =  'even';
4723
4724        $pageObjectId =  $this->objects[$this->currentContents]['onPage'];
4725
4726        if  ($this->objects[$pageObjectId]['info']['pageNum']%2 ==  0) {
4727
4728          $this->addObject($id);
4729          // hacky huh :)
4730
4731        }
4732
4733        break;
4734
4735      case  'odd':
4736
4737        $this->addLooseObjects[$id] =  'odd';
4738
4739        $pageObjectId =  $this->objects[$this->currentContents]['onPage'];
4740
4741        if  ($this->objects[$pageObjectId]['info']['pageNum']%2 ==  1) {
4742
4743          $this->addObject($id);
4744          // hacky huh :)
4745
4746        }
4747
4748        break;
4749
4750      case  'next':
4751
4752        $this->addLooseObjects[$id] =  'all';
4753
4754        break;
4755
4756      case  'nexteven':
4757
4758        $this->addLooseObjects[$id] =  'even';
4759
4760        break;
4761
4762      case  'nextodd':
4763
4764        $this->addLooseObjects[$id] =  'odd';
4765
4766        break;
4767      }
4768    }
4769  }
4770
4771
4772  /**
4773   * return a storable representation of a specific object
4774   */
4775  function  serializeObject($id) {
4776    if  ( array_key_exists($id,  $this->objects))
4777      return  var_export($this->objects[$id],  true);
4778
4779
4780    return  null;
4781  }
4782
4783
4784  /**
4785   * restore an object from its stored representation.  returns its new object id.
4786   */
4787  function  restoreSerializedObject($obj) {
4788
4789
4790    $obj_id =  $this->openObject();
4791
4792    eval('$this->objects[$obj_id] = ' . $obj . ';');
4793
4794    $this->closeObject();
4795
4796    return  $obj_id;
4797  }
4798
4799
4800
4801  /**
4802   * add content to the documents info object
4803   */
4804  function  addInfo($label, $value =  0) {
4805
4806    // this will only work if the label is one of the valid ones.
4807    // modify this so that arrays can be passed as well.
4808    // if $label is an array then assume that it is key => value pairs
4809    // else assume that they are both scalar, anything else will probably error
4810    if  (is_array($label)) {
4811
4812      foreach ($label as  $l => $v) {
4813
4814        $this->o_info($this->infoObject, $l, $v);
4815      }
4816    } else {
4817
4818      $this->o_info($this->infoObject, $label, $value);
4819    }
4820  }
4821
4822
4823  /**
4824   * set the viewer preferences of the document, it is up to the browser to obey these.
4825   */
4826  function  setPreferences($label, $value =  0) {
4827
4828    // this will only work if the label is one of the valid ones.
4829    if  (is_array($label)) {
4830
4831      foreach ($label as  $l => $v) {
4832
4833        $this->o_catalog($this->catalogId, 'viewerPreferences', array($l => $v));
4834      }
4835    } else {
4836
4837      $this->o_catalog($this->catalogId, 'viewerPreferences', array($label => $value));
4838    }
4839  }
4840
4841
4842  /**
4843   * extract an integer from a position in a byte stream
4844   *
4845   * @access private
4846   */
4847  function  PRVT_getBytes(&$data, $pos, $num) {
4848
4849    // return the integer represented by $num bytes from $pos within $data
4850    $ret =  0;
4851
4852    for  ($i =  0;$i<$num;$i++) {
4853
4854      $ret =  $ret*256;
4855
4856      $ret+=  ord($data[$pos+$i]);
4857    }
4858
4859    return  $ret;
4860  }
4861
4862
4863  /**
4864   * add a PNG image into the document, from a GD object
4865   * this should work with remote files
4866   */
4867  function addImagePng($file, $x, $y, $w =  0, $h =  0, &$img) {
4868    //if already cached, need not to read again
4869        if ( isset($this->imagelist[$file]) ) {
4870          $data = null;
4871        } else {
4872          // Example for transparency handling on new image. Retain for current image
4873      // $tIndex = imagecolortransparent($img);
4874      // if ($tIndex > 0) {
4875      //   $tColor    = imagecolorsforindex($img, $tIndex);
4876      //   $new_tIndex    = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']);
4877      //   imagefill($new_img, 0, 0, $new_tIndex);
4878      //   imagecolortransparent($new_img, $new_tIndex);
4879      // }
4880          // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn
4881          //imagealphablending($img, true);
4882          //default, but explicitely set to ensure pdf compatibility
4883      imagesavealpha($img, false);
4884     
4885      $error =  0;
4886      //DEBUG_IMG_TEMP
4887      //debugpng
4888      if (DEBUGPNG) print '[addImagePng '.$file.']';
4889
4890      ob_start();
4891      @imagepng($img);
4892      //$data = ob_get_contents(); ob_end_clean();
4893      $data = ob_get_clean();
4894
4895      if ($data == '') {
4896        $error = 1;
4897        $errormsg = 'trouble writing file from GD';
4898        //DEBUG_IMG_TEMP
4899        //debugpng
4900        if (DEBUGPNG) print 'trouble writing file from GD';
4901          }
4902
4903      if  ($error) {
4904        $this->addMessage('PNG error - ('.$file.') '.$errormsg);
4905        return;
4906      }
4907    }  //End isset($this->imagelist[$file]) (png Duplicate removal)
4908
4909    $this->addPngFromBuf($file, $x, $y, $w, $h, $data);
4910  }
4911
4912
4913  /**
4914   * add a PNG image into the document, from a file
4915   * this should work with remote files
4916   */
4917  function  addPngFromFile($file, $x, $y, $w =  0, $h =  0) {
4918    //if already cached, need not to read again
4919        if ( isset($this->imagelist[$file]) ) {
4920          $img = null;
4921        } else {
4922      //png files typically contain an alpha channel.
4923      //pdf file format or class.pdf does not support alpha blending.
4924      //on alpha blended images, more transparent areas have a color near black.
4925      //This appears in the result on not storing the alpha channel.
4926      //Correct would be the box background image or its parent when transparent.
4927      //But this would make the image dependent on the background.
4928      //Therefore create an image with white background and copy in
4929      //A more natural background than black is white.
4930      //Therefore create an empty image with white background and merge the
4931      //image in with alpha blending.
4932      $imgtmp = @imagecreatefrompng($file);
4933      if (!$imgtmp) {
4934        return;
4935      }
4936      $sx = imagesx($imgtmp);
4937      $sy = imagesy($imgtmp);
4938      $img = imagecreatetruecolor($sx,$sy);
4939      imagealphablending($img, true);
4940          $ti = imagecolortransparent($imgtmp);
4941          if ($ti >= 0) {
4942            $tc = imagecolorsforindex($imgtmp,$ti);
4943        $ti = imagecolorallocate($img,$tc['red'],$tc['green'],$tc['blue']);
4944        imagefill($img,0,0,$ti);
4945        imagecolortransparent($img, $ti);
4946      } else {
4947        imagefill($img,1,1,imagecolorallocate($img,255,255,255));
4948      }
4949      imagecopy($img,$imgtmp,0,0,0,0,$sx,$sy);
4950      imagedestroy($imgtmp);
4951    }
4952    $this->addImagePng($file, $x, $y, $w, $h, $img);
4953  }
4954
4955
4956  /**
4957   * add a PNG image into the document, from a memory buffer of the file
4958   */
4959  function  addPngFromBuf($file, $x, $y, $w =  0, $h =  0, &$data) {
4960
4961        if ( isset($this->imagelist[$file]) ) {
4962      //debugpng
4963      //if (DEBUGPNG) print '[addPngFromBuf Duplicate '.$file.']';
4964          $data = null;
4965      $info['width'] = $this->imagelist[$file]['w'];
4966      $info['height'] = $this->imagelist[$file]['h'];
4967      $label = $this->imagelist[$file]['label'];
4968
4969        } else {
4970     
4971      if ($data == null) {
4972        $this->addMessage('addPngFromBuf error - ('.$imgname.') data not present!');
4973        return;
4974      }
4975      //debugpng
4976      //if (DEBUGPNG) print '[addPngFromBuf file='.$file.']';
4977    $error =  0;
4978
4979    if  (!$error) {
4980
4981      $header =  chr(137) .chr(80) .chr(78) .chr(71) .chr(13) .chr(10) .chr(26) .chr(10);
4982
4983      if  (mb_substr($data, 0, 8, '8bit') !=  $header) {
4984
4985        $error =  1;
4986
4987        //debugpng
4988        if (DEBUGPNG) print '[addPngFromFile this file does not have a valid header '.$file.']';
4989
4990        $errormsg =  'this file does not have a valid header';
4991      }
4992    }
4993
4994
4995    if  (!$error) {
4996
4997      // set pointer
4998      $p =  8;
4999
5000      $len =  mb_strlen($data, '8bit');
5001
5002      // cycle through the file, identifying chunks
5003      $haveHeader =  0;
5004
5005      $info =  array();
5006
5007      $idata =  '';
5008
5009      $pdata =  '';
5010
5011      while  ($p < $len) {
5012
5013        $chunkLen =  $this->PRVT_getBytes($data, $p, 4);
5014
5015        $chunkType =  mb_substr($data, $p+4, 4, '8bit');
5016
5017        //      echo $chunkType.' - '.$chunkLen.'<br>';
5018
5019        switch ($chunkType) {
5020
5021        case  'IHDR':
5022
5023          // this is where all the file information comes from
5024          $info['width'] =  $this->PRVT_getBytes($data, $p+8, 4);
5025
5026          $info['height'] =  $this->PRVT_getBytes($data, $p+12, 4);
5027
5028          $info['bitDepth'] =  ord($data[$p+16]);
5029
5030          $info['colorType'] =  ord($data[$p+17]);
5031
5032          $info['compressionMethod'] =  ord($data[$p+18]);
5033
5034          $info['filterMethod'] =  ord($data[$p+19]);
5035
5036          $info['interlaceMethod'] =  ord($data[$p+20]);
5037
5038          //print_r($info);
5039          $haveHeader =  1;
5040
5041          if  ($info['compressionMethod'] !=  0) {
5042
5043            $error =  1;
5044
5045            //debugpng
5046            if (DEBUGPNG) print '[addPngFromFile unsupported compression method '.$file.']';
5047
5048            $errormsg =  'unsupported compression method';
5049          }
5050
5051          if  ($info['filterMethod'] !=  0) {
5052
5053            $error =  1;
5054
5055            //debugpng
5056            if (DEBUGPNG) print '[addPngFromFile unsupported filter method '.$file.']';
5057
5058            $errormsg =  'unsupported filter method';
5059          }
5060
5061          break;
5062
5063        case  'PLTE':
5064
5065          $pdata.=  mb_substr($data, $p+8, $chunkLen, '8bit');
5066
5067          break;
5068
5069        case  'IDAT':
5070
5071          $idata.=  mb_substr($data, $p+8, $chunkLen, '8bit');
5072
5073          break;
5074
5075        case  'tRNS':
5076
5077          //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk
5078          //print "tRNS found, color type = ".$info['colorType']."\n";
5079          $transparency =  array();
5080
5081          if  ($info['colorType'] ==  3) {
5082            // indexed color, rbg
5083            /* corresponding to entries in the plte chunk
5084             Alpha for palette index 0: 1 byte
5085             Alpha for palette index 1: 1 byte
5086             ...etc...
5087            */
5088            // there will be one entry for each palette entry. up until the last non-opaque entry.
5089            // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent)
5090            $transparency['type'] =  'indexed';
5091
5092            $numPalette =  mb_strlen($pdata, '8bit') /3;
5093
5094            $trans =  0;
5095
5096            for  ($i =  $chunkLen;$i >=  0;$i--) {
5097
5098              if  (ord($data[$p+8+$i]) ==  0) {
5099
5100                $trans =  $i;
5101              }
5102            }
5103
5104            $transparency['data'] =  $trans;
5105          } elseif ($info['colorType'] ==  0) {
5106            // grayscale
5107            /* corresponding to entries in the plte chunk
5108             Gray: 2 bytes, range 0 .. (2^bitdepth)-1
5109            */
5110            //            $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale
5111            $transparency['type'] =  'indexed';
5112
5113            $transparency['data'] =  ord($data[$p+8+1]);
5114          } elseif ($info['colorType'] ==  2) {
5115            // truecolor
5116            /* corresponding to entries in the plte chunk
5117             Red: 2 bytes, range 0 .. (2^bitdepth)-1
5118             Green: 2 bytes, range 0 .. (2^bitdepth)-1
5119             Blue: 2 bytes, range 0 .. (2^bitdepth)-1
5120            */
5121            $transparency['r'] =  $this->PRVT_getBytes($data, $p+8, 2);
5122            // r from truecolor
5123            $transparency['g'] =  $this->PRVT_getBytes($data, $p+10, 2);
5124            // g from truecolor
5125            $transparency['b'] =  $this->PRVT_getBytes($data, $p+12, 2);
5126            // b from truecolor
5127
5128            $transparency['type'] = 'color-key';
5129           
5130          } else {
5131
5132            //unsupported transparency type
5133
5134            //debugpng
5135            if (DEBUGPNG) print '[addPngFromFile unsupported transparency type '.$file.']';
5136          }
5137
5138          // KS End new code
5139          break;
5140
5141        default:
5142
5143          break;
5144        }
5145
5146
5147        $p+=  $chunkLen+12;
5148      }
5149
5150
5151      if (!$haveHeader) {
5152
5153        $error =  1;
5154
5155        //debugpng
5156        if (DEBUGPNG) print '[addPngFromFile information header is missing '.$file.']';
5157
5158        $errormsg =  'information header is missing';
5159      }
5160
5161      if  (isset($info['interlaceMethod']) &&  $info['interlaceMethod']) {
5162
5163        $error =  1;
5164
5165        //debugpng
5166        if (DEBUGPNG) print '[addPngFromFile no support for interlaced images in pdf '.$file.']';
5167
5168        $errormsg =  'There appears to be no support for interlaced images in pdf.';
5169      }
5170    }
5171
5172
5173    if  (!$error &&  $info['bitDepth'] > 8) {
5174
5175      $error =  1;
5176
5177      //debugpng
5178      if (DEBUGPNG) print '[addPngFromFile bit depth of 8 or less is supported '.$file.']';
5179
5180      $errormsg =  'only bit depth of 8 or less is supported';
5181    }
5182
5183
5184    if  (!$error) {
5185
5186      if  ($info['colorType'] !=  2 &&  $info['colorType'] !=  0 &&  $info['colorType'] !=  3) {
5187
5188        $error =  1;
5189
5190        //debugpng
5191        if (DEBUGPNG) print '[addPngFromFile alpha channel not supported: '.$info['colorType'].' '.$file.']';
5192
5193        $errormsg =  'transparancey alpha channel not supported, transparency only supported for palette images.';
5194      } else {
5195
5196        switch  ($info['colorType']) {
5197
5198        case  3:
5199
5200          $color =  'DeviceRGB';
5201
5202          $ncolor =  1;
5203
5204          break;
5205
5206        case  2:
5207
5208          $color =  'DeviceRGB';
5209
5210          $ncolor =  3;
5211
5212          break;
5213
5214        case  0:
5215
5216          $color =  'DeviceGray';
5217
5218          $ncolor =  1;
5219
5220          break;
5221        }
5222      }
5223    }
5224
5225    if  ($error) {
5226
5227      $this->addMessage('PNG error - ('.$file.') '.$errormsg);
5228
5229      return;
5230    }
5231
5232      //print_r($info);
5233      // so this image is ok... add it in.
5234      $this->numImages++;
5235
5236      $im =  $this->numImages;
5237
5238      $label =  'I'.$im;
5239
5240      $this->numObj++;
5241
5242      //  $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width']));
5243      $options =  array('label' => $label, 'data' => $idata, 'bitsPerComponent' => $info['bitDepth'], 'pdata' => $pdata, 'iw' => $info['width'], 'ih' => $info['height'], 'type' => 'png', 'color' => $color, 'ncolor' => $ncolor);
5244
5245      if  (isset($transparency)) {
5246
5247        $options['transparency'] =  $transparency;
5248      }
5249
5250      $this->o_image($this->numObj, 'new', $options);
5251
5252      $this->imagelist[$file] = array('label' =>$label, 'w' => $info['width'], 'h' => $info['height']);
5253    }
5254
5255    if  ($w <=  0 && $h <=  0) {
5256      $w =  $info['width'];
5257      $h =  $info['height'];
5258    }
5259
5260    if  ($w <=  0) {
5261
5262      $w =  $h/$info['height']*$info['width'];
5263    }
5264
5265    if  ($h <=  0) {
5266
5267      $h =  $w*$info['height']/$info['width'];
5268    }
5269
5270    $this->objects[$this->currentContents]['c'].=  "\nq";
5271
5272    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $w) ." 0 0 ".sprintf('%.3F', $h) ." ".sprintf('%.3F', $x) ." ".sprintf('%.3F', $y) ." cm";
5273
5274    $this->objects[$this->currentContents]['c'].=  "\n/".$label.' Do';
5275
5276    $this->objects[$this->currentContents]['c'].=  "\nQ";
5277  }
5278
5279
5280  /**
5281   * add a JPEG image into the document, from a file
5282   */
5283  function  addJpegFromFile($img, $x, $y, $w =  0, $h =  0) {
5284
5285    // attempt to add a jpeg image straight from a file, using no GD commands
5286    // note that this function is unable to operate on a remote file.
5287
5288    if  (!file_exists($img)) {
5289
5290      return;
5291    }
5292
5293        if ( isset($this->imagelist[$img]) ) {
5294          $data = null;
5295      $imageWidth = $this->imagelist[$img]['w'];
5296      $imageHeight = $this->imagelist[$img]['h'];
5297      $channels =  $this->imagelist[$img]['c'];
5298        } else {
5299
5300      $tmp =  getimagesize($img);
5301
5302      $imageWidth =  $tmp[0];
5303
5304      $imageHeight =  $tmp[1];
5305
5306
5307      if  (isset($tmp['channels'])) {
5308
5309        $channels =  $tmp['channels'];
5310      } else {
5311
5312        $channels =  3;
5313      }
5314
5315
5316      //$fp = fopen($img,'rb');
5317
5318      $data =  file_get_contents($img);
5319
5320      //fread($fp,filesize($img));
5321
5322      //fclose($fp);
5323    }
5324
5325    if  ($w <=  0 &&  $h <=  0) {
5326
5327      $w =  $imageWidth;
5328    }
5329
5330    if  ($w ==  0) {
5331
5332      $w =  $h/$imageHeight*$imageWidth;
5333    }
5334
5335    if  ($h ==  0) {
5336
5337      $h =  $w*$imageHeight/$imageWidth;
5338    }
5339
5340    $this->addJpegImage_common($data, $x, $y, $w, $h, $imageWidth, $imageHeight, $channels, $img);
5341  }
5342
5343
5344  /**
5345   * add an image into the document, from a GD object
5346   * this function is not all that reliable, and I would probably encourage people to use
5347   * the file based functions
5348   */
5349  function  addImage(&$img, $x, $y, $w =  0, $h =  0, $quality =  75) {
5350
5351    /* Todo:
5352     * Pass in original filename as $imgname
5353     * If already cached like image_iscached(), allow empty $img
5354     * How to get w  and h in this case?
5355     * Then caller can check with image_iscached() whether generation of image is needed.
5356     *
5357     * But anyway, this function is not used!
5358     */
5359    $imgname = tempnam($this->tmp, "cpdf_img_").'.jpeg';
5360
5361    // add a new image into the current location, as an external object
5362    // add the image at $x,$y, and with width and height as defined by $w & $h
5363
5364    // note that this will only work with full colour images and makes them jpg images for display
5365    // later versions could present lossless image formats if there is interest.
5366
5367    // there seems to be some problem here in that images that have quality set above 75 do not appear
5368    // not too sure why this is, but in the meantime I have restricted this to 75.
5369    if  ($quality>75) {
5370
5371      $quality =  75;
5372    }
5373
5374
5375    // if the width or height are set to zero, then set the other one based on keeping the image
5376    // height/width ratio the same, if they are both zero, then give up :)
5377    $imageWidth =  imagesx($img);
5378
5379    $imageHeight =  imagesy($img);
5380
5381
5382    if  ($w <=  0 &&  $h <=  0) {
5383
5384      return;
5385    }
5386
5387    if  ($w ==  0) {
5388
5389      $w =  $h/$imageHeight*$imageWidth;
5390    }
5391
5392    if  ($h ==  0) {
5393
5394      $h =  $w*$imageHeight/$imageWidth;
5395    }
5396
5397    // gotta get the data out of the img..
5398    ob_start();
5399    imagejpeg($img, '', $quality);
5400    //$data = ob_get_contents(); ob_end_clean();
5401    $data = ob_get_clean();
5402
5403    $this->addJpegImage_common($data, $x, $y, $w, $h, $imageWidth, $imageHeight, $imgname);
5404  }
5405
5406
5407  /* Check if image already added to pdf image directory.
5408   * If yes, need not to create again (pass empty data)
5409   */
5410  function  image_iscached($imgname) {
5411    return isset($this->imagelist[$imgname]);
5412  }
5413
5414
5415  /**
5416   * common code used by the two JPEG adding functions
5417   *
5418   * @access private
5419   */
5420  function  addJpegImage_common(&$data, $x, $y, $w =  0, $h =  0, $imageWidth, $imageHeight, $channels =  3, $imgname) {
5421
5422    if ( isset($this->imagelist[$imgname]) ) {
5423      $label = $this->imagelist[$imgname]['label'];
5424      //debugpng
5425      //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']';
5426
5427    } else {
5428
5429      if ($data == null) {
5430        $this->addMessage('addJpegImage_common error - ('.$imgname.') data not present!');
5431        return;
5432      }
5433
5434      // note that this function is not to be called externally
5435      // it is just the common code between the GD and the file options
5436      $this->numImages++;
5437
5438      $im =  $this->numImages;
5439
5440      $label =  'I'.$im;
5441
5442      $this->numObj++;
5443
5444      $this->o_image($this->numObj, 'new', array('label' => $label, 'data' => &$data, 'iw' => $imageWidth, 'ih' => $imageHeight, 'channels' => $channels));
5445
5446      $this->imagelist[$imgname] = array('label' =>$label, 'w' => $imageWidth, 'h' => $imageHeight, 'c'=> $channels );
5447    }
5448
5449
5450    $this->objects[$this->currentContents]['c'].=  "\nq";
5451
5452    $this->objects[$this->currentContents]['c'].=  "\n".sprintf('%.3F', $w) ." 0 0 ".sprintf('%.3F', $h) ." ".sprintf('%.3F', $x) ." ".sprintf('%.3F', $y) ." cm";
5453
5454    $this->objects[$this->currentContents]['c'].=  "\n/".$label.' Do';
5455
5456    $this->objects[$this->currentContents]['c'].=  "\nQ";
5457  }
5458
5459
5460  /**
5461   * specify where the document should open when it first starts
5462   */
5463  function  openHere($style, $a =  0, $b =  0, $c =  0) {
5464
5465    // this function will open the document at a specified page, in a specified style
5466    // the values for style, and the required paramters are:
5467    // 'XYZ'  left, top, zoom
5468    // 'Fit'
5469    // 'FitH' top
5470    // 'FitV' left
5471    // 'FitR' left,bottom,right
5472    // 'FitB'
5473    // 'FitBH' top
5474    // 'FitBV' left
5475    $this->numObj++;
5476
5477    $this->o_destination($this->numObj, 'new', array('page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c));
5478
5479    $id =  $this->catalogId;
5480
5481    $this->o_catalog($id, 'openHere', $this->numObj);
5482  }
5483
5484
5485  /**
5486   * create a labelled destination within the document
5487   */
5488  function  addDestination($label, $style, $a =  0, $b =  0, $c =  0) {
5489
5490    // associates the given label with the destination, it is done this way so that a destination can be specified after
5491    // it has been linked to
5492    // styles are the same as the 'openHere' function
5493    $this->numObj++;
5494
5495    $this->o_destination($this->numObj, 'new', array('page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c));
5496
5497    $id =  $this->numObj;
5498
5499    // store the label->idf relationship, note that this means that labels can be used only once
5500    $this->destinations["$label"] =  $id;
5501  }
5502
5503
5504  /**
5505   * define font families, this is used to initialize the font families for the default fonts
5506   * and for the user to add new ones for their fonts. The default bahavious can be overridden should
5507   * that be desired.
5508   */
5509  function  setFontFamily($family, $options =  '') {
5510
5511    if  (!is_array($options)) {
5512
5513      if  ($family ==  'init') {
5514
5515        // set the known family groups
5516        // these font families will be used to enable bold and italic markers to be included
5517        // within text streams. html forms will be used... <b></b> <i></i>
5518        $this->fontFamilies['Helvetica.afm'] =
5519          array('b' => 'Helvetica-Bold.afm',
5520                'i' => 'Helvetica-Oblique.afm',
5521                'bi' => 'Helvetica-BoldOblique.afm',
5522                'ib' => 'Helvetica-BoldOblique.afm');
5523
5524        $this->fontFamilies['Courier.afm'] =
5525          array('b' => 'Courier-Bold.afm',
5526                'i' => 'Courier-Oblique.afm',
5527                'bi' => 'Courier-BoldOblique.afm',
5528                'ib' => 'Courier-BoldOblique.afm');
5529
5530        $this->fontFamilies['Times-Roman.afm'] =
5531          array('b' => 'Times-Bold.afm',
5532                'i' => 'Times-Italic.afm',
5533                'bi' => 'Times-BoldItalic.afm',
5534                'ib' => 'Times-BoldItalic.afm');
5535      }
5536    } else {
5537
5538      // the user is trying to set a font family
5539      // note that this can also be used to set the base ones to something else
5540      if  (mb_strlen($family)) {
5541
5542        $this->fontFamilies[$family] =  $options;
5543      }
5544    }
5545  }
5546
5547
5548  /**
5549   * used to add messages for use in debugging
5550   */
5551  function  addMessage($message) {
5552
5553    $this->messages.=  $message."\n";
5554  }
5555
5556
5557  /**
5558   * a few functions which should allow the document to be treated transactionally.
5559   */
5560  function  transaction($action) {
5561
5562    switch  ($action) {
5563
5564    case  'start':
5565
5566      // store all the data away into the checkpoint variable
5567      $data =  get_object_vars($this);
5568
5569      $this->checkpoint =  $data;
5570
5571      unset($data);
5572
5573      break;
5574
5575    case  'commit':
5576
5577      if  (is_array($this->checkpoint) &&  isset($this->checkpoint['checkpoint'])) {
5578
5579        $tmp =  $this->checkpoint['checkpoint'];
5580
5581        $this->checkpoint =  $tmp;
5582
5583        unset($tmp);
5584      } else {
5585
5586        $this->checkpoint =  '';
5587      }
5588
5589      break;
5590
5591    case  'rewind':
5592
5593      // do not destroy the current checkpoint, but move us back to the state then, so that we can try again
5594      if  (is_array($this->checkpoint)) {
5595
5596        // can only abort if were inside a checkpoint
5597        $tmp =  $this->checkpoint;
5598
5599        foreach ($tmp as  $k => $v) {
5600
5601          if  ($k !=  'checkpoint') {
5602
5603            $this->$k =  $v;
5604          }
5605        }
5606
5607        unset($tmp);
5608      }
5609
5610      break;
5611
5612    case  'abort':
5613
5614      if  (is_array($this->checkpoint)) {
5615
5616        // can only abort if were inside a checkpoint
5617        $tmp =  $this->checkpoint;
5618
5619        foreach ($tmp as  $k => $v) {
5620
5621          $this->$k =  $v;
5622        }
5623
5624        unset($tmp);
5625      }
5626
5627      break;
5628    }
5629  }
5630}
5631// end of class
5632
5633?>
Note: See TracBrowser for help on using the repository browser.