* @version 009 * @package Cpdf */ class Cpdf { /** * the current number of pdf objects in the document */ var $numObj=0; /** * this array contains all of the pdf objects, ready for final assembly */ var $objects = array(); /** * the objectId (number within the objects array) of the document catalog */ var $catalogId; /** * array carrying information about the fonts that the system currently knows about * used to ensure that a font is not loaded twice, among other things */ var $fonts=array(); /** * a record of the current font */ var $currentFont=''; /** * the current base font */ var $currentBaseFont=''; /** * the number of the current font within the font array */ var $currentFontNum=0; /** * */ var $currentNode; /** * object number of the current page */ var $currentPage; /** * object number of the currently active contents block */ var $currentContents; /** * number of fonts within the system */ var $numFonts=0; /** * current colour for fill operations, defaults to inactive value, all three components should be between 0 and 1 inclusive when active */ var $currentColour=array('r'=>-1,'g'=>-1,'b'=>-1); /** * current colour for stroke operations (lines etc.) */ var $currentStrokeColour=array('r'=>-1,'g'=>-1,'b'=>-1); /** * current style that lines are drawn in */ var $currentLineStyle=''; /** * an array which is used to save the state of the document, mainly the colours and styles * it is used to temporarily change to another state, the change back to what it was before */ var $stateStack = array(); /** * number of elements within the state stack */ var $nStateStack = 0; /** * number of page objects within the document */ var $numPages=0; /** * object Id storage stack */ var $stack=array(); /** * number of elements within the object Id storage stack */ var $nStack=0; /** * an array which contains information about the objects which are not firmly attached to pages * these have been added with the addObject function */ var $looseObjects=array(); /** * array contains infomation about how the loose objects are to be added to the document */ var $addLooseObjects=array(); /** * the objectId of the information object for the document * this contains authorship, title etc. */ var $infoObject=0; /** * number of images being tracked within the document */ var $numImages=0; /** * an array containing options about the document * it defaults to turning on the compression of the objects */ var $options=array('compression'=>1); /** * the objectId of the first page of the document */ var $firstPageId; /** * used to track the last used value of the inter-word spacing, this is so that it is known * when the spacing is changed. */ var $wordSpaceAdjust=0; /** * the object Id of the procset object */ var $procsetObjectId; /** * store the information about the relationship between font families * this used so that the code knows which font is the bold version of another font, etc. * the value of this array is initialised in the constuctor function. */ var $fontFamilies = array(); /** * track if the current font is bolded or italicised */ var $currentTextState = ''; /** * messages are stored here during processing, these can be selected afterwards to give some useful debug information */ var $messages=''; /** * the ancryption array for the document encryption is stored here */ var $arc4=''; /** * the object Id of the encryption information */ var $arc4_objnum=0; /** * the file identifier, used to uniquely identify a pdf document */ var $fileIdentifier=''; /** * a flag to say if a document is to be encrypted or not */ var $encrypted=0; /** * the ancryption key for the encryption of all the document content (structure is not encrypted) */ var $encryptionKey=''; /** * array which forms a stack to keep track of nested callback functions */ var $callback = array(); /** * the number of callback functions in the callback array */ var $nCallback = 0; /** * store label->id pairs for named destinations, these will be used to replace internal links * done this way so that destinations can be defined after the location that links to them */ var $destinations = array(); /** * store the stack for the transaction commands, each item in here is a record of the values of all the * variables within the class, so that the user can rollback at will (from each 'start' command) * note that this includes the objects array, so these can be large. */ var $checkpoint = ''; /** * class constructor * this will start a new document * @var array array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero. */ function Cpdf ($pageSize=array(0,0,612,792)){ $this->newDocument($pageSize); // also initialize the font families that are known about already $this->setFontFamily('init'); // $this->fileIdentifier = md5('xxxxxxxx'.time()); } /** * Document object methods (internal use only) * * There is about one object method for each type of object in the pdf document * Each function has the same call list ($id,$action,$options). * $id = the object ID of the object, or what it is to be if it is being created * $action = a string specifying the action to be performed, though ALL must support: * 'new' - create the object with the id $id * 'out' - produce the output for the pdf object * $options = optional, a string or array containing the various parameters for the object * * These, in conjunction with the output function are the ONLY way for output to be produced * within the pdf 'file'. */ /** *destination object, used to specify the location for the user to jump to, presently on opening */ function o_destination($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch($action){ case 'new': $this->objects[$id]=array('t'=>'destination','info'=>array()); $tmp = ''; switch ($options['type']){ case 'XYZ': case 'FitR': $tmp = ' '.$options['p3'].$tmp; case 'FitH': case 'FitV': case 'FitBH': case 'FitBV': $tmp = ' '.$options['p1'].' '.$options['p2'].$tmp; case 'Fit': case 'FitB': $tmp = $options['type'].$tmp; $this->objects[$id]['info']['string']=$tmp; $this->objects[$id]['info']['page']=$options['page']; } break; case 'out': $tmp = $o['info']; $res="\n".$id." 0 obj\n".'['.$tmp['page'].' 0 R /'.$tmp['string']."]\nendobj\n"; return $res; break; } } /** * set the viewer preferences */ function o_viewerPreferences($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'viewerPreferences','info'=>array()); break; case 'add': foreach($options as $k=>$v){ switch ($k){ case 'HideToolbar': case 'HideMenubar': case 'HideWindowUI': case 'FitWindow': case 'CenterWindow': case 'NonFullScreenPageMode': case 'Direction': $o['info'][$k]=$v; break; } } break; case 'out': $res="\n".$id." 0 obj\n".'<< '; foreach($o['info'] as $k=>$v){ $res.="\n/".$k.' '.$v; } $res.="\n>>\n"; return $res; break; } } /** * define the document catalog, the overall controller for the document */ function o_catalog($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'catalog','info'=>array()); $this->catalogId=$id; break; case 'outlines': case 'pages': case 'openHere': $o['info'][$action]=$options; break; case 'viewerPreferences': if (!isset($o['info']['viewerPreferences'])){ $this->numObj++; $this->o_viewerPreferences($this->numObj,'new'); $o['info']['viewerPreferences']=$this->numObj; } $vp = $o['info']['viewerPreferences']; $this->o_viewerPreferences($vp,'add',$options); break; case 'out': $res="\n".$id." 0 obj\n".'<< /Type /Catalog'; foreach($o['info'] as $k=>$v){ switch($k){ case 'outlines': $res.="\n".'/Outlines '.$v.' 0 R'; break; case 'pages': $res.="\n".'/Pages '.$v.' 0 R'; break; case 'viewerPreferences': $res.="\n".'/ViewerPreferences '.$o['info']['viewerPreferences'].' 0 R'; break; case 'openHere': $res.="\n".'/OpenAction '.$o['info']['openHere'].' 0 R'; break; } } $res.=" >>\nendobj"; return $res; break; } } /** * object which is a parent to the pages in the document */ function o_pages($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'pages','info'=>array()); $this->o_catalog($this->catalogId,'pages',$id); break; case 'page': if (!is_array($options)){ // then it will just be the id of the new page $o['info']['pages'][]=$options; } else { // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative // and pos is either 'before' or 'after', saying where this page will fit. if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])){ $i = array_search($options['rid'],$o['info']['pages']); if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i]==$options['rid']){ // then there is a match // make a space switch ($options['pos']){ case 'before': $k = $i; break; case 'after': $k=$i+1; break; default: $k=-1; break; } if ($k>=0){ for ($j=count($o['info']['pages'])-1;$j>=$k;$j--){ $o['info']['pages'][$j+1]=$o['info']['pages'][$j]; } $o['info']['pages'][$k]=$options['id']; } } } } break; case 'procset': $o['info']['procset']=$options; break; case 'mediaBox': $o['info']['mediaBox']=$options; // which should be an array of 4 numbers break; case 'font': $o['info']['fonts'][]=array('objNum'=>$options['objNum'],'fontNum'=>$options['fontNum']); break; case 'xObject': $o['info']['xObjects'][]=array('objNum'=>$options['objNum'],'label'=>$options['label']); break; case 'out': if (count($o['info']['pages'])){ $res="\n".$id." 0 obj\n<< /Type /Pages\n/Kids ["; foreach($o['info']['pages'] as $k=>$v){ $res.=$v." 0 R\n"; } $res.="]\n/Count ".count($this->objects[$id]['info']['pages']); if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) || isset($o['info']['procset'])){ $res.="\n/Resources <<"; if (isset($o['info']['procset'])){ $res.="\n/ProcSet ".$o['info']['procset']." 0 R"; } if (isset($o['info']['fonts']) && count($o['info']['fonts'])){ $res.="\n/Font << "; foreach($o['info']['fonts'] as $finfo){ $res.="\n/F".$finfo['fontNum']." ".$finfo['objNum']." 0 R"; } $res.=" >>"; } if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])){ $res.="\n/XObject << "; foreach($o['info']['xObjects'] as $finfo){ $res.="\n/".$finfo['label']." ".$finfo['objNum']." 0 R"; } $res.=" >>"; } $res.="\n>>"; if (isset($o['info']['mediaBox'])){ $tmp=$o['info']['mediaBox']; $res.="\n/MediaBox [".sprintf('%.3f',$tmp[0]).' '.sprintf('%.3f',$tmp[1]).' '.sprintf('%.3f',$tmp[2]).' '.sprintf('%.3f',$tmp[3]).']'; } } $res.="\n >>\nendobj"; } else { $res="\n".$id." 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj"; } return $res; break; } } /** * define the outlines in the doc, empty for now */ function o_outlines($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'outlines','info'=>array('outlines'=>array())); $this->o_catalog($this->catalogId,'outlines',$id); break; case 'outline': $o['info']['outlines'][]=$options; break; case 'out': if (count($o['info']['outlines'])){ $res="\n".$id." 0 obj\n<< /Type /Outlines /Kids ["; foreach($o['info']['outlines'] as $k=>$v){ $res.=$v." 0 R "; } $res.="] /Count ".count($o['info']['outlines'])." >>\nendobj"; } else { $res="\n".$id." 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj"; } return $res; break; } } /** * an object to hold the font description */ function o_font($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'font','info'=>array('name'=>$options['name'],'SubType'=>'Type1')); $fontNum=$this->numFonts; $this->objects[$id]['info']['fontNum']=$fontNum; // deal with the encoding and the differences if (isset($options['differences'])){ // then we'll need an encoding dictionary $this->numObj++; $this->o_fontEncoding($this->numObj,'new',$options); $this->objects[$id]['info']['encodingDictionary']=$this->numObj; } else if (isset($options['encoding'])){ // we can specify encoding here switch($options['encoding']){ case 'WinAnsiEncoding': case 'MacRomanEncoding': case 'MacExpertEncoding': $this->objects[$id]['info']['encoding']=$options['encoding']; break; case 'none': break; default: $this->objects[$id]['info']['encoding']='WinAnsiEncoding'; break; } } else { $this->objects[$id]['info']['encoding']='WinAnsiEncoding'; } // also tell the pages node about the new font $this->o_pages($this->currentNode,'font',array('fontNum'=>$fontNum,'objNum'=>$id)); break; case 'add': foreach ($options as $k=>$v){ switch ($k){ case 'BaseFont': $o['info']['name'] = $v; break; case 'FirstChar': case 'LastChar': case 'Widths': case 'FontDescriptor': case 'SubType': $this->addMessage('o_font '.$k." : ".$v); $o['info'][$k] = $v; break; } } break; case 'out': $res="\n".$id." 0 obj\n<< /Type /Font\n/Subtype /".$o['info']['SubType']."\n"; $res.="/Name /F".$o['info']['fontNum']."\n"; $res.="/BaseFont /".$o['info']['name']."\n"; if (isset($o['info']['encodingDictionary'])){ // then place a reference to the dictionary $res.="/Encoding ".$o['info']['encodingDictionary']." 0 R\n"; } else if (isset($o['info']['encoding'])){ // use the specified encoding $res.="/Encoding /".$o['info']['encoding']."\n"; } if (isset($o['info']['FirstChar'])){ $res.="/FirstChar ".$o['info']['FirstChar']."\n"; } if (isset($o['info']['LastChar'])){ $res.="/LastChar ".$o['info']['LastChar']."\n"; } if (isset($o['info']['Widths'])){ $res.="/Widths ".$o['info']['Widths']." 0 R\n"; } if (isset($o['info']['FontDescriptor'])){ $res.="/FontDescriptor ".$o['info']['FontDescriptor']." 0 R\n"; } $res.=">>\nendobj"; return $res; break; } } /** * a font descriptor, needed for including additional fonts */ function o_fontDescriptor($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'fontDescriptor','info'=>$options); break; case 'out': $res="\n".$id." 0 obj\n<< /Type /FontDescriptor\n"; foreach ($o['info'] as $label => $value){ switch ($label){ case 'Ascent': case 'CapHeight': case 'Descent': case 'Flags': case 'ItalicAngle': case 'StemV': case 'AvgWidth': case 'Leading': case 'MaxWidth': case 'MissingWidth': case 'StemH': case 'XHeight': case 'CharSet': if (strlen($value)){ $res.='/'.$label.' '.$value."\n"; } break; case 'FontFile': case 'FontFile2': case 'FontFile3': $res.='/'.$label.' '.$value." 0 R\n"; break; case 'FontBBox': $res.='/'.$label.' ['.$value[0].' '.$value[1].' '.$value[2].' '.$value[3]."]\n"; break; case 'FontName': $res.='/'.$label.' /'.$value."\n"; break; } } $res.=">>\nendobj"; return $res; break; } } /** * the font encoding */ function o_fontEncoding($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': // the options array should contain 'differences' and maybe 'encoding' $this->objects[$id]=array('t'=>'fontEncoding','info'=>$options); break; case 'out': $res="\n".$id." 0 obj\n<< /Type /Encoding\n"; if (!isset($o['info']['encoding'])){ $o['info']['encoding']='WinAnsiEncoding'; } if ($o['info']['encoding']!='none'){ $res.="/BaseEncoding /".$o['info']['encoding']."\n"; } $res.="/Differences \n["; $onum=-100; foreach($o['info']['differences'] as $num=>$label){ if ($num!=$onum+1){ // we cannot make use of consecutive numbering $res.= "\n".$num." /".$label; } else { $res.= " /".$label; } $onum=$num; } $res.="\n]\n>>\nendobj"; return $res; break; } } /** * the document procset, solves some problems with printing to old PS printers */ function o_procset($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'procset','info'=>array('PDF'=>1,'Text'=>1)); $this->o_pages($this->currentNode,'procset',$id); $this->procsetObjectId=$id; break; case 'add': // this is to add new items to the procset list, despite the fact that this is considered // obselete, the items are required for printing to some postscript printers switch ($options) { case 'ImageB': case 'ImageC': case 'ImageI': $o['info'][$options]=1; break; } break; case 'out': $res="\n".$id." 0 obj\n["; foreach ($o['info'] as $label=>$val){ $res.='/'.$label.' '; } $res.="]\nendobj"; return $res; break; } } /** * define the document information */ function o_info($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->infoObject=$id; $date='D:'.date('Ymd'); $this->objects[$id]=array('t'=>'info','info'=>array('Creator'=>'R and OS php pdf writer, http://www.ros.co.nz','CreationDate'=>$date)); break; case 'Title': case 'Author': case 'Subject': case 'Keywords': case 'Creator': case 'Producer': case 'CreationDate': case 'ModDate': case 'Trapped': $o['info'][$action]=$options; break; case 'out': if ($this->encrypted){ $this->encryptInit($id); } $res="\n".$id." 0 obj\n<<\n"; foreach ($o['info'] as $k=>$v){ $res.='/'.$k.' ('; if ($this->encrypted){ $res.=$this->filterText($this->ARC4($v)); } else { $res.=$this->filterText($v); } $res.=")\n"; } $res.=">>\nendobj"; return $res; break; } } /** * an action object, used to link to URLS initially */ function o_action($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': if (is_array($options)){ $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>$options['type']); } else { // then assume a URI action $this->objects[$id]=array('t'=>'action','info'=>$options,'type'=>'URI'); } break; case 'out': if ($this->encrypted){ $this->encryptInit($id); } $res="\n".$id." 0 obj\n<< /Type /Action"; switch($o['type']){ case 'ilink': // there will be an 'label' setting, this is the name of the destination $res.="\n/S /GoTo\n/D ".$this->destinations[(string)$o['info']['label']]." 0 R"; break; case 'URI': $res.="\n/S /URI\n/URI ("; if ($this->encrypted){ $res.=$this->filterText($this->ARC4($o['info'])); } else { $res.=$this->filterText($o['info']); } $res.=")"; break; } $res.="\n>>\nendobj"; return $res; break; } } /** * an annotation object, this will add an annotation to the current page. * initially will support just link annotations */ function o_annotation($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': // add the annotation to the current page $pageId = $this->currentPage; $this->o_page($pageId,'annot',$id); // and add the action object which is going to be required switch($options['type']){ case 'link': $this->objects[$id]=array('t'=>'annotation','info'=>$options); $this->numObj++; $this->o_action($this->numObj,'new',$options['url']); $this->objects[$id]['info']['actionId']=$this->numObj; break; case 'ilink': // this is to a named internal link $label = $options['label']; $this->objects[$id]=array('t'=>'annotation','info'=>$options); $this->numObj++; $this->o_action($this->numObj,'new',array('type'=>'ilink','label'=>$label)); $this->objects[$id]['info']['actionId']=$this->numObj; break; } break; case 'out': $res="\n".$id." 0 obj\n<< /Type /Annot"; switch($o['info']['type']){ case 'link': case 'ilink': $res.= "\n/Subtype /Link"; break; } $res.="\n/A ".$o['info']['actionId']." 0 R"; $res.="\n/Border [0 0 0]"; $res.="\n/H /I"; $res.="\n/Rect [ "; foreach($o['info']['rect'] as $v){ $res.= sprintf("%.4f ",$v); } $res.="]"; $res.="\n>>\nendobj"; return $res; break; } } /** * a page object, it also creates a contents object to hold its contents */ function o_page($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->numPages++; $this->objects[$id]=array('t'=>'page','info'=>array('parent'=>$this->currentNode,'pageNum'=>$this->numPages)); if (is_array($options)){ // then this must be a page insertion, array shoudl contain 'rid','pos'=[before|after] $options['id']=$id; $this->o_pages($this->currentNode,'page',$options); } else { $this->o_pages($this->currentNode,'page',$id); } $this->currentPage=$id; //make a contents object to go with this page $this->numObj++; $this->o_contents($this->numObj,'new',$id); $this->currentContents=$this->numObj; $this->objects[$id]['info']['contents']=array(); $this->objects[$id]['info']['contents'][]=$this->numObj; $match = ($this->numPages%2 ? 'odd' : 'even'); foreach($this->addLooseObjects as $oId=>$target){ if ($target=='all' || $match==$target){ $this->objects[$id]['info']['contents'][]=$oId; } } break; case 'content': $o['info']['contents'][]=$options; break; case 'annot': // add an annotation to this page if (!isset($o['info']['annot'])){ $o['info']['annot']=array(); } // $options should contain the id of the annotation dictionary $o['info']['annot'][]=$options; break; case 'out': $res="\n".$id." 0 obj\n<< /Type /Page"; $res.="\n/Parent ".$o['info']['parent']." 0 R"; if (isset($o['info']['annot'])){ $res.="\n/Annots ["; foreach($o['info']['annot'] as $aId){ $res.=" ".$aId." 0 R"; } $res.=" ]"; } $count = count($o['info']['contents']); if ($count==1){ $res.="\n/Contents ".$o['info']['contents'][0]." 0 R"; } else if ($count>1){ $res.="\n/Contents [\n"; foreach ($o['info']['contents'] as $cId){ $res.=$cId." 0 R\n"; } $res.="]"; } $res.="\n>>\nendobj"; return $res; break; } } /** * the contents objects hold all of the content which appears on pages */ function o_contents($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch ($action){ case 'new': $this->objects[$id]=array('t'=>'contents','c'=>'','info'=>array()); if (strlen($options) && intval($options)){ // then this contents is the primary for a page $this->objects[$id]['onPage']=$options; } else if ($options=='raw'){ // then this page contains some other type of system object $this->objects[$id]['raw']=1; } break; case 'add': // add more options to the decleration foreach ($options as $k=>$v){ $o['info'][$k]=$v; } case 'out': $tmp=$o['c']; $res= "\n".$id." 0 obj\n"; if (isset($this->objects[$id]['raw'])){ $res.=$tmp; } else { $res.= "<<"; if (function_exists('gzcompress') && $this->options['compression']){ // then implement ZLIB based compression on this content stream $res.=" /Filter /FlateDecode"; $tmp = gzcompress($tmp); } if ($this->encrypted){ $this->encryptInit($id); $tmp = $this->ARC4($tmp); } foreach($o['info'] as $k=>$v){ $res .= "\n/".$k.' '.$v; } $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream"; } $res.="\nendobj\n"; return $res; break; } } /** * an image object, will be an XObject in the document, includes description and data */ function o_image($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch($action){ case 'new': // make the new object $this->objects[$id]=array('t'=>'image','data'=>$options['data'],'info'=>array()); $this->objects[$id]['info']['Type']='/XObject'; $this->objects[$id]['info']['Subtype']='/Image'; $this->objects[$id]['info']['Width']=$options['iw']; $this->objects[$id]['info']['Height']=$options['ih']; if (!isset($options['type']) || $options['type']=='jpg'){ if (!isset($options['channels'])){ $options['channels']=3; } switch($options['channels']){ case 1: $this->objects[$id]['info']['ColorSpace']='/DeviceGray'; break; default: $this->objects[$id]['info']['ColorSpace']='/DeviceRGB'; break; } $this->objects[$id]['info']['Filter']='/DCTDecode'; $this->objects[$id]['info']['BitsPerComponent']=8; } else if ($options['type']=='png'){ $this->objects[$id]['info']['Filter']='/FlateDecode'; $this->objects[$id]['info']['DecodeParms']='<< /Predictor 15 /Colors '.$options['ncolor'].' /Columns '.$options['iw'].' /BitsPerComponent '.$options['bitsPerComponent'].'>>'; if (strlen($options['pdata'])){ $tmp = ' [ /Indexed /DeviceRGB '.(strlen($options['pdata'])/3-1).' '; $this->numObj++; $this->o_contents($this->numObj,'new'); $this->objects[$this->numObj]['c']=$options['pdata']; $tmp.=$this->numObj.' 0 R'; $tmp .=' ]'; $this->objects[$id]['info']['ColorSpace'] = $tmp; if (isset($options['transparency'])){ switch($options['transparency']['type']){ case 'indexed': $tmp=' [ '.$options['transparency']['data'].' '.$options['transparency']['data'].'] '; $this->objects[$id]['info']['Mask'] = $tmp; break; } } } else { $this->objects[$id]['info']['ColorSpace']='/'.$options['color']; } $this->objects[$id]['info']['BitsPerComponent']=$options['bitsPerComponent']; } // assign it a place in the named resource dictionary as an external object, according to // the label passed in with it. $this->o_pages($this->currentNode,'xObject',array('label'=>$options['label'],'objNum'=>$id)); // also make sure that we have the right procset object for it. $this->o_procset($this->procsetObjectId,'add','ImageC'); break; case 'out': $tmp=$o['data']; $res= "\n".$id." 0 obj\n<<"; foreach($o['info'] as $k=>$v){ $res.="\n/".$k.' '.$v; } if ($this->encrypted){ $this->encryptInit($id); $tmp = $this->ARC4($tmp); } $res.="\n/Length ".strlen($tmp)." >>\nstream\n".$tmp."\nendstream\nendobj\n"; return $res; break; } } /** * encryption object. */ function o_encryption($id,$action,$options=''){ if ($action!='new'){ $o =& $this->objects[$id]; } switch($action){ case 'new': // make the new object $this->objects[$id]=array('t'=>'encryption','info'=>$options); $this->arc4_objnum=$id; // figure out the additional paramaters required $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); $len = strlen($options['owner']); if ($len>32){ $owner = substr($options['owner'],0,32); } else if ($len<32){ $owner = $options['owner'].substr($pad,0,32-$len); } else { $owner = $options['owner']; } $len = strlen($options['user']); if ($len>32){ $user = substr($options['user'],0,32); } else if ($len<32){ $user = $options['user'].substr($pad,0,32-$len); } else { $user = $options['user']; } $tmp = $this->md5_16($owner); $okey = substr($tmp,0,5); $this->ARC4_init($okey); $ovalue=$this->ARC4($user); $this->objects[$id]['info']['O']=$ovalue; // now make the u value, phew. $tmp = $this->md5_16($user.$ovalue.chr($options['p']).chr(255).chr(255).chr(255).$this->fileIdentifier); $ukey = substr($tmp,0,5); $this->ARC4_init($ukey); $this->encryptionKey = $ukey; $this->encrypted=1; $uvalue=$this->ARC4($pad); $this->objects[$id]['info']['U']=$uvalue; $this->encryptionKey=$ukey; // initialize the arc4 array break; case 'out': $res= "\n".$id." 0 obj\n<<"; $res.="\n/Filter /Standard"; $res.="\n/V 1"; $res.="\n/R 2"; $res.="\n/O (".$this->filterText($o['info']['O']).')'; $res.="\n/U (".$this->filterText($o['info']['U']).')'; // and the p-value needs to be converted to account for the twos-complement approach $o['info']['p'] = (($o['info']['p']^255)+1)*-1; $res.="\n/P ".($o['info']['p']); $res.="\n>>\nendobj\n"; return $res; break; } } /** * ARC4 functions * A series of function to implement ARC4 encoding in PHP */ /** * calculate the 16 byte version of the 128 bit md5 digest of the string */ function md5_16($string){ $tmp = md5($string); $out=''; for ($i=0;$i<=30;$i=$i+2){ $out.=chr(hexdec(substr($tmp,$i,2))); } return $out; } /** * initialize the encryption for processing a particular object */ function encryptInit($id){ $tmp = $this->encryptionKey; $hex = dechex($id); if (strlen($hex)<6){ $hex = substr('000000',0,6-strlen($hex)).$hex; } $tmp.= chr(hexdec(substr($hex,4,2))).chr(hexdec(substr($hex,2,2))).chr(hexdec(substr($hex,0,2))).chr(0).chr(0); $key = $this->md5_16($tmp); $this->ARC4_init(substr($key,0,10)); } /** * initialize the ARC4 encryption */ function ARC4_init($key=''){ $this->arc4 = ''; // setup the control array if (strlen($key)==0){ return; } $k = ''; while(strlen($k)<256){ $k.=$key; } $k=substr($k,0,256); for ($i=0;$i<256;$i++){ $this->arc4 .= chr($i); } $j=0; for ($i=0;$i<256;$i++){ $t = $this->arc4[$i]; $j = ($j + ord($t) + ord($k[$i]))%256; $this->arc4[$i]=$this->arc4[$j]; $this->arc4[$j]=$t; } } /** * ARC4 encrypt a text string */ function ARC4($text){ $len=strlen($text); $a=0; $b=0; $c = $this->arc4; $out=''; for ($i=0;$i<$len;$i++){ $a = ($a+1)%256; $t= $c[$a]; $b = ($b+ord($t))%256; $c[$a]=$c[$b]; $c[$b]=$t; $k = ord($c[(ord($c[$a])+ord($c[$b]))%256]); $out.=chr(ord($text[$i]) ^ $k); } return $out; } /** * functions which can be called to adjust or add to the document */ /** * add a link in the document to an external URL */ function addLink($url,$x0,$y0,$x1,$y1){ $this->numObj++; $info = array('type'=>'link','url'=>$url,'rect'=>array($x0,$y0,$x1,$y1)); $this->o_annotation($this->numObj,'new',$info); } /** * add a link in the document to an internal destination (ie. within the document) */ function addInternalLink($label,$x0,$y0,$x1,$y1){ $this->numObj++; $info = array('type'=>'ilink','label'=>$label,'rect'=>array($x0,$y0,$x1,$y1)); $this->o_annotation($this->numObj,'new',$info); } /** * set the encryption of the document * can be used to turn it on and/or set the passwords which it will have. * also the functions that the user will have are set here, such as print, modify, add */ function setEncryption($userPass='',$ownerPass='',$pc=array()){ $p=bindec(11000000); $options = array( 'print'=>4 ,'modify'=>8 ,'copy'=>16 ,'add'=>32 ); foreach($pc as $k=>$v){ if ($v && isset($options[$k])){ $p+=$options[$k]; } else if (isset($options[$v])){ $p+=$options[$v]; } } // implement encryption on the document if ($this->arc4_objnum == 0){ // then the block does not exist already, add it. $this->numObj++; if (strlen($ownerPass)==0){ $ownerPass=$userPass; } $this->o_encryption($this->numObj,'new',array('user'=>$userPass,'owner'=>$ownerPass,'p'=>$p)); } } /** * should be used for internal checks, not implemented as yet */ function checkAllHere(){ } /** * return the pdf stream as a string returned from the function */ function output($debug=0){ if ($debug){ // turn compression off $this->options['compression']=0; } if ($this->arc4_objnum){ $this->ARC4_init($this->encryptionKey); } $this->checkAllHere(); $xref=array(); $content="%PDF-1.3\n%����\n"; // $content="%PDF-1.3\n"; $pos=strlen($content); foreach($this->objects as $k=>$v){ $tmp='o_'.$v['t']; $cont=$this->$tmp($k,'out'); $content.=$cont; $xref[]=$pos; $pos+=strlen($cont); } $content.="\nxref\n0 ".(count($xref)+1)."\n0000000000 65535 f \n"; foreach($xref as $p){ $content.=substr('0000000000',0,10-strlen($p)).$p." 00000 n \n"; } $content.="\ntrailer\n << /Size ".(count($xref)+1)."\n /Root 1 0 R\n /Info ".$this->infoObject." 0 R\n"; // if encryption has been applied to this document then add the marker for this dictionary if ($this->arc4_objnum > 0){ $content .= "/Encrypt ".$this->arc4_objnum." 0 R\n"; } if (strlen($this->fileIdentifier)){ $content .= "/ID[<".$this->fileIdentifier."><".$this->fileIdentifier.">]\n"; } $content .= " >>\nstartxref\n".$pos."\n%%EOF\n"; return $content; } /** * intialize a new document * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum * this function is called automatically by the constructor function * * @access private */ function newDocument($pageSize=array(0,0,612,792)){ $this->numObj=0; $this->objects = array(); $this->numObj++; $this->o_catalog($this->numObj,'new'); $this->numObj++; $this->o_outlines($this->numObj,'new'); $this->numObj++; $this->o_pages($this->numObj,'new'); $this->o_pages($this->numObj,'mediaBox',$pageSize); $this->currentNode = 3; $this->numObj++; $this->o_procset($this->numObj,'new'); $this->numObj++; $this->o_info($this->numObj,'new'); $this->numObj++; $this->o_page($this->numObj,'new'); // need to store the first page id as there is no way to get it to the user during // startup $this->firstPageId = $this->currentContents; } /** * open the font file and return a php structure containing it. * first check if this one has been done before and saved in a form more suited to php * note that if a php serialized version does not exist it will try and make one, but will * require write access to the directory to do it... it is MUCH faster to have these serialized * files. * * @access private */ function openFont($font){ // assume that $font contains both the path and perhaps the extension to the file, split them $pos=strrpos($font,'/'); if ($pos===false){ $dir = './'; $name = $font; } else { $dir=substr($font,0,$pos+1); $name=substr($font,$pos+1); } if (substr($name,-4)=='.afm'){ $name=substr($name,0,strlen($name)-4); } $this->addMessage('openFont: '.$font.' - '.$name); if (file_exists($dir.'php_'.$name.'.afm')){ $this->addMessage('openFont: php file exists '.$dir.'php_'.$name.'.afm'); $tmp = file($dir.'php_'.$name.'.afm'); $this->fonts[$font]=unserialize($tmp[0]); if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_']<1){ // if the font file is old, then clear it out and prepare for re-creation $this->addMessage('openFont: clear out, make way for new version.'); unset($this->fonts[$font]); } } if (!isset($this->fonts[$font]) && file_exists($dir.$name.'.afm')){ // then rebuild the php_.afm file from the .afm file $this->addMessage('openFont: build php file from '.$dir.$name.'.afm'); $data = array(); $file = file($dir.$name.'.afm'); foreach ($file as $rowA){ $row=trim($rowA); $pos=strpos($row,' '); if ($pos){ // then there must be some keyword $key = substr($row,0,$pos); switch ($key){ case 'FontName': case 'FullName': case 'FamilyName': case 'Weight': case 'ItalicAngle': case 'IsFixedPitch': case 'CharacterSet': case 'UnderlinePosition': case 'UnderlineThickness': case 'Version': case 'EncodingScheme': case 'CapHeight': case 'XHeight': case 'Ascender': case 'Descender': case 'StdHW': case 'StdVW': case 'StartCharMetrics': $data[$key]=trim(substr($row,$pos)); break; case 'FontBBox': $data[$key]=explode(' ',trim(substr($row,$pos))); break; case 'C': //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ; $bits=explode(';',trim($row)); $dtmp=array(); foreach($bits as $bit){ $bits2 = explode(' ',trim($bit)); if (strlen($bits2[0])){ if (count($bits2)>2){ $dtmp[$bits2[0]]=array(); for ($i=1;$i=0){ $data['C'][$dtmp['C']]=$dtmp; $data['C'][$dtmp['N']]=$dtmp; } else { $data['C'][$dtmp['N']]=$dtmp; } break; case 'KPX': //KPX Adieresis yacute -40 $bits=explode(' ',trim($row)); $data['KPX'][$bits[1]][$bits[2]]=$bits[3]; break; } } } $data['_version_']=1; $this->fonts[$font]=$data; $fp = fopen($dir.'php_'.$name.'.afm','w'); fwrite($fp,serialize($data)); fclose($fp); } else if (!isset($this->fonts[$font])){ $this->addMessage('openFont: no font file found'); // echo 'Font not Found '.$font; } } /** * if the font is not loaded then load it and make the required object * else just make it the current font * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding' * note that encoding='none' will need to be used for symbolic fonts * and 'differences' => an array of mappings between numbers 0->255 and character names. * */ function selectFont($fontName,$encoding='',$set=1){ if (!isset($this->fonts[$fontName])){ // load the file $this->openFont($fontName); if (isset($this->fonts[$fontName])){ $this->numObj++; $this->numFonts++; $pos=strrpos($fontName,'/'); // $dir=substr($fontName,0,$pos+1); $name=substr($fontName,$pos+1); if (substr($name,-4)=='.afm'){ $name=substr($name,0,strlen($name)-4); } $options=array('name'=>$name); if (is_array($encoding)){ // then encoding and differences might be set if (isset($encoding['encoding'])){ $options['encoding']=$encoding['encoding']; } if (isset($encoding['differences'])){ $options['differences']=$encoding['differences']; } } else if (strlen($encoding)){ // then perhaps only the encoding has been set $options['encoding']=$encoding; } $fontObj = $this->numObj; $this->o_font($this->numObj,'new',$options); $this->fonts[$fontName]['fontNum']=$this->numFonts; // if this is a '.afm' font, and there is a '.pfa' file to go with it ( as there // should be for all non-basic fonts), then load it into an object and put the // references into the font object $basefile = substr($fontName,0,strlen($fontName)-4); if (file_exists($basefile.'.pfb')){ $fbtype = 'pfb'; } else if (file_exists($basefile.'.ttf')){ $fbtype = 'ttf'; } else { $fbtype=''; } $fbfile = $basefile.'.'.$fbtype; // $pfbfile = substr($fontName,0,strlen($fontName)-4).'.pfb'; // $ttffile = substr($fontName,0,strlen($fontName)-4).'.ttf'; $this->addMessage('selectFont: checking for - '.$fbfile); if (substr($fontName,-4)=='.afm' && strlen($fbtype) ){ $adobeFontName = $this->fonts[$fontName]['FontName']; // $fontObj = $this->numObj; $this->addMessage('selectFont: adding font file - '.$fbfile.' - '.$adobeFontName); // find the array of fond widths, and put that into an object. $firstChar = -1; $lastChar = 0; $widths = array(); foreach ($this->fonts[$fontName]['C'] as $num=>$d){ if (intval($num)>0 || $num=='0'){ if ($lastChar>0 && $num>$lastChar+1){ for($i=$lastChar+1;$i<$num;$i++){ $widths[] = 0; } } $widths[] = $d['WX']; if ($firstChar==-1){ $firstChar = $num; } $lastChar = $num; } } // also need to adjust the widths for the differences array if (isset($options['differences'])){ foreach($options['differences'] as $charNum=>$charName){ if ($charNum>$lastChar){ for($i=$lastChar+1;$i<=$charNum;$i++){ $widths[]=0; } $lastChar=$charNum; } if (isset($this->fonts[$fontName]['C'][$charName])){ $widths[$charNum-$firstChar]=$this->fonts[$fontName]['C'][$charName]['WX']; } } } $this->addMessage('selectFont: FirstChar='.$firstChar); $this->addMessage('selectFont: LastChar='.$lastChar); $this->numObj++; $this->o_contents($this->numObj,'new','raw'); $this->objects[$this->numObj]['c'].='['; foreach($widths as $width){ $this->objects[$this->numObj]['c'].=' '.$width; } $this->objects[$this->numObj]['c'].=' ]'; $widthid = $this->numObj; // load the pfb file, and put that into an object too. // note that pdf supports only binary format type 1 font files, though there is a // simple utility to convert them from pfa to pfb. $fp = fopen($fbfile,'rb'); $tmp = get_magic_quotes_runtime(); ini_set("magic_quotes_runtime", 0); $data = fread($fp,filesize($fbfile)); ini_set("magic_quotes_runtime", $tmp); fclose($fp); // create the font descriptor $this->numObj++; $fontDescriptorId = $this->numObj; $this->numObj++; $pfbid = $this->numObj; // determine flags (more than a little flakey, hopefully will not matter much) $flags=0; if ($this->fonts[$fontName]['ItalicAngle']!=0){ $flags+=pow(2,6); } if ($this->fonts[$fontName]['IsFixedPitch']=='true'){ $flags+=1; } $flags+=pow(2,5); // assume non-sybolic $list = array('Ascent'=>'Ascender','CapHeight'=>'CapHeight','Descent'=>'Descender','FontBBox'=>'FontBBox','ItalicAngle'=>'ItalicAngle'); $fdopt = array( 'Flags'=>$flags ,'FontName'=>$adobeFontName ,'StemV'=>100 // don't know what the value for this should be! ); foreach($list as $k=>$v){ if (isset($this->fonts[$fontName][$v])){ $fdopt[$k]=$this->fonts[$fontName][$v]; } } if ($fbtype=='pfb'){ $fdopt['FontFile']=$pfbid; } else if ($fbtype=='ttf'){ $fdopt['FontFile2']=$pfbid; } $this->o_fontDescriptor($fontDescriptorId,'new',$fdopt); // embed the font program $this->o_contents($this->numObj,'new'); $this->objects[$pfbid]['c'].=$data; // determine the cruicial lengths within this file if ($fbtype=='pfb'){ $l1 = strpos($data,'eexec')+6; $l2 = strpos($data,'00000000')-$l1; $l3 = strlen($data)-$l2-$l1; $this->o_contents($this->numObj,'add',array('Length1'=>$l1,'Length2'=>$l2,'Length3'=>$l3)); } else if ($fbtype=='ttf'){ $l1 = strlen($data); $this->o_contents($this->numObj,'add',array('Length1'=>$l1)); } // tell the font object about all this new stuff $tmp = array('BaseFont'=>$adobeFontName,'Widths'=>$widthid ,'FirstChar'=>$firstChar,'LastChar'=>$lastChar ,'FontDescriptor'=>$fontDescriptorId); if ($fbtype=='ttf'){ $tmp['SubType']='TrueType'; } $this->addMessage('adding extra info to font.('.$fontObj.')'); foreach($tmp as $fk=>$fv){ $this->addMessage($fk." : ".$fv); } $this->o_font($fontObj,'add',$tmp); } else { $this->addMessage('selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts'); } // also set the differences here, note that this means that these will take effect only the //first time that a font is selected, else they are ignored if (isset($options['differences'])){ $this->fonts[$fontName]['differences']=$options['differences']; } } } if ($set && isset($this->fonts[$fontName])){ // so if for some reason the font was not set in the last one then it will not be selected $this->currentBaseFont=$fontName; // the next line means that if a new font is selected, then the current text state will be // applied to it as well. $this->setCurrentFont(); } return $this->currentFontNum; } /** * sets up the current font, based on the font families, and the current text state * note that this system is quite flexible, a font can be completely different to a * font, and even will have to be defined within the family to have meaning * This function is to be called whenever the currentTextState is changed, it will update * the currentFont setting to whatever the appropriatte family one is. * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont * This function will change the currentFont to whatever it should be, but will not change the * currentBaseFont. * * @access private */ function setCurrentFont(){ if (strlen($this->currentBaseFont)==0){ // then assume an initial font $this->selectFont('./fonts/Helvetica.afm'); } $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1); if (strlen($this->currentTextState) && isset($this->fontFamilies[$cf]) && isset($this->fontFamilies[$cf][$this->currentTextState])){ // then we are in some state or another // and this font has a family, and the current setting exists within it // select the font, then return it $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState]; $this->selectFont($nf,'',0); $this->currentFont = $nf; $this->currentFontNum = $this->fonts[$nf]['fontNum']; } else { // the this font must not have the right family member for the current state // simply assume the base font $this->currentFont = $this->currentBaseFont; $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum']; } } /** * function for the user to find out what the ID is of the first page that was created during * startup - useful if they wish to add something to it later. */ function getFirstPageId(){ return $this->firstPageId; } /** * add content to the currently active object * * @access private */ function addContent($content){ $this->objects[$this->currentContents]['c'].=$content; } /** * sets the colour for fill operations */ function setColor($r,$g,$b,$force=0){ if ($r>=0 && ($force || $r!=$this->currentColour['r'] || $g!=$this->currentColour['g'] || $b!=$this->currentColour['b'])){ $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' rg'; $this->currentColour=array('r'=>$r,'g'=>$g,'b'=>$b); } } /** * sets the colour for stroke operations */ function setStrokeColor($r,$g,$b,$force=0){ if ($r>=0 && ($force || $r!=$this->currentStrokeColour['r'] || $g!=$this->currentStrokeColour['g'] || $b!=$this->currentStrokeColour['b'])){ $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$r).' '.sprintf('%.3f',$g).' '.sprintf('%.3f',$b).' RG'; $this->currentStrokeColour=array('r'=>$r,'g'=>$g,'b'=>$b); } } /** * draw a line from one set of coordinates to another */ function line($x1,$y1,$x2,$y2){ $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' m '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' l S'; } /** * draw a bezier curve based on 4 control points */ function curve($x0,$y0,$x1,$y1,$x2,$y2,$x3,$y3){ // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points // as the control points for the curve. $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' m '.sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1); $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',$x2).' '.sprintf('%.3f',$y2).' '.sprintf('%.3f',$x3).' '.sprintf('%.3f',$y3).' c S'; } /** * draw a part of an ellipse */ function partEllipse($x0,$y0,$astart,$afinish,$r1,$r2=0,$angle=0,$nSeg=8){ $this->ellipse($x0,$y0,$r1,$r2,$angle,$nSeg,$astart,$afinish,0); } /** * draw a filled ellipse */ function filledEllipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360){ return $this->ellipse($x0,$y0,$r1,$r2=0,$angle,$nSeg,$astart,$afinish,1,1); } /** * draw an ellipse * note that the part and filled ellipse are just special cases of this function * * draws an ellipse in the current line style * centered at $x0,$y0, radii $r1,$r2 * if $r2 is not set, then a circle is drawn * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a * pretty crappy shape at 2, as we are approximating with bezier curves. */ function ellipse($x0,$y0,$r1,$r2=0,$angle=0,$nSeg=8,$astart=0,$afinish=360,$close=1,$fill=0){ if ($r1==0){ return; } if ($r2==0){ $r2=$r1; } if ($nSeg<2){ $nSeg=2; } $astart = deg2rad((float)$astart); $afinish = deg2rad((float)$afinish); $totalAngle =$afinish-$astart; $dt = $totalAngle/$nSeg; $dtm = $dt/3; if ($angle != 0){ $a = -1*deg2rad((float)$angle); $tmp = "\n q "; $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' '; $tmp .= sprintf('%.3f',$x0).' '.sprintf('%.3f',$y0).' cm'; $this->objects[$this->currentContents]['c'].= $tmp; $x0=0; $y0=0; } $t1 = $astart; $a0 = $x0+$r1*cos($t1); $b0 = $y0+$r2*sin($t1); $c0 = -$r1*sin($t1); $d0 = $r2*cos($t1); $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$a0).' '.sprintf('%.3f',$b0).' m '; for ($i=1;$i<=$nSeg;$i++){ // draw this bit of the total curve $t1 = $i*$dt+$astart; $a1 = $x0+$r1*cos($t1); $b1 = $y0+$r2*sin($t1); $c1 = -$r1*sin($t1); $d1 = $r2*cos($t1); $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',($a0+$c0*$dtm)).' '.sprintf('%.3f',($b0+$d0*$dtm)); $this->objects[$this->currentContents]['c'].= ' '.sprintf('%.3f',($a1-$c1*$dtm)).' '.sprintf('%.3f',($b1-$d1*$dtm)).' '.sprintf('%.3f',$a1).' '.sprintf('%.3f',$b1).' c'; $a0=$a1; $b0=$b1; $c0=$c1; $d0=$d1; } if ($fill){ $this->objects[$this->currentContents]['c'].=' f'; } else { if ($close){ $this->objects[$this->currentContents]['c'].=' s'; // small 's' signifies closing the path as well } else { $this->objects[$this->currentContents]['c'].=' S'; } } if ($angle !=0){ $this->objects[$this->currentContents]['c'].=' Q'; } } /** * this sets the line drawing style. * width, is the thickness of the line in user units * cap is the type of cap to put on the line, values can be 'butt','round','square' * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the * end of the line. * join can be 'miter', 'round', 'bevel' * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the * on and off dashes. * (2) represents 2 on, 2 off, 2 on , 2 off ... * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts. */ function setLineStyle($width=1,$cap='',$join='',$dash='',$phase=0){ // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day $string = ''; if ($width>0){ $string.= $width.' w'; } $ca = array('butt'=>0,'round'=>1,'square'=>2); if (isset($ca[$cap])){ $string.= ' '.$ca[$cap].' J'; } $ja = array('miter'=>0,'round'=>1,'bevel'=>2); if (isset($ja[$join])){ $string.= ' '.$ja[$join].' j'; } if (is_array($dash)){ $string.= ' ['; foreach ($dash as $len){ $string.=' '.$len; } $string.= ' ] '.$phase.' d'; } $this->currentLineStyle = $string; $this->objects[$this->currentContents]['c'].="\n".$string; } /** * draw a polygon, the syntax for this is similar to the GD polygon command */ function polygon($p,$np,$f=0){ $this->objects[$this->currentContents]['c'].="\n"; $this->objects[$this->currentContents]['c'].=sprintf('%.3f',$p[0]).' '.sprintf('%.3f',$p[1]).' m '; for ($i=2;$i<$np*2;$i=$i+2){ $this->objects[$this->currentContents]['c'].= sprintf('%.3f',$p[$i]).' '.sprintf('%.3f',$p[$i+1]).' l '; } if ($f==1){ $this->objects[$this->currentContents]['c'].=' f'; } else { $this->objects[$this->currentContents]['c'].=' S'; } } /** * a filled rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not * the coordinates of the upper-right corner */ function filledRectangle($x1,$y1,$width,$height){ $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re f'; } /** * draw a rectangle, note that it is the width and height of the rectangle which are the secondary paramaters, not * the coordinates of the upper-right corner */ function rectangle($x1,$y1,$width,$height){ $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$x1).' '.sprintf('%.3f',$y1).' '.sprintf('%.3f',$width).' '.sprintf('%.3f',$height).' re S'; } /** * add a new page to the document * this also makes the new page the current active object */ function newPage($insert=0,$id=0,$pos='after'){ // if there is a state saved, then go up the stack closing them // then on the new page, re-open them with the right setings if ($this->nStateStack){ for ($i=$this->nStateStack;$i>=1;$i--){ $this->restoreState($i); } } $this->numObj++; if ($insert){ // the id from the ezPdf class is the od of the contents of the page, not the page object itself // query that object to find the parent $rid = $this->objects[$id]['onPage']; $opt= array('rid'=>$rid,'pos'=>$pos); $this->o_page($this->numObj,'new',$opt); } else { $this->o_page($this->numObj,'new'); } // if there is a stack saved, then put that onto the page if ($this->nStateStack){ for ($i=1;$i<=$this->nStateStack;$i++){ $this->saveState($i); } } // and if there has been a stroke or fill colour set, then transfer them if ($this->currentColour['r']>=0){ $this->setColor($this->currentColour['r'],$this->currentColour['g'],$this->currentColour['b'],1); } if ($this->currentStrokeColour['r']>=0){ $this->setStrokeColor($this->currentStrokeColour['r'],$this->currentStrokeColour['g'],$this->currentStrokeColour['b'],1); } // if there is a line style set, then put this in too if (strlen($this->currentLineStyle)){ $this->objects[$this->currentContents]['c'].="\n".$this->currentLineStyle; } // the call to the o_page object set currentContents to the present page, so this can be returned as the page id return $this->currentContents; } /** * output the pdf code, streaming it to the browser * the relevant headers are set so that hopefully the browser will recognise it */ function stream($options=''){ // setting the options allows the adjustment of the headers // values at the moment are: // 'Content-Disposition'=>'filename' - sets the filename, though not too sure how well this will // work as in my trial the browser seems to use the filename of the php file with .pdf on the end // 'Accept-Ranges'=>1 or 0 - if this is not set to 1, then this header is not included, off by default // this header seems to have caused some problems despite tha fact that it is supposed to solve // them, so I am leaving it off by default. // 'compress'=> 1 or 0 - apply content stream compression, this is on (1) by default if (!is_array($options)){ $options=array(); } if ( isset($options['compress']) && $options['compress']==0){ $tmp = $this->output(1); } else { $tmp = $this->output(); } header("Content-type: application/pdf"); header("Content-Length: ".strlen(ltrim($tmp))); $fileName = (isset($options['Content-Disposition'])?$options['Content-Disposition']:'file.pdf'); header("Content-Disposition: inline; filename=".$fileName); if (isset($options['Accept-Ranges']) && $options['Accept-Ranges']==1){ header("Accept-Ranges: ".strlen(ltrim($tmp))); } echo ltrim($tmp); } /** * return the height in units of the current font in the given size */ function getFontHeight($size){ if (!$this->numFonts){ $this->selectFont('./fonts/Helvetica'); } // for the current font, and the given size, what is the height of the font in user units $h = $this->fonts[$this->currentFont]['FontBBox'][3]-$this->fonts[$this->currentFont]['FontBBox'][1]; return $size*$h/1000; } /** * return the font decender, this will normally return a negative number * if you add this number to the baseline, you get the level of the bottom of the font * it is in the pdf user units */ function getFontDecender($size){ // note that this will most likely return a negative value if (!$this->numFonts){ $this->selectFont('./fonts/Helvetica'); } $h = $this->fonts[$this->currentFont]['FontBBox'][1]; return $size*$h/1000; } /** * filter the text, this is applied to all text just before being inserted into the pdf document * it escapes the various things that need to be escaped, and so on * * @access private */ function filterText($text){ $text = str_replace('\\','\\\\',$text); $text = str_replace('(','\(',$text); $text = str_replace(')','\)',$text); $text = str_replace('<','<',$text); $text = str_replace('>','>',$text); $text = str_replace(''','\'',$text); $text = str_replace('"','"',$text); $text = str_replace('&','&',$text); return $text; } /** * given a start position and information about how text is to be laid out, calculate where * on the page the text will end * * @access private */ function PRVTgetTextPosition($x,$y,$angle,$size,$wa,$text){ // given this information return an array containing x and y for the end position as elements 0 and 1 $w = $this->getTextWidth($size,$text); // need to adjust for the number of spaces in this text $words = explode(' ',$text); $nspaces=count($words)-1; $w += $wa*$nspaces; $a = deg2rad((float)$angle); return array(cos($a)*$w+$x,-sin($a)*$w+$y); } /** * wrapper function for PRVTcheckTextDirective1 * * @access private */ function PRVTcheckTextDirective(&$text,$i,&$f){ $x=0; $y=0; return $this->PRVTcheckTextDirective1($text,$i,$f,0,$x,$y); } /** * checks if the text stream contains a control directive * if so then makes some changes and returns the number of characters involved in the directive * this has been re-worked to include everything neccesary to fins the current writing point, so that * the location can be sent to the callback function if required * if the directive does not require a font change, then $f should be set to 0 * * @access private */ function PRVTcheckTextDirective1(&$text,$i,&$f,$final,&$x,&$y,$size=0,$angle=0,$wordSpaceAdjust=0){ $directive = 0; $j=$i; if ($text[$j]=='<'){ $j++; switch($text[$j]){ case '/': $j++; if (strlen($text) <= $j){ return $directive; } switch($text[$j]){ case 'b': case 'i': $j++; if ($text[$j]=='>'){ $p = strrpos($this->currentTextState,$text[$j-1]); if ($p !== false){ // then there is one to remove $this->currentTextState = substr($this->currentTextState,0,$p).substr($this->currentTextState,$p+1); } $directive=$j-$i+1; } break; case 'c': // this this might be a callback function $j++; $k = strpos($text,'>',$j); if ($k!==false && $text[$j]==':'){ // then this will be treated as a callback directive $directive = $k-$i+1; $f=0; // split the remainder on colons to get the function name and the paramater $tmp = substr($text,$j+1,$k-$j-1); $b1 = strpos($tmp,':'); if ($b1!==false){ $func = substr($tmp,0,$b1); $parm = substr($tmp,$b1+1); } else { $func=$tmp; $parm=''; } if (!isset($func) || !strlen(trim($func))){ $directive=0; } else { // only call the function if this is the final call if ($final){ // need to assess the text position, calculate the text width to this point // can use getTextWidth to find the text width I think $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,substr($text,0,$i)); $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'end','p'=>$parm,'nCallback'=>$this->nCallback); $x=$tmp[0]; $y=$tmp[1]; $ret = $this->$func($info); if (is_array($ret)){ // then the return from the callback function could set the position, to start with, later will do font colour, and font foreach($ret as $rk=>$rv){ switch($rk){ case 'x': case 'y': $$rk=$rv; break; } } } // also remove from to the stack // for simplicity, just take from the end, fix this another day $this->nCallback--; if ($this->nCallback<0){ $this->nCallBack=0; } } } } break; } break; case 'b': case 'i': $j++; if ($text[$j]=='>'){ $this->currentTextState.=$text[$j-1]; $directive=$j-$i+1; } break; case 'C': $noClose=1; case 'c': // this this might be a callback function $j++; $k = strpos($text,'>',$j); if ($k!==false && $text[$j]==':'){ // then this will be treated as a callback directive $directive = $k-$i+1; $f=0; // split the remainder on colons to get the function name and the paramater // $bits = explode(':',substr($text,$j+1,$k-$j-1)); $tmp = substr($text,$j+1,$k-$j-1); $b1 = strpos($tmp,':'); if ($b1!==false){ $func = substr($tmp,0,$b1); $parm = substr($tmp,$b1+1); } else { $func=$tmp; $parm=''; } if (!isset($func) || !strlen(trim($func))){ $directive=0; } else { // only call the function if this is the final call, ie, the one actually doing printing, not measurement if ($final){ // need to assess the text position, calculate the text width to this point // can use getTextWidth to find the text width I think // also add the text height and decender $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,substr($text,0,$i)); $info = array('x'=>$tmp[0],'y'=>$tmp[1],'angle'=>$angle,'status'=>'start','p'=>$parm,'f'=>$func,'height'=>$this->getFontHeight($size),'decender'=>$this->getFontDecender($size)); $x=$tmp[0]; $y=$tmp[1]; if (!isset($noClose) || !$noClose){ // only add to the stack if this is a small 'c', therefore is a start-stop pair $this->nCallback++; $info['nCallback']=$this->nCallback; $this->callback[$this->nCallback]=$info; } $ret = $this->$func($info); if (is_array($ret)){ // then the return from the callback function could set the position, to start with, later will do font colour, and font foreach($ret as $rk=>$rv){ switch($rk){ case 'x': case 'y': $$rk=$rv; break; } } } } } } break; } } return $directive; } /** * add text to the document, at a specified location, size and angle on the page */ function addText($x,$y,$size,$text,$angle=0,$wordSpaceAdjust=0){ if (!$this->numFonts){$this->selectFont('./fonts/Helvetica');} // if there are any open callbacks, then they should be called, to show the start of the line if ($this->nCallback>0){ for ($i=$this->nCallback;$i>0;$i--){ // call each function $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']); $func = $this->callback[$i]['f']; $this->$func($info); } } if ($angle==0){ $this->objects[$this->currentContents]['c'].="\n".'BT '.sprintf('%.3f',$x).' '.sprintf('%.3f',$y).' Td'; } else { $a = deg2rad((float)$angle); $tmp = "\n".'BT '; $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' '; $tmp .= sprintf('%.3f',$x).' '.sprintf('%.3f',$y).' Tm'; $this->objects[$this->currentContents]['c'] .= $tmp; } if ($wordSpaceAdjust!=0 || $wordSpaceAdjust != $this->wordSpaceAdjust){ $this->wordSpaceAdjust=$wordSpaceAdjust; $this->objects[$this->currentContents]['c'].=' '.sprintf('%.3f',$wordSpaceAdjust).' Tw'; } $len=strlen($text); $start=0; for ($i=0;$i<$len;$i++){ $f=1; $directive = $this->PRVTcheckTextDirective($text,$i,$f); if ($directive){ // then we should write what we need to if ($i>$start){ $part = substr($text,$start,$i-$start); $this->objects[$this->currentContents]['c'].=' /F'.$this->currentFontNum.' '.sprintf('%.1f',$size).' Tf '; $this->objects[$this->currentContents]['c'].=' ('.$this->filterText($part).') Tj'; } if ($f){ // then there was nothing drastic done here, restore the contents $this->setCurrentFont(); } else { $this->objects[$this->currentContents]['c'] .= ' ET'; $f=1; $xp=$x; $yp=$y; $directive = $this->PRVTcheckTextDirective1($text,$i,$f,1,$xp,$yp,$size,$angle,$wordSpaceAdjust); // restart the text object if ($angle==0){ $this->objects[$this->currentContents]['c'].="\n".'BT '.sprintf('%.3f',$xp).' '.sprintf('%.3f',$yp).' Td'; } else { $a = deg2rad((float)$angle); $tmp = "\n".'BT '; $tmp .= sprintf('%.3f',cos($a)).' '.sprintf('%.3f',(-1.0*sin($a))).' '.sprintf('%.3f',sin($a)).' '.sprintf('%.3f',cos($a)).' '; $tmp .= sprintf('%.3f',$xp).' '.sprintf('%.3f',$yp).' Tm'; $this->objects[$this->currentContents]['c'] .= $tmp; } if ($wordSpaceAdjust!=0 || $wordSpaceAdjust != $this->wordSpaceAdjust){ $this->wordSpaceAdjust=$wordSpaceAdjust; $this->objects[$this->currentContents]['c'].=' '.sprintf('%.3f',$wordSpaceAdjust).' Tw'; } } // and move the writing point to the next piece of text $i=$i+$directive-1; $start=$i+1; } } if ($start<$len){ $part = substr($text,$start); $this->objects[$this->currentContents]['c'].=' /F'.$this->currentFontNum.' '.sprintf('%.1f',$size).' Tf '; $this->objects[$this->currentContents]['c'].=' ('.$this->filterText($part).') Tj'; } $this->objects[$this->currentContents]['c'].=' ET'; // if there are any open callbacks, then they should be called, to show the end of the line if ($this->nCallback>0){ for ($i=$this->nCallback;$i>0;$i--){ // call each function $tmp = $this->PRVTgetTextPosition($x,$y,$angle,$size,$wordSpaceAdjust,$text); $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']); $func = $this->callback[$i]['f']; $this->$func($info); } } } /** * calculate how wide a given text string will be on a page, at a given size. * this can be called externally, but is alse used by the other class functions */ function getTextWidth($size,$text){ // this function should not change any of the settings, though it will need to // track any directives which change during calculation, so copy them at the start // and put them back at the end. $store_currentTextState = $this->currentTextState; if (!$this->numFonts){ $this->selectFont('./fonts/Helvetica'); } // converts a number or a float to a string so it can get the width $text = "$text"; // hmm, this is where it all starts to get tricky - use the font information to // calculate the width of each character, add them up and convert to user units $w=0; $len=strlen($text); $cf = $this->currentFont; for ($i=0;$i<$len;$i++){ $f=1; $directive = $this->PRVTcheckTextDirective($text,$i,$f); if ($directive){ if ($f){ $this->setCurrentFont(); $cf = $this->currentFont; } $i=$i+$directive-1; } else { $char=ord($text[$i]); if (isset($this->fonts[$cf]['differences'][$char])){ // then this character is being replaced by another $name = $this->fonts[$cf]['differences'][$char]; if (isset($this->fonts[$cf]['C'][$name]['WX'])){ $w+=$this->fonts[$cf]['C'][$name]['WX']; } } else if (isset($this->fonts[$cf]['C'][$char]['WX'])){ $w+=$this->fonts[$cf]['C'][$char]['WX']; } } } $this->currentTextState = $store_currentTextState; $this->setCurrentFont(); return $w*$size/1000; } /** * do a part of the calculation for sorting out the justification of the text * * @access private */ function PRVTadjustWrapText($text,$actual,$width,&$x,&$adjust,$justification){ switch ($justification){ case 'left': return; break; case 'right': $x+=$width-$actual; break; case 'center': case 'centre': $x+=($width-$actual)/2; break; case 'full': // count the number of words $words = explode(' ',$text); $nspaces=count($words)-1; if ($nspaces>0){ $adjust = ($width-$actual)/$nspaces; } else { $adjust=0; } break; } } /** * add text to the page, but ensure that it fits within a certain width * if it does not fit then put in as much as possible, splitting at word boundaries * and return the remainder. * justification and angle can also be specified for the text */ function addTextWrap($x,$y,$width,$size,$text,$justification='left',$angle=0,$test=0){ // this will display the text, and if it goes beyond the width $width, will backtrack to the // previous space or hyphen, and return the remainder of the text. // $justification can be set to 'left','right','center','centre','full' // need to store the initial text state, as this will change during the width calculation // but will need to be re-set before printing, so that the chars work out right $store_currentTextState = $this->currentTextState; if (!$this->numFonts){$this->selectFont('./fonts/Helvetica');} if ($width<=0){ // error, pretend it printed ok, otherwise risking a loop return ''; } $w=0; $break=0; $breakWidth=0; $len=strlen($text); $cf = $this->currentFont; $tw = $width/$size*1000; for ($i=0;$i<$len;$i++){ $f=1; $directive = $this->PRVTcheckTextDirective($text,$i,$f); if ($directive){ if ($f){ $this->setCurrentFont(); $cf = $this->currentFont; } $i=$i+$directive-1; } else { $cOrd = ord($text[$i]); if (isset($this->fonts[$cf]['differences'][$cOrd])){ // then this character is being replaced by another $cOrd2 = $this->fonts[$cf]['differences'][$cOrd]; } else { $cOrd2 = $cOrd; } if (isset($this->fonts[$cf]['C'][$cOrd2]['WX'])){ $w+=$this->fonts[$cf]['C'][$cOrd2]['WX']; } if ($w>$tw){ // then we need to truncate this line if ($break>0){ // then we have somewhere that we can split :) if ($text[$break]==' '){ $tmp = substr($text,0,$break); } else { $tmp = substr($text,0,$break+1); } $adjust=0; $this->PRVTadjustWrapText($tmp,$breakWidth,$width,$x,$adjust,$justification); // reset the text state $this->currentTextState = $store_currentTextState; $this->setCurrentFont(); if (!$test){ $this->addText($x,$y,$size,$tmp,$angle,$adjust); } return substr($text,$break+1); } else { // just split before the current character $tmp = substr($text,0,$i); $adjust=0; $ctmp=ord($text[$i]); if (isset($this->fonts[$cf]['differences'][$ctmp])){ $ctmp=$this->fonts[$cf]['differences'][$ctmp]; } $tmpw=($w-$this->fonts[$cf]['C'][$ctmp]['WX'])*$size/1000; $this->PRVTadjustWrapText($tmp,$tmpw,$width,$x,$adjust,$justification); // reset the text state $this->currentTextState = $store_currentTextState; $this->setCurrentFont(); if (!$test){ $this->addText($x,$y,$size,$tmp,$angle,$adjust); } return substr($text,$i); } } if ($text[$i]=='-'){ $break=$i; $breakWidth = $w*$size/1000; } if ($text[$i]==' '){ $break=$i; $ctmp=ord($text[$i]); if (isset($this->fonts[$cf]['differences'][$ctmp])){ $ctmp=$this->fonts[$cf]['differences'][$ctmp]; } $breakWidth = ($w-$this->fonts[$cf]['C'][$ctmp]['WX'])*$size/1000; } } } // then there was no need to break this line if ($justification=='full'){ $justification='left'; } $adjust=0; $tmpw=$w*$size/1000; $this->PRVTadjustWrapText($text,$tmpw,$width,$x,$adjust,$justification); // reset the text state $this->currentTextState = $store_currentTextState; $this->setCurrentFont(); if (!$test){ $this->addText($x,$y,$size,$text,$angle,$adjust,$angle); } return ''; } /** * this will be called at a new page to return the state to what it was on the * end of the previous page, before the stack was closed down * This is to get around not being able to have open 'q' across pages * */ function saveState($pageEnd=0){ if ($pageEnd){ // this will be called at a new page to return the state to what it was on the // end of the previous page, before the stack was closed down // This is to get around not being able to have open 'q' across pages $opt = $this->stateStack[$pageEnd]; // ok to use this as stack starts numbering at 1 $this->setColor($opt['col']['r'],$opt['col']['g'],$opt['col']['b'],1); $this->setStrokeColor($opt['str']['r'],$opt['str']['g'],$opt['str']['b'],1); $this->objects[$this->currentContents]['c'].="\n".$opt['lin']; // $this->currentLineStyle = $opt['lin']; } else { $this->nStateStack++; $this->stateStack[$this->nStateStack]=array( 'col'=>$this->currentColour ,'str'=>$this->currentStrokeColour ,'lin'=>$this->currentLineStyle ); } $this->objects[$this->currentContents]['c'].="\nq"; } /** * restore a previously saved state */ function restoreState($pageEnd=0){ if (!$pageEnd){ $n = $this->nStateStack; $this->currentColour = $this->stateStack[$n]['col']; $this->currentStrokeColour = $this->stateStack[$n]['str']; $this->objects[$this->currentContents]['c'].="\n".$this->stateStack[$n]['lin']; $this->currentLineStyle = $this->stateStack[$n]['lin']; unset($this->stateStack[$n]); $this->nStateStack--; } $this->objects[$this->currentContents]['c'].="\nQ"; } /** * make a loose object, the output will go into this object, until it is closed, then will revert to * the current one. * this object will not appear until it is included within a page. * the function will return the object number */ function openObject(){ $this->nStack++; $this->stack[$this->nStack]=array('c'=>$this->currentContents,'p'=>$this->currentPage); // add a new object of the content type, to hold the data flow $this->numObj++; $this->o_contents($this->numObj,'new'); $this->currentContents=$this->numObj; $this->looseObjects[$this->numObj]=1; return $this->numObj; } /** * open an existing object for editing */ function reopenObject($id){ $this->nStack++; $this->stack[$this->nStack]=array('c'=>$this->currentContents,'p'=>$this->currentPage); $this->currentContents=$id; // also if this object is the primary contents for a page, then set the current page to its parent if (isset($this->objects[$id]['onPage'])){ $this->currentPage = $this->objects[$id]['onPage']; } } /** * close an object */ function closeObject(){ // close the object, as long as there was one open in the first place, which will be indicated by // an objectId on the stack. if ($this->nStack>0){ $this->currentContents=$this->stack[$this->nStack]['c']; $this->currentPage=$this->stack[$this->nStack]['p']; $this->nStack--; // easier to probably not worry about removing the old entries, they will be overwritten // if there are new ones. } } /** * stop an object from appearing on pages from this point on */ function stopObject($id){ // if an object has been appearing on pages up to now, then stop it, this page will // be the last one that could contian it. if (isset($this->addLooseObjects[$id])){ $this->addLooseObjects[$id]=''; } } /** * after an object has been created, it wil only show if it has been added, using this function. */ function addObject($id,$options='add'){ // add the specified object to the page if (isset($this->looseObjects[$id]) && $this->currentContents!=$id){ // then it is a valid object, and it is not being added to itself switch($options){ case 'all': // then this object is to be added to this page (done in the next block) and // all future new pages. $this->addLooseObjects[$id]='all'; case 'add': if (isset($this->objects[$this->currentContents]['onPage'])){ // then the destination contents is the primary for the page // (though this object is actually added to that page) $this->o_page($this->objects[$this->currentContents]['onPage'],'content',$id); } break; case 'even': $this->addLooseObjects[$id]='even'; $pageObjectId=$this->objects[$this->currentContents]['onPage']; if ($this->objects[$pageObjectId]['info']['pageNum']%2==0){ $this->addObject($id); // hacky huh :) } break; case 'odd': $this->addLooseObjects[$id]='odd'; $pageObjectId=$this->objects[$this->currentContents]['onPage']; if ($this->objects[$pageObjectId]['info']['pageNum']%2==1){ $this->addObject($id); // hacky huh :) } break; case 'next': $this->addLooseObjects[$id]='all'; break; case 'nexteven': $this->addLooseObjects[$id]='even'; break; case 'nextodd': $this->addLooseObjects[$id]='odd'; break; } } } /** * add content to the documents info object */ function addInfo($label,$value=0){ // this will only work if the label is one of the valid ones. // modify this so that arrays can be passed as well. // if $label is an array then assume that it is key=>value pairs // else assume that they are both scalar, anything else will probably error if (is_array($label)){ foreach ($label as $l=>$v){ $this->o_info($this->infoObject,$l,$v); } } else { $this->o_info($this->infoObject,$label,$value); } } /** * set the viewer preferences of the document, it is up to the browser to obey these. */ function setPreferences($label,$value=0){ // this will only work if the label is one of the valid ones. if (is_array($label)){ foreach ($label as $l=>$v){ $this->o_catalog($this->catalogId,'viewerPreferences',array($l=>$v)); } } else { $this->o_catalog($this->catalogId,'viewerPreferences',array($label=>$value)); } } /** * extract an integer from a position in a byte stream * * @access private */ function PRVT_getBytes(&$data,$pos,$num){ // return the integer represented by $num bytes from $pos within $data $ret=0; for ($i=0;$i<$num;$i++){ $ret=$ret*256; $ret+=ord($data[$pos+$i]); } return $ret; } /** * add a PNG image into the document, from a file * this should work with remote files */ function addPngFromFile($file,$x,$y,$w=0,$h=0){ // read in a png file, interpret it, then add to the system $error=0; $tmp = get_magic_quotes_runtime(); ini_set("magic_quotes_runtime", 0); $fp = @fopen($file,'rb'); if ($fp){ $data=''; while(!feof($fp)){ $data .= fread($fp,1024); } fclose($fp); } else { $error = 1; $errormsg = 'trouble opening file: '.$file; } ini_set("magic_quotes_runtime", $tmp); if (!$error){ $header = chr(137).chr(80).chr(78).chr(71).chr(13).chr(10).chr(26).chr(10); if (substr($data,0,8)!=$header){ $error=1; $errormsg = 'this file does not have a valid header'; } } if (!$error){ // set pointer $p = 8; $len = strlen($data); // cycle through the file, identifying chunks $haveHeader=0; $info=array(); $idata=''; $pdata=''; while ($p<$len){ $chunkLen = $this->PRVT_getBytes($data,$p,4); $chunkType = substr($data,$p+4,4); // echo $chunkType.' - '.$chunkLen.'
'; switch($chunkType){ case 'IHDR': // this is where all the file information comes from $info['width']=$this->PRVT_getBytes($data,$p+8,4); $info['height']=$this->PRVT_getBytes($data,$p+12,4); $info['bitDepth']=ord($data[$p+16]); $info['colorType']=ord($data[$p+17]); $info['compressionMethod']=ord($data[$p+18]); $info['filterMethod']=ord($data[$p+19]); $info['interlaceMethod']=ord($data[$p+20]); //print_r($info); $haveHeader=1; if ($info['compressionMethod']!=0){ $error=1; $errormsg = 'unsupported compression method'; } if ($info['filterMethod']!=0){ $error=1; $errormsg = 'unsupported filter method'; } break; case 'PLTE': $pdata.=substr($data,$p+8,$chunkLen); break; case 'IDAT': $idata.=substr($data,$p+8,$chunkLen); break; case 'tRNS': //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk //print "tRNS found, color type = ".$info['colorType']."
"; $transparency = array(); if ($info['colorType'] == 3) { // indexed color, rbg /* corresponding to entries in the plte chunk Alpha for palette index 0: 1 byte Alpha for palette index 1: 1 byte ...etc... */ // there will be one entry for each palette entry. up until the last non-opaque entry. // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent) $transparency['type']='indexed'; $numPalette = strlen($pdata)/3; $trans=0; for ($i=$chunkLen;$i>=0;$i--){ if (ord($data[$p+8+$i])==0){ $trans=$i; } } $transparency['data'] = $trans; } elseif($info['colorType'] == 0) { // grayscale /* corresponding to entries in the plte chunk Gray: 2 bytes, range 0 .. (2^bitdepth)-1 */ // $transparency['grayscale']=$this->PRVT_getBytes($data,$p+8,2); // g = grayscale $transparency['type']='indexed'; $transparency['data'] = ord($data[$p+8+1]); } elseif($info['colorType'] == 2) { // truecolor /* corresponding to entries in the plte chunk Red: 2 bytes, range 0 .. (2^bitdepth)-1 Green: 2 bytes, range 0 .. (2^bitdepth)-1 Blue: 2 bytes, range 0 .. (2^bitdepth)-1 */ $transparency['r']=$this->PRVT_getBytes($data,$p+8,2); // r from truecolor $transparency['g']=$this->PRVT_getBytes($data,$p+10,2); // g from truecolor $transparency['b']=$this->PRVT_getBytes($data,$p+12,2); // b from truecolor } else { //unsupported transparency type } // KS End new code break; default: break; } $p += $chunkLen+12; } if(!$haveHeader){ $error = 1; $errormsg = 'information header is missing'; } if (isset($info['interlaceMethod']) && $info['interlaceMethod']){ $error = 1; $errormsg = 'There appears to be no support for interlaced images in pdf.'; } } if (!$error && $info['bitDepth'] > 8){ $error = 1; $errormsg = 'only bit depth of 8 or less is supported'; } if (!$error){ if ($info['colorType']!=2 && $info['colorType']!=0 && $info['colorType']!=3){ $error = 1; $errormsg = 'transparancey alpha channel not supported, transparency only supported for palette images.'; } else { switch ($info['colorType']){ case 3: $color = 'DeviceRGB'; $ncolor=1; break; case 2: $color = 'DeviceRGB'; $ncolor=3; break; case 0: $color = 'DeviceGray'; $ncolor=1; break; } } } if ($error){ $this->addMessage('PNG error - ('.$file.') '.$errormsg); return; } if ($w==0){ $w=$h/$info['height']*$info['width']; } if ($h==0){ $h=$w*$info['height']/$info['width']; } //print_r($info); // so this image is ok... add it in. $this->numImages++; $im=$this->numImages; $label='I'.$im; $this->numObj++; // $this->o_image($this->numObj,'new',array('label'=>$label,'data'=>$idata,'iw'=>$w,'ih'=>$h,'type'=>'png','ic'=>$info['width'])); $options = array('label'=>$label,'data'=>$idata,'bitsPerComponent'=>$info['bitDepth'],'pdata'=>$pdata ,'iw'=>$info['width'],'ih'=>$info['height'],'type'=>'png','color'=>$color,'ncolor'=>$ncolor); if (isset($transparency)){ $options['transparency']=$transparency; } $this->o_image($this->numObj,'new',$options); $this->objects[$this->currentContents]['c'].="\nq"; $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$w)." 0 0 ".sprintf('%.3f',$h)." ".sprintf('%.3f',$x)." ".sprintf('%.3f',$y)." cm"; $this->objects[$this->currentContents]['c'].="\n/".$label.' Do'; $this->objects[$this->currentContents]['c'].="\nQ"; } /** * add a JPEG image into the document, from a file */ function addJpegFromFile($img,$x,$y,$w=0,$h=0){ // attempt to add a jpeg image straight from a file, using no GD commands // note that this function is unable to operate on a remote file. if (!file_exists($img)){ return; } $tmp=getimagesize($img); $imageWidth=$tmp[0]; $imageHeight=$tmp[1]; if (isset($tmp['channels'])){ $channels = $tmp['channels']; } else { $channels = 3; } if ($w<=0 && $h<=0){ $w=$imageWidth; } if ($w==0){ $w=$h/$imageHeight*$imageWidth; } if ($h==0){ $h=$w*$imageHeight/$imageWidth; } $fp=fopen($img,'rb'); $tmp = get_magic_quotes_runtime(); ini_set("magic_quotes_runtime", 0); $data = fread($fp,filesize($img)); ini_set("magic_quotes_runtime", $tmp); fclose($fp); $this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight,$channels); } /** * add an image into the document, from a GD object * this function is not all that reliable, and I would probably encourage people to use * the file based functions */ function addImage(&$img,$x,$y,$w=0,$h=0,$quality=75){ // add a new image into the current location, as an external object // add the image at $x,$y, and with width and height as defined by $w & $h // note that this will only work with full colour images and makes them jpg images for display // later versions could present lossless image formats if there is interest. // there seems to be some problem here in that images that have quality set above 75 do not appear // not too sure why this is, but in the meantime I have restricted this to 75. if ($quality>75){ $quality=75; } // if the width or height are set to zero, then set the other one based on keeping the image // height/width ratio the same, if they are both zero, then give up :) $imageWidth=imagesx($img); $imageHeight=imagesy($img); if ($w<=0 && $h<=0){ return; } if ($w==0){ $w=$h/$imageHeight*$imageWidth; } if ($h==0){ $h=$w*$imageHeight/$imageWidth; } // gotta get the data out of the img.. // so I write to a temp file, and then read it back.. soo ugly, my apologies. $tmpDir='/tmp'; $tmpName=tempnam($tmpDir,'img'); imagejpeg($img,$tmpName,$quality); $fp=fopen($tmpName,'rb'); $tmp = get_magic_quotes_runtime(); ini_set("magic_quotes_runtime", 0); $fp = @fopen($tmpName,'rb'); if ($fp){ $data=''; while(!feof($fp)){ $data .= fread($fp,1024); } fclose($fp); } else { $error = 1; $errormsg = 'trouble opening file'; } // $data = fread($fp,filesize($tmpName)); ini_set("magic_quotes_runtime", $tmp); // fclose($fp); unlink($tmpName); $this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight); } /** * common code used by the two JPEG adding functions * * @access private */ function addJpegImage_common(&$data,$x,$y,$w=0,$h=0,$imageWidth,$imageHeight,$channels=3){ // note that this function is not to be called externally // it is just the common code between the GD and the file options $this->numImages++; $im=$this->numImages; $label='I'.$im; $this->numObj++; $this->o_image($this->numObj,'new',array('label'=>$label,'data'=>$data,'iw'=>$imageWidth,'ih'=>$imageHeight,'channels'=>$channels)); $this->objects[$this->currentContents]['c'].="\nq"; $this->objects[$this->currentContents]['c'].="\n".sprintf('%.3f',$w)." 0 0 ".sprintf('%.3f',$h)." ".sprintf('%.3f',$x)." ".sprintf('%.3f',$y)." cm"; $this->objects[$this->currentContents]['c'].="\n/".$label.' Do'; $this->objects[$this->currentContents]['c'].="\nQ"; } /** * specify where the document should open when it first starts */ function openHere($style,$a=0,$b=0,$c=0){ // this function will open the document at a specified page, in a specified style // the values for style, and the required paramters are: // 'XYZ' left, top, zoom // 'Fit' // 'FitH' top // 'FitV' left // 'FitR' left,bottom,right // 'FitB' // 'FitBH' top // 'FitBV' left $this->numObj++; $this->o_destination($this->numObj,'new',array('page'=>$this->currentPage,'type'=>$style,'p1'=>$a,'p2'=>$b,'p3'=>$c)); $id = $this->catalogId; $this->o_catalog($id,'openHere',$this->numObj); } /** * create a labelled destination within the document */ function addDestination($label,$style,$a=0,$b=0,$c=0){ // associates the given label with the destination, it is done this way so that a destination can be specified after // it has been linked to // styles are the same as the 'openHere' function $this->numObj++; $this->o_destination($this->numObj,'new',array('page'=>$this->currentPage,'type'=>$style,'p1'=>$a,'p2'=>$b,'p3'=>$c)); $id = $this->numObj; // store the label->idf relationship, note that this means that labels can be used only once $this->destinations["$label"]=$id; } /** * define font families, this is used to initialize the font families for the default fonts * and for the user to add new ones for their fonts. The default bahavious can be overridden should * that be desired. */ function setFontFamily($family,$options=''){ if (!is_array($options)){ if ($family=='init'){ // set the known family groups // these font families will be used to enable bold and italic markers to be included // within text streams. html forms will be used... $this->fontFamilies['Helvetica.afm']=array( 'b'=>'Helvetica-Bold.afm' ,'i'=>'Helvetica-Oblique.afm' ,'bi'=>'Helvetica-BoldOblique.afm' ,'ib'=>'Helvetica-BoldOblique.afm' ); $this->fontFamilies['Courier.afm']=array( 'b'=>'Courier-Bold.afm' ,'i'=>'Courier-Oblique.afm' ,'bi'=>'Courier-BoldOblique.afm' ,'ib'=>'Courier-BoldOblique.afm' ); $this->fontFamilies['Times-Roman.afm']=array( 'b'=>'Times-Bold.afm' ,'i'=>'Times-Italic.afm' ,'bi'=>'Times-BoldItalic.afm' ,'ib'=>'Times-BoldItalic.afm' ); } } else { // the user is trying to set a font family // note that this can also be used to set the base ones to something else if (strlen($family)){ $this->fontFamilies[$family] = $options; } } } /** * used to add messages for use in debugging */ function addMessage($message){ $this->messages.=$message."\n"; } /** * a few functions which should allow the document to be treated transactionally. */ function transaction($action){ switch ($action){ case 'start': // store all the data away into the checkpoint variable $data = get_object_vars($this); $this->checkpoint = $data; unset($data); break; case 'commit': if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])){ $tmp = $this->checkpoint['checkpoint']; $this->checkpoint = $tmp; unset($tmp); } else { $this->checkpoint=''; } break; case 'rewind': // do not destroy the current checkpoint, but move us back to the state then, so that we can try again if (is_array($this->checkpoint)){ // can only abort if were inside a checkpoint $tmp = $this->checkpoint; foreach ($tmp as $k=>$v){ if ($k != 'checkpoint'){ $this->$k=$v; } } unset($tmp); } break; case 'abort': if (is_array($this->checkpoint)){ // can only abort if were inside a checkpoint $tmp = $this->checkpoint; foreach ($tmp as $k=>$v){ $this->$k=$v; } unset($tmp); } break; } } } // end of class ?>