2 //============================================================+
3 // File name : tcpdf_parser.php
6 // Last Update : 2012-05-03
7 // Author : Nicola Asuni - Tecnick.com LTD - Manor Coach House, Church Hill, Aldershot, Hants, GU12 4RQ, UK - www.tecnick.com - info@tecnick.com
8 // License : http://www.tecnick.com/pagefiles/tcpdf/LICENSE.TXT GNU-LGPLv3
9 // -------------------------------------------------------------------
10 // Copyright (C) 2011-2012 Nicola Asuni - Tecnick.com LTD
12 // This file is part of TCPDF software library.
14 // TCPDF is free software: you can redistribute it and/or modify it
15 // under the terms of the GNU Lesser General Public License as
16 // published by the Free Software Foundation, either version 3 of the
17 // License, or (at your option) any later version.
19 // TCPDF is distributed in the hope that it will be useful, but
20 // WITHOUT ANY WARRANTY; without even the implied warranty of
21 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
22 // See the GNU Lesser General Public License for more details.
24 // You should have received a copy of the License
25 // along with TCPDF. If not, see
26 // <http://www.tecnick.com/pagefiles/tcpdf/LICENSE.TXT>.
28 // See LICENSE.TXT file for more information.
29 // -------------------------------------------------------------------
31 // Description : This is a PHP class for parsing PDF documents.
33 //============================================================+
37 * This is a PHP class for parsing PDF documents.<br>
38 * @package com.tecnick.tcpdf
39 * @author Nicola Asuni
43 // include class for decoding filters
44 require_once(dirname(__FILE__
).'/tcpdf_filters.php');
48 * This is a PHP class for parsing PDF documents.<br>
49 * @package com.tecnick.tcpdf
50 * @brief This is a PHP class for parsing PDF documents..
52 * @author Nicola Asuni - info@tecnick.com
57 * Raw content of the PDF document.
60 private $pdfdata = '';
66 protected $xref = array();
69 * Array of PDF objects.
72 protected $objects = array();
75 * Class object for decoding filters.
78 private $FilterDecoders;
80 // -----------------------------------------------------------------------------
83 * Parse a PDF document an return an array of objects.
84 * @param $data (string) PDF data to parse.
86 * @since 1.0.000 (2011-05-24)
88 public function __construct($data) {
90 $this->Error('Empty PDF data.');
92 $this->pdfdata
= $data;
94 $pdflen = strlen($this->pdfdata
);
95 // initialize class for decoding filters
96 $this->FilterDecoders
= new TCPDF_FILTERS();
97 // get xref and trailer data
98 $this->xref
= $this->getXrefData();
99 // parse all document objects
100 $this->objects
= array();
101 foreach ($this->xref
['xref'] as $obj => $offset) {
102 if (!isset($this->objects
[$obj])) {
103 $this->objects
[$obj] = $this->getIndirectObject($obj, $offset, true);
106 // release some memory
107 unset($this->pdfdata
);
112 * Return an array of parsed PDF document objects.
113 * @return (array) Array of parsed PDF document objects.
115 * @since 1.0.000 (2011-06-26)
117 public function getParsedData() {
118 return array($this->xref
, $this->objects
);
122 * Get xref (cross-reference table) and trailer data from PDF document data.
123 * @param $offset (int) xref offset (if know).
124 * @param $xref (array) previous xref array (if any).
125 * @return Array containing xref and trailer data.
127 * @since 1.0.000 (2011-05-24)
129 protected function getXrefData($offset=0, $xref=array()) {
131 // find last startxref
132 if (preg_match_all('/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', $this->pdfdata
, $matches, PREG_SET_ORDER
, $offset) == 0) {
133 $this->Error('Unable to find startxref');
135 $matches = array_pop($matches);
136 $startxref = $matches[1];
138 // get the first xref at the specified offset
139 if (preg_match('/[\r\n]startxref[\s]*[\r\n]+([0-9]+)[\s]*[\r\n]+%%EOF/i', $this->pdfdata
, $matches, PREG_OFFSET_CAPTURE
, $offset) == 0) {
140 $this->Error('Unable to find startxref');
142 $startxref = $matches[1][0];
144 // check xref position
145 if (strpos($this->pdfdata
, 'xref', $startxref) != $startxref) {
146 $this->Error('Unable to find xref');
148 // extract xref data (object indexes and offsets)
149 $xoffset = $startxref +
5;
150 // initialize object number
153 while (preg_match('/^([0-9]+)[\s]([0-9]+)[\s]?([nf]?)/im', $this->pdfdata
, $matches, PREG_OFFSET_CAPTURE
, $offset) > 0) {
154 $offset = (strlen($matches[0][0]) +
$matches[0][1]);
155 if ($matches[3][0] == 'n') {
156 // create unique object index: [object number]_[generation number]
157 $index = $obj_num.'_'.intval($matches[2][0]);
158 // check if object already exist
159 if (!isset($xref['xref'][$index])) {
160 // store object offset position
161 $xref['xref'][$index] = intval($matches[1][0]);
165 } elseif ($matches[3][0] == 'f') {
169 // object number (index)
170 $obj_num = intval($matches[1][0]);
174 if (preg_match('/trailer[\s]*<<(.*)>>[\s]*[\r\n]+startxref[\s]*[\r\n]+/isU', $this->pdfdata
, $matches, PREG_OFFSET_CAPTURE
, $xoffset) > 0) {
175 $trailer_data = $matches[1][0];
176 if (!isset($xref['trailer'])) {
177 // get only the last updated version
178 $xref['trailer'] = array();
179 // parse trailer_data
180 if (preg_match('/Size[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) {
181 $xref['trailer']['size'] = intval($matches[1]);
183 if (preg_match('/Root[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
184 $xref['trailer']['root'] = intval($matches[1]).'_'.intval($matches[2]);
186 if (preg_match('/Encrypt[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
187 $xref['trailer']['encrypt'] = intval($matches[1]).'_'.intval($matches[2]);
189 if (preg_match('/Info[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) {
190 $xref['trailer']['info'] = intval($matches[1]).'_'.intval($matches[2]);
192 if (preg_match('/ID[\s]*[\[][\s]*[<]([^>]*)[>][\s]*[<]([^>]*)[>]/i', $trailer_data, $matches) > 0) {
193 $xref['trailer']['id'] = array();
194 $xref['trailer']['id'][0] = $matches[1];
195 $xref['trailer']['id'][1] = $matches[2];
198 if (preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) {
200 $xref = $this->getXrefData(intval($matches[1]), $xref);
203 $this->Error('Unable to find trailer');
209 * Get object type, raw value and offset to next object
210 * @param $offset (int) Object offset.
211 * @return array containing object type, raw value and offset to next object
213 * @since 1.0.000 (2011-06-20)
215 protected function getRawObject($offset=0) {
216 $objtype = ''; // object type to be returned
217 $objval = ''; // object value to be returned
218 // skip initial white space chars: \x00 null (NUL), \x09 horizontal tab (HT), \x0A line feed (LF), \x0C form feed (FF), \x0D carriage return (CR), \x20 space (SP)
219 $offset +
= strspn($this->pdfdata
, "\x00\x09\x0a\x0c\x0d\x20", $offset);
221 $char = $this->pdfdata
{$offset};
224 case '%': { // \x25 PERCENT SIGN
225 // skip comment and search for next token
226 $next = strcspn($this->pdfdata
, "\r\n", $offset);
229 return $this->getRawObject($this->pdfdata
, $offset);
233 case '/': { // \x2F SOLIDUS
237 if (preg_match('/^([^\x00\x09\x0a\x0c\x0d\x20\s\x28\x29\x3c\x3e\x5b\x5d\x7b\x7d\x2f\x25]+)/', substr($this->pdfdata
, $offset, 256), $matches) == 1) {
238 $objval = $matches[1]; // unescaped value
239 $offset +
= strlen($objval);
243 case '(': // \x28 LEFT PARENTHESIS
244 case ')': { // \x29 RIGHT PARENTHESIS
245 // literal string object
251 while ($open_bracket > 0) {
252 if (!isset($this->pdfdata
{$strpos})) {
255 $ch = $this->pdfdata
{$strpos};
257 case '\\': { // REVERSE SOLIDUS (5Ch) (Backslash)
258 // skip next character
262 case '(': { // LEFT PARENHESIS (28h)
266 case ')': { // RIGHT PARENTHESIS (29h)
273 $objval = substr($this->pdfdata
, $offset, ($strpos - $offset - 1));
278 case '[': // \x5B LEFT SQUARE BRACKET
279 case ']': { // \x5D RIGHT SQUARE BRACKET
288 $element = $this->getRawObject($offset);
289 $offset = $element[2];
290 $objval[] = $element;
291 } while ($element[0] != ']');
292 // remove closing delimiter
297 case '<': // \x3C LESS-THAN SIGN
298 case '>': { // \x3E GREATER-THAN SIGN
299 if (isset($this->pdfdata
{($offset +
1)}) AND ($this->pdfdata
{($offset +
1)} == $char)) {
301 $objtype = $char.$char;
308 $element = $this->getRawObject($offset);
309 $offset = $element[2];
310 $objval[] = $element;
311 } while ($element[0] != '>>');
312 // remove closing delimiter
316 // hexadecimal string object
319 if (($char == '<') AND (preg_match('/^([0-9A-Fa-f]+)[>]/iU', substr($this->pdfdata
, $offset), $matches) == 1)) {
320 $objval = $matches[1];
321 $offset +
= strlen($matches[0]);
327 if (substr($this->pdfdata
, $offset, 6) == 'endobj') {
331 } elseif (substr($this->pdfdata
, $offset, 4) == 'null') {
336 } elseif (substr($this->pdfdata
, $offset, 4) == 'true') {
337 // boolean true object
338 $objtype = 'boolean';
341 } elseif (substr($this->pdfdata
, $offset, 5) == 'false') {
342 // boolean false object
343 $objtype = 'boolean';
346 } elseif (substr($this->pdfdata
, $offset, 6) == 'stream') {
347 // start stream object
350 if (preg_match('/^[\r\n]+(.*)[\r\n]*endstream/isU', substr($this->pdfdata
, $offset), $matches) == 1) {
351 $objval = $matches[1];
352 $offset +
= strlen($matches[0]);
354 } elseif (substr($this->pdfdata
, $offset, 9) == 'endstream') {
356 $objtype = 'endstream';
358 } elseif (preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+R/iU', substr($this->pdfdata
, $offset, 33), $matches) == 1) {
359 // indirect object reference
361 $offset +
= strlen($matches[0]);
362 $objval = intval($matches[1]).'_'.intval($matches[2]);
363 } elseif (preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+obj/iU', substr($this->pdfdata
, $offset, 33), $matches) == 1) {
366 $objval = intval($matches[1]).'_'.intval($matches[2]);
367 $offset +
= strlen ($matches[0]);
368 } elseif (($numlen = strspn($this->pdfdata
, '+-.0123456789', $offset)) > 0) {
370 $objtype = 'numeric';
371 $objval = substr($this->pdfdata
, $offset, $numlen);
377 return array($objtype, $objval, $offset);
381 * Get content of indirect object.
382 * @param $obj_ref (string) Object number and generation number separated by underscore character.
383 * @param $offset (int) Object offset.
384 * @param $decoding (boolean) If true decode streams.
385 * @return array containing object data.
387 * @since 1.0.000 (2011-05-24)
389 protected function getIndirectObject($obj_ref, $offset=0, $decoding=true) {
390 $obj = explode('_', $obj_ref);
391 if (($obj === false) OR (count($obj) != 2)) {
392 $this->Error('Invalid object reference: '.$obj);
395 $objref = $obj[0].' '.$obj[1].' obj';
396 if (strpos($this->pdfdata
, $objref, $offset) != $offset) {
397 // an indirect reference to an undefined object shall be considered a reference to the null object
398 return array('null', 'null', $offset);
400 // starting position of object content
401 $offset +
= strlen($objref);
402 // get array of object content
404 $i = 0; // object main index
407 $element = $this->getRawObject($offset);
408 $offset = $element[2];
409 // decode stream using stream's dictionary information
410 if ($decoding AND ($element[0] == 'stream') AND (isset($objdata[($i - 1)][0])) AND ($objdata[($i - 1)][0] == '<<')) {
411 $element[3] = $this->decodeStream($objdata[($i - 1)][1], substr($element[1], 1));
413 $objdata[$i] = $element;
415 } while ($element[0] != 'endobj');
416 // remove closing delimiter
418 // return raw object content
423 * Get the content of object, resolving indect object reference if necessary.
424 * @param $obj (string) Object value.
425 * @return array containing object data.
427 * @since 1.0.000 (2011-06-26)
429 protected function getObjectVal($obj) {
430 if ($obj[0] == 'objref') {
431 // reference to indirect object
432 if (isset($this->objects
[$obj[1]])) {
433 // this object has been already parsed
434 return $this->objects
[$obj[1]];
435 } elseif (isset($this->xref
[$obj[1]])) {
437 $this->objects
[$obj[1]] = $this->getIndirectObject($obj[1], $this->xref
[$obj[1]], false);
438 return $this->objects
[$obj[1]];
445 * Decode the specified stream.
446 * @param $sdic (array) Stream's dictionary array.
447 * @param $stream (string) Stream to decode.
448 * @return array containing decoded stream data and remaining filters.
450 * @since 1.0.000 (2011-06-22)
452 protected function decodeStream($sdic, $stream) {
453 // get stream lenght and filters
454 $slength = strlen($stream);
456 foreach ($sdic as $k => $v) {
458 if (($v[1] == 'Length') AND (isset($sdic[($k +
1)])) AND ($sdic[($k +
1)][0] == 'numeric')) {
459 // get declared stream lenght
460 $declength = intval($sdic[($k +
1)][1]);
461 if ($declength < $slength) {
462 $stream = substr($stream, 0, $declength);
463 $slength = $declength;
465 } elseif (($v[1] == 'Filter') AND (isset($sdic[($k +
1)]))) {
466 // resolve indirect object
467 $objval = $this->getObjectVal($sdic[($k +
1)]);
468 if ($objval[0] == '/') {
470 $filters[] = $objval[1];
471 } elseif ($objval[0] == '[') {
473 foreach ($objval[1] as $flt) {
474 if ($flt[0] == '/') {
475 $filters[] = $flt[1];
483 $remaining_filters = array();
484 foreach ($filters as $filter) {
485 if (in_array($filter, $this->FilterDecoders
->getAvailableFilters())) {
486 $stream = $this->FilterDecoders
->decodeFilter($filter, $stream);
488 // add missing filter to array
489 $remaining_filters[] = $filter;
492 return array($stream, $remaining_filters);
496 * This method is automatically called in case of fatal error; it simply outputs the message and halts the execution.
497 * @param $msg (string) The error message
499 * @since 1.0.000 (2011-05-23)
501 public function Error($msg) {
502 // exit program and print error
503 die('<strong>TCPDF_PARSER ERROR: </strong>'.$msg);
506 } // END OF TCPDF_PARSER CLASS
508 //============================================================+
510 //============================================================+