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