Location: PHPKode > scripts > PDFTemplate > pdftemplate/class.PDFTemplate.php
<?php
/*
 *  This library is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Library General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Library General Public License for more details.
 *
 *  You should have received a copy of the GNU Library General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*

XML Template to PDF Class v1.1
Copyright (C) 2001, Edward Rudd
*/
/*
	Include Manuel Lemos's XML_Parser class.
	I'll eventually rewrite my parser...
	But this gets the new functionality working..
*/
	require_once "xml_parser.php";

class PDFTemplate {
	var $pdf;	//PDF File
	var $parser; //XML Parsed data (xml_parse object)
	var $debug; //Debug??
	
	var $page;  //Current Page definition
	/*
		closed = Has the page been "ended" for checks on whether to PDF endpage or PDF beginpage
		fontlead = the height of one line (1.5 times the height of the current font)
		margin = the surrounding margins (default half inch, 36 units)
		width = the width of the page (default 8.5 inches, 612 units)
		height = the height of a page (default 11 inches, 792 units)
		curX = the current X position for font placement (left is 0)
		curY = the current Y position for font placement (bottom of the page is 0)
		tab = the default tab setting,also used for initial indent (default is half inch, 36 units)
		left = used for block indent, set on PARA tag load for new lines
		align = justification
		ticks = enable ticks
		block[name] = Current repeated block
		block[count] = Current iteration in block
	*/
	var $fontstack; //font stack
	/*
		Font = array of Font names
		Size = array of Font sizes
	*/
	var $subs;  //complex array substitutions (global substitutions)
	/*
		key = array of keys in {VARNAME} format
		val = array of values to substitute keys
	*/
	var $blocksubs; //substitutions for BLOCKS
	/*
		
	*/
	var $filename; //XML Filename

	//Class initializer.  the XML filename and optionally enable debug (set to 1)
	//Also sends PDF content-type header;
	function PDFTemplate($filename,$debug=0) {
		// Initialization
		$this->debug = $debug;
		$this->filename = $filename;
		$this->subs["keys"] = array(); //Variables to Substiture with.
		$this->subs["vals"] = array(); //Variables to Substiture with.
		$this->cmnAssign(&$this->subs["keys"],&$this->subs["vals"],"DEFAULTS.VERSION","PDFTemplate v1.1.1");
		$this->cmnAssign(&$this->subs["keys"],&$this->subs["vals"],"DEFAULTS.DATE",date("F j, Y"));
		$this->cmnAssign(&$this->subs["keys"],&$this->subs["vals"],"DEFAULTS.TIME",date("g:m A"));
		$this->fontstack["Font"] = array();
		$this->fontstack["Size"] = array();
		$this->addfont("Helvetica",12);
		//Setup PDF Docuemnt
		$this->pdf = PDF_open();
		//Send Headers
		if ($this->debug) {
//			Header("Content-type: text/html");
		} else {
			Header("Content-type: application/pdf");
//			Header("Content-disposition: attachment; filename=\"report.pdf\"");
		}
	}

	//DebugPrint wrapper..Only prints when debug==1
	function DebugPrint($message) {
		if (!$this->debug)
			return;
		print "<font size=2>".htmlentities($message)."</font><br/>\n";
	}

	//Debug Assignments
	function DebugAssign() {
		if (!$this->debug)
			return;
		print "<font size=2><pre>\nGlobal\n";
		print_r($this->subs);
		foreach ($this->blocksubs as $key=>$val) {
			print "Block: $key\n";
			print_r($val);
		}
		print "</pre></font>";
	}
	//Assign variables
	// ie.  Assign(array('key'=>'val'));
	// or   Assign("key","val");
	function Assign($key,$val=NULL) {
		if (is_array($key)) {
			foreach($key as $t_key=>$t_val) {
				$this->DebugPrint("Assigning $t_key=>$t_val");
				$this->cmnAssign(&$this->subs["keys"],&$this->subs["vals"],$t_key,$t_val);
			}
		} else {		
			$this->DebugPrint("Assigning $key=>$val");
			$this->cmnAssign(&$this->subs["keys"],&$this->subs["vals"],$key,$val);
		}
	}
	//  $block is the name associated with the block.
	// ie.  AssignBlock("block",array('key'=>'val'));
	// or   AssignBlock("block","key","val");
	function AssignBlock($block,$key,$val=NULL) {
		if (!isset($this->blocksubs[$block])) {
			$this->blocksubs[$block]["keys"][0]=array();
			$this->blocksubs[$block]["vals"][0]=array();
			$this->blocksubs[$block]["current"]=0;
			$this->cmnAssign(
				&$this->blocksubs[$block]["keys"][$this->blocksubs[$block]["current"]],
				&$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]],
				"DEFAULTS.BLOCK",$block);
			$this->cmnAssign(
				&$this->blocksubs[$block]["keys"][$this->blocksubs[$block]["current"]],
				&$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]],
				"DEFAULTS.ITERATION",1);
		}
		if (is_array($key)) { //assiciative array passed
			foreach($key as $t_key=>$t_val) {
			$this->DebugPrint("Assigning \"$block\" $t_key=>$t_val");
			$this->cmnAssign(
				&$this->blocksubs[$block]["keys"][$this->blocksubs[$block]["current"]],
				&$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]],
				".$t_key",$t_val);
			}
		} else { //plain old strings were passed
			$this->DebugPrint("Assigning \"$block\" $key=>$val");
			$this->cmnAssign(
				&$this->blocksubs[$block]["keys"][$this->blocksubs[$block]["current"]],
				&$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]],
				".$key",$val);
		}
	}
	//incremenmnt to next block iteration
	function NextBlock($block) {
		if (isset($this->blocksubs[$block])) {
			$this->blocksubs[$block]["keys"][++$this->blocksubs[$block]["current"]]=array();
			$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]]=array();
			$this->cmnAssign(
				&$this->blocksubs[$block]["keys"][$this->blocksubs[$block]["current"]],
				&$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]],
				"DEFAULTS.BLOCK",$block);
			$this->cmnAssign(
				&$this->blocksubs[$block]["keys"][$this->blocksubs[$block]["current"]],
				&$this->blocksubs[$block]["vals"][$this->blocksubs[$block]["current"]],
				"DEFAULTS.ITERATION",$this->blocksubs[$block]["current"]+1);
		}
	}
	//Close PDF and finish flushing output to client
	function Close() {
		if ($this->debug)
			ob_start();
		PDF_close($this->pdf);
		if ($this->debug)
			ob_end_clean();
	}

	//Parse through the XML once document and generate PDF (can be called multiple times)
	//Returns XML Error messages if bad XML. otherwise false.
	function Parse() {
		$this->DebugAssign();
		$error = XMLParseFile (&$this->parser, "pdf.xml", 0, "", 1,"UTF-8");
		if (strcmp ($error, "")) {
			return $error;
		} else {
			$this->WalkXML ("0"); //&$this->parser->structure, &$this->parser->positions);
			return false;
		}
	}
	//END OF use Public functions..  The rest are "internal use only" functions
	//Recursive funtion to "WALK" the XML tree and call rendering routines
	function WalkXML ($path) {
		if (is_array($this->parser->structure[$path])) {
		//Beginning Tag
			$this->startElement($path);
			//Check for looping
			$count=1;
			if (strcmp ($this->parser->structure[$path]["Tag"], "BLOCK") == 0) {
				$this->page["block"]["name"]=$this->parser->structure[$path]["Attributes"]["NAME"];
				for ($this->page["block"]["current"]=0;
						$this->page["block"]["current"]<=$this->blocksubs[$this->page["block"]["name"]]["current"];
						$this->page["block"]["current"]++) {
					for ($element = 0; $element < $this->parser->structure[$path]["Elements"];$element++)
						$this->WalkXML($path.",$element");					
				}
				$this->page["block"]["name"]=""; //Clear out block;
			} else { //not in a block. Normal parsing
				//walk sub tags/content
				for ($element = 0; $element < $this->parser->structure[$path]["Elements"];$element++)
					$this->WalkXML($path.",$element");
			}
			//End Tag
			$this->endElement($path);
		} else {
			//Content
			//Find parent path
			$parentpath = substr($path,0,strrpos($path,","));
			$this->DebugPrint("PATH=".$path."-".strrpos($path,",")."-".$parentpath);

			$data = trim(preg_replace("/(\ +|\n)/"," ",$this->parser->structure[$path]));
			if (strlen($data)>0) {
				$this->characterData($this->parser->structure[$parentpath]["Tag"]
					,$this->parser->structure[$parentpath]["Attributes"]
					,$data);
			}
		}
	}

	//set a font. $index of the font in the font stack (-1 for latest). if $force is true then
	//	this font's height will be forced for line height	otherwise the max of the current 
	//	lineheight and the fonts will be used.
	function setfont($index=-1,$force=false) {
		if ($index<0) {
			$index = count($this->fontstack["Font"])-1;
		}
		//line height
		if ($force) {
			$this->page["fontlead"] = $this->fontstack["Size"][$index]*1.5;
		} else {
			$this->page["fontlead"] = max($this->page["fontlead"],$this->fontstack["Size"][$index]*1.5);
		}
		$this->DebugPrint ("fontlead=".$this->page["fontlead"]);
		PDF_set_font($this->pdf,$this->fontstack["Font"][$index],$this->fontstack["Size"][$index],"host");
	}

	//Add font to the font stack $fontname is the name of the registered font. $size is the point size
	//if either parameter is null("" or 0) the previous fonts value will be used.
	function addfont($fontname="",$size=0) {
		array_push($this->fontstack["Font"],(
			($fontname=="")
			? $this->fontstack["Font"][count($this->fontstack["Size"])-1]
			: $fontname)
		);
		array_push($this->fontstack["Size"],(
			($size<1)
			? $this->fontstack["Size"][count($this->fontstack["Size"])-1]
			: $size)
		);
		return count($this->fontstack["Font"])-1;
	}

	//returns an indexed array of the Font name and Font size of the specified font in the index
	function getfont($index=-1) {
		if ($index<0) {
			$index = count($this->fontstack["Font"])-1;
		}
		return array($this->fontstack["Font"][$index],$this->fontstack["Size"][$index]);
	}

	//removes the last font from the font stack
	function removefont() {
		array_pop($this->fontstack["Font"]);
		array_pop($this->fontstack["Size"]);
	}

	//finds the bold version of the current (or specified) font and returns the name of the font
	function getbold($index=-1) {
		$font = $this->getfont($index);
		$bolds = array(
			'Helvetica'=>"Helvetica-Bold",
			'Courier'=>"Courier-Bold",
			'Times-Roman'=>"Times-Roman-Bold",
			'Helvetica-Oblique'=>"Helvetica-BoldOblique",
			'Courier-Oblique'=>"Courier-BoldOblique",
			'Time-Roman-Italic'=>"Times-Roman-BoldItalic",
		);
		return (empty($bolds[$font[0]])?"":$bolds[$font[0]]);
	}

	//finds the italic version of the current (or specified) font and returns the name of the font
	function getitalic($index=-1) {
		$font = $this->getfont($index);
		$bolds = array(
			'Helvetica'=>"Helvetica-Oblique",
			'Courier'=>"Courier-Oblique",
			'Times-Roman'=>"Times-Roman-Italic",
			'Helvetica-Bold'=>"Helvetica-BoldOblique",
			'Courier-Bold'=>"Courier-BoldOblique",
			'Time-Roman-Bold'=>"Times-Roman-BoldItalic",
		);
		return (empty($bolds[$font[0]])?"":$bolds[$font[0]]);
	}

	//add (and replace) key/value pair in substitution arrays
	function cmnAssign(&$keys,&$vals,$key,$val) {
		if ( ($i=array_search("{".$key."}",$keys))!==NULL) {
			$this->DebugPrint("previous=".$i);
			$keys[$i] = "{".$key."}";
			$vals[$i] = $val;				
		} else {
			array_push($keys,"{".$key."}");
			array_push($vals,$val);
		}
	}
	//Ends the last page
	function cmnEndPage() {
		if (!$this->page["closed"]) {
			if($this->debug)
				ob_start();
			PDF_end_page($this->pdf);
			if ($this->debug)
				ob_end_clean();
			$this->page["closed"]=true;
		}
	}
	//Ends last page (if one exists) and Begins a new page and resets curX and curY. sets font info.
	function cmnNewPage() {
		$this->cmnEndPage(); //End previous page if exists
		//Current Position of cursor
		$this->page["curY"] = $this->page["height"]-$this->page["margin"];	//Current Y position
		$this->page["curX"] = $this->page["margin"];	//Current X position
		//Begin the page
		PDF_begin_page($this->pdf,$this->page["width"],$this->page["height"]);
		$this->page["closed"]=false;
		if ($this->page["ticks"]) {
			$this->cmnDrawTicks();
		} else {
			$this->setfont();
		}
	}
	//Draw ticks on sides of page
	function cmnDrawTicks() {
		//Generate debuging ticks;
		$c=0;
		PDF_set_font($this->pdf,"Helvetica",8,"host");
		//left side
		for ($i=$this->page["height"]; $i>0;$i-=10) {
			if (!($c % 5)) {
				PDF_show_xy($this->pdf,$this->page["height"]-$i,10,$i-3);
			}
			PDF_moveto($this->pdf,0,$i);
			if (!($c % 5)) {
				PDF_lineto($this->pdf,10,$i);
			} else {
				PDF_lineto($this->pdf,7,$i);
			}
			PDF_stroke($this->pdf);
			$c++;
		}
		//top edge
		$c=0;
		for ($i=0; $i<$this->page["width"]; $i+=10) {
			if (!($c % 5)) {
				PDF_show_xy($this->pdf,$i,$i-6,$this->page["height"]-18);
			}
			PDF_moveto($this->pdf,$i,$this->page["height"]);
			if (!($c % 5)) {
				PDF_lineto($this->pdf,$i,$this->page["height"]-7);
			} else {
				PDF_lineto($this->pdf,$i,$this->page["height"]-10);
			}
			PDF_stroke($this->pdf);
			$c++;
		}
		$this->setfont();
	}
	//handles the "beginning" of a tag  and sets parameters appropriatly
	function startElement($path) {
		$attribs = &$this->parser->structure[$path]["Attributes"];
		switch ($this->parser->structure[$path]["Tag"]) {
			case 'PDF':
				$this->DebugPrint("Begining of file");
				$this->page["closed"]=true;
				break;
			case 'PAGE':
				$this->page["fontlead"]=0;
				$this->page["tab"] = 36; //A single 'tab'.  For paragraph formatting.
				//Margin spacing Default = 36 (half inch);
				$this->page["margin"]=(empty($attribs["MARGIN"])?36:$attribs["MARGIN"]*72);
				//Default Width
				$this->page["width"]=(empty($attribs["WIDTH"])?612:$attribs["WIDTH"]*72);
				//Default Height
				$this->page["height"]=(empty($attribs["HEIGHT"])?792:$attribs["HEIGHT"]*72);
				$this->page["ticks"]=($attribs["TICKS"]==1?true:false);
				//Make new page
				$this->cmnNewPage();
				if (!empty($attribs["TOPMARGIN"])) {
					$this->page["curY"]=$this->page["height"]-$attribs["TOPMARGIN"]*72;
				}
				break;
			case 'PARA':
				if (!empty($attribs["INDENT"])) {
					$this->page["curX"]+=$this->page["tab"];
				}
				if (!empty($attribs["TAB"])) {
					$this->page["left"]=$this->page["margin"]+$attribs["TAB"]*72;
					$this->page["curX"]+=$attribs["TAB"]*72;
				} else {
					$this->page["left"]=$this->page["margin"];
				}
				$this->page["align"]=(empty($attribs["ALIGN"])?"left":strtolower($attribs["ALIGN"]));
				break;
			case 'BOLD':
				$this->setfont($this->addfont($this->getbold()));
				break;
			case 'ITALIC':
				$this->setfont($this->addfont($this->getitalic()));
				break;
			case 'FONTDEF':
				$this->DebugPrint(
					"face=".$attribs["FACE"]
					." afm=".$attribs["AFM"]
					." pfm=".$attribs["PFM"]
					." outline=".$attribs["OUTLINE"]);
				PDF_set_parameter($this->pdf,"FontAFM",$attribs["FACE"]."=".$attribs["AFM"]);
				PDF_set_parameter($this->pdf,"FontOutline",$attribs["FACE"]."=".$attribs["OUTLINE"]);
				break;
			case 'FONT':
				$this->DebugPrint("face=".$attribs["FACE"]." size=".$attribs["SIZE"]);
				$this->setfont($this->addfont($attribs["FACE"],$attribs["SIZE"]));
				break;
			case 'TAB':
				$this->page["curX"]=72 * $attribs["TAB"];
				break;
			case 'BR':
				$this->page["curX"]=$this->page["left"];
				$this->page["curY"]-=$this->page["fontlead"];
				break;
			case 'BREAK':
				$this->cmnNewPage();
				break;
			default:
				break;
		}
	}

	//handles the "end" of a tag and (un)sets parameters appropriatly
	function endElement($path) {
		$attribs = &$this->parser->structure[$path]["Attributes"];
		switch ($this->parser->structure[$path]["Tag"]) {
			case 'PDF':
				$this->DebugPrint("End of document");
				$this->cmnEndPage();
				break;
			case 'PAGE':
				//End the page and push to client.
//				PDF_end_page($this->pdf);
				break;
			case 'PARA':
				$this->page["curY"]-=$this->page["fontlead"]; //goto next line.
				$this->setfont(-1,true); //reset font and fontlead (line height)
				$this->page["curX"]=$this->page["margin"]; //Back to the left of the line
				
				if (empty($attribs["NOBREAK"])) {
					$this->page["curY"]-=$this->page["fontlead"]; //insert space between paragraphs
				}
				//$this->setfont(); //Set Default Font
				break;
			case 'FONT':
			case 'ITALIC':
			case 'BOLD':
				$this->removefont(); //Remove Last Font
				$this->setfont(); //Set Previous Font
				break;
			default:
				break;
		}
	} //end Element
	
	//handles the data between tags (the actual text)
	function characterData($tag,$attribs,$data) {
		$this->DebugPrint("CharData tag=$tag");
		switch ($tag) {
			case 'FONT':	//Displays text in different font. Formatting done in beginElement
			case 'BOLD':	//Displays bold text. Formating done in beginElement
			case 'ITALIC':  //Displays italic text. Formating done in beginElement
			//case 'LOOP': //incase loop tag around plain text in a para. (not preferred)
			case 'NOSUB': //Do not substitute variables
			case 'PARA': //Displays a paragraph
				//Substitute variables
				if (strcmp($tag,'NOSUB')<>0) {
					$data = str_replace($this->subs["keys"],$this->subs["vals"],$data);
					if (!empty($this->page["block"]["name"])) {
						$this->DebugPrint("blocksub=".$this->page["block"]["name"]."-".$this->page["block"]["current"]);
						$data = str_replace(
							$this->blocksubs[$this->page["block"]["name"]]["keys"][$this->page["block"]["current"]],
							$this->blocksubs[$this->page["block"]["name"]]["vals"][$this->page["block"]["current"]],
							$data);
					}
				}
				while (strlen($data)>0) {
					//Check to see if we are past the bottom of the page.
					//	It's backward login due to PDF's 0 coord is at the bottom
					if ($this->page["curY"] < ($this->page["margin"]+$this->page["fontlead"]) ) {
						$this->DebugPrint("Ending Current Page");
						$this->cmnNewPage();
						$this->DebugPrint("Starting New Page");
					}
					$extra = PDF_show_boxed($this->pdf,$data,
						$this->page["curX"],	//Left
						$this->page["curY"]-$this->page["fontlead"],	//Top
						$this->page["width"]-$this->page["curX"]-$this->page["margin"],	//Width
						$this->page["fontlead"],	//Height (one line)
						$this->page["align"]);	//Justification

					$this->DebugPrint(
						"X=".$this->page["curX"]
						." Y=".$this->page["curY"]
						." extra=".$extra
						." end=".PDF_get_value($this->pdf,"textx")
						." W=".PDF_stringwidth($this->pdf,$data));
					$this->DebugPrint("text=".$data);

					if (PDF_stringwidth($this->pdf,$data)
							> ($this->page["width"]-$this->page["margin"]-$this->page["curX"])) { //Text wider than page
						$this->page["curX"]=$this->page["left"];	//go back to beginning of line
						$this->page["curY"]-=$this->page["fontlead"];	//Go down one line
						$this->setfont(-1,true); //Reset line height (also resets fontlead)
					} else { //fits on one line get current "X" position
						$this->page["curX"]=PDF_get_value($this->pdf,"textx");
					}
					//update $data with unprinted text
					//if ($extra>0) {
						$data = substr($data,(strlen($data)-$extra));
					//};
				} //while
				break;
			default:
				break;
		}
	}

} //End Class PDFTemplate
?>
Return current item: PDFTemplate