From f5c380bfc94cd195a04e77d3864968a0fd2ae656 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 1 Aug 2019 10:21:43 -0700 Subject: [PATCH] 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 --- src/__phutil_library_map__.php | 31 ++++++ .../pdf/PhabricatorPDFCatalogObject.php | 26 +++++ .../pdf/PhabricatorPDFContentsObject.php | 25 +++++ .../phortune/pdf/PhabricatorPDFFontObject.php | 14 +++ .../phortune/pdf/PhabricatorPDFFragment.php | 38 +++++++ .../pdf/PhabricatorPDFFragmentOffset.php | 27 +++++ .../phortune/pdf/PhabricatorPDFGenerator.php | 59 ++++++++++ .../pdf/PhabricatorPDFHeadFragment.php | 10 ++ .../phortune/pdf/PhabricatorPDFInfoObject.php | 11 ++ .../phortune/pdf/PhabricatorPDFIterator.php | 103 ++++++++++++++++++ .../phortune/pdf/PhabricatorPDFObject.php | 95 ++++++++++++++++ .../phortune/pdf/PhabricatorPDFPageObject.php | 48 ++++++++ .../pdf/PhabricatorPDFPagesObject.php | 38 +++++++ .../pdf/PhabricatorPDFResourcesObject.php | 28 +++++ .../pdf/PhabricatorPDFTailFragment.php | 72 ++++++++++++ 15 files changed, 625 insertions(+) create mode 100644 src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFContentsObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFFontObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFFragment.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFGenerator.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFInfoObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFIterator.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFPageObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFPagesObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFTailFragment.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5abfd1bc4d..03e0163ec8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php b/src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php new file mode 100644 index 0000000000..9cf4d2324e --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php @@ -0,0 +1,26 @@ +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()); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php b/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php new file mode 100644 index 0000000000..f49a32df33 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php @@ -0,0 +1,25 @@ +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); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFontObject.php b/src/applications/phortune/pdf/PhabricatorPDFFontObject.php new file mode 100644 index 0000000000..71f128d3a5 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFontObject.php @@ -0,0 +1,14 @@ +writeLine('/Type /Font'); + + $this->writeLine('/BaseFont /Helvetica-Bold'); + $this->writeLine('/Subtype /Type1'); + $this->writeLine('/Encoding /WinAnsiEncoding'); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFragment.php b/src/applications/phortune/pdf/PhabricatorPDFFragment.php new file mode 100644 index 0000000000..eb113b6140 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFragment.php @@ -0,0 +1,38 @@ +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; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php b/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php new file mode 100644 index 0000000000..c8b2769d7a --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php @@ -0,0 +1,27 @@ +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; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFGenerator.php b/src/applications/phortune/pdf/PhabricatorPDFGenerator.php new file mode 100644 index 0000000000..f2c5c92359 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFGenerator.php @@ -0,0 +1,59 @@ +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; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php b/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php new file mode 100644 index 0000000000..ef12bacce9 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php @@ -0,0 +1,10 @@ +writeLine('%s', '%PDF-1.3'); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php b/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php new file mode 100644 index 0000000000..2aba63c407 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php @@ -0,0 +1,11 @@ +writeLine('/Producer (Phabricator 20190801)'); + $this->writeLine('/CreationDate (D:%s)', date('YmdHis')); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFIterator.php b/src/applications/phortune/pdf/PhabricatorPDFIterator.php new file mode 100644 index 0000000000..d39168369d --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFIterator.php @@ -0,0 +1,103 @@ +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]); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFObject.php b/src/applications/phortune/pdf/PhabricatorPDFObject.php new file mode 100644 index 0000000000..49c14d2f00 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFObject.php @@ -0,0 +1,95 @@ +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); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFPageObject.php b/src/applications/phortune/pdf/PhabricatorPDFPageObject.php new file mode 100644 index 0000000000..3137d45d12 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFPageObject.php @@ -0,0 +1,48 @@ +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); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php b/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php new file mode 100644 index 0000000000..4f0b89886e --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php @@ -0,0 +1,38 @@ +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)); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php b/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php new file mode 100644 index 0000000000..9414708f6d --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php @@ -0,0 +1,28 @@ +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('>>'); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php b/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php new file mode 100644 index 0000000000..2f606a1c8c --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php @@ -0,0 +1,72 @@ +iterator = $iterator; + return $this; + } + + public function getIterator() { + return $this->iterator; + } + + protected function writeFragment() { + $iterator = $this->getIterator(); + $generator = $iterator->getGenerator(); + $objects = $generator->getObjects(); + + $xref_offset = null; + + $this->writeLine('xref'); + $this->writeLine('0 %d', count($objects) + 1); + $this->writeLine('%010d %05d f ', 0, 0xFFFF); + + $offset_map = array(); + + $fragment_offsets = $iterator->getFragmentOffsets(); + foreach ($fragment_offsets as $fragment_offset) { + $fragment = $fragment_offset->getFragment(); + $offset = $fragment_offset->getOffset(); + + if ($fragment === $this) { + $xref_offset = $offset; + } + + if (!$fragment->hasRefTableEntry()) { + continue; + } + + $offset_map[$fragment->getObjectIndex()] = $offset; + } + + ksort($offset_map); + + foreach ($offset_map as $offset) { + $this->writeLine('%010d %05d n ', $offset, 0); + } + + $this->writeLine('trailer'); + $this->writeLine('<<'); + $this->writeLine('/Size %d', count($objects) + 1); + + $info_object = $generator->getInfoObject(); + if ($info_object) { + $this->writeLine('/Info %d 0 R', $info_object->getObjectIndex()); + } + + $catalog_object = $generator->getCatalogObject(); + if ($catalog_object) { + $this->writeLine('/Root %d 0 R', $catalog_object->getObjectIndex()); + } + + $this->writeLine('>>'); + $this->writeLine('startxref'); + $this->writeLine('%d', $xref_offset); + $this->writeLine('%s', '%%EOF'); + } + +}