Last active
October 2, 2019 09:35
-
-
Save bubach/fdea38be06a5257dbed8745b4b3a3a76 to your computer and use it in GitHub Desktop.
Simple PDF
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace PdfBuilder\Document; | |
use PdfBuilder\Interfaces\FilterInterface; | |
use PdfBuilder\Interfaces\StructureInterface; | |
use PdfBuilder\Interfaces\StreamInterface; | |
/** | |
* PdfObject, class to build and parse PDF COS-format. | |
* | |
* @package PdfBuilder | |
* @author Christoffer Bubach | |
* @link https://github.com/bubach/pdfbuilder | |
* @license MIT | |
*/ | |
class PdfObject implements StructureInterface | |
{ | |
/** | |
* The id for indirect objects | |
* | |
* @var int | |
*/ | |
public $objectId = null; | |
/** | |
* The revision number for indirect objects | |
* | |
* @var int | |
*/ | |
public $objectGeneration = 0; | |
/** | |
* Object offset in output stream | |
* | |
* @var int | |
*/ | |
public $offset = 0; | |
/** | |
* Object type | |
* | |
* @var int | |
*/ | |
protected $type = self::UNKNOWN; | |
/** | |
* PdfObject internal data | |
* | |
* @var array | |
*/ | |
protected $data = []; | |
/** | |
* The internal StreamInterface for indirect stream objects | |
* | |
* @var null|StreamInterface | |
*/ | |
protected $stream = null; | |
/** | |
* Array of any filters applied to the internal StreamInterface | |
* | |
* @var array | |
*/ | |
protected $filters = []; | |
/** | |
* Cross reference table | |
* | |
* @var null|PdfObject | |
*/ | |
protected $referenceTable = null; | |
/** | |
* PdfObject constructor. | |
* | |
* @param string|int $type | |
*/ | |
public function __construct($type = self::UNKNOWN) | |
{ | |
$this->setType($type); | |
} | |
/** | |
* @return int|mixed | |
*/ | |
public function getType() | |
{ | |
return $this->type; | |
} | |
/** | |
* Set structure type, will set type to object and also | |
* dictionary named type if provided with a string. | |
* | |
* @param string|int $type | |
* @return self | |
*/ | |
public function setType($type) | |
{ | |
if (is_string($type)) { | |
$this->type = self::OBJECT; | |
$this->set('Type', $this->escName($type)); | |
} | |
$valid = [self::UNKNOWN, self::DOCUMENT, self::XREF_TABLE, self::STREAM_OBJ, self::OBJECT]; | |
if (in_array($type, $valid, true)) { | |
$this->type = $type; | |
} | |
return $this; | |
} | |
/** | |
* Get object reference-callback | |
* | |
* @return callable | |
*/ | |
public function getReference() | |
{ | |
return function($referenceTable = null) { | |
if (is_null($this->objectId)) { | |
/** @var PdfObject $referenceTable */ | |
$referenceTable = empty($referenceTable) ? $this->getReferenceTable() : $referenceTable; | |
$referenceTable->add($this); | |
} | |
return $this->objectId . ' ' . $this->objectGeneration . ' R'; | |
}; | |
} | |
/** | |
* Get cross-reference table | |
* | |
* @return PdfObject | |
*/ | |
public function getReferenceTable() | |
{ | |
if (empty($this->referenceTable)) { | |
$this->referenceTable = new self(self::XREF_TABLE); | |
} | |
return $this->referenceTable; | |
} | |
/** | |
* Returns structure objects as associative or indexed array | |
* | |
* @return array | |
*/ | |
public function getAll() | |
{ | |
return $this->data; | |
} | |
/** | |
* Set structure data objects | |
* | |
* @param array $objects | |
* @return self | |
*/ | |
public function setAll($objects = []) | |
{ | |
$this->data = (array)$objects; | |
return $this; | |
} | |
/** | |
* Adds entry to internal data array. | |
* | |
* @param mixed $object | |
* @return self | |
*/ | |
public function add($object) | |
{ | |
switch ($this->type) { | |
case self::DOCUMENT: | |
if ($object instanceof PdfObject) { | |
$this->getReferenceTable()->add($object); | |
} else { | |
$this->data[] = $object; | |
} | |
break; | |
case self::XREF_TABLE: | |
if ($object instanceof PdfObject) { | |
if (empty($object->objectId)) { | |
$object->objectId = count($this->referenceTable) + 1; | |
} | |
$this->referenceTable[$object->objectId] = [$object->objectGeneration => $object]; | |
$this->set('Size', count($this->referenceTable)); | |
} | |
break; | |
case self::STREAM_OBJ: | |
$this->addStreamObjectData($object); | |
break; | |
default: | |
$this->data[] = $object; | |
} | |
return $this; | |
} | |
/** | |
* Add internal StreamInterface or FilterInterface to stream-object | |
* | |
* @param StreamInterface|FilterInterface $object | |
* @return self | |
*/ | |
protected function addStreamObjectData($object) | |
{ | |
if ($object instanceof StreamInterface) { | |
$this->stream = $object; | |
foreach ($this->filters as $filter) { | |
$filter->add($this->stream); | |
} | |
} elseif ($object instanceof FilterInterface) { | |
if ($this->stream) { | |
$object->add($this->stream); | |
} | |
$this->filters[$object::NAME] = $object; | |
} | |
return $this; | |
} | |
/** | |
* Set data at specific string or indexed key | |
* | |
* @param int|string $key | |
* @param mixed $data | |
*/ | |
public function set($key, $data) | |
{ | |
if (isset($this->data[$key]) && !is_array($this->data[$key]) && !is_array($data)) { | |
$this->data[$key] = $data; | |
} else { | |
$this->data = array_merge_recursive($this->data, [$key => $data]); | |
} | |
} | |
/** | |
* Get structure data from index or name/string key | |
* | |
* @param $key | |
* @return mixed|null | |
*/ | |
public function get($key) | |
{ | |
if (isset($this->data[$key])) { | |
return trim($this->data[$key], '/()%'); | |
} | |
$nameKey = $this->escName($key); | |
if (isset($this->data[$nameKey])) { | |
return trim($this->data[$nameKey], '/()%'); | |
} | |
$stringKey = $this->escString($key); | |
if (isset($this->data[$stringKey])) { | |
return trim($this->data[$stringKey], '/()%'); | |
} | |
return null; | |
} | |
/** | |
* Escapes name object | |
* | |
* @param string $value | |
* @return string | |
*/ | |
public function escName($value) | |
{ | |
return '/' . $this->escapeSpecialChars($value); | |
} | |
/** | |
* Escapes string object | |
* | |
* @param string $value | |
* @return string | |
*/ | |
public function escString($value) | |
{ | |
$value = $this->escapeSpecialChars($value); | |
return '(' . mb_convert_encoding($value, 'cp1252', 'UTF-8') . ')'; | |
} | |
/** | |
* Escapes comment value. | |
* | |
* @param string $value | |
* @return string | |
*/ | |
public function escComment($value) | |
{ | |
return '%' . $this->escapeSpecialChars($value); | |
} | |
/** | |
* Escape special characters | |
* | |
* @param string $string | |
* @return string | |
*/ | |
protected function escapeSpecialChars($string) | |
{ | |
return str_replace(['\\', '(', ')', "\r"], ['\\\\', '\\(', '\\)', '\\r'], $string); | |
} | |
/** | |
* Recursively get structure's direct objects as string. | |
* | |
* @param array $entries | |
* @param int $indent | |
* @return string | |
*/ | |
public function build($entries = [], $indent = 0) | |
{ | |
$res = null; | |
$entries = (empty($entries) ? $this->data : $entries); | |
foreach ($entries as $key => $value) { | |
$res .= $this->buildKey($key, $indent); | |
$res .= $this->buildValue($value, $indent); | |
} | |
return $res; | |
} | |
/** | |
* Build key part of structure | |
* | |
* @param string $key | |
* @param int $indent | |
* @param string $output | |
* @return string | |
*/ | |
protected function buildKey($key, $indent, $output = '') | |
{ | |
if (!is_int($key)) { | |
$output .= "\n" . str_repeat(' ', (4 * $indent)); | |
$output .= $this->escName($key); | |
} | |
if (!is_int($key) || $key !== 0) { | |
$output .= (($this->type == self::DOCUMENT) ? "\n" : ' '); | |
} | |
return $output; | |
} | |
/** | |
* Build value part of structure | |
* | |
* @param mixed $value | |
* @param int $indent | |
* @param string $output | |
* @return string | |
*/ | |
protected function buildValue($value, $indent, $output = '') | |
{ | |
$isDictionary = (is_array($value) && (array_keys($value) !== range(0, count($value) - 1))); | |
switch (true) { | |
case (is_array($value) && $isDictionary): | |
$output .= '<<' . trim($this->build($value, $indent + 1), $indent); | |
$output .= "\n" . str_repeat(' ', (4 * $indent)) . ">>"; | |
break; | |
case (is_array($value)): | |
$output .= '[' . trim($this->build($value), $indent) . "]"; | |
break; | |
case (is_callable($value) && !is_string($value)): | |
$output .= $value(); | |
break; | |
case ($value instanceof PdfObject): | |
$output .= $value->build(); | |
break; | |
case (is_scalar($value)): | |
$output .= $value; | |
break; | |
} | |
return $output; | |
} | |
/** | |
* Update dictionary information about internal StreamInterface | |
* | |
* @return self | |
*/ | |
protected function updateStreamInfo() | |
{ | |
if ($this->type == self::STREAM_OBJ && $this->stream) { | |
$this->set('Length', $this->stream->getSize()); | |
} | |
return $this; | |
} | |
/** | |
* Pipe objects and object offset table | |
* | |
* @param StreamInterface $stream | |
*/ | |
protected function pipeObjectTable(StreamInterface $stream) | |
{ | |
$offsets = []; | |
foreach ($this->referenceTable as $objectId => $generationInfo) { | |
/** @var PdfObject $object */ | |
foreach ($generationInfo as $generationId => $object) { | |
$offsets[] = $object->offset = $stream->getOffset(); | |
$object->pipe($stream); | |
} | |
} | |
$this->offset = $stream->getOffset(); | |
$stream->write(sprintf("\nxref\n0 %s\n0000000000 65535 f \n", count($offsets))); | |
foreach ($offsets as $offset) { | |
$stream->write(sprintf("%010d 00000 n \n", $offset + 1)); | |
} | |
$stream->write("trailer\n<<"); | |
} | |
/** | |
* Get structure piped to provided StreamInterface | |
* | |
* @param StreamInterface $stream | |
* @return StreamInterface | |
*/ | |
public function pipe(StreamInterface $stream) | |
{ | |
$this->offset = $stream->getOffset(); | |
switch ($this->type) { | |
case self::XREF_TABLE: | |
$this->pipeObjectTable($stream); | |
break; | |
case self::OBJECT: | |
case self::STREAM_OBJ: | |
$stream->write("\n{$this->objectId} {$this->objectGeneration} obj\n<<"); | |
$this->updateStreamInfo(); | |
break; | |
case self::DICTIONARY: | |
$stream->write("\n<<"); | |
break; | |
} | |
$stream->write($this->build($this->data, 1)); | |
switch ($this->type) { | |
case self::DOCUMENT: | |
$stream->write("\n"); | |
$this->getReferenceTable()->pipe($stream); | |
$stream->write("%%EOF"); | |
break; | |
case self::XREF_TABLE: | |
$stream->write("\n>>\n"); | |
$stream->write("startxref\n"); | |
$stream->write($this->offset . "\n"); | |
break; | |
case self::OBJECT: | |
$stream->write("\n>>\nendobj\n"); | |
break; | |
case self::STREAM_OBJ: | |
$stream->write("\n>>\n"); | |
if ($this->stream) { | |
$stream->write("stream\n"); | |
$this->stream->pipe($stream); | |
$stream->write("\nendstream\n"); | |
} | |
$stream->write("endobj\n"); | |
break; | |
case self::DICTIONARY: | |
$stream->write("\n>>\n"); | |
break; | |
} | |
return $stream; | |
} | |
/** | |
* Parses input source and sets internal type and data. | |
* | |
* @param StreamInterface $source | |
* @return self | |
*/ | |
public function parse($source) | |
{ | |
return $this; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
include './vendor/autoload.php'; | |
use PdfBuilder\Document\PdfObject; | |
use PdfBuilder\Stream\Stream; // PdfObjct class could eaisly be modified to work on normal fopen-handle | |
$doc = new PdfObject(PdfObject::DOCUMENT); | |
$doc->add($doc->escComment('PDF-1.7')); | |
$doc->add($doc->escComment("âãÏÓ")); | |
$catalog = new PdfObject('Catalog'); | |
$doc->add($catalog); | |
$doc->getReferenceTable()->set('Root', $catalog->getReference()); | |
$pages = new PdfObject('Pages'); | |
$doc->add($pages); | |
$catalog->set('Pages', $pages->getReference()); | |
$page = new PdfObject('Page'); | |
$doc->add($page); | |
$pages->set('Count', 1); | |
$pages->set('Kids', [ | |
$page->getReference(), | |
]); | |
$pages->set('MediaBox', [0, 0, 595.28, 841.89]); // A4 | |
$page->set('Parent', $pages->getReference()); | |
$page->set('Resources', [ | |
'Font' => [ | |
'F1' => [ | |
'Type' => $page->escName('Font'), | |
'Subtype' => $page->escName('Type1'), | |
'BaseFont' => $page->escName('Times-Roman'), | |
], | |
], | |
]); | |
$content = new PdfObject($doc::STREAM_OBJ); | |
$page->set('Contents', $content->getReference()); | |
$doc->add($content); | |
$stream = new Stream(); | |
$stream->write(" BT\n /F1 18 Tf\n 20 800 Td\n (Hello World) Tj\n ET"); | |
$content->add($stream); | |
$doc->pipe(new Stream(STDOUT)); | |
/* | |
%PDF-1.7 | |
%âãÏÓ | |
1 0 obj | |
<< | |
/Type /Catalog | |
/Pages 2 0 R | |
>> | |
endobj | |
2 0 obj | |
<< | |
/Type /Pages | |
/Count 1 | |
/Kids [3 0 R] | |
/MediaBox [0 0 595.28 841.89] | |
>> | |
endobj | |
3 0 obj | |
<< | |
/Type /Page | |
/Parent 2 0 R | |
/Resources << | |
/Font << | |
/F1 << | |
/Type /Font | |
/Subtype /Type1 | |
/BaseFont /Times-Roman | |
>> | |
>> | |
>> | |
/Contents 4 0 R | |
>> | |
endobj | |
4 0 obj | |
<< | |
/Length 58 | |
>> | |
stream | |
BT | |
/F1 18 Tf | |
20 800 Td | |
(Hello World) Tj | |
ET | |
endstream | |
endobj | |
xref | |
0 4 | |
0000000000 65535 f | |
0000000020 00000 n | |
0000000078 00000 n | |
0000000182 00000 n | |
0000000444 00000 n | |
trailer | |
<< | |
/Size 4 | |
/Root 1 0 R | |
>> | |
startxref | |
498 | |
%%EOF | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment