*** empty log message ***
[fa-stable.git] / reporting / includes / class.pdf.inc
1 <?php
2 /* $Revision$ */
3 /**
4 * Cpdf
5 *
6 * http://www.ros.co.nz/pdf
7 *
8 * A PHP class to provide the basic functionality to create a pdf document without
9 * any requirement for additional modules.
10 *
11 * Note that they companion class CezPdf can be used to extend this class and dramatically
12 * simplify the creation of documents.
13 *
14 * IMPORTANT NOTE
15 * there is no warranty, implied or otherwise with this software.
16
17 * LICENCE
18 * This code has been placed in the Public Domain for all to enjoy.
19 *
20 * @author               Wayne Munro <pdf@ros.co.nz>
21 * @version      009
22 * @package      Cpdf
23 */
24 class Cpdf {
25
26 /**
27 * the current number of pdf objects in the document
28 */
29 var $numObj=0;
30 /**
31 * this array contains all of the pdf objects, ready for final assembly
32 */
33 var $objects = array();
34 /**
35 * the objectId (number within the objects array) of the document catalog
36 */
37 var $catalogId;
38 /**
39 * array carrying information about the fonts that the system currently knows about
40 * used to ensure that a font is not loaded twice, among other things
41 */
42 var $fonts=array(); 
43 /**
44 * a record of the current font
45 */
46 var $currentFont='';
47 /**
48 * the current base font
49 */
50 var $currentBaseFont='';
51 /**
52 * the number of the current font within the font array
53 */
54 var $currentFontNum=0;
55 /**
56
57 */
58 var $currentNode;
59 /**
60 * object number of the current page
61 */
62 var $currentPage;
63 /**
64 * object number of the currently active contents block
65 */
66 var $currentContents;
67 /**
68 * number of fonts within the system
69 */
70 var $numFonts=0;
71 /**
72 * current colour for fill operations, defaults to inactive value, all three components should be between 0 and 1 inclusive when active
73 */
74 var $currentColour=array('r'=>-1,'g'=>-1,'b'=>-1);
75 /**
76 * current colour for stroke operations (lines etc.)
77 */
78 var $currentStrokeColour=array('r'=>-1,'g'=>-1,'b'=>-1);
79 /**
80 * current style that lines are drawn in
81 */
82 var $currentLineStyle='';
83 /**
84 * an array which is used to save the state of the document, mainly the colours and styles
85 * it is used to temporarily change to another state, the change back to what it was before
86 */
87 var $stateStack = array();
88 /**
89 * number of elements within the state stack
90 */
91 var $nStateStack = 0;
92 /**
93 * number of page objects within the document
94 */
95 var $numPages=0;
96 /**
97 * object Id storage stack
98 */
99 var $stack=array();
100 /**
101 * number of elements within the object Id storage stack
102 */
103 var $nStack=0;
104 /**
105 * an array which contains information about the objects which are not firmly attached to pages
106 * these have been added with the addObject function
107 */
108 var $looseObjects=array();
109 /**
110 * array contains infomation about how the loose objects are to be added to the document
111 */
112 var $addLooseObjects=array();
113 /**
114 * the objectId of the information object for the document
115 * this contains authorship, title etc.
116 */
117 var $infoObject=0;
118 /**
119 * number of images being tracked within the document
120 */
121 var $numImages=0;
122 /**
123 * an array containing options about the document
124 * it defaults to turning on the compression of the objects
125 */
126 var $options=array('compression'=>1);
127 /**
128 * the objectId of the first page of the document
129 */
130 var $firstPageId;
131 /**
132 * used to track the last used value of the inter-word spacing, this is so that it is known
133 * when the spacing is changed.
134 */
135 var $wordSpaceAdjust=0;
136 /**
137 * the object Id of the procset object
138 */
139 var $procsetObjectId;
140 /**
141 * store the information about the relationship between font families
142 * this used so that the code knows which font is the bold version of another font, etc.
143 * the value of this array is initialised in the constuctor function.
144 */
145 var $fontFamilies = array();
146 /**
147 * track if the current font is bolded or italicised
148 */
149 var $currentTextState = ''; 
150 /**
151 * messages are stored here during processing, these can be selected afterwards to give some useful debug information
152 */
153 var $messages='';
154 /**
155 * the ancryption array for the document encryption is stored here
156 */
157 var $arc4='';
158 /**
159 * the object Id of the encryption information
160 */
161 var $arc4_objnum=0;
162 /**
163 * the file identifier, used to uniquely identify a pdf document
164 */
165 var $fileIdentifier='';
166 /**
167 * a flag to say if a document is to be encrypted or not
168 */
169 var $encrypted=0;
170 /**
171 * the ancryption key for the encryption of all the document content (structure is not encrypted)
172 */
173 var $encryptionKey='';
174 /**
175 * array which forms a stack to keep track of nested callback functions
176 */
177 var $callback = array();
178 /**
179 * the number of callback functions in the callback array
180 */
181 var $nCallback = 0;
182 /**
183 * store label->id pairs for named destinations, these will be used to replace internal links
184 * done this way so that destinations can be defined after the location that links to them
185 */
186 var $destinations = array();
187 /**
188 * store the stack for the transaction commands, each item in here is a record of the values of all the 
189 * variables within the class, so that the user can rollback at will (from each 'start' command)
190 * note that this includes the objects array, so these can be large.
191 */
192 var $checkpoint = '';
193 /**
194 * class constructor
195 * this will start a new document
196 * @var array array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero.
197 */
198 function Cpdf ($pageSize=array(0,0,612,792)){
199   $this->newDocument($pageSize);
200   
201   // also initialize the font families that are known about already
202   $this->setFontFamily('init');
203 //  $this->fileIdentifier = md5('xxxxxxxx'.time());
204
205 }
206
207 /**
208 * Document object methods (internal use only)
209 *
210 * There is about one object method for each type of object in the pdf document
211 * Each function has the same call list ($id,$action,$options).
212 * $id = the object id of the object, or what it is to be if it is being created
213 * $action = a string specifying the action to be performed, though ALL must support:
214 *           'new' - create the object with the id $id
215 *           'out' - produce the output for the pdf object
216 * $options = optional, a string or array containing the various parameters for the object
217 *
218 * These, in conjunction with the output function are the ONLY way for output to be produced 
219 * within the pdf 'file'.
220 */
221
222 /**
223 *destination object, used to specify the location for the user to jump to, presently on opening
224 */
225 function o_destination($id,$action,$options=''){
226   if ($action!='new'){
227     $o =& $this->objects[$id];
228   }
229   switch($action){
230     case 'new':
231       $this->objects[$id]=array('t'=>'destination','info'=>array());
232       $tmp = '';
233       switch ($options['type']){
234         case 'XYZ':
235         case 'FitR':
236           $tmp =  ' '.$options['p3'].$tmp;
237         case 'FitH':
238         case 'FitV':
239         case 'FitBH':
240         case 'FitBV':
241           $tmp =  ' '.$options['p1'].' '.$options['p2'].$tmp;
242         case 'Fit':
243         case 'FitB':
244           $tmp =  $options['type'].$tmp;
245           $this->objects[$id]['info']['string']=$tmp;
246           $this->objects[$id]['info']['page']=$options['page'];
247       }
248       break;
249     case 'out':
250       $tmp = $o['info'];
251       $res="\n".$id." 0 obj\n".'['.$tmp['page'].' 0 R /'.$tmp['string']."]\nendobj\n";
252       return $res;
253       break;
254   }
255 }
256
257 /**
258 * set the viewer preferences
259 */
260 function o_viewerPreferences($id,$action,$options=''){
261   if ($action!='new'){
262     $o =& $this->objects[$id];
263   }
264   switch ($action){
265     case 'new':
266       $this->objects[$id]=array('t'=>'viewerPreferences','info'=>array());
267       break;
268     case 'add':
269       foreach($options as $k=>$v){
270         switch ($k){
271           case 'HideToolbar':
272           case 'HideMenubar':
273           case 'HideWindowUI':
274           case 'FitWindow':
275           case 'CenterWindow':
276           case 'NonFullScreenPageMode':
277           case 'Direction':
278             $o['info'][$k]=$v;
279           break;
280         }
281       }
282       break;
283     case 'out':
284
285       $res="\n".$id." 0 obj\n".'<< ';
286       foreach($o['info'] as $k=>$v){
287         $res.="\n/".$k.' '.$v;
288       }
289       $res.="\n>>\n";
290       return $res;
291       break;
292   }
293 }
294
295 /**
296 * define the document catalog, the overall controller for the document
297 */
298 function o_catalog($id,$action,$options=''){
299   if ($action!='new'){
300     $o =& $this->objects[$id];
301   }
302   switch ($action){
303     case 'new':
304       $this->objects[$id]=array('t'=>'catalog','info'=>array());
305       $this->catalogId=$id;
306       break;
307     case 'outlines':
308     case 'pages':
309     case 'openHere':
310       $o['info'][$action]=$options;
311       break;
312     case 'viewerPreferences':
313       if (!isset($o['info']['viewerPreferences'])){
314         $this->numObj++;
315         $this->o_viewerPreferences($this->numObj,'new');
316         $o['info']['viewerPreferences']=$this->numObj;
317       }
318       $vp = $o['info']['viewerPreferences'];
319       $this->o_viewerPreferences($vp,'add',$options);
320       break;
321     case 'out':
322       $res="\n".$id." 0 obj\n".'<< /Type /Catalog';
323       foreach($o['info'] as $k=>$v){
324         switch($k){
325           case 'outlines':
326             $res.="\n".'/Outlines '.$v.' 0 R';
327             break;
328           case 'pages':
329             $res.="\n".'/Pages '.$v.' 0 R';
330             break;
331           case 'viewerPreferences':
332             $res.="\n".'/ViewerPreferences '.$o['info']['viewerPreferences'].' 0 R';
333             break;
334           case 'openHere':
335             $res.="\n".'/OpenAction '.$o['info']['openHere'].' 0 R';
336             break;
337         }
338       }
339       $res.=" >>\nendobj";
340       return $res;
341       break;
342   }
343 }
344
345 /**
346 * object which is a parent to the pages in the document
347 */
348 function o_pages($id,$action,$options=''){
349   if ($action!='new'){
350     $o =& $this->objects[$id];
351   }
352   switch ($action){
353     case 'new':
354       $this->objects[$id]=array('t'=>'pages','info'=>array());
355       $this->o_catalog($this->catalogId,'pages',$id);
356       break;
357     case 'page':
358       if (!is_array($options)){
359         // then it will just be the id of the new page
360         $o['info']['pages'][]=$options;
361       } else {
362         // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative
363         // and pos is either 'before' or 'after', saying where this page will fit.
364         if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])){
365           $i = array_search($options['rid'],$o['info']['pages']);
366           if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i]==$options['rid']){
367             // then there is a match
368             // make a space
369             switch ($options['pos']){
370               case 'before':
371                 $k = $i;
372                 break;
373               case 'after':
374                 $k=$i+1;
375                 break;
376               default:
377                 $k=-1;
378                 break;
379             }
380             if ($k>=0){
381               for ($j=count($o['info']['pages'])-1;$j>=$k;$j--){
382                 $o['info']['pages'][$j+1]=$o['info']['pages'][$j];
383               }
384               $o['info']['pages'][$k]=$options['id'];
385             }
386           }
387         } 
388       }
389       break;
390     case 'procset':
391       $o['info']['procset']=$options;
392       break;
393     case 'mediaBox':
394       $o['info']['mediaBox']=$options; // which should be an array of 4 numbers
395       break;
396     case 'font':
397       $o['info']['fonts'][]=array('objNum'=>$options['objNum'],'fontNum'=>$options['fontNum']);
398       break;
399     case 'xObject':
400       $o['info']['xObjects'][]=array('objNum'=>$options['objNum'],'label'=>$options['label']);
401       break;
402     case 'out':
403       if (count($o['info']['pages'])){
404         $res="\n".$id." 0 obj\n<< /Type /Pages\n/Kids [";
405         foreach($o['info']['pages'] as $k=>$v){
406           $res.=$v." 0 R\n";
407         }
408         $res.="]\n/Count ".count($this->objects[$id]['info']['pages']);
409         if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) || isset($o['info']['procset'])){
410           $res.="\n/Resources <<";
411           if (isset($o['info']['procset'])){
412             $res.="\n/ProcSet ".$o['info']['procset']." 0 R";
413           }
414           if (isset($o['info']['fonts']) && count($o['info']['fonts'])){
415             $res.="\n/Font << ";
416             foreach($o['info']['fonts'] as $finfo){
417               $res.="\n/F".$finfo['fontNum']." ".$finfo['objNum']." 0 R";
418             }
419             $res.=" >>";
420           }
421           if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])){
422             $res.="\n/XObject << ";
423             foreach($o['info']['xObjects'] as $finfo){
424               $res.="\n/".$finfo['label']." ".$finfo['objNum']." 0 R";
425             }
426             $res.=" >>";
427           }
428           $res.="\n>>";
429           if (isset($o['info']['mediaBox'])){
430             $tmp=$o['info']['mediaBox'];
431             $res.="\n/MediaBox [".sprintf('%.3f',$tmp[0]).' '.sprintf('%.3f',$tmp[1]).' '.sprintf('%.3f',$tmp[2]).' '.sprintf('%.3f',$tmp[3]).']';
432           }
433         }
434         $res.="\n >>\nendobj";
435       } else {
436         $res="\n".$id." 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj";
437       }
438       return $res;
439     break;
440   }
441 }
442
443 /**
444 * define the outlines in the doc, empty for now
445 */
446 function o_outlines($id,$action,$options=''){
447   if ($action!='new'){
448     $o =& $this->objects[$id];
449   }
450   switch ($action){
451     case 'new':
452       $this->objects[$id]=array('t'=>'outlines','info'=>array('outlines'=>array()));
453       $this->o_catalog($this->catalogId,'outlines',$id);
454       break;
455     case 'outline':
456       $o['info']['outlines'][]=$options;
457       break;
458     case 'out':
459       if (count($o['info']['outlines'])){
460         $res="\n".$id." 0 obj\n<< /Type /Outlines /Kids [";
461         foreach($o['info']['outlines'] as $k=>$v){
462           $res.=$v." 0 R ";
463         }
464         $res.="] /Count ".count($o['info']['outlines'])." >>\nendobj";
465       } else {
466         $res="\n".$id." 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj";
467       }
468       return $res;
469       break;
470   }
471 }
472
473 /**
474 * an object to hold the font description
475 */
476 function o_font($id,$action,$options=''){
477   if ($action!='new'){
478     $o =& $this->objects[$id];
479   }
480   switch ($action){
481     case 'new':
482       $this->objects[$id]=array('t'=>'font','info'=>array('name'=>$options['name'],'SubType'=>'Type1'));
483       $fontNum=$this->numFonts;
484       $this->objects[$id]['info']['fontNum']=$fontNum;
485       // deal with the encoding and the differences
486       if (isset($options['differences'])){
487         // then we'll need an encoding dictionary
488         $this->numObj++;
489         $this->o_fontEncoding($this->numObj,'new',$options);
490         $this->objects[$id]['info']['encodingDictionary']=$this->numObj;
491       } else if (isset($options['encoding'])){
492         // we can specify encoding here
493         switch($options['encoding']){
494           case 'WinAnsiEncoding':
495           case 'MacRomanEncoding':
496           case 'MacExpertEncoding':
497             $this->objects[$id]['info']['encoding']=$options['encoding'];
498             break;
499           case 'none':
500             break;
501           default:
502             $this->objects[$id]['info']['encoding']='WinAnsiEncoding';
503             break;
504         }
505       } else {
506         $this->objects[$id]['info']['encoding']='WinAnsiEncoding';
507       }
508       // also tell the pages node about the new font
509       $this->o_pages($this->currentNode,'font',array('fontNum'=>$fontNum,'objNum'=>$id));
510       break;
511     case 'add':
512       foreach ($options as $k=>$v){
513         switch ($k){
514           case 'BaseFont':
515             $o['info']['name'] = $v;
516             break;
517           case 'FirstChar':
518           case 'LastChar':
519           case 'Widths':
520           case 'FontDescriptor':
521           case 'SubType':
522           $this->addMessage('o_font '.$k." : ".$v);
523             $o['info'][$k] = $v;
524             break;
525         }
526      }
527       break;
528     case 'out':
529       $res="\n".$id." 0 obj\n<< /Type /Font\n/Subtype /".$o['info']['SubType']."\n";
530       $res.="/Name /F".$o['info']['fontNum']."\n";
531       $res.="/BaseFont /".$o['info']['name']."\n";
532       if (isset($o['info']['encodingDictionary'])){
533         // then place a reference to the dictionary
534         $res.="/Encoding ".$o['info']['encodingDictionary']." 0 R\n";
535       } else if (isset($o['info']['encoding'])){
536         // use the specified encoding
537         $res.="/Encoding /".$o['info']['encoding']."\n";
538       }
539       if (isset($o['info']['FirstChar'])){
540         $res.="/FirstChar ".$o['info']['FirstChar']."\n";
541       }
542       if (isset($o['info']['LastChar'])){
543         $res.="/LastChar ".$o['info']['LastChar']."\n";
544       }
545       if (isset($o['info']['Widths'])){
546         $res.="/Widths ".$o['info']['Widths']." 0 R\n";
547       }
548       if (isset($o['info']['FontDescriptor'])){
549         $res.="/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n";
550       }
551       $res.=">>\nendobj";
552       return $res;
553       break;
554   }
555 }
556
557 /**
558 * a font descriptor, needed for including additional fonts
559 */
560 function o_fontDescriptor($id,$action,$options=''){
561   if ($action!='new'){
562     $o =& $this->objects[$id];
563   }
564   switch ($action){
565     case 'new':
566       $this->objects[$id]=array('t'=>'fontDescriptor','info'=>$options);
567       break;
568     case 'out':
569       $res="\n".$id." 0 obj\n<< /Type /FontDescriptor\n";
570       foreach ($o['info'] as $label => $value){
571         switch ($label){
572           case 'Ascent':
573           case 'CapHeight':
574           case 'Descent':
575           case 'Flags':
576           case 'ItalicAngle':
577           case 'StemV':
578           case 'AvgWidth':
579           case 'Leading':
580           case 'MaxWidth':
581           case 'MissingWidth':
582           case 'StemH':
583           case 'XHeight':
584           case 'CharSet':
585             if (strlen($value)){
586               $res.='/'.$label.' '.$value."\n";
587             }
588             break;
589           case 'FontFile':
590           case 'FontFile2':
591           case 'FontFile3':
592             $res.='/'.$label.' '.$value." 0 R\n";
593             break;
594           case 'FontBBox':
595             $res.='/'.$label.' ['.$value[0].' '.$value[1].' '.$value[2].' '.$value[3]."]\n";
596             break;
597           case 'FontName':
598             $res.='/'.$label.' /'.$value."\n";
599             break;
600         }
601       }
602       $res.=">>\nendobj";
603       return $res;
604       break;
605   }
606 }
607
608 /**
609 * the font encoding
610 */
611 function o_fontEncoding($id,$action,$options=''){
612   if ($action!='new'){
613     $o =& $this->objects[$id];
614   }
615   switch ($action){
616     case 'new':
617       // the options array should contain 'differences' and maybe 'encoding'
618       $this->objects[$id]=array('t'=>'fontEncoding','info'=>$options);
619       break;
620     case 'out':
621       $res="\n".$id." 0 obj\n<< /Type /Encoding\n";
622       if (!isset($o['info']['encoding'])){
623         $o['info']['encoding']='WinAnsiEncoding';
624       }
625       if ($o['info']['encoding']!='none'){
626         $res.="/BaseEncoding /".$o['info']['encoding']."\n";
627       }
628       $res.="/Differences \n[";
629       $onum=-100;
630       foreach($o['info']['differences'] as $num=>$label){
631         if ($num!=$onum+1){
632           // we cannot make use of consecutive numbering
633           $res.= "\n".$num." /".$label;
634         } else {
635           $res.= " /".$label;
636         }
637         $onum=$num;
638       }
639       $res.="\n]\n>>\nendobj";
640       return $res;
641       break;
642   }
643 }
644
645 /**
646 * the document procset, solves some problems with printing to old PS printers
647 */
648 function o_procset($id,$action,$options=''){
649   if ($action!='new'){
650     $o =& $this->objects[$id];
651   }
652   switch ($action){
653     case 'new':
654       $this->objects[$id]=array('t'=>'procset','info'=>array('PDF'=>1,'Text'=>1));
655       $this->o_pages($this->currentNode,'procset',$id);
656       $this->procsetObjectId=$id;
657       break;
658     case 'add':
659       // this is to add new items to the procset list, despite the fact that this is considered
660       // obselete, the items are required for printing to some postscript printers
661       switch ($options) {
662         case 'ImageB':
663         case 'ImageC':
664         case 'ImageI':
665           $o['info'][$options]=1;
666           break;
667       }
668       break;
669     case 'out':
670       $res="\n".$id." 0 obj\n[";
671       foreach ($o['info'] as $label=>$val){
672         $res.='/'.$label.' ';
673       }
674       $res.="]\nendobj";
675       return $res;
676       break;
677   }
678 }
679
680 /**
681 * define the document information
682 */
683 function o_info($id,$action,$options=''){
684   if ($action!='new'){
685     $o =& $this->objects[$id];
686   }
687   switch ($action){
688     case 'new':
689       $this->infoObject=$id;
690       $date='D:'.date('Ymd');
691       $this->objects[$id]=array('t'=>'info','info'=>array('Creator'=>'R and OS php pdf writer, http://www.ros.co.nz','CreationDate'=>$date));
692       break;
693     case 'Title':
694     case 'Author':
695     case 'Subject':
696     case 'Keywords':
697     case 'Creator':
698     case 'Producer':
699     case 'CreationDate':
700     case 'ModDate':
701     case 'Trapped':
702       $o['info'][$action]=$options;
703       break;
704     case 'out':
705       if ($this->encrypted){
706         $this->encryptInit($id);
707       }
708       $res="\n".$id." 0 obj\n<<\n";
709       foreach ($o['info']  as $k=>$v){
710         $res.='/'.$k.' (';
711         if ($this->encrypted){
712           $res.=$this->filterText($this->ARC4($v));
713         } else {
714           $res.=$this->filterText($v);
715         }
716         $res.=")\n";
717       }
718       $res.=">>\nendobj";
719       return $res;
720       break;
721   }
722 }
723
724 /**
725 * an action object, used to link to URLS initially
726 */
727 function o_action($id,$action,$options=''){
728   if ($action!='new'){
729     $o =& $this->objects[$id];
730   }
731   switch ($action){
732     case 'new':
733       if (is_array($options)){
734         $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>$options['type']);
735       } else {
736         // then assume a URI action
737         $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>'URI');
738       }
739       break;
740     case 'out':
741       if ($this->encrypted){
742         $this->encryptInit($id);
743       }
744       $res="\n".$id." 0 obj\n<< /Type /Action";
745       switch($o['type']){
746         case 'ilink':
747           // there will be an 'label' setting, this is the name of the destination
748           $res.="\n/S /GoTo\n/D ".$this->destinations[(string)$o['info']['label']]." 0 R";
749           break;
750         case 'URI':
751           $res.="\n/S /URI\n/URI (";
752           if ($this->encrypted){
753             $res.=$this->filterText($this->ARC4($o['info']));
754           } else {
755             $res.=$this->filterText($o['info']);
756           }
757           $res.=")";
758           break;
759       }
760       $res.="\n>>\nendobj";
761       return $res;
762       break;
763   }
764 }
765
766 /**
767 * an annotation object, this will add an annotation to the current page.
768 * initially will support just link annotations 
769 */
770 function o_annotation($id,$action,$options=''){
771   if ($action!='new'){
772     $o =& $this->objects[$id];
773   }
774   switch ($action){
775     case 'new':
776       // add the annotation to the current page
777       $pageId = $this->currentPage;
778       $this->o_page($pageId,'annot',$id);
779       // and add the action object which is going to be required
780       switch($options['type']){
781         case 'link':
782           $this->objects[$id]=array('t'=>'annotation','info'=>$options);
783           $this->numObj++;
784           $this->o_action($this->numObj,'new',$options['url']);
785           $this->objects[$id]['info']['actionId']=$this->numObj;
786           break;
787         case 'ilink':
788           // this is to a named internal link
789           $label = $options['label'];
790           $this->objects[$id]=array('t'=>'annotation','info'=>$options);
791           $this->numObj++;
792           $this->o_action($this->numObj,'new',array('type'=>'ilink','label'=>$label));
793           $this->objects[$id]['info']['actionId']=$this->numObj;
794           break;
795       }
796       break;
797     case 'out':
798       $res="\n".$id." 0 obj\n<< /Type /Annot";
799       switch($o['info']['type']){
800         case 'link':
801         case 'ilink':
802           $res.= "\n/Subtype /Link";
803           break;
804       }
805       $res.="\n/A ".$o['info']['actionId']." 0 R";
806       $res.="\n/Border [0 0 0]";
807       $res.="\n/H /I";
808       $res.="\n/Rect [ ";
809       foreach($o['info']['rect'] as $v){
810         $res.= sprintf("%.4f ",$v);
811       }
812       $res.="]";
813       $res.="\n>>\nendobj";
814       return $res;
815       break;
816   }
817 }
818
819 /**
820 * a page object, it also creates a contents object to hold its contents
821 */
822 function o_page($id,$action,$options=''){
823   if ($action!='new'){
824     $o =& $this->objects[$id];
825   }
826   switch ($action){
827     case 'new':
828       $this->numPages++;
829       $this->objects[$id]=array('t'=>'page','info'=>array('parent'=>$this->currentNode,'pageNum'=>$this->numPages));
830       if (is_array($options)){
831         // then this must be a page insertion, array shoudl contain 'rid','pos'=[before|after]
832         $options['id']=$id;
833         $this->o_pages($this->currentNode,'page',$options);
834       } else {
835         $this->o_pages($this->currentNode,'page',$id);
836       }
837       $this->currentPage=$id;
838       //make a contents object to go with this page
839       $this->numObj++;
840       $this->o_contents($this->numObj,'new',$id);
841       $this->currentContents=$this->numObj;
842       $this->objects[$id]['info']['contents']=array();
843       $this->objects[$id]['info']['contents'][]=$this->numObj;
844       $match = ($this->numPages%2 ? 'odd' : 'even');
845       foreach($this->addLooseObjects as $oId=>$target){
846         if ($target=='all' || $match==$target){
847           $this->objects[$id]['info']['contents'][]=$oId;
848         }
849       }
850       break;
851     case 'content':
852       $o['info']['contents'][]=$options;
853       break;
854     case 'annot':
855       // add an annotation to this page
856       if (!isset($o['info']['annot'])){
857         $o['info']['annot']=array();
858       }
859       // $options should contain the id of the annotation dictionary
860       $o['info']['annot'][]=$options;
861       break;
862     case 'out':
863       $res="\n".$id." 0 obj\n<< /Type /Page";
864       $res.="\n/Parent ".$o['info']['parent']." 0 R";
865       if (isset($o['info']['annot'])){
866         $res.="\n/Annots [";
867         foreach($o['info']['annot'] as $aId){
868           $res.=" ".$aId." 0 R";
869         }
870         $res.=" ]";
871       }
872       $count = count($o['info']['contents']);
873       if ($count==1){
874         $res.="\n/Contents ".$o['info']['contents'][0]." 0 R";
875       } else if ($count>1){
876         $res.="\n/Contents [\n";
877         foreach ($o['info']['contents'] as $cId){
878           $res.=$cId." 0 R\n";
879         }
880         $res.="]";
881       }
882       $res.="\n>>\nendobj";
883       return $res;
884       break;
885   }
886 }
887
888 /**
889 * the contents objects hold all of the content which appears on pages
890 */
891 function o_contents($id,$action,$options=''){
892   if ($action!='new'){
893     $o =& $this->objects[$id];
894   }
895   switch ($action){
896     case 'new':
897       $this->objects[$id]=array('t'=>'contents','c'=>'','info'=>array());
898       if (strlen($options) && intval($options)){
899         // then this contents is the primary for a page
900         $this->objects[$id]['onPage']=$options;
901       } else if ($options=='raw'){
902         // then this page contains some other type of system object
903         $this->objects[$id]['raw']=1;
904       }
905       break;
906     case 'add':
907       // add more options to the decleration
908       foreach ($options as $k=>$v){
909         $o['info'][$k]=$v;
910       }
911     case 'out':
912       $tmp=$o['c'];
913       $res= "\n".$id." 0 obj\n";
914       if (isset($this->objects[$id]['raw'])){
915         $res.=$tmp;
916       } else {
917         $res.= "<<";
918         if (function_exists('gzcompress') && $this->options['compression']){
919           // then implement ZLIB based compression on this content stream
920           $res.=" /Filter /FlateDecode";
921           $tmp = gzcompress($tmp);
922         }
923         if ($this->encrypted){
924           $this->encryptInit($id);
925           $tmp = $this->ARC4($tmp);
926         }
927         foreach($o['info'] as $k=>$v){
928           $res .= "\n/".$k.' '.$v;
929         }
930         $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream";
931       }
932       $res.="\nendobj\n";
933       return $res;
934       break;
935   }
936 }
937
938 /**
939 * an image object, will be an XObject in the document, includes description and data
940 */
941 function o_image($id,$action,$options=''){
942   if ($action!='new'){
943     $o =& $this->objects[$id];
944   }
945   switch($action){
946     case 'new':
947       // make the new object
948       $this->objects[$id]=array('t'=>'image','data'=>$options['data'],'info'=>array());
949       $this->objects[$id]['info']['Type']='/XObject';
950       $this->objects[$id]['info']['Subtype']='/Image';
951       $this->objects[$id]['info']['Width']=$options['iw'];
952       $this->objects[$id]['info']['Height']=$options['ih'];
953       if (!isset($options['type']) || $options['type']=='jpg'){
954         if (!isset($options['channels'])){
955           $options['channels']=3;
956         }
957         switch($options['channels']){
958           case 1:
959             $this->objects[$id]['info']['ColorSpace']='/DeviceGray';
960             break;
961           default:
962             $this->objects[$id]['info']['ColorSpace']='/DeviceRGB';
963             break;
964         }
965         $this->objects[$id]['info']['Filter']='/DCTDecode';
966         $this->objects[$id]['info']['BitsPerComponent']=8;
967       } else if ($options['type']=='png'){
968         $this->objects[$id]['info']['Filter']='/FlateDecode';
969         $this->objects[$id]['info']['DecodeParms']='<< /Predictor 15 /Colors '.$options['ncolor'].' /Columns '.$options['iw'].' /BitsPerComponent '.$options['bitsPerComponent'].'>>';
970         if (strlen($options['pdata'])){
971           $tmp = ' [ /Indexed /DeviceRGB '.(strlen($options['pdata'])/3-1).' ';
972           $this->numObj++;
973           $this->o_contents($this->numObj,'new');
974           $this->objects[$this->numObj]['c']=$options['pdata'];
975           $tmp.=$this->numObj.' 0 R';
976           $tmp .=' ]';
977           $this->objects[$id]['info']['ColorSpace'] = $tmp;
978           if (isset($options['transparency'])){
979             switch($options['transparency']['type']){
980               case 'indexed':
981                 $tmp=' [ '.$options['transparency']['data'].' '.$options['transparency']['data'].'] ';
982                 $this->objects[$id]['info']['Mask'] = $tmp;
983                 break;
984             }
985           }
986         } else {
987           $this->objects[$id]['info']['ColorSpace']='/'.$options['color'];
988         }
989         $this->objects[$id]['info']['BitsPerComponent']=$options['bitsPerComponent'];
990       }
991       // assign it a place in the named resource dictionary as an external object, according to
992       // the label passed in with it.
993       $this->o_pages($this->currentNode,'xObject',array('label'=>$options['label'],'objNum'=>$id));
994       // also make sure that we have the right procset object for it.
995       $this->o_procset($this->procsetObjectId,'add','ImageC');
996       break;
997     case 'out':
998       $tmp=$o['data'];
999       $res= "\n".$id." 0 obj\n<<";
1000       foreach($o['info'] as $k=>$v){
1001         $res.="\n/".$k.' '.$v;
1002       }
1003       if ($this->encrypted){
1004         $this->encryptInit($id);
1005         $tmp = $this->ARC4($tmp);
1006       }
1007       $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream\nendobj\n";
1008       return $res;
1009       break;
1010   }
1011 }
1012
1013 /**
1014 * encryption object.
1015 */
1016 function o_encryption($id,$action,$options=''){
1017   if ($action!='new'){
1018     $o =& $this->objects[$id];
1019   }
1020   switch($action){
1021     case 'new':
1022       // make the new object
1023       $this->objects[$id]=array('t'=>'encryption','info'=>$options);
1024       $this->arc4_objnum=$id;
1025       // figure out the additional paramaters required
1026       $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);
1027       $len = strlen($options['owner']);
1028       if ($len>32){
1029         $owner = substr($options['owner'],0,32);
1030       } else if ($len<32){
1031         $owner = $options['owner'].substr($pad,0,32-$len);
1032       } else {
1033         $owner = $options['owner'];
1034       }
1035       $len = strlen($options['user']);
1036       if ($len>32){
1037         $user = substr($options['user'],0,32);
1038       } else if ($len<32){
1039         $user = $options['user'].substr($pad,0,32-$len);
1040       } else {
1041         $user = $options['user'];
1042       }
1043       $tmp = $this->md5_16($owner);
1044       $okey = substr($tmp,0,5);
1045       $this->ARC4_init($okey);
1046       $ovalue=$this->ARC4($user);
1047       $this->objects[$id]['info']['O']=$ovalue;
1048       // now make the u value, phew.
1049       $tmp = $this->md5_16($user.$ovalue.chr($options['p']).chr(255).chr(255).chr(255).$this->fileIdentifier);
1050       $ukey = substr($tmp,0,5);
1051
1052       $this->ARC4_init($ukey);
1053       $this->encryptionKey = $ukey;
1054       $this->encrypted=1;
1055       $uvalue=$this->ARC4($pad);
1056
1057       $this->objects[$id]['info']['U']=$uvalue;
1058       $this->encryptionKey=$ukey;
1059      
1060       // initialize the arc4 array
1061       break;
1062     case 'out':
1063       $res= "\n".$id." 0 obj\n<<";
1064       $res.="\n/Filter /Standard";
1065       $res.="\n/V 1";
1066       $res.="\n/R 2";
1067       $res.="\n/O (".$this->filterText($o['info']['O']).')';
1068       $res.="\n/U (".$this->filterText($o['info']['U']).')';
1069       // and the p-value needs to be converted to account for the twos-complement approach
1070       $o['info']['p'] = (($o['info']['p']^255)+1)*-1;
1071       $res.="\n/P ".($o['info']['p']);
1072       $res.="\n>>\nendobj\n";
1073       
1074       return $res;
1075       break;
1076   }
1077 }
1078       
1079 /**
1080 * ARC4 functions
1081 * A series of function to implement ARC4 encoding in PHP
1082 */
1083
1084 /**
1085 * calculate the 16 byte version of the 128 bit md5 digest of the string
1086 */
1087 function md5_16($string){
1088   $tmp = md5($string);
1089   $out='';
1090   for ($i=0;$i<=30;$i=$i+2){
1091     $out.=chr(hexdec(substr($tmp,$i,2)));
1092   }
1093   return $out;
1094 }
1095
1096 /**
1097 * initialize the encryption for processing a particular object 
1098 */
1099 function encryptInit($id){
1100   $tmp = $this->encryptionKey;
1101   $hex = dechex($id);
1102   if (strlen($hex)<6){
1103     $hex = substr('000000',0,6-strlen($hex)).$hex;
1104   }
1105   $tmp.= chr(hexdec(substr($hex,4,2))).chr(hexdec(substr($hex,2,2))).chr(hexdec(substr($hex,0,2))).chr(0).chr(0);
1106   $key = $this->md5_16($tmp);
1107   $this->ARC4_init(substr($key,0,10));
1108 }
1109
1110 /**
1111 * initialize the ARC4 encryption
1112 */
1113 function ARC4_init($key=''){
1114   $this->arc4 = '';
1115   // setup the control array
1116   if (strlen($key)==0){
1117     return;
1118   }
1119   $k = '';
1120   while(strlen($k)<256){
1121     $k.=$key;
1122   }
1123   $k=substr($k,0,256);
1124   for ($i=0;$i<256;$i++){
1125     $this->arc4 .= chr($i);
1126   }
1127   $j=0;
1128   for ($i=0;$i<256;$i++){
1129     $t = $this->arc4[$i];
1130     $j = ($j + ord($t) + ord($k[$i]))%256;
1131     $this->arc4[$i]=$this->arc4[$j];
1132     $this->arc4[$j]=$t;
1133   }    
1134 }
1135
1136 /**
1137 * ARC4 encrypt a text string
1138 */
1139 function ARC4($text){
1140   $len=strlen($text);
1141   $a=0;
1142   $b=0;
1143   $c = $this->arc4;
1144   $out='';
1145   for ($i=0;$i<$len;$i++){
1146     $a = ($a+1)%256;
1147     $t= $c[$a];
1148     $b = ($b+ord($t))%256;
1149     $c[$a]=$c[$b];
1150     $c[$b]=$t;
1151     $k = ord($c[(ord($c[$a])+ord($c[$b]))%256]);
1152     $out.=chr(ord($text[$i]) ^ $k);
1153   }
1154   
1155   return $out;
1156 }
1157
1158 /**
1159 * functions which can be called to adjust or add to the document
1160 */
1161
1162 /**
1163 * add a link in the document to an external URL
1164 */
1165 function addLink($url,$x0,$y0,$x1,$y1){
1166   $this->numObj++;
1167   $info = array('type'=>'link','url'=>$url,'rect'=>array($x0,$y0,$x1,$y1));
1168   $this->o_annotation($this->numObj,'new',$info);
1169 }
1170
1171 /**
1172 * add a link in the document to an internal destination (ie. within the document)
1173 */
1174 function addInternalLink($label,$x0,$y0,$x1,$y1){
1175   $this->numObj++;
1176   $info = array('type'=>'ilink','label'=>$label,'rect'=>array($x0,$y0,$x1,$y1));
1177   $this->o_annotation($this->numObj,'new',$info);
1178 }
1179
1180 /**
1181 * set the encryption of the document
1182 * can be used to turn it on and/or set the passwords which it will have.
1183 * also the functions that the user will have are set here, such as print, modify, add
1184 */
1185 function setEncryption($userPass='',$ownerPass='',$pc=array()){
1186   $p=bindec(11000000);
1187
1188   $options = array(
1189      'print'=>4
1190     ,'modify'=>8
1191     ,'copy'=>16
1192     ,'add'=>32
1193   );
1194   foreach($pc as $k=>$v){
1195     if ($v && isset($options[$k])){
1196       $p+=$options[$k];
1197     } else if (isset($options[$v])){
1198       $p+=$options[$v];
1199     }
1200   }
1201   // implement encryption on the document
1202   if ($this->arc4_objnum == 0){
1203     // then the block does not exist already, add it.
1204     $this->numObj++;
1205     if (strlen($ownerPass)==0){
1206       $ownerPass=$userPass;
1207     }
1208     $this->o_encryption($this->numObj,'new',array('user'=>$userPass,'owner'=>$ownerPass,'p'=>$p));
1209   }
1210 }
1211
1212 /**
1213 * should be used for internal checks, not implemented as yet
1214 */
1215 function checkAllHere(){
1216 }
1217
1218 /**
1219 * return the pdf stream as a string returned from the function
1220 */
1221 function output($debug=0){
1222
1223   if ($debug){
1224     // turn compression off
1225     $this->options['compression']=0;
1226   }
1227
1228   if ($this->arc4_objnum){
1229     $this->ARC4_init($this->encryptionKey);
1230   }
1231
1232   $this->checkAllHere();
1233
1234   $xref=array();
1235   $content="%PDF-1.3\n%âãÏÓ\n";
1236 //  $content="%PDF-1.3\n";
1237   $pos=strlen($content);
1238   foreach($this->objects as $k=>$v){
1239     $tmp='o_'.$v['t'];
1240     $cont=$this->$tmp($k,'out');
1241     $content.=$cont;
1242     $xref[]=$pos;
1243     $pos+=strlen($cont);
1244   }
1245   $content.="\nxref\n0 ".(count($xref)+1)."\n0000000000 65535 f \n";
1246   foreach($xref as $p){
1247     $content.=substr('0000000000',0,10-strlen($p)).$p." 00000 n \n";
1248   }
1249   $content.="\ntrailer\n  << /Size ".(count($xref)+1)."\n     /Root 1 0 R\n     /Info ".$this->infoObject." 0 R\n";
1250   // if encryption has been applied to this document then add the marker for this dictionary
1251   if ($this->arc4_objnum > 0){
1252     $content .= "/Encrypt ".$this->arc4_objnum." 0 R\n";
1253   }
1254   if (strlen($this->fileIdentifier)){
1255     $content .= "/ID[<".$this->fileIdentifier."><".$this->fileIdentifier.">]\n";
1256   }
1257   $content .= "  >>\nstartxref\n".$pos."\n%%EOF\n";
1258   return $content;
1259 }
1260
1261 /**
1262 * intialize a new document
1263 * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum
1264 * this function is called automatically by the constructor function
1265 *
1266 * @access private
1267 */
1268 function newDocument($pageSize=array(0,0,612,792)){
1269   $this->numObj=0;
1270   $this->objects = array();
1271
1272   $this->numObj++;
1273   $this->o_catalog($this->numObj,'new');
1274
1275   $this->numObj++;
1276   $this->o_outlines($this->numObj,'new');
1277
1278   $this->numObj++;
1279   $this->o_pages($this->numObj,'new');
1280
1281   $this->o_pages($this->numObj,'mediaBox',$pageSize);
1282   $this->currentNode = 3;
1283
1284   $this->numObj++;
1285   $this->o_procset($this->numObj,'new');
1286
1287   $this->numObj++;
1288   $this->o_info($this->numObj,'new');
1289
1290   $this->numObj++;
1291   $this->o_page($this->numObj,'new');
1292
1293   // need to store the first page id as there is no way to get it to the user during 
1294   // startup
1295   $this->firstPageId = $this->currentContents;
1296 }
1297
1298 /**
1299 * open the font file and return a php structure containing it.
1300 * first check if this one has been done before and saved in a form more suited to php
1301 * note that if a php serialized version does not exist it will try and make one, but will
1302 * require write access to the directory to do it... it is MUCH faster to have these serialized
1303 * files.
1304 *
1305 * @access private
1306 */
1307 function openFont($font){
1308   // assume that $font contains both the path and perhaps the extension to the file, split them
1309   $pos=strrpos($font,'/');
1310   if ($pos===false){
1311     $dir = './';
1312     $name = $font;
1313   } else {
1314     $dir=substr($font,0,$pos+1);
1315     $name=substr($font,$pos+1);
1316   }
1317
1318   if (substr($name,-4)=='.afm'){
1319     $name=substr($name,0,strlen($name)-4);
1320   }
1321   $this->addMessage('openFont: '.$font.' - '.$name);
1322   if (file_exists($dir.'php_'.$name.'.afm')){
1323     $this->addMessage('openFont: php file exists '.$dir.'php_'.$name.'.afm');
1324     $tmp = file($dir.'php_'.$name.'.afm');
1325     $this->fonts[$font]=unserialize($tmp[0]);
1326     if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_']<1){
1327       // if the font file is old, then clear it out and prepare for re-creation
1328       $this->addMessage('openFont: clear out, make way for new version.');
1329       unset($this->fonts[$font]);
1330     }
1331   }
1332   if (!isset($this->fonts[$font]) && file_exists($dir.$name.'.afm')){
1333     // then rebuild the php_<font>.afm file from the <font>.afm file
1334     $this->addMessage('openFont: build php file from '.$dir.$name.'.afm');
1335     $data = array();
1336     $file = file($dir.$name.'.afm');
1337     foreach ($file as $rowA){
1338       $row=trim($rowA);
1339       $pos=strpos($row,' ');
1340       if ($pos){
1341         // then there must be some keyword
1342         $key = substr($row,0,$pos);
1343         switch ($key){
1344           case 'FontName':
1345           case 'FullName':
1346           case 'FamilyName':
1347           case 'Weight':
1348           case 'ItalicAngle':
1349           case 'IsFixedPitch':
1350           case 'CharacterSet':
1351           case 'UnderlinePosition':
1352           case 'UnderlineThickness':
1353           case 'Version':
1354           case 'EncodingScheme':
1355           case 'CapHeight':
1356           case 'XHeight':
1357           case 'Ascender':
1358           case 'Descender':
1359           case 'StdHW':
1360           case 'StdVW':
1361           case 'StartCharMetrics':
1362             $data[$key]=trim(substr($row,$pos));
1363             break;
1364           case 'FontBBox':
1365             $data[$key]=explode(' ',trim(substr($row,$pos)));
1366             break;
1367           case 'C':
1368             //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ;
1369             $bits=explode(';',trim($row));
1370             $dtmp=array();
1371             foreach($bits as $bit){
1372               $bits2 = explode(' ',trim($bit));
1373               if (strlen($bits2[0])){
1374                 if (count($bits2)>2){
1375                   $dtmp[$bits2[0]]=array();
1376                   for ($i=1;$i<count($bits2);$i++){
1377                     $dtmp[$bits2[0]][]=$bits2[$i];
1378                   }
1379                 } else if (count($bits2)==2){
1380                   $dtmp[$bits2[0]]=$bits2[1];
1381                 }
1382               }
1383             }
1384             if ($dtmp['C']>=0){
1385               $data['C'][$dtmp['C']]=$dtmp;
1386               $data['C'][$dtmp['N']]=$dtmp;
1387             } else {
1388               $data['C'][$dtmp['N']]=$dtmp;
1389             }
1390             break;
1391           case 'KPX':
1392             //KPX Adieresis yacute -40
1393             $bits=explode(' ',trim($row));
1394             $data['KPX'][$bits[1]][$bits[2]]=$bits[3];
1395             break;
1396         }
1397       }
1398     }
1399     $data['_version_']=1;
1400     $this->fonts[$font]=$data;
1401     $fp = fopen($dir.'php_'.$name.'.afm','w');
1402     fwrite($fp,serialize($data));
1403     fclose($fp);
1404   } else if (!isset($this->fonts[$font])){
1405     $this->addMessage('openFont: no font file found');
1406 //    echo 'Font not Found '.$font;
1407   }
1408 }
1409
1410 /**
1411 * if the font is not loaded then load it and make the required object
1412 * else just make it the current font
1413 * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding'
1414 * note that encoding='none' will need to be used for symbolic fonts
1415 * and 'differences' => an array of mappings between numbers 0->255 and character names.
1416 *
1417 */
1418 function selectFont($fontName,$encoding='',$set=1){
1419   if (!isset($this->fonts[$fontName])){
1420     // load the file
1421     $this->openFont($fontName);
1422     if (isset($this->fonts[$fontName])){
1423       $this->numObj++;
1424       $this->numFonts++;
1425       $pos=strrpos($fontName,'/');
1426 //      $dir=substr($fontName,0,$pos+1);
1427       $name=substr($fontName,$pos+1);
1428       if (substr($name,-4)=='.afm'){
1429         $name=substr($name,0,strlen($name)-4);
1430       }
1431       $options=array('name'=>$name);
1432       if (is_array($encoding)){
1433         // then encoding and differences might be set
1434         if (isset($encoding['encoding'])){
1435           $options['encoding']=$encoding['encoding'];
1436         }
1437         if (isset($encoding['differences'])){
1438           $options['differences']=$encoding['differences'];
1439         }
1440       } else if (strlen($encoding)){
1441         // then perhaps only the encoding has been set
1442         $options['encoding']=$encoding;
1443       }
1444       $fontObj = $this->numObj;
1445       $this->o_font($this->numObj,'new',$options);
1446       $this->fonts[$fontName]['fontNum']=$this->numFonts;
1447       // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there
1448       // should be for all non-basic fonts), then load it into an object and put the
1449       // references into the font object
1450       $basefile = substr($fontName,0,strlen($fontName)-4);
1451       if (file_exists($basefile.'.pfb')){
1452         $fbtype = 'pfb';
1453       } else if (file_exists($basefile.'.ttf')){
1454         $fbtype = 'ttf';
1455       } else {
1456         $fbtype='';
1457       }
1458       $fbfile = $basefile.'.'.$fbtype;
1459       
1460 //      $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb';
1461 //      $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf';
1462       $this->addMessage('selectFont: checking for - '.$fbfile);
1463       if (substr($fontName,-4)=='.afm' && strlen($fbtype) ){
1464         $adobeFontName = $this->fonts[$fontName]['FontName'];
1465 //        $fontObj = $this->numObj;
1466         $this->addMessage('selectFont: adding font file - '.$fbfile.' - '.$adobeFontName);
1467         // find the array of fond widths, and put that into an object.
1468         $firstChar = -1;
1469         $lastChar = 0;
1470         $widths = array();
1471         foreach ($this->fonts[$fontName]['C'] as $num=>$d){
1472           if (intval($num)>0 || $num=='0'){
1473             if ($lastChar>0 && $num>$lastChar+1){
1474               for($i=$lastChar+1;$i<$num;$i++){
1475                 $widths[] = 0;
1476               }
1477             }
1478             $widths[] = $d['WX'];
1479             if ($firstChar==-1){
1480               $firstChar = $num;
1481             }
1482             $lastChar = $num;
1483           }
1484         }
1485         // also need to adjust the widths for the differences array
1486         if (isset($options['differences'])){
1487           foreach($options['differences'] as $charNum=>$charName){
1488             if ($charNum>$lastChar){
1489               for($i=$lastChar+1;$i<=$charNum;$i++){
1490                 $widths[]=0;
1491               }
1492               $lastChar=$charNum;
1493             }
1494             if (isset($this->fonts[$fontName]['C'][$charName])){
1495               $widths[$charNum-$firstChar]=$this->fonts[$fontName]['C'][$charName]['WX'];
1496             }
1497           }
1498         }
1499         $this->addMessage('selectFont: FirstChar='.$firstChar);
1500         $this->addMessage('selectFont: LastChar='.$lastChar);
1501         $this->numObj++;
1502         $this->o_contents($this->numObj,'new','raw');
1503         $this->objects[$this->numObj]['c'].='[';
1504         foreach($widths as $width){
1505           $this->objects[$this->numObj]['c'].=' '.$width;
1506         }
1507         $this->objects[$this->numObj]['c'].=' ]';
1508         $widthid = $this->numObj;
1509
1510         // load the pfb file, and put that into an object too.
1511         // note that pdf supports only binary format type 1 font files, though there is a 
1512         // simple utility to convert them from pfa to pfb.
1513         $fp = fopen($fbfile,'rb');
1514         $tmp = get_magic_quotes_runtime();
1515         set_magic_quotes_runtime(0);
1516         $data = fread($fp,filesize($fbfile));
1517         set_magic_quotes_runtime($tmp);
1518         fclose($fp);
1519
1520         // create the font descriptor
1521         $this->numObj++;
1522         $fontDescriptorId = $this->numObj;
1523         $this->numObj++;
1524         $pfbid = $this->numObj;
1525         // determine flags (more than a little flakey, hopefully will not matter much)
1526         $flags=0;
1527         if ($this->fonts[$fontName]['ItalicAngle']!=0){ $flags+=pow(2,6); }
1528         if ($this->fonts[$fontName]['IsFixedPitch']=='true'){ $flags+=1; }
1529         $flags+=pow(2,5); // assume non-sybolic
1530
1531         $list = array('Ascent'=>'Ascender','CapHeight'=>'CapHeight','Descent'=>'Descender','FontBBox'=>'FontBBox','ItalicAngle'=>'ItalicAngle');
1532         $fdopt = array(
1533          'Flags'=>$flags
1534          ,'FontName'=>$adobeFontName
1535          ,'StemV'=>100  // don't know what the value for this should be!
1536         );
1537         foreach($list as $k=>$v){
1538           if (isset($this->fonts[$fontName][$v])){
1539             $fdopt[$k]=$this->fonts[$fontName][$v];
1540           }
1541         }
1542
1543         if ($fbtype=='pfb'){
1544           $fdopt['FontFile']=$pfbid;
1545         } else if ($fbtype=='ttf'){
1546           $fdopt['FontFile2']=$pfbid;
1547         }
1548         $this->o_fontDescriptor($fontDescriptorId,'new',$fdopt);        
1549
1550         // embed the font program
1551         $this->o_contents($this->numObj,'new');
1552         $this->objects[$pfbid]['c'].=$data;
1553         // determine the cruicial lengths within this file
1554         if ($fbtype=='pfb'){
1555           $l1 = strpos($data,'eexec')+6;
1556           $l2 = strpos($data,'00000000')-$l1;
1557           $l3 = strlen($data)-$l2-$l1;
1558           $this->o_contents($this->numObj,'add',array('Length1'=>$l1,'Length2'=>$l2,'Length3'=>$l3));
1559         } else if ($fbtype=='ttf'){
1560           $l1 = strlen($data);
1561           $this->o_contents($this->numObj,'add',array('Length1'=>$l1));
1562         }
1563
1564
1565         // tell the font object about all this new stuff
1566         $tmp = array('BaseFont'=>$adobeFontName,'Widths'=>$widthid
1567                                       ,'FirstChar'=>$firstChar,'LastChar'=>$lastChar
1568                                       ,'FontDescriptor'=>$fontDescriptorId);
1569         if ($fbtype=='ttf'){
1570           $tmp['SubType']='TrueType';
1571         }
1572         $this->addMessage('adding extra info to font.('.$fontObj.')');
1573         foreach($tmp as $fk=>$fv){
1574           $this->addMessage($fk." : ".$fv);
1575         }
1576         $this->o_font($fontObj,'add',$tmp);
1577
1578       } else {
1579         $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts');
1580       }
1581
1582
1583       // also set the differences here, note that this means that these will take effect only the 
1584       //first time that a font is selected, else they are ignored
1585       if (isset($options['differences'])){
1586         $this->fonts[$fontName]['differences']=$options['differences'];
1587       }
1588     }
1589   }
1590   if ($set && isset($this->fonts[$fontName])){
1591     // so if for some reason the font was not set in the last one then it will not be selected
1592     $this->currentBaseFont=$fontName;
1593     // the next line means that if a new font is selected, then the current text state will be
1594     // applied to it as well.
1595     $this->setCurrentFont();
1596   }
1597   return $this->currentFontNum;
1598 }
1599
1600 /**
1601 * sets up the current font, based on the font families, and the current text state
1602 * note that this system is quite flexible, a <b><i> font can be completely different to a
1603 * <i><b> font, and even <b><b> will have to be defined within the family to have meaning
1604 * This function is to be called whenever the currentTextState is changed, it will update
1605 * the currentFont setting to whatever the appropriatte family one is.
1606 * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont
1607 * This function will change the currentFont to whatever it should be, but will not change the 
1608 * currentBaseFont.
1609 *
1610 * @access private
1611 */
1612 function setCurrentFont(){
1613   if (strlen($this->currentBaseFont)==0){
1614     // then assume an initial font
1615     $this->selectFont('./fonts/Helvetica.afm');
1616   }
1617   $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1);
1618   if (strlen($this->currentTextState)
1619     && isset($this->fontFamilies[$cf]) 
1620       && isset($this->fontFamilies[$cf][$this->currentTextState])){
1621     // then we are in some state or another
1622     // and this font has a family, and the current setting exists within it
1623     // select the font, then return it
1624     $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState];
1625     $this->selectFont($nf,'',0);
1626     $this->currentFont = $nf;
1627     $this->currentFontNum = $this->fonts[$nf]['fontNum'];
1628   } else {
1629     // the this font must not have the right family member for the current state
1630     // simply assume the base font
1631     $this->currentFont = $this->currentBaseFont;
1632     $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum'];    
1633   }
1634 }
1635
1636 /**
1637 * function for the user to find out what the id is of the first page that was created during
1638 * startup - useful if they wish to add something to it later.
1639 */
1640 function getFirstPageId(){
1641   return $this->firstPageId;
1642 }
1643
1644 /**
1645 * add content to the currently active object
1646 *
1647 * @access private
1648 */
1649 function addContent($content){
1650   $this->objects[$this->currentContents]['c'].=$content;
1651 }
1652
1653 /**
1654 * sets the colour for fill operations
1655 */
1656 function setColor($r,$g,$b,$force=0){
1657   if ($r>=0 && ($force || $r!=$this->currentColour['r'] || $g!=$this->currentColour['g'] || $b!=$this->currentColour['b'])){
1658     $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' rg';
1659     $this->currentColour=array('r'=>$r,'g'=>$g,'b'=>$b);
1660   }
1661 }
1662
1663 /**
1664 * sets the colour for stroke operations
1665 */
1666 function setStrokeColor($r,$g,$b,$force=0){
1667   if ($r>=0 && ($force || $r!=$this->currentStrokeColour['r'] || $g!=$this->currentStrokeColour['g'] || $b!=$this->currentStrokeColour['b'])){
1668     $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' RG';
1669     $this->currentStrokeColour=array('r'=>$r,'g'=>$g,'b'=>$b);
1670   }
1671 }
1672
1673 /**
1674 * draw a line from one set of coordinates to another
1675 */
1676 function line($x1,$y1,$x2,$y2){
1677   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' m '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' l S';
1678 }
1679
1680 /**
1681 * draw a bezier curve based on 4 control points
1682 */
1683 function curve($x0,$y0,$x1,$y1,$x2,$y2,$x3,$y3){
1684   // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points
1685   // as the control points for the curve.
1686   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' m '.sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1);
1687   $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' '.sprintf('%.3f',$x3).' '.sprintf('%.3f',$y3).' c S';
1688 }
1689
1690 /**
1691 * draw a part of an ellipse
1692 */
1693 function partEllipse($x0,$y0,$astart,$afinish,$r1,$r2=0,$angle=0,$nSeg=8){
1694   $this->ellipse($x0,$y0,$r1,$r2,$angle,$nSeg,$astart,$afinish,0);
1695 }
1696
1697 /**
1698 * draw a filled ellipse
1699 */
1700 function filledEllipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360){
1701   return $this->ellipse($x0,$y0,$r1,$r2=0,$angle,$nSeg,$astart,$afinish,1,1);
1702 }
1703
1704 /**
1705 * draw an ellipse
1706 * note that the part and filled ellipse are just special cases of this function
1707 *
1708 * draws an ellipse in the current line style
1709 * centered at $x0,$y0, radii $r1,$r2
1710 * if $r2 is not set, then a circle is drawn
1711 * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a 
1712 * pretty crappy shape at 2, as we are approximating with bezier curves.
1713 */
1714 function ellipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360,$close=1,$fill=0){
1715   if ($r1==0){
1716     return;
1717   }
1718   if ($r2==0){
1719     $r2=$r1;
1720   }
1721   if ($nSeg<2){
1722     $nSeg=2;
1723   }
1724
1725   $astart = deg2rad((float)$astart);
1726   $afinish = deg2rad((float)$afinish);
1727   $totalAngle =$afinish-$astart;
1728
1729   $dt = $totalAngle/$nSeg;
1730   $dtm = $dt/3;
1731
1732   if ($angle != 0){
1733     $a = -1*deg2rad((float)$angle);
1734     $tmp = "\n q ";
1735     $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' ';
1736     $tmp .= sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' cm';
1737     $this->objects[$this->currentContents]['c'].= $tmp;
1738     $x0=0;
1739     $y0=0;
1740   }
1741
1742   $t1 = $astart;
1743   $a0 = $x0+$r1*cos($t1);
1744   $b0 = $y0+$r2*sin($t1);
1745   $c0 = -$r1*sin($t1);
1746   $d0 = $r2*cos($t1);
1747
1748   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$a0).' '.sprintf('%.3f',$b0).' m ';
1749   for ($i=1;$i<=$nSeg;$i++){
1750     // draw this bit of the total curve
1751     $t1 = $i*$dt+$astart;
1752     $a1 = $x0+$r1*cos($t1);
1753     $b1 = $y0+$r2*sin($t1);
1754     $c1 = -$r1*sin($t1);
1755     $d1 = $r2*cos($t1);
1756     $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',($a0+$c0*$dtm)).' '.sprintf('%.3f',($b0+$d0*$dtm));
1757     $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',($a1-$c1*$dtm)).' '.sprintf('%.3f',($b1-$d1*$dtm)).' '.sprintf('%.3f',$a1).' '.sprintf('%.3f',$b1).' c';
1758     $a0=$a1;
1759     $b0=$b1;
1760     $c0=$c1;
1761     $d0=$d1;    
1762   }
1763   if ($fill){
1764     $this->objects[$this->currentContents]['c'].=' f';
1765   } else {
1766     if ($close){
1767       $this->objects[$this->currentContents]['c'].=' s'; // small 's' signifies closing the path as well
1768     } else {
1769       $this->objects[$this->currentContents]['c'].=' S';
1770     }
1771   }
1772   if ($angle !=0){
1773     $this->objects[$this->currentContents]['c'].=' Q';
1774   }
1775 }
1776
1777 /**
1778 * this sets the line drawing style.
1779 * width, is the thickness of the line in user units
1780 * cap is the type of cap to put on the line, values can be 'butt','round','square'
1781 *    where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the
1782 *    end of the line.
1783 * join can be 'miter', 'round', 'bevel'
1784 * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the
1785 *   on and off dashes.
1786 *   (2) represents 2 on, 2 off, 2 on , 2 off ...
1787 *   (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
1788 * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts. 
1789 */
1790 function setLineStyle($width=1,$cap='',$join='',$dash='',$phase=0){
1791
1792   // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day
1793   $string = '';
1794   if ($width>0){
1795     $string.= $width.' w';
1796   }
1797   $ca = array('butt'=>0,'round'=>1,'square'=>2);
1798   if (isset($ca[$cap])){
1799     $string.= ' '.$ca[$cap].' J';
1800   }
1801   $ja = array('miter'=>0,'round'=>1,'bevel'=>2);
1802   if (isset($ja[$join])){
1803     $string.= ' '.$ja[$join].' j';
1804   }
1805   if (is_array($dash)){
1806     $string.= ' [';
1807     foreach ($dash as $len){
1808       $string.=' '.$len;
1809     }
1810     $string.= ' ] '.$phase.' d';
1811   }
1812   $this->currentLineStyle = $string;
1813   $this->objects[$this->currentContents]['c'].="\n".$string;
1814 }
1815
1816 /**
1817 * draw a polygon, the syntax for this is similar to the GD polygon command
1818 */
1819 function polygon($p,$np,$f=0){
1820   $this->objects[$this->currentContents]['c'].="\n";
1821   $this->objects[$this->currentContents]['c'].=sprintf('%.3f',$p[0]).' '.sprintf('%.3f',$p[1]).' m ';
1822   for ($i=2;$i<$np*2;$i=$i+2){
1823     $this->objects[$this->currentContents]['c'].= sprintf('%.3f',$p[$i]).' '.sprintf('%.3f',$p[$i+1]).' l ';
1824   }
1825   if ($f==1){
1826     $this->objects[$this->currentContents]['c'].=' f';
1827   } else {
1828     $this->objects[$this->currentContents]['c'].=' S';
1829   }
1830 }
1831
1832 /**
1833 * a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
1834 * the coordinates of the upper-right corner
1835 */
1836 function filledRectangle($x1,$y1,$width,$height){
1837   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re f';
1838 }
1839
1840 /**
1841 * draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not
1842 * the coordinates of the upper-right corner
1843 */
1844 function rectangle($x1,$y1,$width,$height){
1845   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re S';
1846 }
1847
1848 /**
1849 * add a new page to the document
1850 * this also makes the new page the current active object
1851 */
1852 function newPage($insert=0,$id=0,$pos='after'){
1853
1854   // if there is a state saved, then go up the stack closing them
1855   // then on the new page, re-open them with the right setings
1856   
1857   if ($this->nStateStack){
1858     for ($i=$this->nStateStack;$i>=1;$i--){
1859       $this->restoreState($i);
1860     }
1861   }
1862
1863   $this->numObj++;
1864   if ($insert){
1865     // the id from the ezPdf class is the od of the contents of the page, not the page object itself
1866     // query that object to find the parent
1867     $rid = $this->objects[$id]['onPage'];
1868     $opt= array('rid'=>$rid,'pos'=>$pos);
1869     $this->o_page($this->numObj,'new',$opt);
1870   } else {
1871     $this->o_page($this->numObj,'new');
1872   }
1873   // if there is a stack saved, then put that onto the page
1874   if ($this->nStateStack){
1875     for ($i=1;$i<=$this->nStateStack;$i++){
1876       $this->saveState($i);
1877     }
1878   }  
1879   // and if there has been a stroke or fill colour set, then transfer them
1880   if ($this->currentColour['r']>=0){
1881     $this->setColor($this->currentColour['r'],$this->currentColour['g'],$this->currentColour['b'],1);
1882   }
1883   if ($this->currentStrokeColour['r']>=0){
1884     $this->setStrokeColor($this->currentStrokeColour['r'],$this->currentStrokeColour['g'],$this->currentStrokeColour['b'],1);
1885   }
1886
1887   // if there is a line style set, then put this in too
1888   if (strlen($this->currentLineStyle)){
1889     $this->objects[$this->currentContents]['c'].="\n".$this->currentLineStyle;
1890   }
1891
1892   // the call to the o_page object set currentContents to the present page, so this can be returned as the page id
1893   return $this->currentContents;
1894 }
1895
1896 /**
1897 * output the pdf code, streaming it to the browser
1898 * the relevant headers are set so that hopefully the browser will recognise it
1899 */
1900 function stream($options=''){
1901   // setting the options allows the adjustment of the headers
1902   // values at the moment are:
1903   // 'Content-Disposition'=>'filename'  - sets the filename, though not too sure how well this will 
1904   //        work as in my trial the browser seems to use the filename of the php file with .pdf on the end
1905   // 'Accept-Ranges'=>1 or 0 - if this is not set to 1, then this header is not included, off by default
1906   //    this header seems to have caused some problems despite tha fact that it is supposed to solve
1907   //    them, so I am leaving it off by default.
1908   // 'compress'=> 1 or 0 - apply content stream compression, this is on (1) by default
1909   if (!is_array($options)){
1910     $options=array();
1911   }
1912   if ( isset($options['compress']) && $options['compress']==0){
1913     $tmp = $this->output(1);
1914   } else {
1915     $tmp = $this->output();
1916   }
1917   header("Content-type: application/pdf");
1918   header("Content-Length: ".strlen(ltrim($tmp)));
1919   $fileName = (isset($options['Content-Disposition'])?$options['Content-Disposition']:'file.pdf');
1920   header("Content-Disposition: inline; filename=".$fileName);
1921   if (isset($options['Accept-Ranges']) && $options['Accept-Ranges']==1){
1922     header("Accept-Ranges: ".strlen(ltrim($tmp))); 
1923   }
1924   echo ltrim($tmp);
1925 }
1926
1927 /**
1928 * return the height in units of the current font in the given size
1929 */
1930 function getFontHeight($size){
1931   if (!$this->numFonts){
1932     $this->selectFont('./fonts/Helvetica');
1933   }
1934   // for the current font, and the given size, what is the height of the font in user units
1935   $h = $this->fonts[$this->currentFont]['FontBBox'][3]-$this->fonts[$this->currentFont]['FontBBox'][1];
1936   return $size*$h/1000;
1937 }
1938
1939 /**
1940 * return the font decender, this will normally return a negative number
1941 * if you add this number to the baseline, you get the level of the bottom of the font
1942 * it is in the pdf user units
1943 */
1944 function getFontDecender($size){
1945   // note that this will most likely return a negative value
1946   if (!$this->numFonts){
1947     $this->selectFont('./fonts/Helvetica');
1948   }
1949   $h = $this->fonts[$this->currentFont]['FontBBox'][1];
1950   return $size*$h/1000;
1951 }
1952
1953 /**
1954 * filter the text, this is applied to all text just before being inserted into the pdf document
1955 * it escapes the various things that need to be escaped, and so on
1956 *
1957 * @access private
1958 */
1959 function filterText($text){
1960   $text = str_replace('\\','\\\\',$text);
1961   $text = str_replace('(','\(',$text);
1962   $text = str_replace(')','\)',$text);
1963   $text = str_replace('&lt;','<',$text);
1964   $text = str_replace('&gt;','>',$text);
1965   $text = str_replace('&#039;','\'',$text);
1966   $text = str_replace('&quot;','"',$text);
1967   $text = str_replace('&amp;','&',$text);
1968
1969   return $text;
1970 }
1971
1972 /**
1973 * given a start position and information about how text is to be laid out, calculate where 
1974 * on the page the text will end
1975 *
1976 * @access private
1977 */
1978 function PRVTgetTextPosition($x,$y,$angle,$size,$wa,$text){
1979   // given this information return an array containing x and y for the end position as elements 0 and 1
1980   $w = $this->getTextWidth($size,$text);
1981   // need to adjust for the number of spaces in this text
1982   $words = explode(' ',$text);
1983   $nspaces=count($words)-1;
1984   $w += $wa*$nspaces;
1985   $a = deg2rad((float)$angle);
1986   return array(cos($a)*$w+$x,-sin($a)*$w+$y);
1987 }
1988
1989 /**
1990 * wrapper function for PRVTcheckTextDirective1
1991 *
1992 * @access private
1993 */
1994 function PRVTcheckTextDirective(&$text,$i,&$f){
1995   $x=0;
1996   $y=0;
1997   return $this->PRVTcheckTextDirective1($text,$i,$f,0,$x,$y);
1998 }
1999
2000 /**
2001 * checks if the text stream contains a control directive
2002 * if so then makes some changes and returns the number of characters involved in the directive
2003 * this has been re-worked to include everything neccesary to fins the current writing point, so that
2004 * the location can be sent to the callback function if required
2005 * if the directive does not require a font change, then $f should be set to 0
2006 *
2007 * @access private
2008 */
2009 function PRVTcheckTextDirective1(&$text,$i,&$f,$final,&$x,&$y,$size=0,$angle=0,$wordSpaceAdjust=0){
2010   $directive = 0;
2011   $j=$i;
2012   if ($text[$j]=='<'){
2013     $j++;
2014     switch($text[$j]){
2015       case '/':
2016         $j++;
2017         if (strlen($text) <= $j){
2018           return $directive;
2019         }
2020         switch($text[$j]){
2021           case 'b':
2022           case 'i':
2023             $j++;
2024             if ($text[$j]=='>'){
2025               $p = strrpos($this->currentTextState,$text[$j-1]);
2026               if ($p !== false){
2027                 // then there is one to remove
2028                 $this->currentTextState = substr($this->currentTextState,0,$p).substr($this->currentTextState,$p+1);
2029               }
2030               $directive=$j-$i+1;
2031             }
2032             break;
2033           case 'c':
2034             // this this might be a callback function
2035             $j++;
2036             $k = strpos($text,'>',$j);
2037             if ($k!==false && $text[$j]==':'){
2038               // then this will be treated as a callback directive
2039               $directive = $k-$i+1;
2040               $f=0;
2041               // split the remainder on colons to get the function name and the paramater
2042               $tmp = substr($text,$j+1,$k-$j-1);
2043               $b1 = strpos($tmp,':');
2044               if ($b1!==false){
2045                 $func = substr($tmp,0,$b1);
2046                 $parm = substr($tmp,$b1+1);
2047               } else {
2048                 $func=$tmp;
2049                 $parm='';
2050               }
2051               if (!isset($func) || !strlen(trim($func))){
2052                 $directive=0;
2053               } else {
2054                 // only call the function if this is the final call
2055                 if ($final){
2056                   // need to assess the text position, calculate the text width to this point
2057                   // can use getTextWidth to find the text width I think
2058                   $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,substr($text,0,$i));
2059                   $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'end','p'=>$parm,'nCallback'=>$this->nCallback);
2060                   $x=$tmp[0];
2061                   $y=$tmp[1];
2062                   $ret = $this->$func($info);
2063                   if (is_array($ret)){
2064                     // then the return from the callback function could set the position, to start with, later will do font colour, and font
2065                     foreach($ret as $rk=>$rv){
2066                       switch($rk){
2067                         case 'x':
2068                         case 'y':
2069                           $$rk=$rv;
2070                           break;
2071                       }
2072                     }
2073                   }
2074                   // also remove from to the stack
2075                   // for simplicity, just take from the end, fix this another day
2076                   $this->nCallback--;
2077                   if ($this->nCallback<0){
2078                     $this->nCallBack=0;
2079                   }
2080                 }
2081               }
2082             }
2083             break;
2084         }
2085         break;
2086       case 'b':
2087       case 'i':
2088         $j++;
2089         if ($text[$j]=='>'){
2090           $this->currentTextState.=$text[$j-1];
2091           $directive=$j-$i+1;
2092         }
2093         break;
2094       case 'C':
2095         $noClose=1;
2096       case 'c':
2097         // this this might be a callback function
2098         $j++;
2099         $k = strpos($text,'>',$j);
2100         if ($k!==false && $text[$j]==':'){
2101           // then this will be treated as a callback directive
2102           $directive = $k-$i+1;
2103           $f=0;
2104           // split the remainder on colons to get the function name and the paramater
2105 //          $bits = explode(':',substr($text,$j+1,$k-$j-1));
2106           $tmp = substr($text,$j+1,$k-$j-1);
2107           $b1 = strpos($tmp,':');
2108           if ($b1!==false){
2109             $func = substr($tmp,0,$b1);
2110             $parm = substr($tmp,$b1+1);
2111           } else {
2112             $func=$tmp;
2113             $parm='';
2114           }
2115           if (!isset($func) || !strlen(trim($func))){
2116             $directive=0;
2117           } else {
2118             // only call the function if this is the final call, ie, the one actually doing printing, not measurement
2119             if ($final){
2120               // need to assess the text position, calculate the text width to this point
2121               // can use getTextWidth to find the text width I think
2122               // also add the text height and decender
2123               $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,substr($text,0,$i));
2124               $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'start','p'=>$parm,'f'=>$func,'height'=>$this->getFontHeight($size),'decender'=>$this->getFontDecender($size));
2125               $x=$tmp[0];
2126               $y=$tmp[1];
2127               if (!isset($noClose) || !$noClose){
2128                 // only add to the stack if this is a small 'c', therefore is a start-stop pair
2129                 $this->nCallback++;
2130                 $info['nCallback']=$this->nCallback;
2131                 $this->callback[$this->nCallback]=$info;
2132               }
2133               $ret = $this->$func($info);
2134               if (is_array($ret)){
2135                 // then the return from the callback function could set the position, to start with, later will do font colour, and font
2136                 foreach($ret as $rk=>$rv){
2137                   switch($rk){
2138                     case 'x':
2139                     case 'y':
2140                       $$rk=$rv;
2141                       break;
2142                   }
2143                 }
2144               }
2145             }
2146           }
2147         }
2148         break;
2149     }
2150   } 
2151   return $directive;
2152 }
2153
2154 /**
2155 * add text to the document, at a specified location, size and angle on the page
2156 */
2157 function addText($x,$y,$size,$text,$angle=0,$wordSpaceAdjust=0){
2158   if (!$this->numFonts){$this->selectFont('./fonts/Helvetica');}
2159
2160   // if there are any open callbacks, then they should be called, to show the start of the line
2161   if ($this->nCallback>0){
2162     for ($i=$this->nCallback;$i>0;$i--){
2163       // call each function
2164       $info = array('x'=>$x,'y'=>$y,'angle'=>$angle,'status'=>'sol','p'=>$this->callback[$i]['p'],'nCallback'=>$this->callback[$i]['nCallback'],'height'=>$this->callback[$i]['height'],'decender'=>$this->callback[$i]['decender']);
2165       $func = $this->callback[$i]['f'];
2166       $this->$func($info);
2167     }
2168   }
2169   if ($angle==0){
2170     $this->objects[$this->currentContents]['c'].="\n".'BT '.sprintf('%.3f',$x).' '.sprintf('%.3f',$y).' Td';
2171   } else {
2172     $a = deg2rad((float)$angle);
2173     $tmp = "\n".'BT ';
2174     $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' ';
2175     $tmp .= sprintf('%.3f',$x).' '.sprintf('%.3f',$y).' Tm';
2176     $this->objects[$this->currentContents]['c'] .= $tmp;
2177   }
2178   if ($wordSpaceAdjust!=0 || $wordSpaceAdjust != $this->wordSpaceAdjust){
2179     $this->wordSpaceAdjust=$wordSpaceAdjust;
2180     $this->objects[$this->currentContents]['c'].=' '.sprintf('%.3f',$wordSpaceAdjust).' Tw';
2181   }
2182   $len=strlen($text);
2183   $start=0;
2184   for ($i=0;$i<$len;$i++){
2185     $f=1;
2186     $directive = $this->PRVTcheckTextDirective($text,$i,$f);
2187     if ($directive){
2188       // then we should write what we need to
2189       if ($i>$start){
2190         $part = substr($text,$start,$i-$start);
2191         $this->objects[$this->currentContents]['c'].=' /F'.$this->currentFontNum.' '.sprintf('%.1f',$size).' Tf ';
2192         $this->objects[$this->currentContents]['c'].=' ('.$this->filterText($part).') Tj';
2193       }
2194       if ($f){
2195         // then there was nothing drastic done here, restore the contents
2196         $this->setCurrentFont();
2197       } else {
2198         $this->objects[$this->currentContents]['c'] .= ' ET';
2199         $f=1;
2200         $xp=$x;
2201         $yp=$y;
2202         $directive = $this->PRVTcheckTextDirective1($text,$i,$f,1,$xp,$yp,$size,$angle,$wordSpaceAdjust);
2203         
2204         // restart the text object
2205           if ($angle==0){
2206             $this->objects[$this->currentContents]['c'].="\n".'BT '.sprintf('%.3f',$xp).' '.sprintf('%.3f',$yp).' Td';
2207           } else {
2208             $a = deg2rad((float)$angle);
2209             $tmp = "\n".'BT ';
2210             $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' ';
2211             $tmp .= sprintf('%.3f',$xp).' '.sprintf('%.3f',$yp).' Tm';
2212             $this->objects[$this->currentContents]['c'] .= $tmp;
2213           }
2214           if ($wordSpaceAdjust!=0 || $wordSpaceAdjust != $this->wordSpaceAdjust){
2215             $this->wordSpaceAdjust=$wordSpaceAdjust;
2216             $this->objects[$this->currentContents]['c'].=' '.sprintf('%.3f',$wordSpaceAdjust).' Tw';
2217           }
2218       }
2219       // and move the writing point to the next piece of text
2220       $i=$i+$directive-1;
2221       $start=$i+1;
2222     }
2223     
2224   }
2225   if ($start<$len){
2226     $part = substr($text,$start);
2227     $this->objects[$this->currentContents]['c'].=' /F'.$this->currentFontNum.' '.sprintf('%.1f',$size).' Tf ';
2228     $this->objects[$this->currentContents]['c'].=' ('.$this->filterText($part).') Tj';
2229   }
2230   $this->objects[$this->currentContents]['c'].=' ET';
2231
2232   // if there are any open callbacks, then they should be called, to show the end of the line
2233   if ($this->nCallback>0){
2234     for ($i=$this->nCallback;$i>0;$i--){
2235       // call each function
2236       $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,$text);
2237       $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'],'decender'=>$this->callback[$i]['decender']);
2238       $func = $this->callback[$i]['f'];
2239       $this->$func($info);
2240     }
2241   }
2242
2243 }
2244
2245 /**
2246 * calculate how wide a given text string will be on a page, at a given size.
2247 * this can be called externally, but is alse used by the other class functions
2248 */
2249 function getTextWidth($size,$text){
2250   // this function should not change any of the settings, though it will need to
2251   // track any directives which change during calculation, so copy them at the start
2252   // and put them back at the end.
2253   $store_currentTextState = $this->currentTextState;
2254
2255   if (!$this->numFonts){
2256     $this->selectFont('./fonts/Helvetica');
2257   }
2258
2259   // converts a number or a float to a string so it can get the width
2260   $text = "$text";
2261
2262   // hmm, this is where it all starts to get tricky - use the font information to
2263   // calculate the width of each character, add them up and convert to user units
2264   $w=0;
2265   $len=strlen($text);
2266   $cf = $this->currentFont;
2267   for ($i=0;$i<$len;$i++){
2268     $f=1;
2269     $directive = $this->PRVTcheckTextDirective($text,$i,$f);
2270     if ($directive){
2271       if ($f){
2272         $this->setCurrentFont();
2273         $cf = $this->currentFont;
2274       }
2275       $i=$i+$directive-1;
2276     } else {
2277       $char=ord($text[$i]);
2278       if (isset($this->fonts[$cf]['differences'][$char])){
2279         // then this character is being replaced by another
2280         $name = $this->fonts[$cf]['differences'][$char];
2281         if (isset($this->fonts[$cf]['C'][$name]['WX'])){
2282           $w+=$this->fonts[$cf]['C'][$name]['WX'];
2283         }
2284       } else if (isset($this->fonts[$cf]['C'][$char]['WX'])){
2285         $w+=$this->fonts[$cf]['C'][$char]['WX'];
2286       }
2287     }
2288   }
2289   
2290   $this->currentTextState = $store_currentTextState;
2291   $this->setCurrentFont();
2292
2293   return $w*$size/1000;
2294 }
2295
2296 /**
2297 * do a part of the calculation for sorting out the justification of the text
2298 *
2299 * @access private
2300 */
2301 function PRVTadjustWrapText($text,$actual,$width,&$x,&$adjust,$justification){
2302   switch ($justification){
2303     case 'left':
2304       return;
2305       break;
2306     case 'right':
2307       $x+=$width-$actual;
2308       break;
2309     case 'center':
2310     case 'centre':
2311       $x+=($width-$actual)/2;
2312       break;
2313     case 'full':
2314       // count the number of words
2315       $words = explode(' ',$text);
2316       $nspaces=count($words)-1;
2317       if ($nspaces>0){
2318         $adjust = ($width-$actual)/$nspaces;
2319       } else {
2320         $adjust=0;
2321       }
2322       break;
2323   }
2324 }
2325
2326 /**
2327 * add text to the page, but ensure that it fits within a certain width
2328 * if it does not fit then put in as much as possible, splitting at word boundaries
2329 * and return the remainder.
2330 * justification and angle can also be specified for the text
2331 */
2332 function addTextWrap($x,$y,$width,$size,$text,$justification='left',$angle=0,$test=0){
2333   // this will display the text, and if it goes beyond the width $width, will backtrack to the 
2334   // previous space or hyphen, and return the remainder of the text.
2335
2336   // $justification can be set to 'left','right','center','centre','full'
2337
2338   // need to store the initial text state, as this will change during the width calculation
2339   // but will need to be re-set before printing, so that the chars work out right
2340   $store_currentTextState = $this->currentTextState;
2341
2342   if (!$this->numFonts){$this->selectFont('./fonts/Helvetica');}
2343   if ($width<=0){
2344     // error, pretend it printed ok, otherwise risking a loop
2345     return '';
2346   }
2347   $w=0;
2348   $break=0;
2349   $breakWidth=0;
2350   $len=strlen($text);
2351   $cf = $this->currentFont;
2352   $tw = $width/$size*1000;
2353   for ($i=0;$i<$len;$i++){
2354     $f=1;
2355     $directive = $this->PRVTcheckTextDirective($text,$i,$f);
2356     if ($directive){
2357       if ($f){
2358         $this->setCurrentFont();
2359         $cf = $this->currentFont;
2360       }
2361       $i=$i+$directive-1;
2362     } else {
2363       $cOrd = ord($text[$i]);
2364       if (isset($this->fonts[$cf]['differences'][$cOrd])){
2365         // then this character is being replaced by another
2366         $cOrd2 = $this->fonts[$cf]['differences'][$cOrd];
2367       } else {
2368         $cOrd2 = $cOrd;
2369       }
2370   
2371       if (isset($this->fonts[$cf]['C'][$cOrd2]['WX'])){
2372         $w+=$this->fonts[$cf]['C'][$cOrd2]['WX'];
2373       }
2374       if ($w>$tw){
2375         // then we need to truncate this line
2376         if ($break>0){
2377           // then we have somewhere that we can split :)
2378           if ($text[$break]==' '){
2379             $tmp = substr($text,0,$break);
2380           } else {
2381             $tmp = substr($text,0,$break+1);
2382           }
2383           $adjust=0;
2384           $this->PRVTadjustWrapText($tmp,$breakWidth,$width,$x,$adjust,$justification);
2385
2386           // reset the text state
2387           $this->currentTextState = $store_currentTextState;
2388           $this->setCurrentFont();
2389           if (!$test){
2390             $this->addText($x,$y,$size,$tmp,$angle,$adjust);
2391           }
2392           return substr($text,$break+1);
2393         } else {
2394           // just split before the current character
2395           $tmp = substr($text,0,$i);
2396           $adjust=0;
2397           $ctmp=ord($text[$i]);
2398           if (isset($this->fonts[$cf]['differences'][$ctmp])){
2399             $ctmp=$this->fonts[$cf]['differences'][$ctmp];
2400           }
2401           $tmpw=($w-$this->fonts[$cf]['C'][$ctmp]['WX'])*$size/1000;
2402           $this->PRVTadjustWrapText($tmp,$tmpw,$width,$x,$adjust,$justification);
2403           // reset the text state
2404           $this->currentTextState = $store_currentTextState;
2405           $this->setCurrentFont();
2406           if (!$test){
2407             $this->addText($x,$y,$size,$tmp,$angle,$adjust);
2408           }
2409           return substr($text,$i);
2410         }
2411       }
2412       if ($text[$i]=='-'){
2413         $break=$i;
2414         $breakWidth = $w*$size/1000;
2415       }
2416       if ($text[$i]==' '){
2417         $break=$i;
2418         $ctmp=ord($text[$i]);
2419         if (isset($this->fonts[$cf]['differences'][$ctmp])){
2420           $ctmp=$this->fonts[$cf]['differences'][$ctmp];
2421         }
2422         $breakWidth = ($w-$this->fonts[$cf]['C'][$ctmp]['WX'])*$size/1000;
2423       }
2424     }
2425   }
2426   // then there was no need to break this line
2427   if ($justification=='full'){
2428     $justification='left';
2429   }
2430   $adjust=0;
2431   $tmpw=$w*$size/1000;
2432   $this->PRVTadjustWrapText($text,$tmpw,$width,$x,$adjust,$justification);
2433   // reset the text state
2434   $this->currentTextState = $store_currentTextState;
2435   $this->setCurrentFont();
2436   if (!$test){
2437     $this->addText($x,$y,$size,$text,$angle,$adjust,$angle);
2438   }
2439   return '';
2440 }
2441
2442 /**
2443 * this will be called at a new page to return the state to what it was on the 
2444 * end of the previous page, before the stack was closed down
2445 * This is to get around not being able to have open 'q' across pages
2446 *
2447 */
2448 function saveState($pageEnd=0){
2449   if ($pageEnd){
2450     // this will be called at a new page to return the state to what it was on the 
2451     // end of the previous page, before the stack was closed down
2452     // This is to get around not being able to have open 'q' across pages
2453     $opt = $this->stateStack[$pageEnd]; // ok to use this as stack starts numbering at 1
2454     $this->setColor($opt['col']['r'],$opt['col']['g'],$opt['col']['b'],1);
2455     $this->setStrokeColor($opt['str']['r'],$opt['str']['g'],$opt['str']['b'],1);
2456     $this->objects[$this->currentContents]['c'].="\n".$opt['lin'];
2457 //    $this->currentLineStyle = $opt['lin'];
2458   } else {
2459     $this->nStateStack++;
2460     $this->stateStack[$this->nStateStack]=array(
2461       'col'=>$this->currentColour
2462      ,'str'=>$this->currentStrokeColour
2463      ,'lin'=>$this->currentLineStyle
2464     );
2465   }
2466   $this->objects[$this->currentContents]['c'].="\nq";
2467 }
2468
2469 /**
2470 * restore a previously saved state
2471 */
2472 function restoreState($pageEnd=0){
2473   if (!$pageEnd){
2474     $n = $this->nStateStack;
2475     $this->currentColour = $this->stateStack[$n]['col'];
2476     $this->currentStrokeColour = $this->stateStack[$n]['str'];
2477     $this->objects[$this->currentContents]['c'].="\n".$this->stateStack[$n]['lin'];
2478     $this->currentLineStyle = $this->stateStack[$n]['lin'];
2479     unset($this->stateStack[$n]);
2480     $this->nStateStack--;
2481   }
2482   $this->objects[$this->currentContents]['c'].="\nQ";
2483 }
2484
2485 /**
2486 * make a loose object, the output will go into this object, until it is closed, then will revert to
2487 * the current one.
2488 * this object will not appear until it is included within a page.
2489 * the function will return the object number
2490 */
2491 function openObject(){
2492   $this->nStack++;
2493   $this->stack[$this->nStack]=array('c'=>$this->currentContents,'p'=>$this->currentPage);
2494   // add a new object of the content type, to hold the data flow
2495   $this->numObj++;
2496   $this->o_contents($this->numObj,'new');
2497   $this->currentContents=$this->numObj;
2498   $this->looseObjects[$this->numObj]=1;
2499   
2500   return $this->numObj;
2501 }
2502
2503 /**
2504 * open an existing object for editing
2505 */
2506 function reopenObject($id){
2507    $this->nStack++;
2508    $this->stack[$this->nStack]=array('c'=>$this->currentContents,'p'=>$this->currentPage);
2509    $this->currentContents=$id;
2510    // also if this object is the primary contents for a page, then set the current page to its parent
2511    if (isset($this->objects[$id]['onPage'])){
2512      $this->currentPage = $this->objects[$id]['onPage'];
2513    }
2514 }
2515
2516 /**
2517 * close an object
2518 */
2519 function closeObject(){
2520   // close the object, as long as there was one open in the first place, which will be indicated by
2521   // an objectId on the stack.
2522   if ($this->nStack>0){
2523     $this->currentContents=$this->stack[$this->nStack]['c'];
2524     $this->currentPage=$this->stack[$this->nStack]['p'];
2525     $this->nStack--;
2526     // easier to probably not worry about removing the old entries, they will be overwritten
2527     // if there are new ones.
2528   }
2529 }
2530
2531 /**
2532 * stop an object from appearing on pages from this point on
2533 */
2534 function stopObject($id){
2535   // if an object has been appearing on pages up to now, then stop it, this page will
2536   // be the last one that could contian it.
2537   if (isset($this->addLooseObjects[$id])){
2538     $this->addLooseObjects[$id]='';
2539   }
2540 }
2541
2542 /**
2543 * after an object has been created, it wil only show if it has been added, using this function.
2544 */
2545 function addObject($id,$options='add'){
2546   // add the specified object to the page
2547   if (isset($this->looseObjects[$id]) && $this->currentContents!=$id){
2548     // then it is a valid object, and it is not being added to itself
2549     switch($options){
2550       case 'all':
2551         // then this object is to be added to this page (done in the next block) and 
2552         // all future new pages. 
2553         $this->addLooseObjects[$id]='all';
2554       case 'add':
2555         if (isset($this->objects[$this->currentContents]['onPage'])){
2556           // then the destination contents is the primary for the page
2557           // (though this object is actually added to that page)
2558           $this->o_page($this->objects[$this->currentContents]['onPage'],'content',$id);
2559         }
2560         break;
2561       case 'even':
2562         $this->addLooseObjects[$id]='even';
2563         $pageObjectId=$this->objects[$this->currentContents]['onPage'];
2564         if ($this->objects[$pageObjectId]['info']['pageNum']%2==0){
2565           $this->addObject($id); // hacky huh :)
2566         }
2567         break;
2568       case 'odd':
2569         $this->addLooseObjects[$id]='odd';
2570         $pageObjectId=$this->objects[$this->currentContents]['onPage'];
2571         if ($this->objects[$pageObjectId]['info']['pageNum']%2==1){
2572           $this->addObject($id); // hacky huh :)
2573         }
2574         break;
2575       case 'next':
2576         $this->addLooseObjects[$id]='all';
2577         break;
2578       case 'nexteven':
2579         $this->addLooseObjects[$id]='even';
2580         break;
2581       case 'nextodd':
2582         $this->addLooseObjects[$id]='odd';
2583         break;
2584     }
2585   }
2586 }
2587
2588 /**
2589 * add content to the documents info object
2590 */
2591 function addInfo($label,$value=0){
2592   // this will only work if the label is one of the valid ones.
2593   // modify this so that arrays can be passed as well.
2594   // if $label is an array then assume that it is key=>value pairs
2595   // else assume that they are both scalar, anything else will probably error
2596   if (is_array($label)){
2597     foreach ($label as $l=>$v){
2598       $this->o_info($this->infoObject,$l,$v);
2599     }
2600   } else {
2601     $this->o_info($this->infoObject,$label,$value);
2602   }
2603 }
2604
2605 /**
2606 * set the viewer preferences of the document, it is up to the browser to obey these.
2607 */
2608 function setPreferences($label,$value=0){
2609   // this will only work if the label is one of the valid ones.
2610   if (is_array($label)){
2611     foreach ($label as $l=>$v){
2612       $this->o_catalog($this->catalogId,'viewerPreferences',array($l=>$v));
2613     }
2614   } else {
2615     $this->o_catalog($this->catalogId,'viewerPreferences',array($label=>$value));
2616   }
2617 }
2618
2619 /**
2620 * extract an integer from a position in a byte stream
2621 *
2622 * @access private
2623 */
2624 function PRVT_getBytes(&$data,$pos,$num){
2625   // return the integer represented by $num bytes from $pos within $data
2626   $ret=0;
2627   for ($i=0;$i<$num;$i++){
2628     $ret=$ret*256;
2629     $ret+=ord($data[$pos+$i]);
2630   }
2631   return $ret;
2632 }
2633
2634 /**
2635 * add a PNG image into the document, from a file
2636 * this should work with remote files
2637 */
2638 function addPngFromFile($file,$x,$y,$w=0,$h=0){
2639   // read in a png file, interpret it, then add to the system
2640   $error=0;
2641   $tmp = get_magic_quotes_runtime();
2642   set_magic_quotes_runtime(0);
2643   $fp = @fopen($file,'rb');
2644   if ($fp){
2645     $data='';
2646     while(!feof($fp)){
2647       $data .= fread($fp,1024);
2648     }
2649     fclose($fp);
2650   } else {
2651     $error = 1;
2652     $errormsg = 'trouble opening file: '.$file;
2653   }
2654   set_magic_quotes_runtime($tmp);
2655   
2656   if (!$error){
2657     $header = chr(137).chr(80).chr(78).chr(71).chr(13).chr(10).chr(26).chr(10);
2658     if (substr($data,0,8)!=$header){
2659       $error=1;
2660       $errormsg = 'this file does not have a valid header';
2661     }
2662   }
2663
2664   if (!$error){
2665     // set pointer
2666     $p = 8;
2667     $len = strlen($data);
2668     // cycle through the file, identifying chunks
2669     $haveHeader=0;
2670     $info=array();
2671     $idata='';
2672     $pdata='';
2673     while ($p<$len){
2674       $chunkLen = $this->PRVT_getBytes($data,$p,4);
2675       $chunkType = substr($data,$p+4,4);
2676 //      echo $chunkType.' - '.$chunkLen.'<br>';
2677     
2678       switch($chunkType){
2679         case 'IHDR':
2680           // this is where all the file information comes from
2681           $info['width']=$this->PRVT_getBytes($data,$p+8,4);
2682           $info['height']=$this->PRVT_getBytes($data,$p+12,4);
2683           $info['bitDepth']=ord($data[$p+16]);
2684           $info['colorType']=ord($data[$p+17]);
2685           $info['compressionMethod']=ord($data[$p+18]);
2686           $info['filterMethod']=ord($data[$p+19]);
2687           $info['interlaceMethod']=ord($data[$p+20]);
2688 //print_r($info);
2689           $haveHeader=1;
2690           if ($info['compressionMethod']!=0){
2691             $error=1;
2692             $errormsg = 'unsupported compression method';
2693           }
2694           if ($info['filterMethod']!=0){
2695             $error=1;
2696             $errormsg = 'unsupported filter method';
2697           }
2698           break;
2699         case 'PLTE':
2700           $pdata.=substr($data,$p+8,$chunkLen);
2701           break;
2702         case 'IDAT':
2703           $idata.=substr($data,$p+8,$chunkLen);
2704           break;
2705         case 'tRNS': 
2706           //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk 
2707           //print "tRNS found, color type = ".$info['colorType']."<BR>"; 
2708           $transparency = array();
2709           if ($info['colorType'] == 3) { // indexed color, rbg 
2710           /* corresponding to entries in the plte chunk 
2711           Alpha for palette index 0: 1 byte 
2712           Alpha for palette index 1: 1 byte 
2713           ...etc... 
2714           */ 
2715             // there will be one entry for each palette entry. up until the last non-opaque entry.
2716             // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent)
2717             $transparency['type']='indexed';
2718             $numPalette = strlen($pdata)/3;
2719             $trans=0;
2720             for ($i=$chunkLen;$i>=0;$i--){
2721               if (ord($data[$p+8+$i])==0){
2722                 $trans=$i;
2723               }
2724             }
2725             $transparency['data'] = $trans;
2726             
2727           } elseif($info['colorType'] == 0) { // grayscale 
2728           /* corresponding to entries in the plte chunk 
2729           Gray: 2 bytes, range 0 .. (2^bitdepth)-1 
2730           */ 
2731 //            $transparency['grayscale']=$this->PRVT_getBytes($data,$p+8,2); // g = grayscale 
2732             $transparency['type']='indexed';
2733             $transparency['data'] = ord($data[$p+8+1]);
2734           
2735           } elseif($info['colorType'] == 2) { // truecolor 
2736           /* corresponding to entries in the plte chunk 
2737           Red: 2 bytes, range 0 .. (2^bitdepth)-1 
2738           Green: 2 bytes, range 0 .. (2^bitdepth)-1 
2739           Blue: 2 bytes, range 0 .. (2^bitdepth)-1 
2740           */ 
2741             $transparency['r']=$this->PRVT_getBytes($data,$p+8,2); // r from truecolor 
2742             $transparency['g']=$this->PRVT_getBytes($data,$p+10,2); // g from truecolor 
2743             $transparency['b']=$this->PRVT_getBytes($data,$p+12,2); // b from truecolor 
2744           
2745           } else { 
2746           //unsupported transparency type 
2747           } 
2748           // KS End new code 
2749           break; 
2750         default:
2751           break;
2752       }
2753     
2754       $p += $chunkLen+12;
2755     }
2756     
2757     if(!$haveHeader){
2758       $error = 1;
2759       $errormsg = 'information header is missing';
2760     }
2761     if (isset($info['interlaceMethod']) && $info['interlaceMethod']){
2762       $error = 1;
2763       $errormsg = 'There appears to be no support for interlaced images in pdf.';
2764     }
2765   }
2766
2767   if (!$error && $info['bitDepth'] > 8){
2768     $error = 1;
2769     $errormsg = 'only bit depth of 8 or less is supported';
2770   }
2771
2772   if (!$error){
2773     if ($info['colorType']!=2 && $info['colorType']!=0 && $info['colorType']!=3){
2774       $error = 1;
2775       $errormsg = 'transparancey alpha channel not supported, transparency only supported for palette images.';
2776     } else {
2777       switch ($info['colorType']){
2778         case 3:
2779           $color = 'DeviceRGB';
2780           $ncolor=1;
2781           break;
2782         case 2:
2783           $color = 'DeviceRGB';
2784           $ncolor=3;
2785           break;
2786         case 0:
2787           $color = 'DeviceGray';
2788           $ncolor=1;
2789           break;
2790       }
2791     }
2792   }
2793   if ($error){
2794     $this->addMessage('PNG error - ('.$file.') '.$errormsg);
2795     return;
2796   }
2797   if ($w==0){
2798     $w=$h/$info['height']*$info['width'];
2799   }
2800   if ($h==0){
2801     $h=$w*$info['height']/$info['width'];
2802   }
2803 //print_r($info);
2804   // so this image is ok... add it in.
2805   $this->numImages++;
2806   $im=$this->numImages;
2807   $label='I'.$im;
2808   $this->numObj++;
2809 //  $this->o_image($this->numObj,'new',array('label'=>$label,'data'=>$idata,'iw'=>$w,'ih'=>$h,'type'=>'png','ic'=>$info['width']));
2810   $options = array('label'=>$label,'data'=>$idata,'bitsPerComponent'=>$info['bitDepth'],'pdata'=>$pdata
2811                                       ,'iw'=>$info['width'],'ih'=>$info['height'],'type'=>'png','color'=>$color,'ncolor'=>$ncolor);
2812   if (isset($transparency)){
2813     $options['transparency']=$transparency;
2814   }
2815   $this->o_image($this->numObj,'new',$options);
2816
2817   $this->objects[$this->currentContents]['c'].="\nq";
2818   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$w)." 0 0 ".sprintf('%.3f',$h)." ".sprintf('%.3f',$x)." ".sprintf('%.3f',$y)." cm";
2819   $this->objects[$this->currentContents]['c'].="\n/".$label.' Do';
2820   $this->objects[$this->currentContents]['c'].="\nQ";
2821 }
2822
2823 /**
2824 * add a JPEG image into the document, from a file
2825 */
2826 function addJpegFromFile($img,$x,$y,$w=0,$h=0){
2827   // attempt to add a jpeg image straight from a file, using no GD commands
2828   // note that this function is unable to operate on a remote file.
2829
2830   if (!file_exists($img)){
2831     return;
2832   }
2833
2834   $tmp=getimagesize($img);
2835   $imageWidth=$tmp[0];
2836   $imageHeight=$tmp[1];
2837
2838   if (isset($tmp['channels'])){
2839     $channels = $tmp['channels'];
2840   } else {
2841     $channels = 3;
2842   }
2843
2844   if ($w<=0 && $h<=0){
2845     $w=$imageWidth;
2846   }
2847   if ($w==0){
2848     $w=$h/$imageHeight*$imageWidth;
2849   }
2850   if ($h==0){
2851     $h=$w*$imageHeight/$imageWidth;
2852   }
2853
2854   $fp=fopen($img,'rb');
2855
2856   $tmp = get_magic_quotes_runtime();
2857   set_magic_quotes_runtime(0);
2858   $data = fread($fp,filesize($img));
2859   set_magic_quotes_runtime($tmp);
2860   
2861   fclose($fp);
2862
2863   $this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight,$channels);
2864 }
2865
2866 /**
2867 * add an image into the document, from a GD object
2868 * this function is not all that reliable, and I would probably encourage people to use 
2869 * the file based functions
2870 */
2871 function addImage(&$img,$x,$y,$w=0,$h=0,$quality=75){
2872   // add a new image into the current location, as an external object
2873   // add the image at $x,$y, and with width and height as defined by $w & $h
2874   
2875   // note that this will only work with full colour images and makes them jpg images for display
2876   // later versions could present lossless image formats if there is interest.
2877   
2878   // there seems to be some problem here in that images that have quality set above 75 do not appear
2879   // not too sure why this is, but in the meantime I have restricted this to 75.  
2880   if ($quality>75){
2881     $quality=75;
2882   }
2883
2884   // if the width or height are set to zero, then set the other one based on keeping the image
2885   // height/width ratio the same, if they are both zero, then give up :)
2886   $imageWidth=imagesx($img);
2887   $imageHeight=imagesy($img);
2888   
2889   if ($w<=0 && $h<=0){
2890     return;
2891   }
2892   if ($w==0){
2893     $w=$h/$imageHeight*$imageWidth;
2894   }
2895   if ($h==0){
2896     $h=$w*$imageHeight/$imageWidth;
2897   }
2898   
2899   // gotta get the data out of the img..
2900
2901   // so I write to a temp file, and then read it back.. soo ugly, my apologies.
2902   $tmpDir='/tmp';
2903   $tmpName=tempnam($tmpDir,'img');
2904   imagejpeg($img,$tmpName,$quality);
2905   $fp=fopen($tmpName,'rb');
2906
2907   $tmp = get_magic_quotes_runtime();
2908   set_magic_quotes_runtime(0);
2909   $fp = @fopen($tmpName,'rb');
2910   if ($fp){
2911     $data='';
2912     while(!feof($fp)){
2913       $data .= fread($fp,1024);
2914     }
2915     fclose($fp);
2916   } else {
2917     $error = 1;
2918     $errormsg = 'trouble opening file';
2919   }
2920 //  $data = fread($fp,filesize($tmpName));
2921   set_magic_quotes_runtime($tmp);
2922 //  fclose($fp);
2923   unlink($tmpName);
2924   $this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight);
2925 }
2926
2927 /**
2928 * common code used by the two JPEG adding functions
2929 *
2930 * @access private
2931 */
2932 function addJpegImage_common(&$data,$x,$y,$w=0,$h=0,$imageWidth,$imageHeight,$channels=3){
2933   // note that this function is not to be called externally
2934   // it is just the common code between the GD and the file options
2935   $this->numImages++;
2936   $im=$this->numImages;
2937   $label='I'.$im;
2938   $this->numObj++;
2939   $this->o_image($this->numObj,'new',array('label'=>$label,'data'=>$data,'iw'=>$imageWidth,'ih'=>$imageHeight,'channels'=>$channels));
2940
2941   $this->objects[$this->currentContents]['c'].="\nq";
2942   $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$w)." 0 0 ".sprintf('%.3f',$h)." ".sprintf('%.3f',$x)." ".sprintf('%.3f',$y)." cm";
2943   $this->objects[$this->currentContents]['c'].="\n/".$label.' Do';
2944   $this->objects[$this->currentContents]['c'].="\nQ";
2945 }
2946
2947 /**
2948 * specify where the document should open when it first starts
2949 */
2950 function openHere($style,$a=0,$b=0,$c=0){
2951   // this function will open the document at a specified page, in a specified style
2952   // the values for style, and the required paramters are:
2953   // 'XYZ'  left, top, zoom
2954   // 'Fit'
2955   // 'FitH' top
2956   // 'FitV' left
2957   // 'FitR' left,bottom,right
2958   // 'FitB'
2959   // 'FitBH' top
2960   // 'FitBV' left
2961   $this->numObj++;
2962   $this->o_destination($this->numObj,'new',array('page'=>$this->currentPage,'type'=>$style,'p1'=>$a,'p2'=>$b,'p3'=>$c));
2963   $id = $this->catalogId;
2964   $this->o_catalog($id,'openHere',$this->numObj);
2965 }
2966
2967 /**
2968 * create a labelled destination within the document
2969 */
2970 function addDestination($label,$style,$a=0,$b=0,$c=0){
2971   // associates the given label with the destination, it is done this way so that a destination can be specified after
2972   // it has been linked to
2973   // styles are the same as the 'openHere' function
2974   $this->numObj++;
2975   $this->o_destination($this->numObj,'new',array('page'=>$this->currentPage,'type'=>$style,'p1'=>$a,'p2'=>$b,'p3'=>$c));
2976   $id = $this->numObj;
2977   // store the label->idf relationship, note that this means that labels can be used only once
2978   $this->destinations["$label"]=$id;
2979 }
2980
2981 /**
2982 * define font families, this is used to initialize the font families for the default fonts
2983 * and for the user to add new ones for their fonts. The default bahavious can be overridden should
2984 * that be desired.
2985 */
2986 function setFontFamily($family,$options=''){
2987   if (!is_array($options)){
2988     if ($family=='init'){
2989       // set the known family groups
2990       // these font families will be used to enable bold and italic markers to be included
2991       // within text streams. html forms will be used... <b></b> <i></i>
2992       $this->fontFamilies['Helvetica.afm']=array(
2993          'b'=>'Helvetica-Bold.afm'
2994         ,'i'=>'Helvetica-Oblique.afm'
2995         ,'bi'=>'Helvetica-BoldOblique.afm'
2996         ,'ib'=>'Helvetica-BoldOblique.afm'
2997       );
2998       $this->fontFamilies['Courier.afm']=array(
2999          'b'=>'Courier-Bold.afm'
3000         ,'i'=>'Courier-Oblique.afm'
3001         ,'bi'=>'Courier-BoldOblique.afm'
3002         ,'ib'=>'Courier-BoldOblique.afm'
3003       );
3004       $this->fontFamilies['Times-Roman.afm']=array(
3005          'b'=>'Times-Bold.afm'
3006         ,'i'=>'Times-Italic.afm'
3007         ,'bi'=>'Times-BoldItalic.afm'
3008         ,'ib'=>'Times-BoldItalic.afm'
3009       );
3010     }
3011   } else {
3012     // the user is trying to set a font family
3013     // note that this can also be used to set the base ones to something else
3014     if (strlen($family)){
3015       $this->fontFamilies[$family] = $options;
3016     }
3017   }
3018 }
3019
3020 /**
3021 * used to add messages for use in debugging
3022 */
3023 function addMessage($message){
3024   $this->messages.=$message."\n";
3025 }
3026
3027 /**
3028 * a few functions which should allow the document to be treated transactionally.
3029 */
3030 function transaction($action){
3031   switch ($action){
3032     case 'start':
3033       // store all the data away into the checkpoint variable
3034       $data = get_object_vars($this);
3035       $this->checkpoint = $data;
3036       unset($data);
3037       break;
3038     case 'commit':
3039       if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])){
3040         $tmp = $this->checkpoint['checkpoint'];
3041         $this->checkpoint = $tmp;
3042         unset($tmp);
3043       } else {
3044         $this->checkpoint='';
3045       }
3046       break;
3047     case 'rewind':
3048       // do not destroy the current checkpoint, but move us back to the state then, so that we can try again
3049       if (is_array($this->checkpoint)){
3050         // can only abort if were inside a checkpoint
3051         $tmp = $this->checkpoint;
3052         foreach ($tmp as $k=>$v){
3053           if ($k != 'checkpoint'){
3054             $this->$k=$v;
3055           }
3056         }
3057         unset($tmp);
3058       }
3059       break;
3060     case 'abort':
3061       if (is_array($this->checkpoint)){
3062         // can only abort if were inside a checkpoint
3063         $tmp = $this->checkpoint;
3064         foreach ($tmp as $k=>$v){
3065           $this->$k=$v;
3066         }
3067         unset($tmp);
3068       }
3069       break;
3070   }
3071
3072 }
3073
3074 } // end of class
3075
3076 ?>