From e681a49fe57802022a8c9feba7b050f80fd08f47 Mon Sep 17 00:00:00 2001 From: wlx Date: Thu, 15 May 2014 13:34:18 +0000 Subject: [PATCH] upgrade fpdi to version 1.5.1 --- include/fpdi/filters/FilterASCII85.php | 157 +- include/fpdi/filters/FilterASCIIHexDecode.php | 52 + include/fpdi/filters/FilterLZW.php | 278 +-- include/fpdi/fpdf_tpl.php | 964 +++++----- include/fpdi/fpdi.php | 534 +++--- include/fpdi/fpdi_bridge.php | 215 +++ include/fpdi/fpdi_pdf_parser.php | 420 ++--- include/fpdi/pdf_context.php | 219 ++- include/fpdi/pdf_parser.php | 1585 +++++++++-------- 9 files changed, 2585 insertions(+), 1839 deletions(-) create mode 100644 include/fpdi/filters/FilterASCIIHexDecode.php create mode 100644 include/fpdi/fpdi_bridge.php diff --git a/include/fpdi/filters/FilterASCII85.php b/include/fpdi/filters/FilterASCII85.php index 01402ba1..b1d53a4d 100644 --- a/include/fpdi/filters/FilterASCII85.php +++ b/include/fpdi/filters/FilterASCII85.php @@ -1,8 +1,8 @@ ord('~'), + 'z' => ord('z'), + 'u' => ord('u'), + 'z' => ord('z'), + '!' => ord('!') + ); -if (!class_exists('FilterASCII85', false)) { + $out = ''; + $state = 0; + $chn = null; - class FilterASCII85 { - - function error($msg) { - die($msg); - } - - function decode($in) { - $out = ''; - $state = 0; - $chn = null; - - $l = strlen($in); - - for ($k = 0; $k < $l; ++$k) { - $ch = ord($in[$k]) & 0xff; - - if ($ch == ORD_tilde) { - break; - } - if (preg_match('/^\s$/',chr($ch))) { - continue; - } - if ($ch == ORD_z && $state == 0) { - $out .= chr(0) . chr(0) . chr(0) . chr(0); - continue; - } - if ($ch < ORD_exclmark || $ch > ORD_u) { - return $this->error('Illegal character in ASCII85Decode.'); - } - - $chn[$state++] = $ch - ORD_exclmark; - - if ($state == 5) { - $state = 0; - $r = 0; - for ($j = 0; $j < 5; ++$j) - $r = $r * 85 + $chn[$j]; - $out .= chr($r >> 24); - $out .= chr($r >> 16); - $out .= chr($r >> 8); - $out .= chr($r); - } + $l = strlen($in); + + for ($k = 0; $k < $l; ++$k) { + $ch = ord($in[$k]) & 0xff; + + if ($ch == $ord['~']) { + break; } - $r = 0; - - if ($state == 1) - return $this->error('Illegal length in ASCII85Decode.'); - if ($state == 2) { - $r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1]+1) * 85 * 85 * 85; - $out .= chr($r >> 24); + if (preg_match('/^\s$/',chr($ch))) { + continue; } - else if ($state == 3) { - $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2]+1) * 85 * 85; - $out .= chr($r >> 24); - $out .= chr($r >> 16); + if ($ch == $ord['z'] && $state == 0) { + $out .= chr(0) . chr(0) . chr(0) . chr(0); + continue; } - else if ($state == 4) { - $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3]+1) * 85 ; + if ($ch < $ord['!'] || $ch > $ord['u']) { + throw new Exception('Illegal character in ASCII85Decode.'); + } + + $chn[$state++] = $ch - $ord['!']; + + if ($state == 5) { + $state = 0; + $r = 0; + for ($j = 0; $j < 5; ++$j) + $r = $r * 85 + $chn[$j]; $out .= chr($r >> 24); $out .= chr($r >> 16); $out .= chr($r >> 8); + $out .= chr($r); } - - return $out; } - - function encode($in) { - return $this->error("ASCII85 encoding not implemented."); + $r = 0; + + if ($state == 1) { + throw new Exception('Illegal length in ASCII85Decode.'); } + + if ($state == 2) { + $r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1]+1) * 85 * 85 * 85; + $out .= chr($r >> 24); + + } else if ($state == 3) { + $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + ($chn[2]+1) * 85 * 85; + $out .= chr($r >> 24); + $out .= chr($r >> 16); + + } else if ($state == 4) { + $r = $chn[0] * 85 * 85 * 85 * 85 + $chn[1] * 85 * 85 * 85 + $chn[2] * 85 * 85 + ($chn[3]+1) * 85 ; + $out .= chr($r >> 24); + $out .= chr($r >> 16); + $out .= chr($r >> 8); + } + + return $out; } -} + + /** + * NOT IMPLEMENTED + * + * @param string $in + * @return string + * @throws LogicException + */ + public function encode($in) + { + throw new LogicException("ASCII85 encoding not implemented."); + } +} \ No newline at end of file diff --git a/include/fpdi/filters/FilterASCIIHexDecode.php b/include/fpdi/filters/FilterASCIIHexDecode.php new file mode 100644 index 00000000..7372456a --- /dev/null +++ b/include/fpdi/filters/FilterASCIIHexDecode.php @@ -0,0 +1,52 @@ +')); + if ((strlen($data) % 2) == 1) { + $data .= '0'; + } + + return pack('H*', $data); + } + + /** + * Converts a string into ASCII hexadecimal representation. + * + * @param string $data The input string + * @param boolean $leaveEOD + * @return string + */ + public function encode($data, $leaveEOD = false) + { + return current(unpack('H*', $data)) . ($leaveEOD ? '' : '>'); + } +} \ No newline at end of file diff --git a/include/fpdi/filters/FilterLZW.php b/include/fpdi/filters/FilterLZW.php index 292461d2..bb4698e0 100644 --- a/include/fpdi/filters/FilterLZW.php +++ b/include/fpdi/filters/FilterLZW.php @@ -1,8 +1,8 @@ error('LZW flavour not supported.'); - } - - $this->initsTable(); - - $this->data = $data; - $this->dataLength = strlen($data); - - // Initialize pointers - $this->bytePointer = 0; - $this->bitPointer = 0; - - $this->nextData = 0; - $this->nextBits = 0; - - $oldCode = 0; - - $string = ''; - $uncompData = ''; - - while (($code = $this->getNextCode()) != 257) { - if ($code == 256) { - $this->initsTable(); - $code = $this->getNextCode(); - - if ($code == 257) { - break; - } - - $uncompData .= $this->sTable[$code]; + + $this->_initsTable(); + + $this->_data = $data; + $this->_dataLength = strlen($data); + + // Initialize pointers + $this->_bytePointer = 0; + $this->_bitPointer = 0; + + $this->_nextData = 0; + $this->_nextBits = 0; + + $oldCode = 0; + + $unCompData = ''; + + while (($code = $this->_getNextCode()) != 257) { + if ($code == 256) { + $this->_initsTable(); + $code = $this->_getNextCode(); + + if ($code == 257) { + break; + } + + if (!isset($this->_sTable[$code])) { + throw new Exception('Error while decompression LZW compressed data.'); + } + + $unCompData .= $this->_sTable[$code]; + $oldCode = $code; + + } else { + + if ($code < $this->_tIdx) { + $string = $this->_sTable[$code]; + $unCompData .= $string; + + $this->_addStringToTable($this->_sTable[$oldCode], $string[0]); $oldCode = $code; - } else { - - if ($code < $this->tIdx) { - $string = $this->sTable[$code]; - $uncompData .= $string; - - $this->addStringToTable($this->sTable[$oldCode], $string[0]); - $oldCode = $code; - } else { - $string = $this->sTable[$oldCode]; - $string = $string . $string[0]; - $uncompData .= $string; - - $this->addStringToTable($string); - $oldCode = $code; - } + $string = $this->_sTable[$oldCode]; + $string = $string . $string[0]; + $unCompData .= $string; + + $this->_addStringToTable($string); + $oldCode = $code; } } - - return $uncompData; } - - - /** - * Initialize the string table. - */ - function initsTable() { - $this->sTable = array(); - - for ($i = 0; $i < 256; $i++) - $this->sTable[$i] = chr($i); - - $this->tIdx = 258; - $this->bitsToGet = 9; - } - - /** - * Add a new string to the string table. - */ - function addStringToTable ($oldString, $newString='') { - $string = $oldString.$newString; - - // Add this new String to the table - $this->sTable[$this->tIdx++] = $string; - - if ($this->tIdx == 511) { - $this->bitsToGet = 10; - } else if ($this->tIdx == 1023) { - $this->bitsToGet = 11; - } else if ($this->tIdx == 2047) { - $this->bitsToGet = 12; - } - } - - // Returns the next 9, 10, 11 or 12 bits - function getNextCode() { - if ($this->bytePointer == $this->dataLength) { - return 257; - } - - $this->nextData = ($this->nextData << 8) | (ord($this->data[$this->bytePointer++]) & 0xff); - $this->nextBits += 8; - - if ($this->nextBits < $this->bitsToGet) { - $this->nextData = ($this->nextData << 8) | (ord($this->data[$this->bytePointer++]) & 0xff); - $this->nextBits += 8; - } - - $code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet-9]; - $this->nextBits -= $this->bitsToGet; - - return $code; - } - - function encode($in) { - $this->error("LZW encoding not implemented."); + + return $unCompData; + } + + + /** + * Initialize the string table. + */ + protected function _initsTable() + { + $this->_sTable = array(); + + for ($i = 0; $i < 256; $i++) + $this->_sTable[$i] = chr($i); + + $this->_tIdx = 258; + $this->_bitsToGet = 9; + } + + /** + * Add a new string to the string table. + */ + protected function _addStringToTable($oldString, $newString = '') + { + $string = $oldString . $newString; + + // Add this new String to the table + $this->_sTable[$this->_tIdx++] = $string; + + if ($this->_tIdx == 511) { + $this->_bitsToGet = 10; + } else if ($this->_tIdx == 1023) { + $this->_bitsToGet = 11; + } else if ($this->_tIdx == 2047) { + $this->_bitsToGet = 12; } } -} + + /** + * Returns the next 9, 10, 11 or 12 bits + * + * @return int + */ + protected function _getNextCode() + { + if ($this->_bytePointer == $this->_dataLength) { + return 257; + } + + $this->_nextData = ($this->_nextData << 8) | (ord($this->_data[$this->_bytePointer++]) & 0xff); + $this->_nextBits += 8; + + if ($this->_nextBits < $this->_bitsToGet) { + $this->_nextData = ($this->_nextData << 8) | (ord($this->_data[$this->_bytePointer++]) & 0xff); + $this->_nextBits += 8; + } + + $code = ($this->_nextData >> ($this->_nextBits - $this->_bitsToGet)) & $this->_andTable[$this->_bitsToGet-9]; + $this->_nextBits -= $this->_bitsToGet; + + return $code; + } + + /** + * NOT IMPLEMENTED + * + * @param string $in + * @return string + * @throws LogicException + */ + public function encode($in) + { + throw new LogicException("LZW encoding not implemented."); + } +} \ No newline at end of file diff --git a/include/fpdi/fpdf_tpl.php b/include/fpdi/fpdf_tpl.php index fe36d29f..33f97c2b 100644 --- a/include/fpdi/fpdf_tpl.php +++ b/include/fpdi/fpdf_tpl.php @@ -1,409 +1,555 @@ -page <= 0) - $this->error("You have to add a page to fpdf first!"); - - if ($x == null) - $x = 0; - if ($y == null) - $y = 0; - if ($w == null) - $w = $this->w; - if ($h == null) - $h = $this->h; - - // Save settings - $this->tpl++; - $tpl =& $this->tpls[$this->tpl]; - $tpl = array( - 'o_x' => $this->x, - 'o_y' => $this->y, - 'o_AutoPageBreak' => $this->AutoPageBreak, - 'o_bMargin' => $this->bMargin, - 'o_tMargin' => $this->tMargin, - 'o_lMargin' => $this->lMargin, - 'o_rMargin' => $this->rMargin, - 'o_h' => $this->h, - 'o_w' => $this->w, - 'buffer' => '', - 'x' => $x, - 'y' => $y, - 'w' => $w, - 'h' => $h - ); - - $this->SetAutoPageBreak(false); - - // Define own high and width to calculate possitions correct - $this->h = $h; - $this->w = $w; - - $this->_intpl = true; - $this->SetXY($x+$this->lMargin, $y+$this->tMargin); - $this->SetRightMargin($this->w-$w+$this->rMargin); - - return $this->tpl; - } - - /** - * End Template - * - * This method ends a template and reset initiated variables on beginTemplate. - * - * @return mixed If a template is opened, the ID is returned. If not a false is returned. - */ - function endTemplate() { - if ($this->_intpl) { - $this->_intpl = false; - $tpl =& $this->tpls[$this->tpl]; - $this->SetXY($tpl['o_x'], $tpl['o_y']); - $this->tMargin = $tpl['o_tMargin']; - $this->lMargin = $tpl['o_lMargin']; - $this->rMargin = $tpl['o_rMargin']; - $this->h = $tpl['o_h']; - $this->w = $tpl['o_w']; - $this->SetAutoPageBreak($tpl['o_AutoPageBreak'], $tpl['o_bMargin']); - - return $this->tpl; - } else { - return false; - } - } - - /** - * Use a Template in current Page or other Template - * - * You can use a template in a page or in another template. - * You can give the used template a new size like you use the Image()-method. - * All parameters are optional. The width or height is calculated automaticaly - * if one is given. If no parameter is given the origin size as defined in - * beginTemplate() is used. - * The calculated or used width and height are returned as an array. - * - * @param int $tplidx A valid template-Id - * @param int $_x The x-position - * @param int $_y The y-position - * @param int $_w The new width of the template - * @param int $_h The new height of the template - * @retrun array The height and width of the template - */ - function useTemplate($tplidx, $_x=null, $_y=null, $_w=0, $_h=0) { - if ($this->page <= 0) - $this->error("You have to add a page to fpdf first!"); - - if (!isset($this->tpls[$tplidx])) - $this->error("Template does not exist!"); - - if ($this->_intpl) { - $this->_res['tpl'][$this->tpl]['tpls'][$tplidx] =& $this->tpls[$tplidx]; - } - - $tpl =& $this->tpls[$tplidx]; - $w = $tpl['w']; - $h = $tpl['h']; - - if ($_x == null) - $_x = 0; - if ($_y == null) - $_y = 0; - - $_x += $tpl['x']; - $_y += $tpl['y']; - - $wh = $this->getTemplateSize($tplidx, $_w, $_h); - $_w = $wh['w']; - $_h = $wh['h']; - - $tData = array( - 'x' => $this->x, - 'y' => $this->y, - 'w' => $_w, - 'h' => $_h, - 'scaleX' => ($_w/$w), - 'scaleY' => ($_h/$h), - 'tx' => $_x, - 'ty' => ($this->h-$_y-$_h), - 'lty' => ($this->h-$_y-$_h) - ($this->h-$h) * ($_h/$h) - ); - - $this->_out(sprintf("q %.4F 0 0 %.4F %.4F %.4F cm", $tData['scaleX'], $tData['scaleY'], $tData['tx']*$this->k, $tData['ty']*$this->k)); // Translate - $this->_out(sprintf('%s%d Do Q', $this->tplprefix, $tplidx)); - - $this->lastUsedTemplateData = $tData; - - return array("w" => $_w, "h" => $_h); - } - - /** - * Get The calculated Size of a Template - * - * If one size is given, this method calculates the other one. - * - * @param int $tplidx A valid template-Id - * @param int $_w The width of the template - * @param int $_h The height of the template - * @return array The height and width of the template - */ - function getTemplateSize($tplidx, $_w=0, $_h=0) { - if (!$this->tpls[$tplidx]) - return false; - - $tpl =& $this->tpls[$tplidx]; - $w = $tpl['w']; - $h = $tpl['h']; - - if ($_w == 0 and $_h == 0) { - $_w = $w; - $_h = $h; - } - - if($_w==0) - $_w = $_h*$w/$h; - if($_h==0) - $_h = $_w*$h/$w; - - return array("w" => $_w, "h" => $_h); - } - - /** - * See FPDF/TCPDF-Documentation ;-) - */ - function SetFont($family, $style='', $size=0, $fontfile='') { - if (!is_subclass_of($this, 'TCPDF') && func_num_args() > 3) { - $this->Error('More than 3 arguments for the SetFont method are only available in TCPDF.'); - } - /** - * force the resetting of font changes in a template - */ - if ($this->_intpl) - $this->FontFamily = ''; - - parent::SetFont($family, $style, $size, $fontfile); - - $fontkey = $this->FontFamily.$this->FontStyle; - - if ($this->_intpl) { - $this->_res['tpl'][$this->tpl]['fonts'][$fontkey] =& $this->fonts[$fontkey]; - } else { - $this->_res['page'][$this->page]['fonts'][$fontkey] =& $this->fonts[$fontkey]; - } - } - - /** - * See FPDF/TCPDF-Documentation ;-) - */ - function Image($file, $x, $y, $w=0, $h=0, $type='', $link='', $align='', $resize=false, $dpi=300, $palign='', $ismask=false, $imgmask=false, $border=0) { - if (!is_subclass_of($this, 'TCPDF') && func_num_args() > 7) { - $this->Error('More than 7 arguments for the Image method are only available in TCPDF.'); - } - - parent::Image($file, $x, $y, $w, $h, $type, $link, $align, $resize, $dpi, $palign, $ismask, $imgmask, $border); - if ($this->_intpl) { - $this->_res['tpl'][$this->tpl]['images'][$file] =& $this->images[$file]; - } else { - $this->_res['page'][$this->page]['images'][$file] =& $this->images[$file]; - } - } - - /** - * See FPDF-Documentation ;-) - * - * AddPage is not available when you're "in" a template. - */ - function AddPage($orientation='', $format='') { - if ($this->_intpl) - $this->Error('Adding pages in templates isn\'t possible!'); - parent::AddPage($orientation, $format); - } - - /** - * Preserve adding Links in Templates ...won't work - */ - function Link($x, $y, $w, $h, $link, $spaces=0) { - if (!is_subclass_of($this, 'TCPDF') && func_num_args() > 5) { - $this->Error('More than 7 arguments for the Image method are only available in TCPDF.'); - } - - if ($this->_intpl) - $this->Error('Using links in templates aren\'t possible!'); - parent::Link($x, $y, $w, $h, $link, $spaces); - } - - function AddLink() { - if ($this->_intpl) - $this->Error('Adding links in templates aren\'t possible!'); - return parent::AddLink(); - } - - function SetLink($link, $y=0, $page=-1) { - if ($this->_intpl) - $this->Error('Setting links in templates aren\'t possible!'); - parent::SetLink($link, $y, $page); - } - - /** - * Private Method that writes the form xobjects - */ - function _putformxobjects() { - $filter=($this->compress) ? '/Filter /FlateDecode ' : ''; - reset($this->tpls); - foreach($this->tpls AS $tplidx => $tpl) { - - $p=($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer']; - $this->_newobj(); - $this->tpls[$tplidx]['n'] = $this->n; - $this->_out('<<'.$filter.'/Type /XObject'); - $this->_out('/Subtype /Form'); - $this->_out('/FormType 1'); - $this->_out(sprintf('/BBox [%.2F %.2F %.2F %.2F]', - // llx - $tpl['x'], - // lly - -$tpl['y'], - // urx - ($tpl['w']+$tpl['x'])*$this->k, - // ury - ($tpl['h']-$tpl['y'])*$this->k - )); - - if ($tpl['x'] != 0 || $tpl['y'] != 0) { - $this->_out(sprintf('/Matrix [1 0 0 1 %.5F %.5F]', - -$tpl['x']*$this->k*2, $tpl['y']*$this->k*2 - )); - } - - $this->_out('/Resources '); - - $this->_out('<_res['tpl'][$tplidx]['fonts']) && count($this->_res['tpl'][$tplidx]['fonts'])) { - $this->_out('/Font <<'); - foreach($this->_res['tpl'][$tplidx]['fonts'] as $font) - $this->_out('/F'.$font['i'].' '.$font['n'].' 0 R'); - $this->_out('>>'); - } - if(isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images']) || - isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) - { - $this->_out('/XObject <<'); - if (isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images'])) { - foreach($this->_res['tpl'][$tplidx]['images'] as $image) - $this->_out('/I'.$image['i'].' '.$image['n'].' 0 R'); - } - if (isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) { - foreach($this->_res['tpl'][$tplidx]['tpls'] as $i => $tpl) - $this->_out($this->tplprefix.$i.' '.$tpl['n'].' 0 R'); - } - $this->_out('>>'); - } - $this->_out('>>'); - - $this->_out('/Length '.strlen($p).' >>'); - $this->_putstream($p); - $this->_out('endobj'); - } - } - - /** - * Overwritten to add _putformxobjects() after _putimages() - * - */ - function _putimages() { - parent::_putimages(); - $this->_putformxobjects(); - } - - function _putxobjectdict() { - parent::_putxobjectdict(); - - if (count($this->tpls)) { - foreach($this->tpls as $tplidx => $tpl) { - $this->_out(sprintf('%s%d %d 0 R', $this->tplprefix, $tplidx, $tpl['n'])); - } - } - } - - /** - * Private Method - */ - function _out($s) { - if ($this->state==2 && $this->_intpl) { - $this->tpls[$this->tpl]['buffer'] .= $s."\n"; - } else { - parent::_out($s); - } - } -} +page <= 0) { + throw new LogicException("You have to add at least a page first!"); + } + + if ($x == null) + $x = 0; + if ($y == null) + $y = 0; + if ($w == null) + $w = $this->w; + if ($h == null) + $h = $this->h; + + // Save settings + $this->tpl++; + $tpl =& $this->_tpls[$this->tpl]; + $tpl = array( + 'o_x' => $this->x, + 'o_y' => $this->y, + 'o_AutoPageBreak' => $this->AutoPageBreak, + 'o_bMargin' => $this->bMargin, + 'o_tMargin' => $this->tMargin, + 'o_lMargin' => $this->lMargin, + 'o_rMargin' => $this->rMargin, + 'o_h' => $this->h, + 'o_w' => $this->w, + 'o_FontFamily' => $this->FontFamily, + 'o_FontStyle' => $this->FontStyle, + 'o_FontSizePt' => $this->FontSizePt, + 'o_FontSize' => $this->FontSize, + 'buffer' => '', + 'x' => $x, + 'y' => $y, + 'w' => $w, + 'h' => $h + ); + + $this->SetAutoPageBreak(false); + + // Define own high and width to calculate correct positions + $this->h = $h; + $this->w = $w; + + $this->_inTpl = true; + $this->SetXY($x + $this->lMargin, $y + $this->tMargin); + $this->SetRightMargin($this->w - $w + $this->rMargin); + + if ($this->CurrentFont) { + $fontKey = $this->FontFamily . $this->FontStyle; + if ($fontKey) { + $this->_res['tpl'][$this->tpl]['fonts'][$fontKey] =& $this->fonts[$fontKey]; + $this->_out(sprintf('BT /F%d %.2f Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + } + + return $this->tpl; + } + + /** + * End template. + * + * This method ends a template and reset initiated variables collected in {@link beginTemplate()}. + * + * @return int|boolean If a template is opened, the id is returned. If not a false is returned. + */ + public function endTemplate() + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::endTemplate'), $args); + } + + if ($this->_inTpl) { + $this->_inTpl = false; + $tpl = $this->_tpls[$this->tpl]; + $this->SetXY($tpl['o_x'], $tpl['o_y']); + $this->tMargin = $tpl['o_tMargin']; + $this->lMargin = $tpl['o_lMargin']; + $this->rMargin = $tpl['o_rMargin']; + $this->h = $tpl['o_h']; + $this->w = $tpl['o_w']; + $this->SetAutoPageBreak($tpl['o_AutoPageBreak'], $tpl['o_bMargin']); + + $this->FontFamily = $tpl['o_FontFamily']; + $this->FontStyle = $tpl['o_FontStyle']; + $this->FontSizePt = $tpl['o_FontSizePt']; + $this->FontSize = $tpl['o_FontSize']; + + $fontKey = $this->FontFamily . $this->FontStyle; + if ($fontKey) + $this->CurrentFont =& $this->fonts[$fontKey]; + + return $this->tpl; + } else { + return false; + } + } + + /** + * Use a template in current page or other template. + * + * You can use a template in a page or in another template. + * You can give the used template a new size. + * All parameters are optional. The width or height is calculated automatically + * if one is given. If no parameter is given the origin size as defined in + * {@link beginTemplate()} method is used. + * + * The calculated or used width and height are returned as an array. + * + * @param int $tplIdx A valid template-id + * @param int $x The x-position + * @param int $y The y-position + * @param int $w The new width of the template + * @param int $h The new height of the template + * @return array The height and width of the template (array('w' => ..., 'h' => ...)) + * @throws LogicException|InvalidArgumentException + */ + public function useTemplate($tplIdx, $x = null, $y = null, $w = 0, $h = 0) + { + if ($this->page <= 0) { + throw new LogicException('You have to add at least a page first!'); + } + + if (!isset($this->_tpls[$tplIdx])) { + throw new InvalidArgumentException('Template does not exist!'); + } + + if ($this->_inTpl) { + $this->_res['tpl'][$this->tpl]['tpls'][$tplIdx] =& $this->_tpls[$tplIdx]; + } + + $tpl = $this->_tpls[$tplIdx]; + $_w = $tpl['w']; + $_h = $tpl['h']; + + if ($x == null) { + $x = 0; + } + + if ($y == null) { + $y = 0; + } + + $x += $tpl['x']; + $y += $tpl['y']; + + $wh = $this->getTemplateSize($tplIdx, $w, $h); + $w = $wh['w']; + $h = $wh['h']; + + $tplData = array( + 'x' => $this->x, + 'y' => $this->y, + 'w' => $w, + 'h' => $h, + 'scaleX' => ($w / $_w), + 'scaleY' => ($h / $_h), + 'tx' => $x, + 'ty' => ($this->h - $y - $h), + 'lty' => ($this->h - $y - $h) - ($this->h - $_h) * ($h / $_h) + ); + + $this->_out(sprintf('q %.4F 0 0 %.4F %.4F %.4F cm', + $tplData['scaleX'], $tplData['scaleY'], $tplData['tx'] * $this->k, $tplData['ty'] * $this->k) + ); // Translate + $this->_out(sprintf('%s%d Do Q', $this->tplPrefix, $tplIdx)); + + $this->lastUsedTemplateData = $tplData; + + return array('w' => $w, 'h' => $h); + } + + /** + * Get the calculated size of a template. + * + * If one size is given, this method calculates the other one. + * + * @param int $tplIdx A valid template-id + * @param int $w The width of the template + * @param int $h The height of the template + * @return array The height and width of the template (array('w' => ..., 'h' => ...)) + */ + public function getTemplateSize($tplIdx, $w = 0, $h = 0) + { + if (!isset($this->_tpls[$tplIdx])) + return false; + + $tpl = $this->_tpls[$tplIdx]; + $_w = $tpl['w']; + $_h = $tpl['h']; + + if ($w == 0 && $h == 0) { + $w = $_w; + $h = $_h; + } + + if ($w == 0) + $w = $h * $_w / $_h; + if($h == 0) + $h = $w * $_h / $_w; + + return array("w" => $w, "h" => $h); + } + + /** + * Sets the font used to print character strings. + * + * See FPDF/TCPDF documentation. + * + * @see http://fpdf.org/en/doc/setfont.htm + * @see http://www.tcpdf.org/doc/code/classTCPDF.html#afd56e360c43553830d543323e81bc045 + */ + public function SetFont($family, $style = '', $size = null, $fontfile = '', $subset = 'default', $out = true) + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::SetFont'), $args); + } + + parent::SetFont($family, $style, $size); + + $fontkey = $this->FontFamily . $this->FontStyle; + + if ($this->_inTpl) { + $this->_res['tpl'][$this->tpl]['fonts'][$fontkey] =& $this->fonts[$fontkey]; + } else { + $this->_res['page'][$this->page]['fonts'][$fontkey] =& $this->fonts[$fontkey]; + } + } + + /** + * Puts an image. + * + * See FPDF/TCPDF documentation. + * + * @see http://fpdf.org/en/doc/image.htm + * @see http://www.tcpdf.org/doc/code/classTCPDF.html#a714c2bee7d6b39d4d6d304540c761352 + */ + public function Image( + $file, $x = '', $y = '', $w = 0, $h = 0, $type = '', $link = '', $align = '', $resize = false, + $dpi = 300, $palign = '', $ismask = false, $imgmask = false, $border = 0, $fitbox = false, + $hidden = false, $fitonpage = false, $alt = false, $altimgs = array() + ) + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::Image'), $args); + } + + $ret = parent::Image($file, $x, $y, $w, $h, $type, $link); + if ($this->_inTpl) { + $this->_res['tpl'][$this->tpl]['images'][$file] =& $this->images[$file]; + } else { + $this->_res['page'][$this->page]['images'][$file] =& $this->images[$file]; + } + + return $ret; + } + + /** + * Adds a new page to the document. + * + * See FPDF/TCPDF documentation. + * + * This method cannot be used if you'd started a template. + * + * @see http://fpdf.org/en/doc/addpage.htm + * @see http://www.tcpdf.org/doc/code/classTCPDF.html#a5171e20b366b74523709d84c349c1ced + */ + public function AddPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false) + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::AddPage'), $args); + } + + if ($this->_inTpl) { + throw new LogicException('Adding pages in templates is not possible!'); + } + + parent::AddPage($orientation, $format); + } + + /** + * Puts a link on a rectangular area of the page. + * + * Overwritten because adding links in a template will not work. + * + * @see http://fpdf.org/en/doc/link.htm + * @see http://www.tcpdf.org/doc/code/classTCPDF.html#ab87bf1826384fbfe30eb499d42f1d994 + */ + public function Link($x, $y, $w, $h, $link, $spaces = 0) + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::Link'), $args); + } + + if ($this->_inTpl) { + throw new LogicException('Using links in templates is not posible!'); + } + + parent::Link($x, $y, $w, $h, $link); + } + + /** + * Creates a new internal link and returns its identifier. + * + * Overwritten because adding links in a template will not work. + * + * @see http://fpdf.org/en/doc/addlink.htm + * @see http://www.tcpdf.org/doc/code/classTCPDF.html#a749522038ed7786c3e1701435dcb891e + */ + public function AddLink() + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::AddLink'), $args); + } + + if ($this->_inTpl) { + throw new LogicException('Adding links in templates is not possible!'); + } + + return parent::AddLink(); + } + + /** + * Defines the page and position a link points to. + * + * Overwritten because adding links in a template will not work. + * + * @see http://fpdf.org/en/doc/setlink.htm + * @see http://www.tcpdf.org/doc/code/classTCPDF.html#ace5be60e7857953ea5e2b89cb90df0ae + */ + public function SetLink($link, $y = 0, $page = -1) + { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::SetLink'), $args); + } + + if ($this->_inTpl) { + throw new LogicException('Setting links in templates is not possible!'); + } + + parent::SetLink($link, $y, $page); + } + + /** + * Writes the form XObjects to the PDF document. + */ + protected function _putformxobjects() + { + $filter=($this->compress) ? '/Filter /FlateDecode ' : ''; + reset($this->_tpls); + + foreach($this->_tpls AS $tplIdx => $tpl) { + $this->_newobj(); + $this->_tpls[$tplIdx]['n'] = $this->n; + $this->_out('<<'.$filter.'/Type /XObject'); + $this->_out('/Subtype /Form'); + $this->_out('/FormType 1'); + $this->_out(sprintf('/BBox [%.2F %.2F %.2F %.2F]', + // llx + $tpl['x'] * $this->k, + // lly + -$tpl['y'] * $this->k, + // urx + ($tpl['w'] + $tpl['x']) * $this->k, + // ury + ($tpl['h'] - $tpl['y']) * $this->k + )); + + if ($tpl['x'] != 0 || $tpl['y'] != 0) { + $this->_out(sprintf('/Matrix [1 0 0 1 %.5F %.5F]', + -$tpl['x'] * $this->k * 2, $tpl['y'] * $this->k * 2 + )); + } + + $this->_out('/Resources '); + $this->_out('<_res['tpl'][$tplIdx])) { + $res = $this->_res['tpl'][$tplIdx]; + if (isset($res['fonts']) && count($res['fonts'])) { + $this->_out('/Font <<'); + + foreach($res['fonts'] as $font) { + $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + } + + $this->_out('>>'); + } + + if(isset($res['images']) || isset($res['tpls'])) { + $this->_out('/XObject <<'); + + if (isset($res['images'])) { + foreach($res['images'] as $image) + $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + + if (isset($res['tpls'])) { + foreach($res['tpls'] as $i => $_tpl) + $this->_out($this->tplPrefix . $i . ' ' . $_tpl['n'] . ' 0 R'); + } + + $this->_out('>>'); + } + } + + $this->_out('>>'); + + $buffer = ($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer']; + $this->_out('/Length ' . strlen($buffer) . ' >>'); + $this->_putstream($buffer); + $this->_out('endobj'); + } + } + + /** + * Output images. + * + * Overwritten to add {@link _putformxobjects()} after _putimages(). + */ + public function _putimages() + { + parent::_putimages(); + $this->_putformxobjects(); + } + + /** + * Writes the references of XObject resources to the document. + * + * Overwritten to add the the templates to the XObject resource dictionary. + */ + public function _putxobjectdict() + { + parent::_putxobjectdict(); + + foreach($this->_tpls as $tplIdx => $tpl) { + $this->_out(sprintf('%s%d %d 0 R', $this->tplPrefix, $tplIdx, $tpl['n'])); + } + } + + /** + * Writes bytes to the resulting document. + * + * Overwritten to delegate the data to the template buffer. + * + * @param string $s + */ + public function _out($s) + { + if ($this->state == 2 && $this->_inTpl) { + $this->_tpls[$this->tpl]['buffer'] .= $s . "\n"; + } else { + parent::_out($s); + } + } +} diff --git a/include/fpdi/fpdi.php b/include/fpdi/fpdi.php index 0ae2c637..6adcfa00 100644 --- a/include/fpdi/fpdi.php +++ b/include/fpdi/fpdi.php @@ -1,8 +1,8 @@ current_filename = $filename; + public function setSourceFile($filename) + { + $_filename = realpath($filename); + if (false !== $_filename) + $filename = $_filename; + + $this->currentFilename = $filename; - if (!isset($this->parsers[$filename])) + if (!isset($this->parsers[$filename])) { $this->parsers[$filename] = $this->_getPdfParser($filename); - $this->current_parser =& $this->parsers[$filename]; + $this->setPdfVersion( + max($this->getPdfVersion(), $this->parsers[$filename]->getPdfVersion()) + ); + } + + $this->currentParser =& $this->parsers[$filename]; return $this->parsers[$filename]->getPageCount(); } @@ -99,53 +122,80 @@ class FPDI extends FPDF_TPL { * @param string $filename * @return fpdi_pdf_parser */ - function _getPdfParser($filename) { - return new fpdi_pdf_parser($filename, $this); + protected function _getPdfParser($filename) + { + require_once('fpdi_pdf_parser.php'); + return new fpdi_pdf_parser($filename); } /** - * Get the current PDF version + * Get the current PDF version. * * @return string */ - function getPDFVersion() { + public function getPdfVersion() + { return $this->PDFVersion; } /** - * Set the PDF version + * Set the PDF version. * - * @return string + * @param string $version */ - function setPDFVersion($version = '1.3') { + public function setPdfVersion($version = '1.3') + { $this->PDFVersion = $version; } /** - * Import a page + * Import a page. * - * @param int $pageno pagenumber - * @return int Index of imported page - to use with fpdf_tpl::useTemplate() + * The second parameter defines the bounding box that should be used to transform the page into a + * form XObject. + * + * Following values are available: MediaBox, CropBox, BleedBox, TrimBox, ArtBox. + * If a box is not especially defined its default box will be used: + * + * + * + * It is possible to get the used page box by the {@link getLastUsedPageBox()} method. + * + * @param int $pageNo The page number + * @param string $boxName The boundary box to use when transforming the page into a form XObject + * @param boolean $groupXObject Define the form XObject as a group XObject to support transparency (if used) + * @return int An id of the imported page/template to use with e.g. fpdf_tpl::useTemplate() + * @throws LogicException|InvalidArgumentException + * @see getLastUsedPageBox() */ - function importPage($pageno, $boxName = '/CropBox') { - if ($this->_intpl) { - return $this->error('Please import the desired pages before creating a new template.'); + public function importPage($pageNo, $boxName = 'CropBox', $groupXObject = true) + { + if ($this->_inTpl) { + throw new LogicException('Please import the desired pages before creating a new template.'); } - $fn = $this->current_filename; - - // check if page already imported - $pageKey = $fn . '-' . ((int)$pageno) . $boxName; - if (isset($this->_importedPages[$pageKey])) - return $this->_importedPages[$pageKey]; - - $parser =& $this->parsers[$fn]; - $parser->setPageno($pageno); + $fn = $this->currentFilename; + $boxName = '/' . ltrim($boxName, '/'); - if (!in_array($boxName, $parser->availableBoxes)) - return $this->Error(sprintf('Unknown box: %s', $boxName)); + // check if page already imported + $pageKey = $fn . '-' . ((int)$pageNo) . $boxName; + if (isset($this->_importedPages[$pageKey])) { + return $this->_importedPages[$pageKey]; + } + + $parser = $this->parsers[$fn]; + $parser->setPageNo($pageNo); + + if (!in_array($boxName, $parser->availableBoxes)) { + throw new InvalidArgumentException(sprintf('Unknown box: %s', $boxName)); + } - $pageboxes = $parser->getPageBoxes($pageno, $this->k); + $pageBoxes = $parser->getPageBoxes($pageNo, $this->k); /** * MediaBox @@ -154,35 +204,39 @@ class FPDI extends FPDF_TPL { * TrimBox: Default -> CropBox * ArtBox: Default -> CropBox */ - if (!isset($pageboxes[$boxName]) && ($boxName == '/BleedBox' || $boxName == '/TrimBox' || $boxName == '/ArtBox')) + if (!isset($pageBoxes[$boxName]) && ($boxName == '/BleedBox' || $boxName == '/TrimBox' || $boxName == '/ArtBox')) $boxName = '/CropBox'; - if (!isset($pageboxes[$boxName]) && $boxName == '/CropBox') + if (!isset($pageBoxes[$boxName]) && $boxName == '/CropBox') $boxName = '/MediaBox'; - if (!isset($pageboxes[$boxName])) + if (!isset($pageBoxes[$boxName])) return false; $this->lastUsedPageBox = $boxName; - $box = $pageboxes[$boxName]; + $box = $pageBoxes[$boxName]; $this->tpl++; - $this->tpls[$this->tpl] = array(); - $tpl =& $this->tpls[$this->tpl]; - $tpl['parser'] =& $parser; + $this->_tpls[$this->tpl] = array(); + $tpl =& $this->_tpls[$this->tpl]; + $tpl['parser'] = $parser; $tpl['resources'] = $parser->getPageResources(); $tpl['buffer'] = $parser->getContent(); $tpl['box'] = $box; - + $tpl['groupXObject'] = $groupXObject; + if ($groupXObject) { + $this->setPdfVersion(max($this->getPdfVersion(), 1.4)); + } + // To build an array that can be used by PDF_TPL::useTemplate() - $this->tpls[$this->tpl] = array_merge($this->tpls[$this->tpl], $box); + $this->_tpls[$this->tpl] = array_merge($this->_tpls[$this->tpl], $box); - // An imported page will start at 0,0 everytime. Translation will be set in _putformxobjects() + // An imported page will start at 0,0 all the time. Translation will be set in _putformxobjects() $tpl['x'] = 0; $tpl['y'] = 0; // handle rotated pages - $rotation = $parser->getPageRotation($pageno); + $rotation = $parser->getPageRotation($pageNo); $tpl['_rotationAngle'] = 0; if (isset($rotation[1]) && ($angle = $rotation[1] % 360) != 0) { $steps = $angle / 90; @@ -204,18 +258,41 @@ class FPDI extends FPDF_TPL { } /** - * Returns the last used page box + * Returns the last used page boundary box. * - * @return string + * @return string The used boundary box: MediaBox, CropBox, BleedBox, TrimBox or ArtBox */ - function getLastUsedPageBox() { + public function getLastUsedPageBox() + { return $this->lastUsedPageBox; } - - - function useTemplate($tplidx, $_x = null, $_y = null, $_w = 0, $_h = 0, $adjustPageSize = false) { - if ($adjustPageSize == true && is_null($_x) && is_null($_y)) { - $size = $this->getTemplateSize($tplidx, $_w, $_h); + + /** + * Use a template or imported page in current page or other template. + * + * You can use a template in a page or in another template. + * You can give the used template a new size. All parameters are optional. + * The width or height is calculated automatically if one is given. If no + * parameter is given the origin size as defined in beginTemplate() or of + * the imported page is used. + * + * The calculated or used width and height are returned as an array. + * + * @param int $tplIdx A valid template-id + * @param int $x The x-position + * @param int $y The y-position + * @param int $w The new width of the template + * @param int $h The new height of the template + * @param boolean $adjustPageSize If set to true the current page will be resized to fit the dimensions + * of the template + * + * @return array The height and width of the template (array('w' => ..., 'h' => ...)) + * @throws LogicException|InvalidArgumentException + */ + public function useTemplate($tplIdx, $x = null, $y = null, $w = 0, $h = 0, $adjustPageSize = false) + { + if ($adjustPageSize == true && is_null($x) && is_null($y)) { + $size = $this->getTemplateSize($tplIdx, $w, $h); $orientation = $size['w'] > $size['h'] ? 'L' : 'P'; $size = array($size['w'], $size['h']); @@ -224,22 +301,21 @@ class FPDI extends FPDF_TPL { } else { $size = $this->_getpagesize($size); - if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1]) - { + if($orientation != $this->CurOrientation || + $size[0] != $this->CurPageSize[0] || + $size[1] != $this->CurPageSize[1] + ) { // New size or orientation - if($orientation=='P') - { + if ($orientation=='P') { $this->w = $size[0]; $this->h = $size[1]; - } - else - { + } else { $this->w = $size[1]; $this->h = $size[0]; } - $this->wPt = $this->w*$this->k; - $this->hPt = $this->h*$this->k; - $this->PageBreakTrigger = $this->h-$this->bMargin; + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + $this->PageBreakTrigger = $this->h - $this->bMargin; $this->CurOrientation = $orientation; $this->CurPageSize = $size; $this->PageSizes[$this->page] = array($this->wPt, $this->hPt); @@ -248,54 +324,53 @@ class FPDI extends FPDF_TPL { } $this->_out('q 0 J 1 w 0 j 0 G 0 g'); // reset standard values - $s = parent::useTemplate($tplidx, $_x, $_y, $_w, $_h); + $size = parent::useTemplate($tplIdx, $x, $y, $w, $h); $this->_out('Q'); - return $s; + return $size; } /** - * Private method, that rebuilds all needed objects of source files + * Copy all imported objects to the resulting document. */ - function _putimportedobjects() { - if (is_array($this->parsers) && count($this->parsers) > 0) { - foreach($this->parsers AS $filename => $p) { - $this->current_parser =& $this->parsers[$filename]; - if (isset($this->_obj_stack[$filename]) && is_array($this->_obj_stack[$filename])) { - while(($n = key($this->_obj_stack[$filename])) !== null) { - $nObj = $this->current_parser->pdf_resolve_object($this->current_parser->c, $this->_obj_stack[$filename][$n][1]); - - $this->_newobj($this->_obj_stack[$filename][$n][0]); - - if ($nObj[0] == PDF_TYPE_STREAM) { - $this->pdf_write_value($nObj); - } else { - $this->pdf_write_value($nObj[1]); - } - - $this->_out('endobj'); - $this->_obj_stack[$filename][$n] = null; // free memory - unset($this->_obj_stack[$filename][$n]); - reset($this->_obj_stack[$filename]); - } + protected function _putimportedobjects() + { + foreach($this->parsers AS $filename => $p) { + $this->currentParser =& $p; + if (!isset($this->_objStack[$filename]) || !is_array($this->_objStack[$filename])) { + continue; + } + while(($n = key($this->_objStack[$filename])) !== null) { + $nObj = $this->currentParser->resolveObject($this->_objStack[$filename][$n][1]); + + $this->_newobj($this->_objStack[$filename][$n][0]); + + if ($nObj[0] == pdf_parser::TYPE_STREAM) { + $this->_writeValue($nObj); + } else { + $this->_writeValue($nObj[1]); } + + $this->_out("\nendobj"); + $this->_objStack[$filename][$n] = null; // free memory + unset($this->_objStack[$filename][$n]); + reset($this->_objStack[$filename]); } } } - - + /** - * Private Method that writes the form xobjects + * Writes the form XObjects to the PDF document. */ - function _putformxobjects() { - $filter=($this->compress) ? '/Filter /FlateDecode ' : ''; - reset($this->tpls); - foreach($this->tpls AS $tplidx => $tpl) { - $p=($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer']; - $this->_newobj(); - $cN = $this->n; // TCPDF/Protection: rem current "n" + protected function _putformxobjects() + { + $filter = ($this->compress) ? '/Filter /FlateDecode ' : ''; + reset($this->_tpls); + foreach($this->_tpls AS $tplIdx => $tpl) { + $this->_newobj(); + $currentN = $this->n; // TCPDF/Protection: rem current "n" - $this->tpls[$tplidx]['n'] = $this->n; + $this->_tpls[$tplIdx]['n'] = $this->n; $this->_out('<<' . $filter . '/Type /XObject'); $this->_out('/Subtype /Form'); $this->_out('/FormType 1'); @@ -318,8 +393,8 @@ class FPDI extends FPDF_TPL { if ($tpl['_rotationAngle'] <> 0) { $angle = $tpl['_rotationAngle'] * M_PI/180; - $c=cos($angle); - $s=sin($angle); + $c = cos($angle); + $s = sin($angle); switch($tpl['_rotationAngle']) { case -90: @@ -353,87 +428,107 @@ class FPDI extends FPDF_TPL { $this->_out('/Resources '); if (isset($tpl['resources'])) { - $this->current_parser =& $tpl['parser']; - $this->pdf_write_value($tpl['resources']); // "n" will be changed + $this->currentParser = $tpl['parser']; + $this->_writeValue($tpl['resources']); // "n" will be changed } else { + $this->_out('<_res['tpl'][$tplidx]['fonts']) && count($this->_res['tpl'][$tplidx]['fonts'])) { - $this->_out('/Font <<'); - foreach($this->_res['tpl'][$tplidx]['fonts'] as $font) - $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); - $this->_out('>>'); - } - if(isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images']) || - isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) - { - $this->_out('/XObject <<'); - if (isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images'])) { - foreach($this->_res['tpl'][$tplidx]['images'] as $image) - $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + if (isset($this->_res['tpl'][$tplIdx])) { + $res = $this->_res['tpl'][$tplIdx]; + + if (isset($res['fonts']) && count($res['fonts'])) { + $this->_out('/Font <<'); + foreach ($res['fonts'] as $font) + $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + $this->_out('>>'); } - if (isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) { - foreach($this->_res['tpl'][$tplidx]['tpls'] as $i => $tpl) - $this->_out($this->tplprefix . $i . ' ' . $tpl['n'] . ' 0 R'); + if (isset($res['images']) && count($res['images']) || + isset($res['tpls']) && count($res['tpls'])) + { + $this->_out('/XObject <<'); + if (isset($res['images'])) { + foreach ($res['images'] as $image) + $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + if (isset($res['tpls'])) { + foreach ($res['tpls'] as $i => $_tpl) + $this->_out($this->tplPrefix . $i . ' ' . $_tpl['n'] . ' 0 R'); + } + $this->_out('>>'); } $this->_out('>>'); - } - $this->_out('>>'); + } } - $nN = $this->n; // TCPDF: rem new "n" - $this->n = $cN; // TCPDF: reset to current "n" + if (isset($tpl['groupXObject']) && $tpl['groupXObject']) { + $this->_out('/Group <>'); + } + + $newN = $this->n; // TCPDF: rem new "n" + $this->n = $currentN; // TCPDF: reset to current "n" + + $buffer = ($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer']; + if (is_subclass_of($this, 'TCPDF')) { - $p = $this->_getrawstream($p); - $this->_out('/Length ' . strlen($p) . ' >>'); - $this->_out("stream\n" . $p . "\nendstream"); + $buffer = $this->_getrawstream($buffer); + $this->_out('/Length ' . strlen($buffer) . ' >>'); + $this->_out("stream\n" . $buffer . "\nendstream"); } else { - $this->_out('/Length ' . strlen($p) . ' >>'); - $this->_putstream($p); + $this->_out('/Length ' . strlen($buffer) . ' >>'); + $this->_putstream($buffer); } $this->_out('endobj'); - $this->n = $nN; // TCPDF: reset to new "n" + $this->n = $newN; // TCPDF: reset to new "n" } $this->_putimportedobjects(); } /** + * Creates and optionally write the object definition to the document. + * * Rewritten to handle existing own defined objects + * + * @param bool $objId + * @param bool $onlyNewObj + * @return bool|int */ - function _newobj($obj_id = false, $onlynewobj = false) { - if (!$obj_id) { - $obj_id = ++$this->n; + public function _newobj($objId = false, $onlyNewObj = false) + { + if (!$objId) { + $objId = ++$this->n; } //Begin a new object - if (!$onlynewobj) { - $this->offsets[$obj_id] = is_subclass_of($this, 'TCPDF') ? $this->bufferlen : strlen($this->buffer); - $this->_out($obj_id . ' 0 obj'); - $this->_current_obj_id = $obj_id; // for later use with encryption + if (!$onlyNewObj) { + $this->offsets[$objId] = is_subclass_of($this, 'TCPDF') ? $this->bufferlen : strlen($this->buffer); + $this->_out($objId . ' 0 obj'); + $this->_currentObjId = $objId; // for later use with encryption } - return $obj_id; + return $objId; } /** - * Writes a value + * Writes a PDF value to the resulting document. + * * Needed to rebuild the source document * * @param mixed $value A PDF-Value. Structure of values see cases in this method */ - function pdf_write_value(&$value) + protected function _writeValue(&$value) { if (is_subclass_of($this, 'TCPDF')) { - parent::pdf_write_value($value); + parent::_prepareValue($value); } switch ($value[0]) { - case PDF_TYPE_TOKEN: + case pdf_parser::TYPE_TOKEN: $this->_straightOut($value[1] . ' '); break; - case PDF_TYPE_NUMERIC: - case PDF_TYPE_REAL: + case pdf_parser::TYPE_NUMERIC: + case pdf_parser::TYPE_REAL: if (is_float($value[1]) && $value[1] != 0) { $this->_straightOut(rtrim(rtrim(sprintf('%F', $value[1]), '0'), '.') . ' '); } else { @@ -441,20 +536,20 @@ class FPDI extends FPDF_TPL { } break; - case PDF_TYPE_ARRAY: + case pdf_parser::TYPE_ARRAY: // An array. Output the proper // structure and move on. $this->_straightOut('['); for ($i = 0; $i < count($value[1]); $i++) { - $this->pdf_write_value($value[1][$i]); + $this->_writeValue($value[1][$i]); } $this->_out(']'); break; - case PDF_TYPE_DICTIONARY: + case pdf_parser::TYPE_DICTIONARY: // A dictionary. $this->_straightOut('<<'); @@ -463,55 +558,54 @@ class FPDI extends FPDF_TPL { while (list($k, $v) = each($value[1])) { $this->_straightOut($k . ' '); - $this->pdf_write_value($v); + $this->_writeValue($v); } $this->_straightOut('>>'); break; - case PDF_TYPE_OBJREF: + case pdf_parser::TYPE_OBJREF: // An indirect object reference // Fill the object stack if needed - $cpfn =& $this->current_parser->filename; - - if (!isset($this->_don_obj_stack[$cpfn][$value[1]])) { + $cpfn =& $this->currentParser->filename; + if (!isset($this->_doneObjStack[$cpfn][$value[1]])) { $this->_newobj(false, true); - $this->_obj_stack[$cpfn][$value[1]] = array($this->n, $value); - $this->_don_obj_stack[$cpfn][$value[1]] = array($this->n, $value); // Value is maybee obsolete!!! + $this->_objStack[$cpfn][$value[1]] = array($this->n, $value); + $this->_doneObjStack[$cpfn][$value[1]] = array($this->n, $value); } - $objid = $this->_don_obj_stack[$cpfn][$value[1]][0]; + $objId = $this->_doneObjStack[$cpfn][$value[1]][0]; - $this->_out($objid . ' 0 R'); + $this->_out($objId . ' 0 R'); break; - case PDF_TYPE_STRING: + case pdf_parser::TYPE_STRING: // A string. $this->_straightOut('(' . $value[1] . ')'); break; - case PDF_TYPE_STREAM: + case pdf_parser::TYPE_STREAM: // A stream. First, output the // stream dictionary, then the // stream data itself. - $this->pdf_write_value($value[1]); + $this->_writeValue($value[1]); $this->_out('stream'); $this->_out($value[2][1]); - $this->_out('endstream'); + $this->_straightOut("endstream"); break; - case PDF_TYPE_HEX: + case pdf_parser::TYPE_HEX: $this->_straightOut('<' . $value[1] . '>'); break; - case PDF_TYPE_BOOLEAN: + case pdf_parser::TYPE_BOOLEAN: $this->_straightOut($value[1] ? 'true ' : 'false '); break; - case PDF_TYPE_NULL: + case pdf_parser::TYPE_NULL: // The null object. $this->_straightOut('null '); @@ -521,51 +615,77 @@ class FPDI extends FPDF_TPL { /** - * Modified so not each call will add a newline to the output. + * Modified _out() method so not each call will add a newline to the output. */ - function _straightOut($s) { + protected function _straightOut($s) + { if (!is_subclass_of($this, 'TCPDF')) { - if($this->state==2) + if ($this->state == 2) { $this->pages[$this->page] .= $s; - else + } else { $this->buffer .= $s; + } + } else { if ($this->state == 2) { - if (isset($this->footerlen[$this->page]) AND ($this->footerlen[$this->page] > 0)) { + if ($this->inxobj) { + // we are inside an XObject template + $this->xobjects[$this->xobjid]['outdata'] .= $s; + } else if ((!$this->InFooter) AND isset($this->footerlen[$this->page]) AND ($this->footerlen[$this->page] > 0)) { // puts data before page footer - $page = substr($this->getPageBuffer($this->page), 0, -$this->footerlen[$this->page]); - $footer = substr($this->getPageBuffer($this->page), -$this->footerlen[$this->page]); - $this->setPageBuffer($this->page, $page . ' ' . $s . "\n" . $footer); + $pagebuff = $this->getPageBuffer($this->page); + $page = substr($pagebuff, 0, -$this->footerlen[$this->page]); + $footer = substr($pagebuff, -$this->footerlen[$this->page]); + $this->setPageBuffer($this->page, $page . $s . $footer); + // update footer position + $this->footerpos[$this->page] += strlen($s); } else { + // set page data $this->setPageBuffer($this->page, $s, true); } - } else { + } else if ($this->state > 0) { + // set general data $this->setBuffer($s); } } } /** - * rewritten to close opened parsers + * Ends the document * + * Overwritten to close opened parsers */ - function _enddoc() { + public function _enddoc() + { parent::_enddoc(); $this->_closeParsers(); } /** - * close all files opened by parsers + * Close all files opened by parsers. + * + * @return boolean */ - function _closeParsers() { - if ($this->state > 2 && count($this->parsers) > 0) { - foreach ($this->parsers as $k => $_){ - $this->parsers[$k]->closeFile(); - $this->parsers[$k] = null; - unset($this->parsers[$k]); - } + protected function _closeParsers() + { + if ($this->state > 2) { + $this->cleanUp(); return true; } + return false; } + + /** + * Removes cycled references and closes the file handles of the parser objects. + */ + public function cleanUp() + { + while (($parser = array_pop($this->parsers)) !== null) { + /** + * @var fpdi_pdf_parser $parser + */ + $parser->closeFile(); + } + } } \ No newline at end of file diff --git a/include/fpdi/fpdi_bridge.php b/include/fpdi/fpdi_bridge.php new file mode 100644 index 00000000..5ec4d677 --- /dev/null +++ b/include/fpdi/fpdi_bridge.php @@ -0,0 +1,215 @@ +_tpls as $tplIdx => $tpl) { + $out .= sprintf('%s%d %d 0 R', $this->tplPrefix, $tplIdx, $tpl['n']); + } + + return $out; + } + + /** + * Writes a PDF value to the resulting document. + * + * Prepares the value for encryption of imported data by FPDI + * + * @param array $value + */ + protected function _prepareValue(&$value) + { + switch ($value[0]) { + case pdf_parser::TYPE_STRING: + if ($this->encrypted) { + $value[1] = $this->_unescape($value[1]); + $value[1] = $this->_encrypt_data($this->_currentObjId, $value[1]); + $value[1] = TCPDF_STATIC::_escape($value[1]); + } + break; + + case pdf_parser::TYPE_STREAM: + if ($this->encrypted) { + $value[2][1] = $this->_encrypt_data($this->_currentObjId, $value[2][1]); + $value[1][1]['/Length'] = array( + pdf_parser::TYPE_NUMERIC, + strlen($value[2][1]) + ); + } + break; + + case pdf_parser::TYPE_HEX: + if ($this->encrypted) { + $value[1] = $this->hex2str($value[1]); + $value[1] = $this->_encrypt_data($this->_currentObjId, $value[1]); + + // remake hexstring of encrypted string + $value[1] = $this->str2hex($value[1]); + } + break; + } + } + + /** + * Un-escapes a PDF string + * + * @param string $s + * @return string + */ + protected function _unescape($s) + { + $out = ''; + for ($count = 0, $n = strlen($s); $count < $n; $count++) { + if ($s[$count] != '\\' || $count == $n-1) { + $out .= $s[$count]; + } else { + switch ($s[++$count]) { + case ')': + case '(': + case '\\': + $out .= $s[$count]; + break; + case 'f': + $out .= chr(0x0C); + break; + case 'b': + $out .= chr(0x08); + break; + case 't': + $out .= chr(0x09); + break; + case 'r': + $out .= chr(0x0D); + break; + case 'n': + $out .= chr(0x0A); + break; + case "\r": + if ($count != $n-1 && $s[$count+1] == "\n") + $count++; + break; + case "\n": + break; + default: + // Octal-Values + if (ord($s[$count]) >= ord('0') && + ord($s[$count]) <= ord('9')) { + $oct = ''. $s[$count]; + + if (ord($s[$count+1]) >= ord('0') && + ord($s[$count+1]) <= ord('9')) { + $oct .= $s[++$count]; + + if (ord($s[$count+1]) >= ord('0') && + ord($s[$count+1]) <= ord('9')) { + $oct .= $s[++$count]; + } + } + + $out .= chr(octdec($oct)); + } else { + $out .= $s[$count]; + } + } + } + } + return $out; + } + + /** + * Hexadecimal to string + * + * @param string $data + * @return string + */ + public function hex2str($data) + { + $data = preg_replace('/[^0-9A-Fa-f]/', '', rtrim($data, '>')); + if ((strlen($data) % 2) == 1) { + $data .= '0'; + } + + return pack('H*', $data); + } + + /** + * String to hexadecimal + * + * @param string $str + * @return string + */ + public function str2hex($str) + { + return current(unpack('H*', $str)); + } + } +} \ No newline at end of file diff --git a/include/fpdi/fpdi_pdf_parser.php b/include/fpdi/fpdi_pdf_parser.php index fd2b4414..882f2a65 100644 --- a/include/fpdi/fpdi_pdf_parser.php +++ b/include/fpdi/fpdi_pdf_parser.php @@ -1,8 +1,8 @@ fpdi =& $fpdi; - - parent::pdf_parser($filename); + public function __construct($filename) + { + parent::__construct($filename); // resolve Pages-Dictonary - $pages = $this->pdf_resolve_object($this->c, $this->root[1][1]['/Pages']); + $pages = $this->resolveObject($this->_root[1][1]['/Pages']); // Read pages - $this->read_pages($this->c, $pages, $this->pages); + $this->_readPages($pages, $this->_pages); // count pages; - $this->page_count = count($this->pages); + $this->_pageCount = count($this->_pages); } /** - * Overwrite parent::error() - * - * @param string $msg Error-Message - */ - function error($msg) { - $this->fpdi->error($msg); - } - - /** - * Get pagecount from sourcefile + * Get page count from source file. * * @return int */ - function getPageCount() { - return $this->page_count; + public function getPageCount() + { + return $this->_pageCount; } - /** - * Set pageno + * Set the page number. * - * @param int $pageno Pagenumber to use + * @param int $pageNo Page number to use + * @throws InvalidArgumentException */ - function setPageno($pageno) { - $pageno = ((int) $pageno) - 1; + public function setPageNo($pageNo) + { + $pageNo = ((int) $pageNo) - 1; - if ($pageno < 0 || $pageno >= $this->getPageCount()) { - $this->fpdi->error('Pagenumber is wrong!'); + if ($pageNo < 0 || $pageNo >= $this->getPageCount()) { + throw new InvalidArgumentException('Invalid page number!'); } - $this->pageno = $pageno; + $this->pageNo = $pageNo; } /** * Get page-resources from current page * - * @return array + * @return array|boolean */ - function getPageResources() { - return $this->_getPageResources($this->pages[$this->pageno]); + public function getPageResources() + { + return $this->_getPageResources($this->_pages[$this->pageNo]); } /** - * Get page-resources from /Page + * Get page-resources from a /Page dictionary. * * @param array $obj Array of pdf-data + * @return array|boolean */ - function _getPageResources ($obj) { // $obj = /Page - $obj = $this->pdf_resolve_object($this->c, $obj); + protected function _getPageResources($obj) + { + $obj = $this->resolveObject($obj); // If the current object has a resources // dictionary associated with it, we use // it. Otherwise, we move back to its // parent object. - if (isset ($obj[1][1]['/Resources'])) { - $res = $this->pdf_resolve_object($this->c, $obj[1][1]['/Resources']); - if ($res[0] == PDF_TYPE_OBJECT) + if (isset($obj[1][1]['/Resources'])) { + $res = $this->resolveObject($obj[1][1]['/Resources']); + if ($res[0] == pdf_parser::TYPE_OBJECT) return $res[1]; return $res; - } else { - if (!isset ($obj[1][1]['/Parent'])) { - return false; - } else { - $res = $this->_getPageResources($obj[1][1]['/Parent']); - if ($res[0] == PDF_TYPE_OBJECT) - return $res[1]; - return $res; - } } + + if (!isset($obj[1][1]['/Parent'])) { + return false; + } + + $res = $this->_getPageResources($obj[1][1]['/Parent']); + if ($res[0] == pdf_parser::TYPE_OBJECT) + return $res[1]; + return $res; } - /** - * Get content of current page + * Get content of current page. * - * If more /Contents is an array, the streams are concated + * If /Contents is an array, the streams are concatenated * * @return string */ - function getContent() { + public function getContent() + { $buffer = ''; - if (isset($this->pages[$this->pageno][1][1]['/Contents'])) { - $contents = $this->_getPageContent($this->pages[$this->pageno][1][1]['/Contents']); - foreach($contents AS $tmp_content) { - $buffer .= $this->_rebuildContentStream($tmp_content) . ' '; + if (isset($this->_pages[$this->pageNo][1][1]['/Contents'])) { + $contents = $this->_getPageContent($this->_pages[$this->pageNo][1][1]['/Contents']); + foreach ($contents AS $tmpContent) { + $buffer .= $this->_unFilterStream($tmpContent) . ' '; } } return $buffer; } - - + /** - * Resolve all content-objects + * Resolve all content objects. * - * @param array $content_ref + * @param array $contentRef * @return array */ - function _getPageContent($content_ref) { + protected function _getPageContent($contentRef) + { $contents = array(); - if ($content_ref[0] == PDF_TYPE_OBJREF) { - $content = $this->pdf_resolve_object($this->c, $content_ref); - if ($content[1][0] == PDF_TYPE_ARRAY) { + if ($contentRef[0] == pdf_parser::TYPE_OBJREF) { + $content = $this->resolveObject($contentRef); + if ($content[1][0] == pdf_parser::TYPE_ARRAY) { $contents = $this->_getPageContent($content[1]); } else { $contents[] = $content; } - } else if ($content_ref[0] == PDF_TYPE_ARRAY) { - foreach ($content_ref[1] AS $tmp_content_ref) { - $contents = array_merge($contents,$this->_getPageContent($tmp_content_ref)); + } else if ($contentRef[0] == pdf_parser::TYPE_ARRAY) { + foreach ($contentRef[1] AS $tmp_content_ref) { + $contents = array_merge($contents, $this->_getPageContent($tmp_content_ref)); } } return $contents; } - /** - * Rebuild content-streams + * Get a boundary box from a page * - * @param array $obj - * @return string - */ - function _rebuildContentStream($obj) { - $filters = array(); - - if (isset($obj[1][1]['/Filter'])) { - $_filter = $obj[1][1]['/Filter']; - - if ($_filter[0] == PDF_TYPE_OBJREF) { - $tmpFilter = $this->pdf_resolve_object($this->c, $_filter); - $_filter = $tmpFilter[1]; - } - - if ($_filter[0] == PDF_TYPE_TOKEN) { - $filters[] = $_filter; - } else if ($_filter[0] == PDF_TYPE_ARRAY) { - $filters = $_filter[1]; - } - } - - $stream = $obj[2][1]; - - foreach ($filters AS $_filter) { - switch ($_filter[1]) { - case '/FlateDecode': - case '/Fl': - // $stream .= "\x0F\x0D"; // in an errorious stream this suffix could work - // $stream .= "\x0A"; - // $stream .= "\x0D"; - if (function_exists('gzuncompress')) { - $stream = (strlen($stream) > 0) ? @gzuncompress($stream) : ''; - } else { - $this->error(sprintf('To handle %s filter, please compile php with zlib support.',$_filter[1])); - } - - if ($stream === false) { - $this->error('Error while decompressing stream.'); - } - break; - case '/LZWDecode': - include_once('filters/FilterLZW_FPDI.php'); - $decoder = new FilterLZW_FPDI($this->fpdi); - $stream = $decoder->decode($stream); - break; - case '/ASCII85Decode': - include_once('filters/FilterASCII85_FPDI.php'); - $decoder = new FilterASCII85_FPDI($this->fpdi); - $stream = $decoder->decode($stream); - break; - case null: - $stream = $stream; - break; - default: - $this->error(sprintf('Unsupported Filter: %s',$_filter[1])); - } - } - - return $stream; - } - - - /** - * Get a Box from a page - * Arrayformat is same as used by fpdf_tpl + * Array format is same as used by FPDF_TPL. * - * @param array $page a /Page - * @param string $box_index Type of Box @see $availableBoxes + * @param array $page a /Page dictionary + * @param string $boxIndex Type of box {see {@link $availableBoxes}) * @param float Scale factor from user space units to points - * @return array + * + * @return array|boolean */ - function getPageBox($page, $box_index, $k) { - $page = $this->pdf_resolve_object($this->c, $page); + protected function _getPageBox($page, $boxIndex, $k) + { + $page = $this->resolveObject($page); $box = null; - if (isset($page[1][1][$box_index])) - $box =& $page[1][1][$box_index]; + if (isset($page[1][1][$boxIndex])) { + $box = $page[1][1][$boxIndex]; + } - if (!is_null($box) && $box[0] == PDF_TYPE_OBJREF) { - $tmp_box = $this->pdf_resolve_object($this->c, $box); + if (!is_null($box) && $box[0] == pdf_parser::TYPE_OBJREF) { + $tmp_box = $this->resolveObject($box); $box = $tmp_box[1]; } - if (!is_null($box) && $box[0] == PDF_TYPE_ARRAY) { - $b =& $box[1]; - return array('x' => $b[0][1] / $k, - 'y' => $b[1][1] / $k, - 'w' => abs($b[0][1] - $b[2][1]) / $k, - 'h' => abs($b[1][1] - $b[3][1]) / $k, - 'llx' => min($b[0][1], $b[2][1]) / $k, - 'lly' => min($b[1][1], $b[3][1]) / $k, - 'urx' => max($b[0][1], $b[2][1]) / $k, - 'ury' => max($b[1][1], $b[3][1]) / $k, - ); - } else if (!isset ($page[1][1]['/Parent'])) { + if (!is_null($box) && $box[0] == pdf_parser::TYPE_ARRAY) { + $b = $box[1]; + return array( + 'x' => $b[0][1] / $k, + 'y' => $b[1][1] / $k, + 'w' => abs($b[0][1] - $b[2][1]) / $k, + 'h' => abs($b[1][1] - $b[3][1]) / $k, + 'llx' => min($b[0][1], $b[2][1]) / $k, + 'lly' => min($b[1][1], $b[3][1]) / $k, + 'urx' => max($b[0][1], $b[2][1]) / $k, + 'ury' => max($b[1][1], $b[3][1]) / $k, + ); + } else if (!isset($page[1][1]['/Parent'])) { return false; } else { - return $this->getPageBox($this->pdf_resolve_object($this->c, $page[1][1]['/Parent']), $box_index, $k); + return $this->_getPageBox($this->resolveObject($page[1][1]['/Parent']), $boxIndex, $k); } } /** - * Get all page boxes by page no + * Get all page boundary boxes by page number * - * @param int The page number - * @param float Scale factor from user space units to points + * @param int $pageNo The page number + * @param float $k Scale factor from user space units to points * @return array + * @throws InvalidArgumentException */ - function getPageBoxes($pageno, $k) { - return $this->_getPageBoxes($this->pages[$pageno - 1], $k); + public function getPageBoxes($pageNo, $k) + { + if (!isset($this->_pages[$pageNo - 1])) { + throw new InvalidArgumentException('Page ' . $pageNo . ' does not exists.'); + } + + return $this->_getPageBoxes($this->_pages[$pageNo - 1], $k); } /** - * Get all boxes from /Page + * Get all boxes from /Page dictionary * - * @param array a /Page + * @param array $page A /Page dictionary + * @param float $k Scale factor from user space units to points * @return array */ - function _getPageBoxes($page, $k) { + protected function _getPageBoxes($page, $k) + { $boxes = array(); foreach($this->availableBoxes AS $box) { - if ($_box = $this->getPageBox($page, $box, $k)) { + if ($_box = $this->_getPageBox($page, $box, $k)) { $boxes[$box] = $_box; } } @@ -334,75 +276,79 @@ class fpdi_pdf_parser extends pdf_parser { } /** - * Get the page rotation by pageno + * Get the page rotation by page number * - * @param integer $pageno + * @param integer $pageNo + * @throws InvalidArgumentException * @return array */ - function getPageRotation($pageno) { - return $this->_getPageRotation($this->pages[$pageno - 1]); + public function getPageRotation($pageNo) + { + if (!isset($this->_pages[$pageNo - 1])) { + throw new InvalidArgumentException('Page ' . $pageNo . ' does not exists.'); + } + + return $this->_getPageRotation($this->_pages[$pageNo - 1]); } - - function _getPageRotation($obj) { // $obj = /Page - $obj = $this->pdf_resolve_object($this->c, $obj); - if (isset ($obj[1][1]['/Rotate'])) { - $res = $this->pdf_resolve_object($this->c, $obj[1][1]['/Rotate']); - if ($res[0] == PDF_TYPE_OBJECT) + + /** + * Get the rotation value of a page + * + * @param array $obj A /Page dictionary + * @return array|bool + */ + protected function _getPageRotation($obj) + { + $obj = $this->resolveObject($obj); + if (isset($obj[1][1]['/Rotate'])) { + $res = $this->resolveObject($obj[1][1]['/Rotate']); + if ($res[0] == pdf_parser::TYPE_OBJECT) return $res[1]; return $res; - } else { - if (!isset ($obj[1][1]['/Parent'])) { - return false; - } else { - $res = $this->_getPageRotation($obj[1][1]['/Parent']); - if ($res[0] == PDF_TYPE_OBJECT) - return $res[1]; - return $res; - } } - } - - /** - * Read all /Page(es) - * - * @param object pdf_context - * @param array /Pages - * @param array the result-array - */ - function read_pages(&$c, &$pages, &$result) { - // Get the kids dictionary - $_kids = $this->pdf_resolve_object ($c, $pages[1][1]['/Kids']); - - if (!is_array($_kids)) - $this->error('Cannot find /Kids in current /Page-Dictionary'); - - if ($_kids[1][0] == PDF_TYPE_ARRAY) { - $kids = $_kids[1][1]; - } else { - $kids = $_kids[1]; + + if (!isset($obj[1][1]['/Parent'])) { + return false; } - + + $res = $this->_getPageRotation($obj[1][1]['/Parent']); + if ($res[0] == pdf_parser::TYPE_OBJECT) + return $res[1]; + + return $res; + } + + /** + * Read all pages + * + * @param array $pages /Pages dictionary + * @param array $result The result array + * @throws Exception + */ + protected function _readPages(&$pages, &$result) + { + // Get the kids dictionary + $_kids = $this->resolveObject($pages[1][1]['/Kids']); + + if (!is_array($_kids)) { + throw new Exception('Cannot find /Kids in current /Page-Dictionary'); + } + + if ($_kids[0] === self::TYPE_OBJECT) { + $_kids = $_kids[1]; + } + + $kids = $_kids[1]; + foreach ($kids as $v) { - $pg = $this->pdf_resolve_object ($c, $v); + $pg = $this->resolveObject($v); if ($pg[1][1]['/Type'][1] === '/Pages') { // If one of the kids is an embedded // /Pages array, resolve it as well. - $this->read_pages($c, $pg, $result); + $this->_readPages($pg, $result); } else { $result[] = $pg; } } } - - - - /** - * Get PDF-Version - * - * And reset the PDF Version used in FPDI if needed - */ - function getPDFVersion() { - parent::getPDFVersion(); - $this->fpdi->setPDFVersion(max($this->fpdi->getPDFVersion(), $this->pdfVersion)); - } } \ No newline at end of file diff --git a/include/fpdi/pdf_context.php b/include/fpdi/pdf_context.php index 435df1e8..84b7e5f9 100644 --- a/include/fpdi/pdf_context.php +++ b/include/fpdi/pdf_context.php @@ -1,8 +1,8 @@ file =& $f; - if (is_string($this->file)) - $this->_mode = 1; - $this->reset(); - } - - // Optionally move the file - // pointer to a new location - // and reset the buffered data - - function reset($pos = null, $l = 100) { - if ($this->_mode == 0) { - if (!is_null ($pos)) { - fseek ($this->file, $pos); - } - - $this->buffer = $l > 0 ? fread($this->file, $l) : ''; - $this->length = strlen($this->buffer); - if ($this->length < $l) - $this->increase_length($l - $this->length); - } else { - $this->buffer = $this->file; - $this->length = strlen($this->buffer); - } - $this->offset = 0; - $this->stack = array(); - } - - // Make sure that there is at least one - // character beyond the current offset in - // the buffer to prevent the tokenizer - // from attempting to access data that does - // not exist - - function ensure_content() { - if ($this->offset >= $this->length - 1) { - return $this->increase_length(); - } else { - return true; - } - } - - // Forcefully read more data into the buffer - - function increase_length($l = 100) { - if ($this->_mode == 0 && feof($this->file)) { - return false; - } else if ($this->_mode == 0) { - $totalLength = $this->length + $l; - do { - $toRead = $totalLength - $this->length; - if ($toRead < 1) - break; - - $this->buffer .= fread($this->file, $toRead); - } while ((($this->length = strlen($this->buffer)) != $totalLength) && !feof($this->file)); - - return true; - } else { - return false; - } - } +/** + * Class pdf_context + */ +class pdf_context +{ + /** + * Mode + * + * @var integer 0 = file | 1 = string + */ + protected $_mode = 0; + + /** + * @var resource|string + */ + public $file; + + /** + * @var string + */ + public $buffer; + + /** + * @var integer + */ + public $offset; + + /** + * @var integer + */ + public $length; + + /** + * @var array + */ + public $stack; + + /** + * The constructor + * + * @param resource $f + */ + public function __construct(&$f) + { + $this->file =& $f; + if (is_string($this->file)) + $this->_mode = 1; + + $this->reset(); + } + + /** + * Get the position in the file stream + * + * @return int + */ + public function getPos() + { + if ($this->_mode == 0) { + return ftell($this->file); + } else { + return 0; + } + } + + /** + * Reset the position in the file stream. + * + * Optionally move the file pointer to a new location and reset the buffered data. + * + * @param null $pos + * @param int $l + */ + public function reset($pos = null, $l = 100) + { + if ($this->_mode == 0) { + if (!is_null($pos)) { + fseek ($this->file, $pos); + } + + $this->buffer = $l > 0 ? fread($this->file, $l) : ''; + $this->length = strlen($this->buffer); + if ($this->length < $l) + $this->increaseLength($l - $this->length); + } else { + $this->buffer = $this->file; + $this->length = strlen($this->buffer); + } + $this->offset = 0; + $this->stack = array(); + } + + /** + * Make sure that there is at least one character beyond the current offset in the buffer. + * + * To prevent the tokenizer from attempting to access data that does not exist. + * + * @return bool + */ + public function ensureContent() + { + if ($this->offset >= $this->length - 1) { + return $this->increaseLength(); + } else { + return true; + } + } + + /** + * Forcefully read more data into the buffer + * + * @param int $l + * @return bool + */ + public function increaseLength($l = 100) + { + if ($this->_mode == 0 && feof($this->file)) { + return false; + } else if ($this->_mode == 0) { + $totalLength = $this->length + $l; + do { + $toRead = $totalLength - $this->length; + if ($toRead < 1) + break; + + $this->buffer .= fread($this->file, $toRead); + } while ((($this->length = strlen($this->buffer)) != $totalLength) && !feof($this->file)); + + return true; + } else { + return false; + } } } \ No newline at end of file diff --git a/include/fpdi/pdf_parser.php b/include/fpdi/pdf_parser.php index 4c6dd8e8..9af4762c 100644 --- a/include/fpdi/pdf_parser.php +++ b/include/fpdi/pdf_parser.php @@ -1,8 +1,8 @@ filename = $filename; - - $this->f = @fopen($this->filename, 'rb'); - - if (!$this->f) - $this->error(sprintf('Cannot open %s !', $filename)); - - $this->getPDFVersion(); - - $this->c = new pdf_context($this->f); - - // Read xref-Data - $this->xref = array(); - $this->pdf_read_xref($this->xref, $this->pdf_find_xref()); - - // Check for Encryption - $this->getEncryption(); - - // Read root - $this->pdf_read_root(); + /** + * Type constant + * + * @var integer + */ + const TYPE_NUMERIC = 1; + + /** + * Type constant + * + * @var integer + */ + const TYPE_TOKEN = 2; + + /** + * Type constant + * + * @var integer + */ + const TYPE_HEX = 3; + + /** + * Type constant + * + * @var integer + */ + const TYPE_STRING = 4; + + /** + * Type constant + * + * @var integer + */ + const TYPE_DICTIONARY = 5; + + /** + * Type constant + * + * @var integer + */ + const TYPE_ARRAY = 6; + + /** + * Type constant + * + * @var integer + */ + const TYPE_OBJDEC = 7; + + /** + * Type constant + * + * @var integer + */ + const TYPE_OBJREF = 8; + + /** + * Type constant + * + * @var integer + */ + const TYPE_OBJECT = 9; + + /** + * Type constant + * + * @var integer + */ + const TYPE_STREAM = 10; + + /** + * Type constant + * + * @var integer + */ + const TYPE_BOOLEAN = 11; + + /** + * Type constant + * + * @var integer + */ + const TYPE_REAL = 12; + + /** + * Define the amount of byte in which the initial keyword of a PDF document should be searched. + * + * @var int + */ + static public $searchForStartxrefLength = 5500; + + /** + * Filename + * + * @var string + */ + public $filename; + + /** + * File resource + * + * @var resource + */ + protected $_f; + + /** + * PDF Context + * + * @var pdf_context + */ + protected $_c; + + /** + * xref-Data + * + * @var array + */ + protected $_xref; + + /** + * Data of the Root object + * + * @var array + */ + protected $_root; + + /** + * PDF version of the loaded document + * + * @var string + */ + protected $_pdfVersion; + + /** + * For reading encrypted documents and xref/object streams are in use + * + * @var boolean + */ + protected $_readPlain = true; + + /** + * The current read object + * + * @var array + */ + protected $_currentObj; + + /** + * Constructor + * + * @param string $filename Source filename + * @throws InvalidArgumentException + */ + public function __construct($filename) + { + $this->filename = $filename; + + $this->_f = @fopen($this->filename, 'rb'); + + if (!$this->_f) { + throw new InvalidArgumentException(sprintf('Cannot open %s !', $filename)); } - - /** - * Close the opened file - */ - function closeFile() { - if (isset($this->f) && is_resource($this->f)) { - fclose($this->f); - unset($this->f); - } - } - - /** - * Print Error and die - * - * @param string $msg Error-Message - */ - function error($msg) { - die('PDF-Parser Error: ' . $msg); - } - - /** - * Check Trailer for Encryption - */ - function getEncryption() { - if (isset($this->xref['trailer'][1]['/Encrypt'])) { - $this->error('File is encrypted!'); - } - } - - /** - * Find/Return /Root - * - * @return array - */ - function pdf_find_root() { - if ($this->xref['trailer'][1]['/Root'][0] != PDF_TYPE_OBJREF) { - $this->error('Wrong Type of Root-Element! Must be an indirect reference'); - } - - return $this->xref['trailer'][1]['/Root']; - } - - /** - * Read the /Root - */ - function pdf_read_root() { - // read root - $this->root = $this->pdf_resolve_object($this->c, $this->pdf_find_root()); - } - - /** - * Get PDF-Version - * - * And reset the PDF Version used in FPDI if needed - */ - function getPDFVersion() { - fseek($this->f, 0); - preg_match('/\d\.\d/',fread($this->f, 16), $m); - if (isset($m[0])) - $this->pdfVersion = $m[0]; - return $this->pdfVersion; - } - - /** - * Find the xref-Table - */ - function pdf_find_xref() { - $toRead = 1500; - - $stat = fseek ($this->f, -$toRead, SEEK_END); - if ($stat === -1) { - fseek ($this->f, 0); - } - $data = fread($this->f, $toRead); - - $pos = strlen($data) - strpos(strrev($data), strrev('startxref')); - $data = substr($data, $pos); - - if (!preg_match('/\s*(\d+).*$/s', $data, $matches)) { - $this->error('Unable to find pointer to xref table'); - } - - return (int) $matches[1]; - } - - /** - * Read xref-table - * - * @param array $result Array of xref-table - * @param integer $offset of xref-table - */ - function pdf_read_xref(&$result, $offset) { - $o_pos = $offset-min(20, $offset); - fseek($this->f, $o_pos); // set some bytes backwards to fetch errorious docs - - $data = fread($this->f, 100); - - $xrefPos = strrpos($data, 'xref'); - - if ($xrefPos === false) { - fseek($this->f, $offset); - $c = new pdf_context($this->f); - $xrefStreamObjDec = $this->pdf_read_value($c); - - if (is_array($xrefStreamObjDec) && isset($xrefStreamObjDec[0]) && $xrefStreamObjDec[0] == PDF_TYPE_OBJDEC) { - $this->error(sprintf('This document (%s) probably uses a compression technique which is not supported by the free parser shipped with FPDI.', $this->filename)); - } else { - $this->error('Unable to find xref table.'); - } - } - - if (!isset($result['xref_location'])) { - $result['xref_location'] = $o_pos + $xrefPos; - $result['max_object'] = 0; - } - - $cylces = -1; - $bytesPerCycle = 100; - - fseek($this->f, $o_pos = $o_pos + $xrefPos + 4); // set the handle directly after the "xref"-keyword - $data = fread($this->f, $bytesPerCycle); - - while (($trailerPos = strpos($data, 'trailer', max($bytesPerCycle * $cylces++, 0))) === false && !feof($this->f)) { - $data .= fread($this->f, $bytesPerCycle); - } - - if ($trailerPos === false) { - $this->error('Trailer keyword not found after xref table'); - } - - $data = substr($data, 0, $trailerPos); - - // get Line-Ending - preg_match_all("/(\r\n|\n|\r)/", substr($data, 0, 100), $m); // check the first 100 bytes for linebreaks - - $differentLineEndings = count(array_unique($m[0])); - if ($differentLineEndings > 1) { - $lines = preg_split("/(\r\n|\n|\r)/", $data, -1, PREG_SPLIT_NO_EMPTY); - } else { - $lines = explode($m[0][1], $data); - } - - $data = $differentLineEndings = $m = null; - unset($data, $differentLineEndings, $m); - - $linesCount = count($lines); - - $start = 1; - - for ($i = 0; $i < $linesCount; $i++) { - $line = trim($lines[$i]); - if ($line) { - $pieces = explode(' ', $line); - $c = count($pieces); - switch($c) { - case 2: - $start = (int)$pieces[0]; - $end = $start + (int)$pieces[1]; - if ($end > $result['max_object']) - $result['max_object'] = $end; - break; - case 3: - if (!isset($result['xref'][$start])) - $result['xref'][$start] = array(); - - if (!array_key_exists($gen = (int) $pieces[1], $result['xref'][$start])) { - $result['xref'][$start][$gen] = $pieces[2] == 'n' ? (int) $pieces[0] : null; - } - $start++; - break; - default: - $this->error('Unexpected data in xref table'); - } - } - } - - $lines = $pieces = $line = $start = $end = $gen = null; - unset($lines, $pieces, $line, $start, $end, $gen); - - fseek($this->f, $o_pos + $trailerPos + 7); - - $c = new pdf_context($this->f); - $trailer = $this->pdf_read_value($c); - - $c = null; - unset($c); - - if (!isset($result['trailer'])) { - $result['trailer'] = $trailer; - } - - if (isset($trailer[1]['/Prev'])) { - $this->pdf_read_xref($result, $trailer[1]['/Prev'][1]); - } - - $trailer = null; - unset($trailer); - - return true; - } - - /** - * Reads an Value - * - * @param object $c pdf_context - * @param string $token a Token - * @return mixed - */ - function pdf_read_value(&$c, $token = null) { - if (is_null($token)) { - $token = $this->pdf_read_token($c); - } - - if ($token === false) { - return false; - } - - switch ($token) { - case '<': - // This is a hex string. - // Read the value, then the terminator - - $pos = $c->offset; - - while(1) { - - $match = strpos ($c->buffer, '>', $pos); - - // If you can't find it, try - // reading more data from the stream - - if ($match === false) { - if (!$c->increase_length()) { - return false; - } else { - continue; - } - } - - $result = substr ($c->buffer, $c->offset, $match - $c->offset); - $c->offset = $match + 1; - - return array (PDF_TYPE_HEX, $result); - } - - break; - case '<<': - // This is a dictionary. - - $result = array(); - - // Recurse into this function until we reach - // the end of the dictionary. - while (($key = $this->pdf_read_token($c)) !== '>>') { - if ($key === false) { - return false; - } - - if (($value = $this->pdf_read_value($c)) === false) { - return false; - } - - // Catch missing value - if ($value[0] == PDF_TYPE_TOKEN && $value[1] == '>>') { - $result[$key] = array(PDF_TYPE_NULL); - break; - } - - $result[$key] = $value; - } - - return array (PDF_TYPE_DICTIONARY, $result); - - case '[': - // This is an array. - - $result = array(); - - // Recurse into this function until we reach - // the end of the array. - while (($token = $this->pdf_read_token($c)) !== ']') { - if ($token === false) { - return false; - } - - if (($value = $this->pdf_read_value($c, $token)) === false) { - return false; - } - - $result[] = $value; - } - - return array (PDF_TYPE_ARRAY, $result); - - case '(' : - // This is a string - $pos = $c->offset; - - $openBrackets = 1; - do { - for (; $openBrackets != 0 && $pos < $c->length; $pos++) { - switch (ord($c->buffer[$pos])) { - case 0x28: // '(' - $openBrackets++; - break; - case 0x29: // ')' - $openBrackets--; - break; - case 0x5C: // backslash - $pos++; - } - } - } while($openBrackets != 0 && $c->increase_length()); - - $result = substr($c->buffer, $c->offset, $pos - $c->offset - 1); - $c->offset = $pos; - - return array (PDF_TYPE_STRING, $result); - - case 'stream': - $o_pos = ftell($c->file)-strlen($c->buffer); - $o_offset = $c->offset; - - $c->reset($startpos = $o_pos + $o_offset); - - $e = 0; // ensure line breaks in front of the stream - if ($c->buffer[0] == chr(10) || $c->buffer[0] == chr(13)) - $e++; - if ($c->buffer[1] == chr(10) && $c->buffer[0] != chr(10)) - $e++; - - if ($this->actual_obj[1][1]['/Length'][0] == PDF_TYPE_OBJREF) { - $tmp_c = new pdf_context($this->f); - $tmp_length = $this->pdf_resolve_object($tmp_c, $this->actual_obj[1][1]['/Length']); - $length = $tmp_length[1][1]; - } else { - $length = $this->actual_obj[1][1]['/Length'][1]; - } - - if ($length > 0) { - $c->reset($startpos + $e,$length); - $v = $c->buffer; - } else { - $v = ''; - } - $c->reset($startpos + $e + $length + 9); // 9 = strlen("endstream") - - return array(PDF_TYPE_STREAM, $v); - - default : - if (is_numeric ($token)) { - // A numeric token. Make sure that - // it is not part of something else. - if (($tok2 = $this->pdf_read_token ($c)) !== false) { - if (is_numeric ($tok2)) { - - // Two numeric tokens in a row. - // In this case, we're probably in - // front of either an object reference - // or an object specification. - // Determine the case and return the data - if (($tok3 = $this->pdf_read_token ($c)) !== false) { - switch ($tok3) { - case 'obj': - return array (PDF_TYPE_OBJDEC, (int) $token, (int) $tok2); - case 'R': - return array (PDF_TYPE_OBJREF, (int) $token, (int) $tok2); - } - // If we get to this point, that numeric value up - // there was just a numeric value. Push the extra - // tokens back into the stack and return the value. - array_push ($c->stack, $tok3); - } - } - - array_push ($c->stack, $tok2); - } - - if ($token === (string)((int)$token)) - return array (PDF_TYPE_NUMERIC, (int)$token); - else - return array (PDF_TYPE_REAL, (float)$token); - } else if ($token == 'true' || $token == 'false') { - return array (PDF_TYPE_BOOLEAN, $token == 'true'); - } else if ($token == 'null') { - return array (PDF_TYPE_NULL); - } else { - // Just a token. Return it. - return array (PDF_TYPE_TOKEN, $token); - } - } - } - - /** - * Resolve an object - * - * @param object $c pdf_context - * @param array $obj_spec The object-data - * @param boolean $encapsulate Must set to true, cause the parsing and fpdi use this method only without this para - */ - function pdf_resolve_object(&$c, $obj_spec, $encapsulate = true) { - // Exit if we get invalid data - if (!is_array($obj_spec)) { - $ret = false; - return $ret; - } - - if ($obj_spec[0] == PDF_TYPE_OBJREF) { - - // This is a reference, resolve it - if (isset($this->xref['xref'][$obj_spec[1]][$obj_spec[2]])) { - - // Save current file position - // This is needed if you want to resolve - // references while you're reading another object - // (e.g.: if you need to determine the length - // of a stream) - - $old_pos = ftell($c->file); - - // Reposition the file pointer and - // load the object header. - - $c->reset($this->xref['xref'][$obj_spec[1]][$obj_spec[2]]); - - $header = $this->pdf_read_value($c); - - if ($header[0] != PDF_TYPE_OBJDEC || $header[1] != $obj_spec[1] || $header[2] != $obj_spec[2]) { - $toSearchFor = $obj_spec[1] . ' ' . $obj_spec[2] . ' obj'; - if (preg_match('/' . $toSearchFor . '/', $c->buffer)) { - $c->offset = strpos($c->buffer, $toSearchFor) + strlen($toSearchFor); - // reset stack - $c->stack = array(); - } else { - $this->error("Unable to find object ({$obj_spec[1]}, {$obj_spec[2]}) at expected location"); - } - } - - // If we're being asked to store all the information - // about the object, we add the object ID and generation - // number for later use - $result = array(); - $this->actual_obj =& $result; - if ($encapsulate) { - $result = array ( - PDF_TYPE_OBJECT, - 'obj' => $obj_spec[1], - 'gen' => $obj_spec[2] - ); - } - - // Now simply read the object data until - // we encounter an end-of-object marker - while(1) { - $value = $this->pdf_read_value($c); - if ($value === false || count($result) > 4) { - // in this case the parser coudn't find an endobj so we break here - break; - } - - if ($value[0] == PDF_TYPE_TOKEN && $value[1] === 'endobj') { - break; - } - - $result[] = $value; - } - - $c->reset($old_pos); - - if (isset($result[2][0]) && $result[2][0] == PDF_TYPE_STREAM) { - $result[0] = PDF_TYPE_STREAM; - } - - return $result; - } - } else { - return $obj_spec; - } - } - - - - /** - * Reads a token from the file - * - * @param object $c pdf_context - * @return mixed - */ - function pdf_read_token(&$c) - { - // If there is a token available - // on the stack, pop it out and - // return it. - - if (count($c->stack)) { - return array_pop($c->stack); - } - - // Strip away any whitespace - - do { - if (!$c->ensure_content()) { - return false; - } - $c->offset += strspn($c->buffer, " \n\r\t", $c->offset); - } while ($c->offset >= $c->length - 1); - - // Get the first character in the stream - - $char = $c->buffer[$c->offset++]; - - switch ($char) { - - case '[': - case ']': - case '(': - case ')': - - // This is either an array or literal string - // delimiter, Return it - - return $char; - - case '<': - case '>': - - // This could either be a hex string or - // dictionary delimiter. Determine the - // appropriate case and return the token - - if ($c->buffer[$c->offset] == $char) { - if (!$c->ensure_content()) { - return false; - } - $c->offset++; - return $char . $char; - } else { - return $char; - } - - case '%': - - // This is a comment - jump over it! - - $pos = $c->offset; - while(1) { - $match = preg_match("/(\r\n|\r|\n)/", $c->buffer, $m, PREG_OFFSET_CAPTURE, $pos); - if ($match === 0) { - if (!$c->increase_length()) { - return false; - } else { - continue; - } - } - - $c->offset = $m[0][1]+strlen($m[0][0]); - - return $this->pdf_read_token($c); - } - - default: - - // This is "another" type of token (probably - // a dictionary entry or a numeric value) - // Find the end and return it. - - if (!$c->ensure_content()) { - return false; - } - - while(1) { - - // Determine the length of the token - - $pos = strcspn($c->buffer, " %[]<>()\r\n\t/", $c->offset); - - if ($c->offset + $pos <= $c->length - 1) { - break; - } else { - // If the script reaches this point, - // the token may span beyond the end - // of the current buffer. Therefore, - // we increase the size of the buffer - // and try again--just to be safe. - - $c->increase_length(); - } - } - - $result = substr($c->buffer, $c->offset - 1, $pos + 1); - - $c->offset += $pos; - return $result; - } + + $this->getPdfVersion(); + + require_once('pdf_context.php'); + $this->_c = new pdf_context($this->_f); + + // Read xref-Data + $this->_xref = array(); + $this->_readXref($this->_xref, $this->_findXref()); + + // Check for Encryption + $this->getEncryption(); + + // Read root + $this->_readRoot(); + } + + /** + * Destructor + */ + public function __destruct() + { + $this->closeFile(); + } + + /** + * Close the opened file + */ + public function closeFile() + { + if (isset($this->_f) && is_resource($this->_f)) { + fclose($this->_f); + unset($this->_f); } } -} + + /** + * Check Trailer for Encryption + * + * @throws Exception + */ + public function getEncryption() + { + if (isset($this->_xref['trailer'][1]['/Encrypt'])) { + throw new Exception('File is encrypted!'); + } + } + + /** + * Get PDF-Version + * + * @return string + */ + public function getPdfVersion() + { + if ($this->_pdfVersion === null) { + fseek($this->_f, 0); + preg_match('/\d\.\d/', fread($this->_f, 16), $m); + if (isset($m[0])) + $this->_pdfVersion = $m[0]; + } + + return $this->_pdfVersion; + } + + /** + * Read the /Root dictionary + */ + protected function _readRoot() + { + if ($this->_xref['trailer'][1]['/Root'][0] != self::TYPE_OBJREF) { + throw new Exception('Wrong Type of Root-Element! Must be an indirect reference'); + } + + $this->_root = $this->resolveObject($this->_xref['trailer'][1]['/Root']); + } + + /** + * Find the xref table + * + * @return integer + * @throws Exception + */ + protected function _findXref() + { + $toRead = self::$searchForStartxrefLength; + + $stat = fseek($this->_f, -$toRead, SEEK_END); + if ($stat === -1) { + fseek($this->_f, 0); + } + + $data = fread($this->_f, $toRead); + + $keywordPos = strpos(strrev($data), strrev('startxref')); + if (false === $keywordPos) { + $keywordPos = strpos(strrev($data), strrev('startref')); + } + + if (false === $keywordPos) { + throw new Exception('Unable to find "startxref" keyword.'); + } + + $pos = strlen($data) - $keywordPos; + $data = substr($data, $pos); + + if (!preg_match('/\s*(\d+).*$/s', $data, $matches)) { + throw new Exception('Unable to find pointer to xref table.'); + } + + return (int) $matches[1]; + } + + /** + * Read the xref table + * + * @param array $result Array of xref table entries + * @param integer $offset of xref table + * @return boolean + * @throws Exception + */ + protected function _readXref(&$result, $offset) + { + $tempPos = $offset - min(20, $offset); + fseek($this->_f, $tempPos); // set some bytes backwards to fetch corrupted docs + + $data = fread($this->_f, 100); + + $xrefPos = strrpos($data, 'xref'); + + if ($xrefPos === false) { + $this->_c->reset($offset); + $xrefStreamObjDec = $this->_readValue($this->_c); + + if (is_array($xrefStreamObjDec) && isset($xrefStreamObjDec[0]) && $xrefStreamObjDec[0] == self::TYPE_OBJDEC) { + throw new Exception( + sprintf( + 'This document (%s) probably uses a compression technique which is not supported by the ' . + 'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)', + $this->filename + ) + ); + } else { + throw new Exception('Unable to find xref table.'); + } + } + + if (!isset($result['xrefLocation'])) { + $result['xrefLocation'] = $tempPos + $xrefPos; + $result['maxObject'] = 0; + } + + $cycles = -1; + $bytesPerCycle = 100; + + fseek($this->_f, $tempPos = $tempPos + $xrefPos + 4); // set the handle directly after the "xref"-keyword + $data = fread($this->_f, $bytesPerCycle); + + while (($trailerPos = strpos($data, 'trailer', max($bytesPerCycle * $cycles++, 0))) === false && !feof($this->_f)) { + $data .= fread($this->_f, $bytesPerCycle); + } + + if ($trailerPos === false) { + throw new Exception('Trailer keyword not found after xref table'); + } + + $data = ltrim(substr($data, 0, $trailerPos)); + + // get Line-Ending + preg_match_all("/(\r\n|\n|\r)/", substr($data, 0, 100), $m); // check the first 100 bytes for line breaks + + $differentLineEndings = count(array_unique($m[0])); + if ($differentLineEndings > 1) { + $lines = preg_split("/(\r\n|\n|\r)/", $data, -1, PREG_SPLIT_NO_EMPTY); + } else { + $lines = explode($m[0][0], $data); + } + + $data = $differentLineEndings = $m = null; + unset($data, $differentLineEndings, $m); + + $linesCount = count($lines); + + $start = 1; + + for ($i = 0; $i < $linesCount; $i++) { + $line = trim($lines[$i]); + if ($line) { + $pieces = explode(' ', $line); + $c = count($pieces); + switch($c) { + case 2: + $start = (int)$pieces[0]; + $end = $start + (int)$pieces[1]; + if ($end > $result['maxObject']) + $result['maxObject'] = $end; + break; + case 3: + if (!isset($result['xref'][$start])) + $result['xref'][$start] = array(); + + if (!array_key_exists($gen = (int) $pieces[1], $result['xref'][$start])) { + $result['xref'][$start][$gen] = $pieces[2] == 'n' ? (int) $pieces[0] : null; + } + $start++; + break; + default: + throw new Exception('Unexpected data in xref table'); + } + } + } + + $lines = $pieces = $line = $start = $end = $gen = null; + unset($lines, $pieces, $line, $start, $end, $gen); + + $this->_c->reset($tempPos + $trailerPos + 7); + $trailer = $this->_readValue($this->_c); + + if (!isset($result['trailer'])) { + $result['trailer'] = $trailer; + } + + if (isset($trailer[1]['/Prev'])) { + $this->_readXref($result, $trailer[1]['/Prev'][1]); + } + + $trailer = null; + unset($trailer); + + return true; + } + + /** + * Reads a PDF value + * + * @param pdf_context $c + * @param string $token A token + * @return mixed + */ + protected function _readValue(&$c, $token = null) + { + if (is_null($token)) { + $token = $this->_readToken($c); + } + + if ($token === false) { + return false; + } + + switch ($token) { + case '<': + // This is a hex string. + // Read the value, then the terminator + + $pos = $c->offset; + + while(1) { + + $match = strpos ($c->buffer, '>', $pos); + + // If you can't find it, try + // reading more data from the stream + + if ($match === false) { + if (!$c->increaseLength()) { + return false; + } else { + continue; + } + } + + $result = substr ($c->buffer, $c->offset, $match - $c->offset); + $c->offset = $match + 1; + + return array (self::TYPE_HEX, $result); + } + break; + + case '<<': + // This is a dictionary. + + $result = array(); + + // Recurse into this function until we reach + // the end of the dictionary. + while (($key = $this->_readToken($c)) !== '>>') { + if ($key === false) { + return false; + } + + if (($value = $this->_readValue($c)) === false) { + return false; + } + + // Catch missing value + if ($value[0] == self::TYPE_TOKEN && $value[1] == '>>') { + $result[$key] = array(self::TYPE_NULL); + break; + } + + $result[$key] = $value; + } + + return array (self::TYPE_DICTIONARY, $result); + + case '[': + // This is an array. + + $result = array(); + + // Recurse into this function until we reach + // the end of the array. + while (($token = $this->_readToken($c)) !== ']') { + if ($token === false) { + return false; + } + + if (($value = $this->_readValue($c, $token)) === false) { + return false; + } + + $result[] = $value; + } + + return array (self::TYPE_ARRAY, $result); + + case '(': + // This is a string + $pos = $c->offset; + + $openBrackets = 1; + do { + for (; $openBrackets != 0 && $pos < $c->length; $pos++) { + switch (ord($c->buffer[$pos])) { + case 0x28: // '(' + $openBrackets++; + break; + case 0x29: // ')' + $openBrackets--; + break; + case 0x5C: // backslash + $pos++; + } + } + } while($openBrackets != 0 && $c->increaseLength()); + + $result = substr($c->buffer, $c->offset, $pos - $c->offset - 1); + $c->offset = $pos; + + return array (self::TYPE_STRING, $result); + + case 'stream': + $tempPos = $c->getPos() - strlen($c->buffer); + $tempOffset = $c->offset; + + $c->reset($startPos = $tempPos + $tempOffset); + + $e = 0; // ensure line breaks in front of the stream + if ($c->buffer[0] == chr(10) || $c->buffer[0] == chr(13)) + $e++; + if ($c->buffer[1] == chr(10) && $c->buffer[0] != chr(10)) + $e++; + + if ($this->_currentObj[1][1]['/Length'][0] == self::TYPE_OBJREF) { + $tmpLength = $this->resolveObject($this->_currentObj[1][1]['/Length']); + $length = $tmpLength[1][1]; + } else { + $length = $this->_currentObj[1][1]['/Length'][1]; + } + + if ($length > 0) { + $c->reset($startPos + $e, $length); + $v = $c->buffer; + } else { + $v = ''; + } + + $c->reset($startPos + $e + $length); + $endstream = $this->_readToken($c); + + if ($endstream != 'endstream') { + $c->reset($startPos + $e + $length + 9); // 9 = strlen("endstream") + // We don't throw an error here because the next + // round trip will start at a new offset + } + + return array(self::TYPE_STREAM, $v); + + default : + if (is_numeric($token)) { + // A numeric token. Make sure that + // it is not part of something else. + if (($tok2 = $this->_readToken($c)) !== false) { + if (is_numeric($tok2)) { + + // Two numeric tokens in a row. + // In this case, we're probably in + // front of either an object reference + // or an object specification. + // Determine the case and return the data + if (($tok3 = $this->_readToken($c)) !== false) { + switch ($tok3) { + case 'obj': + return array(self::TYPE_OBJDEC, (int)$token, (int)$tok2); + case 'R': + return array(self::TYPE_OBJREF, (int)$token, (int)$tok2); + } + // If we get to this point, that numeric value up + // there was just a numeric value. Push the extra + // tokens back into the stack and return the value. + array_push($c->stack, $tok3); + } + } + + array_push($c->stack, $tok2); + } + + if ($token === (string)((int)$token)) + return array(self::TYPE_NUMERIC, (int)$token); + else + return array(self::TYPE_REAL, (float)$token); + } else if ($token == 'true' || $token == 'false') { + return array(self::TYPE_BOOLEAN, $token == 'true'); + } else if ($token == 'null') { + return array(self::TYPE_NULL); + } else { + // Just a token. Return it. + return array(self::TYPE_TOKEN, $token); + } + } + } + + /** + * Resolve an object + * + * @param array $objSpec The object-data + * @return array|boolean + * @throws Exception + */ + public function resolveObject($objSpec) + { + $c = $this->_c; + + // Exit if we get invalid data + if (!is_array($objSpec)) { + return false; + } + + if ($objSpec[0] == self::TYPE_OBJREF) { + + // This is a reference, resolve it + if (isset($this->_xref['xref'][$objSpec[1]][$objSpec[2]])) { + + // Save current file position + // This is needed if you want to resolve + // references while you're reading another object + // (e.g.: if you need to determine the length + // of a stream) + + $oldPos = $c->getPos(); + + // Reposition the file pointer and + // load the object header. + + $c->reset($this->_xref['xref'][$objSpec[1]][$objSpec[2]]); + + $header = $this->_readValue($c); + + if ($header[0] != self::TYPE_OBJDEC || $header[1] != $objSpec[1] || $header[2] != $objSpec[2]) { + $toSearchFor = $objSpec[1] . ' ' . $objSpec[2] . ' obj'; + if (preg_match('/' . $toSearchFor . '/', $c->buffer)) { + $c->offset = strpos($c->buffer, $toSearchFor) + strlen($toSearchFor); + // reset stack + $c->stack = array(); + } else { + throw new Exception( + sprintf("Unable to find object (%s, %s) at expected location.", $objSpec[1], $objSpec[2]) + ); + } + } + + // If we're being asked to store all the information + // about the object, we add the object ID and generation + // number for later use + $result = array ( + self::TYPE_OBJECT, + 'obj' => $objSpec[1], + 'gen' => $objSpec[2] + ); + + $this->_currentObj =& $result; + + // Now simply read the object data until + // we encounter an end-of-object marker + while (true) { + $value = $this->_readValue($c); + if ($value === false || count($result) > 4) { + // in this case the parser couldn't find an "endobj" so we break here + break; + } + + if ($value[0] == self::TYPE_TOKEN && $value[1] === 'endobj') { + break; + } + + $result[] = $value; + } + + $c->reset($oldPos); + + if (isset($result[2][0]) && $result[2][0] == self::TYPE_STREAM) { + $result[0] = self::TYPE_STREAM; + } + + return $result; + } + } else { + return $objSpec; + } + } + + /** + * Reads a token from the context + * + * @param pdf_context $c + * @return mixed + */ + protected function _readToken($c) + { + // If there is a token available + // on the stack, pop it out and + // return it. + + if (count($c->stack)) { + return array_pop($c->stack); + } + + // Strip away any whitespace + + do { + if (!$c->ensureContent()) { + return false; + } + $c->offset += strspn($c->buffer, "\x20\x0A\x0C\x0D\x09\x00", $c->offset); + } while ($c->offset >= $c->length - 1); + + // Get the first character in the stream + + $char = $c->buffer[$c->offset++]; + + switch ($char) { + + case '[': + case ']': + case '(': + case ')': + + // This is either an array or literal string + // delimiter, Return it + + return $char; + + case '<': + case '>': + + // This could either be a hex string or + // dictionary delimiter. Determine the + // appropriate case and return the token + + if ($c->buffer[$c->offset] == $char) { + if (!$c->ensureContent()) { + return false; + } + $c->offset++; + return $char . $char; + } else { + return $char; + } + + case '%': + + // This is a comment - jump over it! + + $pos = $c->offset; + while(1) { + $match = preg_match("/(\r\n|\r|\n)/", $c->buffer, $m, PREG_OFFSET_CAPTURE, $pos); + if ($match === 0) { + if (!$c->increaseLength()) { + return false; + } else { + continue; + } + } + + $c->offset = $m[0][1] + strlen($m[0][0]); + + return $this->_readToken($c); + } + + default: + + // This is "another" type of token (probably + // a dictionary entry or a numeric value) + // Find the end and return it. + + if (!$c->ensureContent()) { + return false; + } + + while(1) { + + // Determine the length of the token + + $pos = strcspn($c->buffer, "\x20%[]<>()/\x0A\x0C\x0D\x09\x00", $c->offset); + + if ($c->offset + $pos <= $c->length - 1) { + break; + } else { + // If the script reaches this point, + // the token may span beyond the end + // of the current buffer. Therefore, + // we increase the size of the buffer + // and try again--just to be safe. + + $c->increaseLength(); + } + } + + $result = substr($c->buffer, $c->offset - 1, $pos + 1); + + $c->offset += $pos; + + return $result; + } + } + + /** + * Un-filter a stream object + * + * @param array $obj + * @return string + * @throws Exception + */ + protected function _unFilterStream($obj) + { + $filters = array(); + + if (isset($obj[1][1]['/Filter'])) { + $filter = $obj[1][1]['/Filter']; + + if ($filter[0] == pdf_parser::TYPE_OBJREF) { + $tmpFilter = $this->resolveObject($filter); + $filter = $tmpFilter[1]; + } + + if ($filter[0] == pdf_parser::TYPE_TOKEN) { + $filters[] = $filter; + } else if ($filter[0] == pdf_parser::TYPE_ARRAY) { + $filters = $filter[1]; + } + } + + $stream = $obj[2][1]; + + foreach ($filters AS $filter) { + switch ($filter[1]) { + case '/FlateDecode': + case '/Fl': + if (function_exists('gzuncompress')) { + $oStream = $stream; + $stream = (strlen($stream) > 0) ? @gzuncompress($stream) : ''; + } else { + throw new Exception( + sprintf('To handle %s filter, please compile php with zlib support.', $filter[1]) + ); + } + + if ($stream === false) { + $tries = 0; + while ($tries < 8 && ($stream === false || strlen($stream) < strlen($oStream))) { + $oStream = substr($oStream, 1); + $stream = @gzinflate($oStream); + $tries++; + } + + if ($stream === false) { + throw new Exception('Error while decompressing stream.'); + } + } + break; + case '/LZWDecode': + require_once('filters/FilterLZW.php'); + $decoder = new FilterLZW(); + $stream = $decoder->decode($stream); + break; + case '/ASCII85Decode': + require_once('filters/FilterASCII85.php'); + $decoder = new FilterASCII85(); + $stream = $decoder->decode($stream); + break; + case '/ASCIIHexDecode': + require_once('filters/FilterASCIIHexDecode.php'); + $decoder = new FilterASCIIHexDecode(); + $stream = $decoder->decode($stream); + break; + case null: + break; + default: + throw new Exception(sprintf('Unsupported Filter: %s', $filter[1])); + } + } + + return $stream; + } +} \ No newline at end of file