/** * Object constructor. * * If resource is not a \Zend\Pdf\InternalType\AbstractTypeObject object, * then stream object with specified value is generated. * * @param \Zend\Pdf\InternalType\AbstractTypeObject|string $resource */ public function __construct($resource) { $this->_objectFactory = ObjectFactory\ElementFactory::createFactory(1); if ($resource instanceof InternalType\AbstractTypeObject) { $this->_resource = $this->_objectFactory->newObject($resource); } else { $this->_resource = $this->_objectFactory->newStreamObject($resource); } }
/** * Dereference. * Take inderect object, take $value member of this object (must be \Zend\Pdf\InternalType\AbstractTypeObject), * take reference to the $value member of this object and assign it to * $value member of current PDF Reference object * $obj can be null * * @throws \Zend\Pdf\Exception */ private function _dereference() { if (($obj = $this->_factory->fetchObject($this->_objNum . ' ' . $this->_genNum)) === null) { $obj = $this->_context->getParser()->getObject($this->_context->getRefTable()->getOffset($this->_objNum . ' ' . $this->_genNum . ' R'), $this->_context); } if ($obj === null) { $this->_ref = new NullObject(); return; } if ($obj->toString() != $this->_objNum . ' ' . $this->_genNum . ' R') { throw new Exception\RuntimeException('Incorrect reference to the object'); } $this->_ref = $obj; }
/** * Clone page, extract it and dependent objects from the current document, * so it can be used within other docs. */ public function __clone() { $factory = \Zend\Pdf\ObjectFactory::createFactory(1); $processed = array(); // Clone dictionary object. // Do it explicitly to prevent sharing page attributes between different // results of clonePage() operation (other resources are still shared) $dictionary = new InternalType\DictionaryObject(); foreach ($this->_pageDictionary->getKeys() as $key) { $dictionary->{$key} = $this->_pageDictionary->{$key}->makeClone($factory, $processed, InternalType\AbstractTypeObject::CLONE_MODE_SKIP_PAGES); } $this->_pageDictionary = $factory->newObject($dictionary); $this->_objFactory = $factory; $this->_attached = false; $this->_style = null; $this->_font = null; }
/** * * @param \Zend\Pdf\Annotation\AbstractAnnotation $annotation * @return \Zend\Pdf\Page */ public function attachAnnotation(Annotation\AbstractAnnotation $annotation) { $annotationDictionary = $annotation->getResource(); if (!$annotationDictionary instanceof InternalType\IndirectObject && !$annotationDictionary instanceof InternalType\IndirectObjectReference) { $annotationDictionary = $this->_objFactory->newObject($annotationDictionary); } if ($this->_pageDictionary->Annots === null) { $this->_pageDictionary->touch(); $this->_pageDictionary->Annots = new InternalType\ArrayObject(); } else { $this->_pageDictionary->Annots->touch(); } $this->_pageDictionary->Annots->items[] = $annotationDictionary; $annotationDictionary->touch(); $annotationDictionary->P = $this->_pageDictionary; return $this; }
/** * Prepare page to be rendered into PDF. * * @todo Don't forget to close all current graphics operations (like path drawing) * * @param \Zend\Pdf\ObjectFactory $objFactory * @throws \Zend\Pdf\Exception */ public function render(ObjectFactory $objFactory) { $this->flush(); if ($objFactory === $this->_objFactory) { // Page is already attached to the document. return; } if ($this->_attached) { throw new Exception\LogicException('Page is attached to other document. Use clone $page to get it context free.'); } else { $objFactory->attach($this->_objFactory); } }
/** * Attach factory to the current; * * @param \Zend\Pdf\ObjectFactory $factory */ public function attach(ObjectFactory $factory) { if ($factory === $this || isset($this->_attachedFactories[$factory->getId()])) { /** * Don't attach factory twice. * We do not check recusively because of nature of attach operation * (Pages are always attached to the Documents, Fonts are always attached * to the pages even if pages already use Document level object factory and so on) */ return; } $this->_attachedFactories[$factory->getId()] = $factory; }
/** * Mark object as modified, to include it into new PDF file segment */ public function touch() { $this->_factory->markAsModified($this); }
/** * Read inderect object from a PDF stream * * @param integer $offset * @param \Zend\Pdf\InternalType\IndirectObjectReference\Context $context * @return \Zend\Pdf\InternalType\IndirectObject */ public function getObject($offset, IndirectObjectReference\Context $context) { if ($offset === null) { return new InternalType\NullObject(); } // Save current offset to make getObject() reentrant $offsetSave = $this->offset; $this->offset = $offset; $this->_context = $context; $this->_elements = array(); $objNum = $this->readLexeme(); if (!ctype_digit($objNum)) { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. Object number expected.', $this->offset - strlen($objNum))); } $genNum = $this->readLexeme(); if (!ctype_digit($genNum)) { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. Object generation number expected.', $this->offset - strlen($genNum))); } $objKeyword = $this->readLexeme(); if ($objKeyword != 'obj') { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. \'obj\' keyword expected.', $this->offset - strlen($objKeyword))); } $objValue = $this->readElement(); $nextLexeme = $this->readLexeme(); if ($nextLexeme == 'endobj') { /** * Object is not generated by factory (thus it's not marked as modified object). * But factory is assigned to the obect. */ $obj = new InternalType\IndirectObject($objValue, (int) $objNum, (int) $genNum, $this->_objFactory->resolve()); foreach ($this->_elements as $element) { $element->setParentObject($obj); } // Restore offset value $this->offset = $offsetSave; return $obj; } /** * It's a stream object */ if ($nextLexeme != 'stream') { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. \'endobj\' or \'stream\' keywords expected.', $this->offset - strlen($nextLexeme))); } if (!$objValue instanceof InternalType\DictionaryObject) { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. Stream extent must be preceded by stream dictionary.', $this->offset - strlen($nextLexeme))); } /** * References are automatically dereferenced at this moment. */ $streamLength = $objValue->Length->value; /** * 'stream' keyword must be followed by either cr-lf sequence or lf character only. * This restriction gives the possibility to recognize all cases exactly */ if ($this->data[$this->offset] == "\r" && $this->data[$this->offset + 1] == "\n") { $this->offset += 2; } else { if ($this->data[$this->offset] == "\n") { $this->offset++; } else { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. \'stream\' must be followed by either cr-lf sequence or lf character only.', $this->offset - strlen($nextLexeme))); } } $dataOffset = $this->offset; $this->offset += $streamLength; $nextLexeme = $this->readLexeme(); if ($nextLexeme != 'endstream') { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. \'endstream\' keyword expected.', $this->offset - strlen($nextLexeme))); } $nextLexeme = $this->readLexeme(); if ($nextLexeme != 'endobj') { throw new Pdf\Exception(sprintf('PDF file syntax error. Offset - 0x%X. \'endobj\' keyword expected.', $this->offset - strlen($nextLexeme))); } $obj = new InternalType\StreamObject(substr($this->data, $dataOffset, $streamLength), (int) $objNum, (int) $genNum, $this->_objFactory->resolve(), $objValue); foreach ($this->_elements as $element) { $element->setParentObject($obj); } // Restore offset value $this->offset = $offsetSave; return $obj; }
/** * Dump Outline and its child outlines into PDF structures * * Returns dictionary indirect object or reference * * @internal * @param \Zend\Pdf\ObjectFactory $factory object factory for newly created indirect objects * @param boolean $updateNavigation Update navigation flag * @param \Zend\Pdf\InternalType\AbstractTypeObject $parent Parent outline dictionary reference * @param \Zend\Pdf\InternalType\AbstractTypeObject $prev Previous outline dictionary reference * @param SplObjectStorage $processedOutlines List of already processed outlines * @return \Zend\Pdf\InternalType\AbstractTypeObject * @throws \Zend\Pdf\Exception */ public function dumpOutline(ObjectFactory $factory, $updateNavigation, InternalType\AbstractTypeObject $parent, InternalType\AbstractTypeObject $prev = null, \SplObjectStorage $processedOutlines = null) { if ($processedOutlines === null) { $processedOutlines = new \SplObjectStorage(); } $processedOutlines->attach($this); $outlineDictionary = $factory->newObject(new InternalType\DictionaryObject()); $outlineDictionary->Title = new InternalType\StringObject($this->getTitle()); $target = $this->getTarget(); if ($target === null) { // Do nothing } else { if ($target instanceof Pdf\Destination\AbstractDestination) { $outlineDictionary->Dest = $target->getResource(); } else { if ($target instanceof Action\AbstractAction) { $outlineDictionary->A = $target->getResource(); } else { throw new Exception\CorruptedPdfException('Outline target has to be \\Zend\\Pdf\\Destination, \\Zend\\Pdf\\Action object or null'); } } } $color = $this->getColor(); if ($color !== null) { $components = $color->getComponents(); $colorComponentElements = array(new InternalType\NumericObject($components[0]), new InternalType\NumericObject($components[1]), new InternalType\NumericObject($components[2])); $outlineDictionary->C = new InternalType\ArrayObject($colorComponentElements); } if ($this->isItalic() || $this->isBold()) { $outlineDictionary->F = new InternalType\NumericObject(($this->isItalic() ? 1 : 0) | ($this->isBold() ? 2 : 0)); // Bit 2 - Bold } $outlineDictionary->Parent = $parent; $outlineDictionary->Prev = $prev; $lastChild = null; foreach ($this->childOutlines as $childOutline) { if ($processedOutlines->contains($childOutline)) { throw new Exception\CorruptedPdfException('Outlines cyclyc reference is detected.'); } if ($lastChild === null) { $lastChild = $childOutline->dumpOutline($factory, true, $outlineDictionary, null, $processedOutlines); $outlineDictionary->First = $lastChild; } else { $childOutlineDictionary = $childOutline->dumpOutline($factory, true, $outlineDictionary, $lastChild, $processedOutlines); $lastChild->Next = $childOutlineDictionary; $lastChild = $childOutlineDictionary; } } $outlineDictionary->Last = $lastChild; if (count($this->childOutlines) != 0) { $outlineDictionary->Count = new InternalType\NumericObject(($this->isOpen() ? 1 : -1) * count($this->childOutlines)); } return $outlineDictionary; }
/** * Dump object to a string to save within PDF file * * $factory parameter defines operation context. * * @param \Zend\Pdf\ObjectFactory $factory * @return string */ public function dump(ObjectFactory $factory) { $shift = $factory->getEnumerationShift($this->_factory); if ($this->_streamDecoded) { $this->_initialDictionaryData = $this->_extractDictionaryData(); $this->_encodeStream(); } else { if ($this->_initialDictionaryData != null) { $newDictionary = $this->_extractDictionaryData(); if ($this->_initialDictionaryData !== $newDictionary) { $this->_decodeStream(); $this->_initialDictionaryData = $newDictionary; $this->_encodeStream(); } } } // Update stream length $this->dictionary->Length->value = $this->_value->length(); return $this->_objNum + $shift . " " . $this->_genNum . " obj \n" . $this->dictionary->toString($factory) . "\n" . $this->_value->toString($factory) . "\n" . "endobj\n"; }
/** * Render the completed PDF to a string. * If $newSegmentOnly is true, then only appended part of PDF is returned. * * @param boolean $newSegmentOnly * @param resource $outputStream * @return string * @throws \Zend\Pdf\Exception */ public function render($newSegmentOnly = false, $outputStream = null) { // Save document properties if necessary if ($this->properties != $this->_originalProperties) { $docInfo = $this->_objFactory->newObject(new InternalType\DictionaryObject()); foreach ($this->properties as $key => $value) { switch ($key) { case 'Trapped': switch ($value) { case true: $docInfo->{$key} = new InternalType\NameObject('True'); break; case false: $docInfo->{$key} = new InternalType\NameObject('False'); break; case null: $docInfo->{$key} = new InternalType\NameObject('Unknown'); break; default: throw new Exception\LogicException('Wrong Trapped document property vale: \'' . $value . '\'. Only true, false and null values are allowed.'); break; } case 'CreationDate': // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted case 'ModDate': $docInfo->{$key} = new InternalType\StringObject((string) $value); break; case 'Title': // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted case 'Author': // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted case 'Subject': // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted case 'Keywords': // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted case 'Creator': // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted // break intentionally omitted case 'Producer': if (extension_loaded('mbstring') === true) { $detected = mb_detect_encoding($value); if ($detected !== 'ASCII') { $value = chr(254) . chr(255) . mb_convert_encoding($value, 'UTF-16', $detected); } } $docInfo->{$key} = new InternalType\StringObject((string) $value); break; default: // Set property using PDF type based on PHP type $docInfo->{$key} = InternalType\AbstractTypeObject::phpToPDF($value); break; } } $this->_trailer->Info = $docInfo; } $this->_dumpPages(); $this->_dumpNamedDestinations(); $this->_dumpOutlines(); // Check, that PDF file was modified // File is always modified by _dumpPages() now, but future implementations may eliminate this. if (!$this->_objFactory->isModified()) { if ($newSegmentOnly) { // Do nothing, return return ''; } if ($outputStream === null) { return $this->_trailer->getPDFString(); } else { $pdfData = $this->_trailer->getPDFString(); while (strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false) { $pdfData = substr($pdfData, $byteCount); } return ''; } } // offset (from a start of PDF file) of new PDF file segment $offset = $this->_trailer->getPDFLength(); // Last Object number in a list of free objects $lastFreeObject = $this->_trailer->getLastFreeObject(); // Array of cross-reference table subsections $xrefTable = array(); // Object numbers of first objects in each subsection $xrefSectionStartNums = array(); // Last cross-reference table subsection $xrefSection = array(); // Dummy initialization of the first element (specail case - header of linked list of free objects). $xrefSection[] = 0; $xrefSectionStartNums[] = 0; // Object number of last processed PDF object. // Used to manage cross-reference subsections. // Initialized by zero (specail case - header of linked list of free objects). $lastObjNum = 0; if ($outputStream !== null) { if (!$newSegmentOnly) { $pdfData = $this->_trailer->getPDFString(); while (strlen($pdfData) > 0 && ($byteCount = fwrite($outputStream, $pdfData)) != false) { $pdfData = substr($pdfData, $byteCount); } } } else { $pdfSegmentBlocks = $newSegmentOnly ? array() : array($this->_trailer->getPDFString()); } // Iterate objects to create new reference table foreach ($this->_objFactory->listModifiedObjects() as $updateInfo) { $objNum = $updateInfo->getObjNum(); if ($objNum - $lastObjNum != 1) { // Save cross-reference table subsection and start new one $xrefTable[] = $xrefSection; $xrefSection = array(); $xrefSectionStartNums[] = $objNum; } if ($updateInfo->isFree()) { // Free object cross-reference table entry $xrefSection[] = sprintf("%010d %05d f \n", $lastFreeObject, $updateInfo->getGenNum()); $lastFreeObject = $objNum; } else { // In-use object cross-reference table entry $xrefSection[] = sprintf("%010d %05d n \n", $offset, $updateInfo->getGenNum()); $pdfBlock = $updateInfo->getObjectDump(); $offset += strlen($pdfBlock); if ($outputStream === null) { $pdfSegmentBlocks[] = $pdfBlock; } else { while (strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false) { $pdfBlock = substr($pdfBlock, $byteCount); } } } $lastObjNum = $objNum; } // Save last cross-reference table subsection $xrefTable[] = $xrefSection; // Modify first entry (specail case - header of linked list of free objects). $xrefTable[0][0] = sprintf("%010d 65535 f \n", $lastFreeObject); $xrefTableStr = "xref\n"; foreach ($xrefTable as $sectId => $xrefSection) { $xrefTableStr .= sprintf("%d %d \n", $xrefSectionStartNums[$sectId], count($xrefSection)); foreach ($xrefSection as $xrefTableEntry) { $xrefTableStr .= $xrefTableEntry; } } $this->_trailer->Size->value = $this->_objFactory->getObjectCount(); $pdfBlock = $xrefTableStr . $this->_trailer->toString() . "startxref\n" . $offset . "\n" . "%%EOF\n"; $this->_objFactory->cleanEnumerationShiftCache(); if ($outputStream === null) { $pdfSegmentBlocks[] = $pdfBlock; return implode('', $pdfSegmentBlocks); } else { while (strlen($pdfBlock) > 0 && ($byteCount = fwrite($outputStream, $pdfBlock)) != false) { $pdfBlock = substr($pdfBlock, $byteCount); } return ''; } }
/** * Creates or loads PDF document. * * If $source is null, then it creates a new document. * * If $source is a string and $load is false, then it loads document * from a binary string. * * If $source is a string and $load is true, then it loads document * from a file. * $revision used to roll back document to specified version * (0 - currtent version, 1 - previous version, 2 - ...) * * @param string $source - PDF file to load * @param integer $revision * @throws \Zend\Pdf\Exception * @return \Zend\Pdf\PdfDocument */ public function __construct($source = null, $revision = null, $load = false) { $this->_objFactory = ObjectFactory::createFactory(1); if ($source !== null) { $this->_parser = new PdfParser\StructureParser($source, $this->_objFactory, $load); $this->_pdfHeaderVersion = $this->_parser->getPDFVersion(); $this->_trailer = $this->_parser->getTrailer(); if ($this->_trailer->Encrypt !== null) { throw new Exception\NotImplementedException('Encrypted document modification is not supported'); } if ($revision !== null) { $this->rollback($revision); } else { $this->_loadPages($this->_trailer->Root->Pages); } $this->_loadNamedDestinations($this->_trailer->Root, $this->_parser->getPDFVersion()); $this->_loadOutlines($this->_trailer->Root); if ($this->_trailer->Info !== null) { $this->properties = $this->_trailer->Info->toPhp(); if (isset($this->properties['Trapped'])) { switch ($this->properties['Trapped']) { case 'True': $this->properties['Trapped'] = true; break; case 'False': $this->properties['Trapped'] = false; break; case 'Unknown': $this->properties['Trapped'] = null; break; default: // Wrong property value // Do nothing break; } } $this->_originalProperties = $this->properties; } } else { $this->_pdfHeaderVersion = self::PDF_VERSION; $trailerDictionary = new InternalType\DictionaryObject(); /** * Document id */ $docId = md5(uniqid(rand(), true)); // 32 byte (128 bit) identifier $docIdLow = substr($docId, 0, 16); // first 16 bytes $docIdHigh = substr($docId, 16, 16); // second 16 bytes $trailerDictionary->ID = new InternalType\ArrayObject(); $trailerDictionary->ID->items[] = new InternalType\BinaryStringObject($docIdLow); $trailerDictionary->ID->items[] = new InternalType\BinaryStringObject($docIdHigh); $trailerDictionary->Size = new InternalType\NumericObject(0); $this->_trailer = new Trailer\Generated($trailerDictionary); /** * Document catalog indirect object. */ $docCatalog = $this->_objFactory->newObject(new InternalType\DictionaryObject()); $docCatalog->Type = new InternalType\NameObject('Catalog'); $docCatalog->Version = new InternalType\NameObject(self::PDF_VERSION); $this->_trailer->Root = $docCatalog; /** * Pages container */ $docPages = $this->_objFactory->newObject(new InternalType\DictionaryObject()); $docPages->Type = new InternalType\NameObject('Pages'); $docPages->Kids = new InternalType\ArrayObject(); $docPages->Count = new InternalType\NumericObject(0); $docCatalog->Pages = $docPages; } }
/** * Object constructor * * @param string $imageFileName * @throws \Zend\Pdf\Exception * @todo Add compression conversions to support compression strategys other than PNG_COMPRESSION_DEFAULT_STRATEGY. * @todo Add pre-compression filtering. * @todo Add interlaced image handling. * @todo Add support for 16-bit images. Requires PDF version bump to 1.5 at least. * @todo Add processing for all PNG chunks defined in the spec. gAMA etc. * @todo Fix tRNS chunk support for Indexed Images to a SMask. */ public function __construct($imageFileName) { if (($imageFile = @fopen($imageFileName, 'rb')) === false) { throw new Exception\IOException("Can not open '{$imageFileName}' file for reading."); } parent::__construct(); //Check if the file is a PNG fseek($imageFile, 1, SEEK_CUR); //First signature byte (%) if ('PNG' != fread($imageFile, 3)) { throw new Exception\DomainException('Image is not a PNG'); } fseek($imageFile, 12, SEEK_CUR); //Signature bytes (Includes the IHDR chunk) IHDR processed linerarly because it doesnt contain a variable chunk length $wtmp = unpack('Ni', fread($imageFile, 4)); //Unpack a 4-Byte Long $width = $wtmp['i']; $htmp = unpack('Ni', fread($imageFile, 4)); $height = $htmp['i']; $bits = ord(fread($imageFile, 1)); //Higher than 8 bit depths are only supported in later versions of PDF. $color = ord(fread($imageFile, 1)); $compression = ord(fread($imageFile, 1)); $prefilter = ord(fread($imageFile, 1)); if (($interlacing = ord(fread($imageFile, 1))) != self::PNG_INTERLACING_DISABLED) { throw new Exception\NotImplementedException('Only non-interlaced images are currently supported.'); } $this->_width = $width; $this->_height = $height; $this->_imageProperties = array(); $this->_imageProperties['bitDepth'] = $bits; $this->_imageProperties['pngColorType'] = $color; $this->_imageProperties['pngFilterType'] = $prefilter; $this->_imageProperties['pngCompressionType'] = $compression; $this->_imageProperties['pngInterlacingType'] = $interlacing; fseek($imageFile, 4, SEEK_CUR); //4 Byte Ending Sequence $imageData = ''; /* * The following loop processes PNG chunks. 4 Byte Longs are packed first give the chunk length * followed by the chunk signature, a four byte code. IDAT and IEND are manditory in any PNG. */ while (($chunkLengthBytes = fread($imageFile, 4)) !== false) { $chunkLengthtmp = unpack('Ni', $chunkLengthBytes); $chunkLength = $chunkLengthtmp['i']; $chunkType = fread($imageFile, 4); switch ($chunkType) { case 'IDAT': //Image Data /* * Reads the actual image data from the PNG file. Since we know at this point that the compression * strategy is the default strategy, we also know that this data is Zip compressed. We will either copy * the data directly to the PDF and provide the correct FlateDecode predictor, or decompress the data * decode the filters and output the data as a raw pixel map. */ $imageData .= fread($imageFile, $chunkLength); fseek($imageFile, 4, SEEK_CUR); break; case 'PLTE': //Palette $paletteData = fread($imageFile, $chunkLength); fseek($imageFile, 4, SEEK_CUR); break; case 'tRNS': //Basic (non-alpha channel) transparency. $trnsData = fread($imageFile, $chunkLength); switch ($color) { case self::PNG_CHANNEL_GRAY: $baseColor = ord(substr($trnsData, 1, 1)); $transparencyData = array(new InternalType\NumericObject($baseColor), new InternalType\NumericObject($baseColor)); break; case self::PNG_CHANNEL_RGB: $red = ord(substr($trnsData, 1, 1)); $green = ord(substr($trnsData, 3, 1)); $blue = ord(substr($trnsData, 5, 1)); $transparencyData = array(new InternalType\NumericObject($red), new InternalType\NumericObject($red), new InternalType\NumericObject($green), new InternalType\NumericObject($green), new InternalType\NumericObject($blue), new InternalType\NumericObject($blue)); break; case self::PNG_CHANNEL_INDEXED: //Find the first transparent color in the index, we will mask that. (This is a bit of a hack. This should be a SMask and mask all entries values). if (($trnsIdx = strpos($trnsData, "")) !== false) { $transparencyData = array(new InternalType\NumericObject($trnsIdx), new InternalType\NumericObject($trnsIdx)); } break; case self::PNG_CHANNEL_GRAY_ALPHA: // Fall through to the next case // Fall through to the next case // Fall through to the next case // Fall through to the next case case self::PNG_CHANNEL_RGB_ALPHA: throw new Exception\CorruptedImageException("tRNS chunk illegal for Alpha Channel Images"); break; } fseek($imageFile, 4, SEEK_CUR); //4 Byte Ending Sequence break; case 'IEND': break 2; //End the loop too //End the loop too //End the loop too //End the loop too default: fseek($imageFile, $chunkLength + 4, SEEK_CUR); //Skip the section break; } } fclose($imageFile); $compressed = true; $imageDataTmp = ''; $smaskData = ''; switch ($color) { case self::PNG_CHANNEL_RGB: $colorSpace = new InternalType\NameObject('DeviceRGB'); break; case self::PNG_CHANNEL_GRAY: $colorSpace = new InternalType\NameObject('DeviceGray'); break; case self::PNG_CHANNEL_INDEXED: if (empty($paletteData)) { throw new Exception\CorruptedImageException("PNG Corruption: No palette data read for indexed type PNG."); } $colorSpace = new InternalType\ArrayObject(); $colorSpace->items[] = new InternalType\NameObject('Indexed'); $colorSpace->items[] = new InternalType\NameObject('DeviceRGB'); $colorSpace->items[] = new InternalType\NumericObject(strlen($paletteData) / 3 - 1); $paletteObject = $this->_objectFactory->newObject(new InternalType\BinaryStringObject($paletteData)); $colorSpace->items[] = $paletteObject; break; case self::PNG_CHANNEL_GRAY_ALPHA: /* * To decode PNG's with alpha data we must create two images from one. One image will contain the Gray data * the other will contain the Gray transparency overlay data. The former will become the object data and the latter * will become the Shadow Mask (SMask). */ if ($bits > 8) { throw new Exception\NotImplementedException('Alpha PNGs with bit depth > 8 are not yet supported'); } $colorSpace = new InternalType\NameObject('DeviceGray'); $decodingObjFactory = ObjectFactory::createFactory(1); $decodingStream = $decodingObjFactory->newStreamObject($imageData); $decodingStream->dictionary->Filter = new InternalType\NameObject('FlateDecode'); $decodingStream->dictionary->DecodeParms = new InternalType\DictionaryObject(); $decodingStream->dictionary->DecodeParms->Predictor = new InternalType\NumericObject(15); $decodingStream->dictionary->DecodeParms->Columns = new InternalType\NumericObject($width); $decodingStream->dictionary->DecodeParms->Colors = new InternalType\NumericObject(2); //GreyAlpha $decodingStream->dictionary->DecodeParms->BitsPerComponent = new InternalType\NumericObject($bits); $decodingStream->skipFilters(); $pngDataRawDecoded = $decodingStream->value; //Iterate every pixel and copy out gray data and alpha channel (this will be slow) for ($pixel = 0, $pixelcount = $width * $height; $pixel < $pixelcount; $pixel++) { $imageDataTmp .= $pngDataRawDecoded[$pixel * 2]; $smaskData .= $pngDataRawDecoded[$pixel * 2 + 1]; } $compressed = false; $imageData = $imageDataTmp; //Overwrite image data with the gray channel without alpha break; case self::PNG_CHANNEL_RGB_ALPHA: /* * To decode PNG's with alpha data we must create two images from one. One image will contain the RGB data * the other will contain the Gray transparency overlay data. The former will become the object data and the latter * will become the Shadow Mask (SMask). */ if ($bits > 8) { throw new Exception\NotImplementedException('Alpha PNGs with bit depth > 8 are not yet supported'); } $colorSpace = new InternalType\NameObject('DeviceRGB'); $decodingObjFactory = ObjectFactory::createFactory(1); $decodingStream = $decodingObjFactory->newStreamObject($imageData); $decodingStream->dictionary->Filter = new InternalType\NameObject('FlateDecode'); $decodingStream->dictionary->DecodeParms = new InternalType\DictionaryObject(); $decodingStream->dictionary->DecodeParms->Predictor = new InternalType\NumericObject(15); $decodingStream->dictionary->DecodeParms->Columns = new InternalType\NumericObject($width); $decodingStream->dictionary->DecodeParms->Colors = new InternalType\NumericObject(4); //RGBA $decodingStream->dictionary->DecodeParms->BitsPerComponent = new InternalType\NumericObject($bits); $decodingStream->skipFilters(); $pngDataRawDecoded = $decodingStream->value; //Iterate every pixel and copy out rgb data and alpha channel (this will be slow) for ($pixel = 0, $pixelcount = $width * $height; $pixel < $pixelcount; $pixel++) { $imageDataTmp .= $pngDataRawDecoded[$pixel * 4 + 0] . $pngDataRawDecoded[$pixel * 4 + 1] . $pngDataRawDecoded[$pixel * 4 + 2]; $smaskData .= $pngDataRawDecoded[$pixel * 4 + 3]; } $compressed = false; $imageData = $imageDataTmp; //Overwrite image data with the RGB channel without alpha break; default: throw new Exception\CorruptedImageException('PNG Corruption: Invalid color space.'); } if (empty($imageData)) { throw new Exception\CorruptedImageException('Corrupt PNG Image. Mandatory IDAT chunk not found.'); } $imageDictionary = $this->_resource->dictionary; if (!empty($smaskData)) { /* * Includes the Alpha transparency data as a Gray Image, then assigns the image as the Shadow Mask for the main image data. */ $smaskStream = $this->_objectFactory->newStreamObject($smaskData); $smaskStream->dictionary->Type = new InternalType\NameObject('XObject'); $smaskStream->dictionary->Subtype = new InternalType\NameObject('Image'); $smaskStream->dictionary->Width = new InternalType\NumericObject($width); $smaskStream->dictionary->Height = new InternalType\NumericObject($height); $smaskStream->dictionary->ColorSpace = new InternalType\NameObject('DeviceGray'); $smaskStream->dictionary->BitsPerComponent = new InternalType\NumericObject($bits); $imageDictionary->SMask = $smaskStream; // Encode stream with FlateDecode filter $smaskStreamDecodeParms = array(); $smaskStreamDecodeParms['Predictor'] = new InternalType\NumericObject(15); $smaskStreamDecodeParms['Columns'] = new InternalType\NumericObject($width); $smaskStreamDecodeParms['Colors'] = new InternalType\NumericObject(1); $smaskStreamDecodeParms['BitsPerComponent'] = new InternalType\NumericObject(8); $smaskStream->dictionary->DecodeParms = new InternalType\DictionaryObject($smaskStreamDecodeParms); $smaskStream->dictionary->Filter = new InternalType\NameObject('FlateDecode'); } if (!empty($transparencyData)) { //This is experimental and not properly tested. $imageDictionary->Mask = new InternalType\ArrayObject($transparencyData); } $imageDictionary->Width = new InternalType\NumericObject($width); $imageDictionary->Height = new InternalType\NumericObject($height); $imageDictionary->ColorSpace = $colorSpace; $imageDictionary->BitsPerComponent = new InternalType\NumericObject($bits); $imageDictionary->Filter = new InternalType\NameObject('FlateDecode'); $decodeParms = array(); $decodeParms['Predictor'] = new InternalType\NumericObject(15); // Optimal prediction $decodeParms['Columns'] = new InternalType\NumericObject($width); $decodeParms['Colors'] = new InternalType\NumericObject($color == self::PNG_CHANNEL_RGB || $color == self::PNG_CHANNEL_RGB_ALPHA ? 3 : 1); $decodeParms['BitsPerComponent'] = new InternalType\NumericObject($bits); $imageDictionary->DecodeParms = new InternalType\DictionaryObject($decodeParms); //Include only the image IDAT section data. $this->_resource->value = $imageData; //Skip double compression if ($compressed) { $this->_resource->skipFilters(); } }
/** * Object constructor * * Note: PHP duplicates string, which is sent by value, only of it's updated. * Thus we don't need to care about overhead * * @param mixed $source * @param \Zend\Pdf\ObjectFactory $factory * @param boolean $load * @throws \Zend\Exception */ public function __construct($source, Pdf\ObjectFactory $factory, $load) { if ($load) { if (($pdfFile = @fopen($source, 'rb')) === false) { throw new Pdf\Exception("Can not open '{$source}' file for reading."); } $byteCount = filesize($source); $data = fread($pdfFile, $byteCount); $byteCount -= strlen($data); while ($byteCount > 0 && ($nextBlock = fread($pdfFile, $byteCount)) != false) { $data .= $nextBlock; $byteCount -= strlen($nextBlock); } fclose($pdfFile); $this->_stringParser = new DataParser($data, $factory); } else { $this->_stringParser = new DataParser($source, $factory); } $pdfVersionComment = $this->_stringParser->readComment(); if (substr($pdfVersionComment, 0, 5) != '%PDF-') { throw new Pdf\Exception('File is not a PDF.'); } $pdfVersion = substr($pdfVersionComment, 5); if (version_compare($pdfVersion, '0.9', '<') || version_compare($pdfVersion, '1.61', '>=')) { /** * @todo * To support PDF versions 1.5 (Acrobat 6) and PDF version 1.7 (Acrobat 7) * Stream compression filter must be implemented (for compressed object streams). * Cross reference streams must be implemented */ throw new Pdf\Exception(sprintf('Unsupported PDF version. Zend_PDF supports PDF 1.0-1.4. Current version - \'%f\'', $pdfVersion)); } $this->_pdfVersion = $pdfVersion; $this->_stringParser->offset = strrpos($this->_stringParser->data, '%%EOF'); if ($this->_stringParser->offset === false || strlen($this->_stringParser->data) - $this->_stringParser->offset > 7) { throw new Pdf\Exception('PDF file syntax error. End-of-fle marker expected at the end of file.'); } $this->_stringParser->offset--; /** * Go to end of cross-reference table offset */ while (DataParser::isWhiteSpace(ord($this->_stringParser->data[$this->_stringParser->offset])) && $this->_stringParser->offset > 0) { $this->_stringParser->offset--; } /** * Go to the start of cross-reference table offset */ while (!DataParser::isWhiteSpace(ord($this->_stringParser->data[$this->_stringParser->offset])) && $this->_stringParser->offset > 0) { $this->_stringParser->offset--; } /** * Go to the end of 'startxref' keyword */ while (DataParser::isWhiteSpace(ord($this->_stringParser->data[$this->_stringParser->offset])) && $this->_stringParser->offset > 0) { $this->_stringParser->offset--; } /** * Go to the white space (eol marker) before 'startxref' keyword */ $this->_stringParser->offset -= 9; $nextLexeme = $this->_stringParser->readLexeme(); if ($nextLexeme != 'startxref') { throw new Pdf\Exception(sprintf('PDF file syntax error. \'startxref\' keyword expected. Offset - 0x%X.', $this->_stringParser->offset - strlen($nextLexeme))); } $startXref = $this->_stringParser->readLexeme(); if (!ctype_digit($startXref)) { throw new Pdf\Exception(sprintf('PDF file syntax error. Cross-reference table offset must contain only digits. Offset - 0x%X.', $this->_stringParser->offset - strlen($nextLexeme))); } $this->_trailer = $this->_loadXRefTable($startXref); $factory->setObjectCount($this->_trailer->Size->value); }
/** * Object constructor. */ public function __construct() { $this->_factory = Pdf\ObjectFactory::createFactory(1); $this->_processed = array(); }
/** * Check if PDF file was modified * * @return boolean */ public function isModified() { return $this->_factory->isModified(); }