Source for file class.pdf.php
Documentation is available at class.pdf.php
* A PHP class to provide the basic functionality to create a pdf document without
* any requirement for additional modules.
* Note that they companion class CezPdf can be used to extend this class and dramatically
* simplify the creation of documents.
* there is no warranty, implied or otherwise with this software.
* This code has been placed in the Public Domain for all to enjoy.
* @link http://www.ros.co.nz/pdf
* the current number of pdf objects in the document
* this array contains all of the pdf objects, ready for final assembly
* the objectId (number within the objects array) of the document catalog
* 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
* a record of the current font
* the number of the current font within the font array
* object number of the current page
* object number of the currently active contents block
* number of fonts within the system
* current colour for fill operations, defaults to inactive value, all three components should be between 0 and 1 inclusive when active
* current colour for stroke operations (lines etc.)
* current style that lines are drawn in
* 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
* number of elements within the state stack
* number of page objects within the document
* object Id storage stack
* number of elements within the object Id storage stack
* an array which contains information about the objects which are not firmly attached to pages
* these have been added with the addObject function
* array contains infomation about how the loose objects are to be added to the document
* the objectId of the information object for the document
* this contains authorship, title etc.
* number of images being tracked within the document
* an array containing options about the document
* it defaults to turning on the compression of the objects
* the objectId of the first page of the document
* used to track the last used value of the inter-word spacing, this is so that it is known
* when the spacing is changed.
* the object Id of the procset object
* 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.
* track if the current font is bolded or italicised
* messages are stored here during processing, these can be selected afterwards to give some useful debug information
* the ancryption array for the document encryption is stored here
* the object Id of the encryption information
* the file identifier, used to uniquely identify a pdf document
* a flag to say if a document is to be encrypted or not
* the ancryption key for the encryption of all the document content (structure is not encrypted)
* array which forms a stack to keep track of nested callback functions
* the number of callback functions in the callback array
* 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
* 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.
* 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->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
*destination object, used to specify the location for the user to jump to, presently on opening
$this->objects[$id]= array('t'=> 'destination','info'=> array());
switch ($options['type']){
$tmp = ' '. $options['p3']. $tmp;
$tmp = ' '. $options['p1']. ' '. $options['p2']. $tmp;
$tmp = $options['type']. $tmp;
$this->objects[$id]['info']['string']= $tmp;
$this->objects[$id]['info']['page']= $options['page'];
$res= "\n". $id. " 0 obj\n". '['. $tmp['page']. ' 0 R /'. $tmp['string']. "]\nendobj\n";
* set the viewer preferences
$this->objects[$id]= array('t'=> 'viewerPreferences','info'=> array());
foreach($options as $k=> $v){
case 'NonFullScreenPageMode':
$res= "\n". $id. " 0 obj\n". '<< ';
foreach($o['info'] as $k=> $v){
* define the document catalog, the overall controller for the document
$this->objects[$id]= array('t'=> 'catalog','info'=> array());
$o['info'][$action]= $options;
case 'viewerPreferences':
if (!isset ($o['info']['viewerPreferences'])){
$o['info']['viewerPreferences']= $this->numObj;
$vp = $o['info']['viewerPreferences'];
$res= "\n". $id. " 0 obj\n". '<< /Type /Catalog';
foreach($o['info'] as $k=> $v){
$res.= "\n". '/Outlines '. $v. ' 0 R';
$res.= "\n". '/Pages '. $v. ' 0 R';
case 'viewerPreferences':
$res.= "\n". '/ViewerPreferences '. $o['info']['viewerPreferences']. ' 0 R';
$res.= "\n". '/OpenAction '. $o['info']['openHere']. ' 0 R';
* object which is a parent to the pages in the document
function o_pages($id,$action,$options= ''){
$this->objects[$id]= array('t'=> 'pages','info'=> array());
// then it will just be the id of the new page
$o['info']['pages'][]= $options;
// 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'])){
if (isset ($o['info']['pages'][$i]) && $o['info']['pages'][$i]== $options['rid']){
switch ($options['pos']){
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'];
$o['info']['procset']= $options;
$o['info']['mediaBox']= $options; // which should be an array of 4 numbers
$o['info']['fonts'][]= array('objNum'=> $options['objNum'],'fontNum'=> $options['fontNum']);
$o['info']['xObjects'][]= array('objNum'=> $options['objNum'],'label'=> $options['label']);
if (count($o['info']['pages'])){
$res= "\n". $id. " 0 obj\n<< /Type /Pages\n/Kids [";
foreach($o['info']['pages'] as $k=> $v){
$res.= "]\n/Count ". count($this->objects[$id]['info']['pages']);
if ((isset ($o['info']['fonts']) && count($o['info']['fonts'])) || isset ($o['info']['procset'])){
if (isset ($o['info']['procset'])){
$res.= "\n/ProcSet ". $o['info']['procset']. " 0 R";
if (isset ($o['info']['fonts']) && count($o['info']['fonts'])){
foreach($o['info']['fonts'] as $finfo){
$res.= "\n/F". $finfo['fontNum']. " ". $finfo['objNum']. " 0 R";
if (isset ($o['info']['xObjects']) && count($o['info']['xObjects'])){
foreach($o['info']['xObjects'] as $finfo){
$res.= "\n/". $finfo['label']. " ". $finfo['objNum']. " 0 R";
if (isset ($o['info']['mediaBox'])){
$tmp= $o['info']['mediaBox'];
$res= "\n". $id. " 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj";
* define the outlines in the doc, empty for now
$this->objects[$id]= array('t'=> 'outlines','info'=> array('outlines'=> array()));
$o['info']['outlines'][]= $options;
if (count($o['info']['outlines'])){
$res= "\n". $id. " 0 obj\n<< /Type /Outlines /Kids [";
foreach($o['info']['outlines'] as $k=> $v){
$res.= "] /Count ". count($o['info']['outlines']). " >>\nendobj";
$res= "\n". $id. " 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj";
* an object to hold the font description
function o_font($id,$action,$options= ''){
$this->objects[$id]= array('t'=> 'font','info'=> array('name'=> $options['name'],'SubType'=> 'Type1'));
$this->objects[$id]['info']['fontNum']= $fontNum;
// deal with the encoding and the differences
if (isset ($options['differences'])){
// then we'll need an encoding dictionary
} else if (isset ($options['encoding'])){
// we can specify encoding here
switch($options['encoding']){
case 'MacExpertEncoding':
$this->objects[$id]['info']['encoding']= $options['encoding'];
$this->objects[$id]['info']['encoding']= 'WinAnsiEncoding';
$this->objects[$id]['info']['encoding']= 'WinAnsiEncoding';
// also tell the pages node about the new font
foreach ($options as $k=> $v){
$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";
* a font descriptor, needed for including additional fonts
$this->objects[$id]= array('t'=> 'fontDescriptor','info'=> $options);
$res= "\n". $id. " 0 obj\n<< /Type /FontDescriptor\n";
foreach ($o['info'] as $label => $value){
$res.= '/'. $label. ' '. $value. "\n";
$res.= '/'. $label. ' '. $value. " 0 R\n";
$res.= '/'. $label. ' ['. $value[0]. ' '. $value[1]. ' '. $value[2]. ' '. $value[3]. "]\n";
$res.= '/'. $label. ' /'. $value. "\n";
// the options array should contain 'differences' and maybe 'encoding'
$this->objects[$id]= array('t'=> 'fontEncoding','info'=> $options);
$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[";
foreach($o['info']['differences'] as $num=> $label){
// we cannot make use of consecutive numbering
$res.= "\n". $num. " /". $label;
* the document procset, solves some problems with printing to old PS printers
$this->objects[$id]= array('t'=> 'procset','info'=> array('PDF'=> 1,'Text'=> 1));
// 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
$res= "\n". $id. " 0 obj\n[";
foreach ($o['info'] as $label=> $val){
* define the document information
function o_info($id,$action,$options= ''){
$this->objects[$id]= array('t'=> 'info','info'=> array('Creator'=> 'R and OS php pdf writer, http://www.ros.co.nz','CreationDate'=> $date));
$o['info'][$action]= $options;
$res= "\n". $id. " 0 obj\n<<\n";
foreach ($o['info'] as $k=> $v){
$res.= $this->filterText($this->ARC4($v));
$res.= $this->filterText($v);
* an action object, used to link to URLS initially
function o_action($id,$action,$options= ''){
$this->objects[$id]= array('t'=> 'action','info'=> $options,'type'=> $options['type']);
// then assume a URI action
$this->objects[$id]= array('t'=> 'action','info'=> $options,'type'=> 'URI');
$res= "\n". $id. " 0 obj\n<< /Type /Action";
// 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";
$res.= "\n/S /URI\n/URI (";
$res.= $this->filterText($this->ARC4($o['info']));
$res.= $this->filterText($o['info']);
* an annotation object, this will add an annotation to the current page.
* initially will support just link annotations
// add the annotation to the current page
$this->o_page($pageId,'annot',$id);
// and add the action object which is going to be required
switch($options['type']){
$this->objects[$id]= array('t'=> 'annotation','info'=> $options);
// this is to a named internal link
$label = $options['label'];
$this->objects[$id]= array('t'=> 'annotation','info'=> $options);
$this->o_action($this->numObj,'new',array('type'=> 'ilink','label'=> $label));
$res= "\n". $id. " 0 obj\n<< /Type /Annot";
switch($o['info']['type']){
$res.= "\n/Subtype /Link";
$res.= "\n/A ". $o['info']['actionId']. " 0 R";
$res.= "\n/Border [0 0 0]";
foreach($o['info']['rect'] as $v){
* a page object, it also creates a contents object to hold its contents
function o_page($id,$action,$options= ''){
// then this must be a page insertion, array shoudl contain 'rid','pos'=[before|after]
//make a contents object to go with this page
$this->objects[$id]['info']['contents']= array();
$match = ($this->numPages% 2 ? 'odd' : 'even');
if ($target== 'all' || $match== $target){
$this->objects[$id]['info']['contents'][]= $oId;
$o['info']['contents'][]= $options;
// 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;
$res= "\n". $id. " 0 obj\n<< /Type /Page";
$res.= "\n/Parent ". $o['info']['parent']. " 0 R";
if (isset ($o['info']['annot'])){
foreach($o['info']['annot'] as $aId){
$count = count($o['info']['contents']);
$res.= "\n/Contents ". $o['info']['contents'][0]. " 0 R";
foreach ($o['info']['contents'] as $cId){
* the contents objects hold all of the content which appears on pages
$this->objects[$id]= array('t'=> 'contents','c'=> '','info'=> array());
// 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
// add more options to the decleration
foreach ($options as $k=> $v){
$res= "\n". $id. " 0 obj\n";
if (isset ($this->objects[$id]['raw'])){
// then implement ZLIB based compression on this content stream
$res.= " /Filter /FlateDecode";
$tmp = $this->ARC4($tmp);
foreach($o['info'] as $k=> $v){
$res.= "\n/Length ". strlen($tmp). " >>\nstream\n". $tmp. "\nendstream";
* an image object, will be an XObject in the document, includes description and data
function o_image($id,$action,$options= ''){
$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'])){
switch($options['channels']){
$this->objects[$id]['info']['ColorSpace']= '/DeviceGray';
$this->objects[$id]['info']['ColorSpace']= '/DeviceRGB';
$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->objects[$id]['info']['ColorSpace'] = $tmp;
if (isset ($options['transparency'])){
switch($options['transparency']['type']){
$tmp= ' [ '. $options['transparency']['data']. ' '. $options['transparency']['data']. '] ';
$this->objects[$id]['info']['Mask'] = $tmp;
$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.
$res= "\n". $id. " 0 obj\n<<";
foreach($o['info'] as $k=> $v){
$tmp = $this->ARC4($tmp);
$res.= "\n/Length ". strlen($tmp). " >>\nstream\n". $tmp. "\nendstream\nendobj\n";
$this->objects[$id]= array('t'=> 'encryption','info'=> $options);
// figure out the additional paramaters required
$len = strlen($options['owner']);
$owner = substr($options['owner'],0,32);
$owner = $options['owner']. substr($pad,0,32- $len);
$owner = $options['owner'];
$len = strlen($options['user']);
$user = substr($options['user'],0,32);
$user = $options['user']. substr($pad,0,32- $len);
$user = $options['user'];
$ovalue= $this->ARC4($user);
$this->objects[$id]['info']['O']= $ovalue;
// now make the u value, phew.
$uvalue= $this->ARC4($pad);
$this->objects[$id]['info']['U']= $uvalue;
// initialize the arc4 array
$res= "\n". $id. " 0 obj\n<<";
$res.= "\n/Filter /Standard";
$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']);
* A series of function to implement ARC4 encoding in PHP
* calculate the 16 byte version of the 128 bit md5 digest of the string
for ($i= 0;$i<= 30;$i= $i+ 2){
* initialize the encryption for processing a particular object
* initialize the ARC4 encryption
// setup the control array
$j = ($j + ord($t) + ord($k[$i]))% 256;
* ARC4 encrypt a text string
$out.= chr(ord($text[$i]) ^ $k);
* 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){
$info = array('type'=> 'link','url'=> $url,'rect'=> array($x0,$y0,$x1,$y1));
* add a link in the document to an internal destination (ie. within the document)
$info = array('type'=> 'ilink','label'=> $label,'rect'=> array($x0,$y0,$x1,$y1));
* 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
if ($v && isset ($options[$k])){
} else if (isset ($options[$v])){
// implement encryption on the document
// then the block does not exist already, add it.
$this->o_encryption($this->numObj,'new',array('user'=> $userPass,'owner'=> $ownerPass,'p'=> $p));
* should be used for internal checks, not implemented as yet
* return the pdf stream as a string returned from the function
$content= "%PDF-1.3\n%âãÏÓ\n";
// $content="%PDF-1.3\n";
$cont= $this->$tmp($k,'out');
$content.= "\nxref\n0 ". (count($xref)+ 1). "\n0000000000 65535 f \n";
$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
$content .= " >>\nstartxref\n". $pos. "\n%%EOF\n";
* 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
function newDocument($pageSize= array(0,0,612,792)){
// need to store the first page id as there is no way to get it to the user during
* 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
function openFont($font){
// assume that $font contains both the path and perhaps the extension to the file, split them
if (substr($name,- 4)== '.afm'){
$this->addMessage('openFont: '. $font. ' - '. $name);
$this->addMessage('openFont: php file exists '. $dir. 'php_'. $name. '.afm');
$tmp = file($dir. 'php_'. $name. '.afm');
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]);
// then rebuild the php_<font>.afm file from the <font>.afm file
$this->addMessage('openFont: build php file from '. $dir. $name. '.afm');
$file = file($dir. $name. '.afm');
foreach ($file as $rowA){
// then there must be some keyword
case 'UnderlinePosition':
case 'UnderlineThickness':
//C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ;
$dtmp[$bits2[0]]= array();
for ($i= 1;$i< count($bits2);$i++ ){
$dtmp[$bits2[0]][]= $bits2[$i];
} else if (count($bits2)== 2){
$dtmp[$bits2[0]]= $bits2[1];
$data['C'][$dtmp['C']]= $dtmp;
$data['C'][$dtmp['N']]= $dtmp;
$data['C'][$dtmp['N']]= $dtmp;
//KPX Adieresis yacute -40
$data['KPX'][$bits[1]][$bits[2]]= $bits[3];
$this->fonts[$font]= $data;
$fp = fopen($dir. 'php_'. $name. '.afm','w');
} 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])){
$this->openFont($fontName);
if (isset ($this->fonts[$fontName])){
// $dir=substr($fontName,0,$pos+1);
$name= substr($fontName,$pos+ 1);
if (substr($name,- 4)== '.afm'){
$options= array('name'=> $name);
// 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;
// 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
$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);
$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.
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++ ){
// also need to adjust the widths for the differences array
if (isset ($options['differences'])){
foreach($options['differences'] as $charNum=> $charName){
for($i= $lastChar+ 1;$i<= $charNum;$i++ ){
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);
foreach($widths as $width){
// 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');
// create the font descriptor
$fontDescriptorId = $this->numObj;
// determine flags (more than a little flakey, hopefully will not matter much)
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');
,'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];
$fdopt['FontFile']= $pfbid;
} else if ($fbtype== 'ttf'){
$fdopt['FontFile2']= $pfbid;
// embed the font program
$this->objects[$pfbid]['c'].= $data;
// determine the cruicial lengths within this file
$l1 = strpos($data,'eexec')+ 6;
$l2 = strpos($data,'00000000')- $l1;
$this->o_contents($this->numObj,'add',array('Length1'=> $l1,'Length2'=> $l2,'Length3'=> $l3));
} else if ($fbtype== 'ttf'){
// tell the font object about all this new stuff
$tmp = array('BaseFont'=> $adobeFontName,'Widths'=> $widthid
,'FirstChar'=> $firstChar,'LastChar'=> $lastChar
,'FontDescriptor'=> $fontDescriptorId);
$tmp['SubType']= 'TrueType';
$this->addMessage('adding extra info to font.('. $fontObj. ')');
foreach($tmp as $fk=> $fv){
$this->o_font($fontObj,'add',$tmp);
$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
// the next line means that if a new font is selected, then the current text state will be
// applied to it as well.
* sets up the current font, based on the font families, and the current text state
* note that this system is quite flexible, a <<b>><<i>> font can be completely different to a
* <<i>><<b>> font, and even <<b>><<b>> 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
function setCurrentFont(){
// then assume an initial font
// 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
// the this font must not have the right family member for the current state
// simply assume the base font
* 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.
* add content to the currently active object
function addContent($content){
* sets the colour for fill operations
* sets the colour for stroke operations
* draw a line from one set of coordinates to another
function line($x1,$y1,$x2,$y2){
* 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.
* 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);
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);
* 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){
$afinish = deg2rad((float) $afinish);
$totalAngle = $afinish- $astart;
for ($i= 1;$i<= $nSeg;$i++ ){
// draw this bit of the total curve
* 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
* 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
* (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
$ca = array('butt'=> 0,'round'=> 1,'square'=> 2);
$string.= ' '. $ca[$cap]. ' J';
$ja = array('miter'=> 0,'round'=> 1,'bevel'=> 2);
$string.= ' '. $ja[$join]. ' j';
$string.= ' ] '. $phase. ' d';
* draw a polygon, the syntax for this is similar to the GD polygon command
for ($i= 2;$i< $np* 2;$i= $i+ 2){
* 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
* 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
* 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
// 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);
// if there is a stack saved, then put that onto the page
// and if there has been a stroke or fill colour set, then transfer them
// if there is a line style set, then put this in too
// the call to the o_page object set currentContents to the present page, so this can be returned as the page id
* output the pdf code, streaming it to the browser
* the relevant headers are set so that hopefully the browser will recognise it
// 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 ( isset ($options['compress']) && $options['compress']== 0){
header("Content-type: application/pdf");
$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){
* return the height in units of the current font in the given size
// for the current font, and the given size, what is the height of the font in user units
* 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
// note that this will most likely return a negative value
* 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
function filterText($text){
* given a start position and information about how text is to be laid out, calculate where
* on the page the text will end
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
// need to adjust for the number of spaces in this text
$nspaces= count($words)- 1;
return array(cos($a)* $w+ $x,- sin($a)* $w+ $y);
* wrapper function for PRVTcheckTextDirective1
function PRVTcheckTextDirective(&$text,$i,&$f){
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
function PRVTcheckTextDirective1(&$text,$i,&$f,$final,&$x,&$y,$size= 0,$angle= 0,$wordSpaceAdjust= 0){
// then there is one to remove
// this this might be a callback function
if ($k!== false && $text[$j]== ':'){
// then this will be treated as a callback directive
// split the remainder on colons to get the function name and the paramater
$tmp = substr($text,$j+ 1,$k- $j- 1);
// only call the function if this is the final call
// 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);
$ret = $this->$func($info);
// 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){
// also remove from to the stack
// for simplicity, just take from the end, fix this another day
// this this might be a callback function
if ($k!== false && $text[$j]== ':'){
// then this will be treated as a callback directive
// 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);
// only call the function if this is the final call, ie, the one actually doing printing, not measurement
// 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));
if (!isset ($noClose) || !$noClose){
// only add to the stack if this is a small 'c', therefore is a start-stop pair
$ret = $this->$func($info);
// 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){
* 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 there are any open callbacks, then they should be called, to show the start of the line
$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']);
$directive = $this->PRVTcheckTextDirective($text,$i,$f);
// then we should write what we need to
$part = substr($text,$start,$i- $start);
// then there was nothing drastic done here, restore the contents
$directive = $this->PRVTcheckTextDirective1($text,$i,$f,1,$xp,$yp,$size,$angle,$wordSpaceAdjust);
// restart the text object
// and move the writing point to the next piece of text
// if there are any open callbacks, then they should be called, to show the end of the line
$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']);
* 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
// 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.
// converts a number or a float to a string so it can get the width
// 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
$directive = $this->PRVTcheckTextDirective($text,$i,$f);
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'];
* do a part of the calculation for sorting out the justification of the text
function PRVTadjustWrapText($text,$actual,$width,&$x,&$adjust,$justification){
// count the number of words
$nspaces= count($words)- 1;
$adjust = ($width- $actual)/ $nspaces;
* 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
// error, pretend it printed ok, otherwise risking a loop
$directive = $this->PRVTcheckTextDirective($text,$i,$f);
if (isset ($this->fonts[$cf]['differences'][$cOrd])){
// then this character is being replaced by another
$cOrd2 = $this->fonts[$cf]['differences'][$cOrd];
if (isset ($this->fonts[$cf]['C'][$cOrd2]['WX'])){
$w+= $this->fonts[$cf]['C'][$cOrd2]['WX'];
// then we need to truncate this line
// then we have somewhere that we can split :)
$tmp = substr($text,0,$break);
$tmp = substr($text,0,$break+ 1);
$this->PRVTadjustWrapText($tmp,$breakWidth,$width,$x,$adjust,$justification);
$this->addText($x,$y,$size,$tmp,$angle,$adjust);
return substr($text,$break+ 1);
// just split before the current character
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);
$this->addText($x,$y,$size,$tmp,$angle,$adjust);
$breakWidth = $w* $size/ 1000;
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'){
$this->PRVTadjustWrapText($text,$tmpw,$width,$x,$adjust,$justification);
$this->addText($x,$y,$size,$text,$angle,$adjust,$angle);
* 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
// 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->currentLineStyle = $opt['lin'];
* restore a previously saved state
* make a loose object, the output will go into this object, until it is closed, then will revert to
* this object will not appear until it is included within a page.
* the function will return the object number
// add a new object of the content type, to hold the data flow
* open an existing object for editing
// 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'])){
// close the object, as long as there was one open in the first place, which will be indicated by
// an objectId on the stack.
// 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
// 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.
* after an object has been created, it wil only show if it has been added, using this function.
// add the specified object to the page
// then it is a valid object, and it is not being added to itself
// then this object is to be added to this page (done in the next block) and
// then the destination contents is the primary for the page
// (though this object is actually added to that page)
if ($this->objects[$pageObjectId]['info']['pageNum']% 2== 0){
if ($this->objects[$pageObjectId]['info']['pageNum']% 2== 1){
* add content to the documents info object
// 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
foreach ($label as $l=> $v){
* set the viewer preferences of the document, it is up to the browser to obey these.
// this will only work if the label is one of the valid ones.
foreach ($label as $l=> $v){
* extract an integer from a position in a byte stream
function PRVT_getBytes(&$data,$pos,$num){
// return the integer represented by $num bytes from $pos within $data
$ret+= ord($data[$pos+ $i]);
* add a PNG image into the document, from a file
* this should work with remote files
// read in a png file, interpret it, then add to the system
$fp = @fopen($file,'rb');
$data .= fread($fp,1024);
$errormsg = 'trouble opening file: '. $file;
if (substr($data,0,8)!= $header){
$errormsg = 'this file does not have a valid header';
// cycle through the file, identifying chunks
$chunkLen = $this->PRVT_getBytes($data,$p,4);
$chunkType = substr($data,$p+ 4,4);
// echo $chunkType.' - '.$chunkLen.'<br>';
// 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]);
if ($info['compressionMethod']!= 0){
$errormsg = 'unsupported compression method';
if ($info['filterMethod']!= 0){
$errormsg = 'unsupported filter method';
$pdata.= substr($data,$p+ 8,$chunkLen);
$idata.= substr($data,$p+ 8,$chunkLen);
//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']."<BR>";
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
// 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;
for ($i= $chunkLen;$i>= 0;$i-- ){
if (ord($data[$p+ 8+ $i])== 0){
$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
//unsupported transparency type
$errormsg = 'information header is missing';
if (isset ($info['interlaceMethod']) && $info['interlaceMethod']){
$errormsg = 'There appears to be no support for interlaced images in pdf.';
if (!$error && $info['bitDepth'] > 8){
$errormsg = 'only bit depth of 8 or less is supported';
if ($info['colorType']!= 2 && $info['colorType']!= 0 && $info['colorType']!= 3){
$errormsg = 'transparancey alpha channel not supported, transparency only supported for palette images.';
switch ($info['colorType']){
$this->addMessage('PNG error - ('. $file. ') '. $errormsg);
$w= $h/ $info['height']* $info['width'];
$h= $w* $info['height']/ $info['width'];
// so this image is ok... add it in.
// $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;
* add a JPEG image into the document, from a file
// 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 (isset ($tmp['channels'])){
$channels = $tmp['channels'];
$w= $h/ $imageHeight* $imageWidth;
$h= $w* $imageHeight/ $imageWidth;
$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 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 :)
$w= $h/ $imageHeight* $imageWidth;
$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.
$fp= fopen($tmpName,'rb');
$fp = @fopen($tmpName,'rb');
$data .= fread($fp,1024);
$errormsg = 'trouble opening file';
// $data = fread($fp,filesize($tmpName));
$this->addJpegImage_common($data,$x,$y,$w,$h,$imageWidth,$imageHeight);
* common code used by the two JPEG adding functions
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->o_image($this->numObj,'new',array('label'=> $label,'data'=> $data,'iw'=> $imageWidth,'ih'=> $imageHeight,'channels'=> $channels));
* 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:
// 'FitR' left,bottom,right
* create a labelled destination within the document
// associates the given label with the destination, it is done this way so that a destination can be specified after
// styles are the same as the 'openHere' function
// store the label->idf relationship, note that this means that labels can be used only once
* 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
// 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... <b></b> <i></i>
'b'=> 'Helvetica-Bold.afm'
,'i'=> 'Helvetica-Oblique.afm'
,'bi'=> 'Helvetica-BoldOblique.afm'
,'ib'=> 'Helvetica-BoldOblique.afm'
,'i'=> 'Courier-Oblique.afm'
,'bi'=> 'Courier-BoldOblique.afm'
,'ib'=> 'Courier-BoldOblique.afm'
,'bi'=> 'Times-BoldItalic.afm'
,'ib'=> 'Times-BoldItalic.afm'
// the user is trying to set a font family
// note that this can also be used to set the base ones to something else
* used to add messages for use in debugging
* a few functions which should allow the document to be treated transactionally.
// store all the data away into the checkpoint variable
// do not destroy the current checkpoint, but move us back to the state then, so that we can try again
// can only abort if were inside a checkpoint
foreach ($tmp as $k=> $v){
// can only abort if were inside a checkpoint
foreach ($tmp as $k=> $v){
|