Commit f5c380bf authored by epriestley's avatar epriestley
Browse files

Add very basic support for generating PDF documents

Summary: Ref T13358. This is very minimal, but technically works. The eventual goal is to generate PDF invoices to make my life easier when I have to interact with Enterprise Vendor Procurement.

Test Plan: {F6672439}

Maniphest Tasks: T13358

Differential Revision: https://secure.phabricator.com/D20692
parent b81c8380
......@@ -3878,7 +3878,21 @@ phutil_register_library_map(array(
'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php',
'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php',
'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php',
'PhabricatorPDFCatalogObject' => 'applications/phortune/pdf/PhabricatorPDFCatalogObject.php',
'PhabricatorPDFContentsObject' => 'applications/phortune/pdf/PhabricatorPDFContentsObject.php',
'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php',
'PhabricatorPDFFontObject' => 'applications/phortune/pdf/PhabricatorPDFFontObject.php',
'PhabricatorPDFFragment' => 'applications/phortune/pdf/PhabricatorPDFFragment.php',
'PhabricatorPDFFragmentOffset' => 'applications/phortune/pdf/PhabricatorPDFFragmentOffset.php',
'PhabricatorPDFGenerator' => 'applications/phortune/pdf/PhabricatorPDFGenerator.php',
'PhabricatorPDFHeadFragment' => 'applications/phortune/pdf/PhabricatorPDFHeadFragment.php',
'PhabricatorPDFInfoObject' => 'applications/phortune/pdf/PhabricatorPDFInfoObject.php',
'PhabricatorPDFIterator' => 'applications/phortune/pdf/PhabricatorPDFIterator.php',
'PhabricatorPDFObject' => 'applications/phortune/pdf/PhabricatorPDFObject.php',
'PhabricatorPDFPageObject' => 'applications/phortune/pdf/PhabricatorPDFPageObject.php',
'PhabricatorPDFPagesObject' => 'applications/phortune/pdf/PhabricatorPDFPagesObject.php',
'PhabricatorPDFResourcesObject' => 'applications/phortune/pdf/PhabricatorPDFResourcesObject.php',
'PhabricatorPDFTailFragment' => 'applications/phortune/pdf/PhabricatorPDFTailFragment.php',
'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
......@@ -10101,7 +10115,24 @@ phutil_register_library_map(array(
'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorPDFCatalogObject' => 'PhabricatorPDFObject',
'PhabricatorPDFContentsObject' => 'PhabricatorPDFObject',
'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorPDFFontObject' => 'PhabricatorPDFObject',
'PhabricatorPDFFragment' => 'Phobject',
'PhabricatorPDFFragmentOffset' => 'Phobject',
'PhabricatorPDFGenerator' => 'Phobject',
'PhabricatorPDFHeadFragment' => 'PhabricatorPDFFragment',
'PhabricatorPDFInfoObject' => 'PhabricatorPDFObject',
'PhabricatorPDFIterator' => array(
'Phobject',
'Iterator',
),
'PhabricatorPDFObject' => 'PhabricatorPDFFragment',
'PhabricatorPDFPageObject' => 'PhabricatorPDFObject',
'PhabricatorPDFPagesObject' => 'PhabricatorPDFObject',
'PhabricatorPDFResourcesObject' => 'PhabricatorPDFObject',
'PhabricatorPDFTailFragment' => 'PhabricatorPDFFragment',
'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHID' => 'Phobject',
'PhabricatorPHIDConstants' => 'Phobject',
......
<?php
final class PhabricatorPDFCatalogObject
extends PhabricatorPDFObject {
private $pagesObject;
public function setPagesObject(PhabricatorPDFPagesObject $pages_object) {
$this->pagesObject = $this->newChildObject($pages_object);
return $this;
}
public function getPagesObject() {
return $this->pagesObject;
}
protected function writeObject() {
$this->writeLine('/Type /Catalog');
$pages_object = $this->getPagesObject();
if ($pages_object) {
$this->writeLine('/Pages %d 0 R', $pages_object->getObjectIndex());
}
}
}
<?php
final class PhabricatorPDFContentsObject
extends PhabricatorPDFObject {
private $rawContent;
public function setRawContent($raw_content) {
$this->rawContent = $raw_content;
return $this;
}
public function getRawContent() {
return $this->rawContent;
}
protected function writeObject() {
$data = $this->getRawContent();
$stream_length = $this->newStream($data);
$this->writeLine('/Filter /FlateDecode /Length %d', $stream_length);
}
}
<?php
final class PhabricatorPDFFontObject
extends PhabricatorPDFObject {
protected function writeObject() {
$this->writeLine('/Type /Font');
$this->writeLine('/BaseFont /Helvetica-Bold');
$this->writeLine('/Subtype /Type1');
$this->writeLine('/Encoding /WinAnsiEncoding');
}
}
<?php
abstract class PhabricatorPDFFragment
extends Phobject {
private $rope;
public function getAsBytes() {
$this->rope = new PhutilRope();
$this->writeFragment();
$rope = $this->rope;
$this->rope = null;
return $rope->getAsString();
}
public function hasRefTableEntry() {
return false;
}
abstract protected function writeFragment();
final protected function writeLine($pattern) {
$pattern = $pattern."\n";
$argv = func_get_args();
$argv[0] = $pattern;
$line = call_user_func_array('sprintf', $argv);
$this->rope->append($line);
return $this;
}
}
<?php
final class PhabricatorPDFFragmentOffset
extends Phobject {
private $fragment;
private $offset;
public function setFragment(PhabricatorPDFFragment $fragment) {
$this->fragment = $fragment;
return $this;
}
public function getFragment() {
return $this->fragment;
}
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function getOffset() {
return $this->offset;
}
}
<?php
final class PhabricatorPDFGenerator
extends Phobject {
private $objects = array();
private $hasIterator = false;
private $infoObject;
private $catalogObject;
public function addObject(PhabricatorPDFObject $object) {
if ($this->hasIterator) {
throw new Exception(
pht(
'This generator has already emitted an iterator. You can not '.
'modify the PDF document after you begin writing it.'));
}
$this->objects[] = $object;
$index = count($this->objects);
$object->setGenerator($this, $index);
return $this;
}
public function getObjects() {
return $this->objects;
}
public function newIterator() {
$this->hasIterator = true;
return id(new PhabricatorPDFIterator())
->setGenerator($this);
}
public function setInfoObject(PhabricatorPDFInfoObject $info_object) {
$this->addObject($info_object);
$this->infoObject = $info_object;
return $this;
}
public function getInfoObject() {
return $this->infoObject;
}
public function setCatalogObject(
PhabricatorPDFCatalogObject $catalog_object) {
$this->addObject($catalog_object);
$this->catalogObject = $catalog_object;
return $this;
}
public function getCatalogObject() {
return $this->catalogObject;
}
}
<?php
final class PhabricatorPDFHeadFragment
extends PhabricatorPDFFragment {
protected function writeFragment() {
$this->writeLine('%s', '%PDF-1.3');
}
}
<?php
final class PhabricatorPDFInfoObject
extends PhabricatorPDFObject {
final protected function writeObject() {
$this->writeLine('/Producer (Phabricator 20190801)');
$this->writeLine('/CreationDate (D:%s)', date('YmdHis'));
}
}
<?php
final class PhabricatorPDFIterator
extends Phobject
implements Iterator {
private $generator;
private $hasRewound;
private $fragments;
private $fragmentKey;
private $fragmentBytes;
private $fragmentOffsets = array();
private $byteLength;
public function setGenerator(PhabricatorPDFGenerator $generator) {
if ($this->generator) {
throw new Exception(
pht(
'This iterator already has a generator. You can not modify the '.
'generator for a given iterator.'));
}
$this->generator = $generator;
return $this;
}
public function getGenerator() {
if (!$this->generator) {
throw new Exception(
pht(
'This PDF iterator has no associated PDF generator.'));
}
return $this->generator;
}
public function getFragmentOffsets() {
return $this->fragmentOffsets;
}
public function current() {
return $this->fragmentBytes;
}
public function key() {
return $this->framgentKey;
}
public function next() {
$this->fragmentKey++;
if (!$this->valid()) {
return;
}
$fragment = $this->fragments[$this->fragmentKey];
$this->fragmentOffsets[] = id(new PhabricatorPDFFragmentOffset())
->setFragment($fragment)
->setOffset($this->byteLength);
$bytes = $fragment->getAsBytes();
$this->fragmentBytes = $bytes;
$this->byteLength += strlen($bytes);
}
public function rewind() {
if ($this->hasRewound) {
throw new Exception(
pht(
'PDF iterators may not be rewound. Create a new iterator to emit '.
'another PDF.'));
}
$generator = $this->getGenerator();
$objects = $generator->getObjects();
$this->fragments = array();
$this->fragments[] = new PhabricatorPDFHeadFragment();
foreach ($objects as $object) {
$this->fragments[] = $object;
}
$this->fragments[] = id(new PhabricatorPDFTailFragment())
->setIterator($this);
$this->hasRewound = true;
$this->fragmentKey = -1;
$this->byteLength = 0;
$this->next();
}
public function valid() {
return isset($this->fragments[$this->fragmentKey]);
}
}
<?php
abstract class PhabricatorPDFObject
extends PhabricatorPDFFragment {
private $generator;
private $objectIndex;
private $children = array();
private $streams = array();
final public function hasRefTableEntry() {
return true;
}
final protected function writeFragment() {
$this->writeLine('%d 0 obj', $this->getObjectIndex());
$this->writeLine('<<');
$this->writeObject();
$this->writeLine('>>');
$streams = $this->streams;
$this->streams = array();
foreach ($streams as $stream) {
$this->writeLine('stream');
$this->writeLine('%s', $stream);
$this->writeLine('endstream');
}
$this->writeLine('endobj');
}
final public function setGenerator(
PhabricatorPDFGenerator $generator,
$index) {
if ($this->getGenerator()) {
throw new Exception(
pht(
'This PDF object is already registered with a PDF generator. You '.
'can not register an object with more than one generator.'));
}
$this->generator = $generator;
$this->objectIndex = $index;
foreach ($this->getChildren() as $child) {
$generator->addObject($child);
}
return $this;
}
final public function getGenerator() {
return $this->generator;
}
final public function getObjectIndex() {
if (!$this->objectIndex) {
throw new Exception(
pht(
'Trying to get index for object ("%s") which has not been '.
'registered with a generator.',
get_class($this)));
}
return $this->objectIndex;
}
final protected function newChildObject(PhabricatorPDFObject $object) {
if ($this->generator) {
throw new Exception(
pht(
'Trying to add a new PDF Object child after already registering '.
'the object with a generator.'));
}
$this->children[] = $object;
return $object;
}
private function getChildren() {
return $this->children;
}
abstract protected function writeObject();
final protected function newStream($raw_data) {
$stream_data = gzcompress($raw_data);
$this->streams[] = $stream_data;
return strlen($stream_data);
}
}
<?php
final class PhabricatorPDFPageObject
extends PhabricatorPDFObject {
private $pagesObject;
private $contentsObject;
private $resourcesObject;
public function setPagesObject(PhabricatorPDFPagesObject $pages) {
$this->pagesObject = $pages;
return $this;
}
public function setContentsObject(PhabricatorPDFContentsObject $contents) {
$this->contentsObject = $this->newChildObject($contents);
return $this;
}
public function setResourcesObject(PhabricatorPDFResourcesObject $resources) {
$this->resourcesObject = $this->newChildObject($resources);
return $this;
}
protected function writeObject() {
$this->writeLine('/Type /Page');
$pages_object = $this->pagesObject;
$contents_object = $this->contentsObject;
$resources_object = $this->resourcesObject;
if ($pages_object) {
$pages_index = $pages_object->getObjectIndex();
$this->writeLine('/Parent %d 0 R', $pages_index);
}
if ($contents_object) {
$contents_index = $contents_object->getObjectIndex();
$this->writeLine('/Contents %d 0 R', $contents_index);
}
if ($resources_object) {
$resources_index = $resources_object->getObjectIndex();
$this->writeLine('/Resources %d 0 R', $resources_index);
}
}
}
<?php
final class PhabricatorPDFPagesObject
extends PhabricatorPDFObject {
private $pageObjects = array();
public function addPageObject(PhabricatorPDFPageObject $page) {
$page->setPagesObject($this);
$this->pageObjects[] = $this->newChildObject($page);
return $this;
}
public function getPageObjects() {
return $this->pageObjects;
}
protected function writeObject() {
$this->writeLine('/Type /Pages');
$page_objects = $this->getPageObjects();
$this->writeLine('/Count %d', count($page_objects));
$this->writeLine('/MediaBox [%d %d %0.2f %0.2f]', 0, 0, 595.28, 841.89);
if ($page_objects) {
$kids = array();
foreach ($page_objects as $page_object) {
$kids[] = sprintf(
'%d 0 R',
$page_object->getObjectIndex());
}
$this->writeLine('/Kids [%s]', implode(' ', $kids));
}
}
}
<?php
final class PhabricatorPDFResourcesObject
extends PhabricatorPDFObject {
private $fontObjects = array();
public function addFontObject(PhabricatorPDFFontObject $font) {
$this->fontObjects[] = $this->newChildObject($font);
return $this;
}
public function getFontObjects() {
return $this->fontObjects;
}
protected function writeObject() {
$this->writeLine('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
$fonts = $this->getFontObjects();
foreach ($fonts as $font) {
$this->writeLine('/Font <<');
$this->writeLine('/F%d %d 0 R', 1, $font->getObjectIndex());
$this->writeLine('>>');
}
}
}
<?php
final class PhabricatorPDFTailFragment
extends PhabricatorPDFFragment {
private $iterator;
public function setIterator(PhabricatorPDFIterator $iterator) {
$this->iterator = $iterator;
return $this;
}
public function getIterator() {