PageRenderTime 90ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/phppdf/class.pdf.php

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