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