Skip to content

Instantly share code, notes, and snippets.

@bubach
Last active October 2, 2019 09:35
Show Gist options
  • Save bubach/fdea38be06a5257dbed8745b4b3a3a76 to your computer and use it in GitHub Desktop.
Save bubach/fdea38be06a5257dbed8745b4b3a3a76 to your computer and use it in GitHub Desktop.
Simple PDF
<?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;
}
}
<?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