File indexing completed on 2025-01-19 05:21:23

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Pdf
0017  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0018  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0019  * @version    $Id$
0020  */
0021 
0022 
0023 /** Internally used classes */
0024 // require_once 'Zend/Pdf/Element/Array.php';
0025 // require_once 'Zend/Pdf/Element/Dictionary.php';
0026 // require_once 'Zend/Pdf/Element/Name.php';
0027 // require_once 'Zend/Pdf/Element/Numeric.php';
0028 // require_once 'Zend/Pdf/Element/String/Binary.php';
0029 
0030 
0031 /** Zend_Pdf_Resource_Image */
0032 // require_once 'Zend/Pdf/Resource/Image.php';
0033 
0034 /**
0035  * PNG image
0036  *
0037  * @package    Zend_Pdf
0038  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0039  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0040  */
0041 class Zend_Pdf_Resource_Image_Png extends Zend_Pdf_Resource_Image
0042 {
0043     const PNG_COMPRESSION_DEFAULT_STRATEGY = 0;
0044     const PNG_COMPRESSION_FILTERED = 1;
0045     const PNG_COMPRESSION_HUFFMAN_ONLY = 2;
0046     const PNG_COMPRESSION_RLE = 3;
0047 
0048     const PNG_FILTER_NONE = 0;
0049     const PNG_FILTER_SUB = 1;
0050     const PNG_FILTER_UP = 2;
0051     const PNG_FILTER_AVERAGE = 3;
0052     const PNG_FILTER_PAETH = 4;
0053 
0054     const PNG_INTERLACING_DISABLED = 0;
0055     const PNG_INTERLACING_ENABLED = 1;
0056 
0057     const PNG_CHANNEL_GRAY = 0;
0058     const PNG_CHANNEL_RGB = 2;
0059     const PNG_CHANNEL_INDEXED = 3;
0060     const PNG_CHANNEL_GRAY_ALPHA = 4;
0061     const PNG_CHANNEL_RGB_ALPHA = 6;
0062 
0063     protected $_width;
0064     protected $_height;
0065     protected $_imageProperties;
0066 
0067     /**
0068      * Object constructor
0069      *
0070      * @param string $imageFileName
0071      * @throws Zend_Pdf_Exception
0072      * @todo Add compression conversions to support compression strategys other than PNG_COMPRESSION_DEFAULT_STRATEGY.
0073      * @todo Add pre-compression filtering.
0074      * @todo Add interlaced image handling.
0075      * @todo Add support for 16-bit images. Requires PDF version bump to 1.5 at least.
0076      * @todo Add processing for all PNG chunks defined in the spec. gAMA etc.
0077      * @todo Fix tRNS chunk support for Indexed Images to a SMask.
0078      */
0079     public function __construct($imageFileName)
0080     {
0081         if (($imageFile = @fopen($imageFileName, 'rb')) === false ) {
0082             // require_once 'Zend/Pdf/Exception.php';
0083             throw new Zend_Pdf_Exception( "Can not open '$imageFileName' file for reading." );
0084         }
0085 
0086         parent::__construct();
0087 
0088         //Check if the file is a PNG
0089         fseek($imageFile, 1, SEEK_CUR); //First signature byte (%)
0090         if ('PNG' != fread($imageFile, 3)) {
0091             // require_once 'Zend/Pdf/Exception.php';
0092             throw new Zend_Pdf_Exception('Image is not a PNG');
0093         }
0094         fseek($imageFile, 12, SEEK_CUR); //Signature bytes (Includes the IHDR chunk) IHDR processed linerarly because it doesnt contain a variable chunk length
0095         $wtmp = unpack('Ni',fread($imageFile, 4)); //Unpack a 4-Byte Long
0096         $width = $wtmp['i'];
0097         $htmp = unpack('Ni',fread($imageFile, 4));
0098         $height = $htmp['i'];
0099         $bits = ord(fread($imageFile, 1)); //Higher than 8 bit depths are only supported in later versions of PDF.
0100         $color = ord(fread($imageFile, 1));
0101 
0102         $compression = ord(fread($imageFile, 1));
0103         $prefilter = ord(fread($imageFile,1));
0104 
0105         if (($interlacing = ord(fread($imageFile,1))) != Zend_Pdf_Resource_Image_Png::PNG_INTERLACING_DISABLED) {
0106             // require_once 'Zend/Pdf/Exception.php';
0107             throw new Zend_Pdf_Exception( "Only non-interlaced images are currently supported." );
0108         }
0109 
0110         $this->_width = $width;
0111         $this->_height = $height;
0112         $this->_imageProperties = array();
0113         $this->_imageProperties['bitDepth'] = $bits;
0114         $this->_imageProperties['pngColorType'] = $color;
0115         $this->_imageProperties['pngFilterType'] = $prefilter;
0116         $this->_imageProperties['pngCompressionType'] = $compression;
0117         $this->_imageProperties['pngInterlacingType'] = $interlacing;
0118 
0119         fseek($imageFile, 4, SEEK_CUR); //4 Byte Ending Sequence
0120         $imageData = '';
0121 
0122         /*
0123          * The following loop processes PNG chunks. 4 Byte Longs are packed first give the chunk length
0124          * followed by the chunk signature, a four byte code. IDAT and IEND are manditory in any PNG.
0125          */
0126         while (!feof($imageFile)) {
0127             $chunkLengthBytes = fread($imageFile, 4);
0128             if ($chunkLengthBytes === false) {
0129                 // require_once 'Zend/Pdf/Exception.php';
0130                 throw new Zend_Pdf_Exception('Error ocuured while image file reading.');
0131             }
0132 
0133             $chunkLengthtmp = unpack('Ni', $chunkLengthBytes);
0134             $chunkLength    = $chunkLengthtmp['i'];
0135             $chunkType      = fread($imageFile, 4);
0136             switch($chunkType) {
0137                 case 'IDAT': //Image Data
0138                     /*
0139                      * Reads the actual image data from the PNG file. Since we know at this point that the compression
0140                      * strategy is the default strategy, we also know that this data is Zip compressed. We will either copy
0141                      * the data directly to the PDF and provide the correct FlateDecode predictor, or decompress the data
0142                      * decode the filters and output the data as a raw pixel map.
0143                      */
0144                     $imageData .= fread($imageFile, $chunkLength);
0145                     fseek($imageFile, 4, SEEK_CUR);
0146                     break;
0147 
0148                 case 'PLTE': //Palette
0149                     $paletteData = fread($imageFile, $chunkLength);
0150                     fseek($imageFile, 4, SEEK_CUR);
0151                     break;
0152 
0153                 case 'tRNS': //Basic (non-alpha channel) transparency.
0154                     $trnsData = fread($imageFile, $chunkLength);
0155                     switch ($color) {
0156                         case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY:
0157                             $baseColor = ord(substr($trnsData, 1, 1));
0158                             $transparencyData = array(new Zend_Pdf_Element_Numeric($baseColor),
0159                                                       new Zend_Pdf_Element_Numeric($baseColor));
0160                             break;
0161 
0162                         case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB:
0163                             $red = ord(substr($trnsData,1,1));
0164                             $green = ord(substr($trnsData,3,1));
0165                             $blue = ord(substr($trnsData,5,1));
0166                             $transparencyData = array(new Zend_Pdf_Element_Numeric($red),
0167                                                       new Zend_Pdf_Element_Numeric($red),
0168                                                       new Zend_Pdf_Element_Numeric($green),
0169                                                       new Zend_Pdf_Element_Numeric($green),
0170                                                       new Zend_Pdf_Element_Numeric($blue),
0171                                                       new Zend_Pdf_Element_Numeric($blue));
0172                             break;
0173 
0174                         case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_INDEXED:
0175                             //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).
0176                             if(($trnsIdx = strpos($trnsData, "\0")) !== false) {
0177                                 $transparencyData = array(new Zend_Pdf_Element_Numeric($trnsIdx),
0178                                                           new Zend_Pdf_Element_Numeric($trnsIdx));
0179                             }
0180                             break;
0181 
0182                         case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY_ALPHA:
0183                             // Fall through to the next case
0184 
0185                         case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB_ALPHA:
0186                             // require_once 'Zend/Pdf/Exception.php';
0187                             throw new Zend_Pdf_Exception( "tRNS chunk illegal for Alpha Channel Images" );
0188                             break;
0189                     }
0190                     fseek($imageFile, 4, SEEK_CUR); //4 Byte Ending Sequence
0191                     break;
0192 
0193                 case 'IEND';
0194                     break 2; //End the loop too
0195 
0196                 default:
0197                     fseek($imageFile, $chunkLength + 4, SEEK_CUR); //Skip the section
0198                     break;
0199             }
0200         }
0201         fclose($imageFile);
0202 
0203         $compressed = true;
0204         $imageDataTmp = '';
0205         $smaskData = '';
0206         switch ($color) {
0207             case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB:
0208                 $colorSpace = new Zend_Pdf_Element_Name('DeviceRGB');
0209                 break;
0210 
0211             case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY:
0212                 $colorSpace = new Zend_Pdf_Element_Name('DeviceGray');
0213                 break;
0214 
0215             case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_INDEXED:
0216                 if(empty($paletteData)) {
0217                     // require_once 'Zend/Pdf/Exception.php';
0218                     throw new Zend_Pdf_Exception( "PNG Corruption: No palette data read for indexed type PNG." );
0219                 }
0220                 $colorSpace = new Zend_Pdf_Element_Array();
0221                 $colorSpace->items[] = new Zend_Pdf_Element_Name('Indexed');
0222                 $colorSpace->items[] = new Zend_Pdf_Element_Name('DeviceRGB');
0223                 $colorSpace->items[] = new Zend_Pdf_Element_Numeric((strlen($paletteData)/3-1));
0224                 $paletteObject = $this->_objectFactory->newObject(new Zend_Pdf_Element_String_Binary($paletteData));
0225                 $colorSpace->items[] = $paletteObject;
0226                 break;
0227 
0228             case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY_ALPHA:
0229                 /*
0230                  * To decode PNG's with alpha data we must create two images from one. One image will contain the Gray data
0231                  * the other will contain the Gray transparency overlay data. The former will become the object data and the latter
0232                  * will become the Shadow Mask (SMask).
0233                  */
0234                 if($bits > 8) {
0235                     // require_once 'Zend/Pdf/Exception.php';
0236                     throw new Zend_Pdf_Exception("Alpha PNGs with bit depth > 8 are not yet supported");
0237                 }
0238 
0239                 $colorSpace = new Zend_Pdf_Element_Name('DeviceGray');
0240 
0241                 // require_once 'Zend/Pdf/ElementFactory.php';
0242                 $decodingObjFactory = Zend_Pdf_ElementFactory::createFactory(1);
0243                 $decodingStream = $decodingObjFactory->newStreamObject($imageData);
0244                 $decodingStream->dictionary->Filter      = new Zend_Pdf_Element_Name('FlateDecode');
0245                 $decodingStream->dictionary->DecodeParms = new Zend_Pdf_Element_Dictionary();
0246                 $decodingStream->dictionary->DecodeParms->Predictor        = new Zend_Pdf_Element_Numeric(15);
0247                 $decodingStream->dictionary->DecodeParms->Columns          = new Zend_Pdf_Element_Numeric($width);
0248                 $decodingStream->dictionary->DecodeParms->Colors           = new Zend_Pdf_Element_Numeric(2);   //GreyAlpha
0249                 $decodingStream->dictionary->DecodeParms->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
0250                 $decodingStream->skipFilters();
0251 
0252                 $pngDataRawDecoded = $decodingStream->value;
0253 
0254                 //Iterate every pixel and copy out gray data and alpha channel (this will be slow)
0255                 for($pixel = 0, $pixelcount = ($width * $height); $pixel < $pixelcount; $pixel++) {
0256                     $imageDataTmp .= $pngDataRawDecoded[($pixel*2)];
0257                     $smaskData .= $pngDataRawDecoded[($pixel*2)+1];
0258                 }
0259                 $compressed = false;
0260                 $imageData  = $imageDataTmp; //Overwrite image data with the gray channel without alpha
0261                 break;
0262 
0263             case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB_ALPHA:
0264                 /*
0265                  * To decode PNG's with alpha data we must create two images from one. One image will contain the RGB data
0266                  * the other will contain the Gray transparency overlay data. The former will become the object data and the latter
0267                  * will become the Shadow Mask (SMask).
0268                  */
0269                 if($bits > 8) {
0270                     // require_once 'Zend/Pdf/Exception.php';
0271                     throw new Zend_Pdf_Exception("Alpha PNGs with bit depth > 8 are not yet supported");
0272                 }
0273 
0274                 $colorSpace = new Zend_Pdf_Element_Name('DeviceRGB');
0275 
0276                 // require_once 'Zend/Pdf/ElementFactory.php';
0277                 $decodingObjFactory = Zend_Pdf_ElementFactory::createFactory(1);
0278                 $decodingStream = $decodingObjFactory->newStreamObject($imageData);
0279                 $decodingStream->dictionary->Filter      = new Zend_Pdf_Element_Name('FlateDecode');
0280                 $decodingStream->dictionary->DecodeParms = new Zend_Pdf_Element_Dictionary();
0281                 $decodingStream->dictionary->DecodeParms->Predictor        = new Zend_Pdf_Element_Numeric(15);
0282                 $decodingStream->dictionary->DecodeParms->Columns          = new Zend_Pdf_Element_Numeric($width);
0283                 $decodingStream->dictionary->DecodeParms->Colors           = new Zend_Pdf_Element_Numeric(4);   //RGBA
0284                 $decodingStream->dictionary->DecodeParms->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
0285                 $decodingStream->skipFilters();
0286 
0287                 $pngDataRawDecoded = $decodingStream->value;
0288 
0289                 //Iterate every pixel and copy out rgb data and alpha channel (this will be slow)
0290                 for($pixel = 0, $pixelcount = ($width * $height); $pixel < $pixelcount; $pixel++) {
0291                     $imageDataTmp .= $pngDataRawDecoded[($pixel*4)+0] . $pngDataRawDecoded[($pixel*4)+1] . $pngDataRawDecoded[($pixel*4)+2];
0292                     $smaskData .= $pngDataRawDecoded[($pixel*4)+3];
0293                 }
0294 
0295                 $compressed = false;
0296                 $imageData  = $imageDataTmp; //Overwrite image data with the RGB channel without alpha
0297                 break;
0298 
0299             default:
0300                 // require_once 'Zend/Pdf/Exception.php';
0301                 throw new Zend_Pdf_Exception( "PNG Corruption: Invalid color space." );
0302         }
0303 
0304         if(empty($imageData)) {
0305             // require_once 'Zend/Pdf/Exception.php';
0306             throw new Zend_Pdf_Exception( "Corrupt PNG Image. Mandatory IDAT chunk not found." );
0307         }
0308 
0309         $imageDictionary = $this->_resource->dictionary;
0310         if(!empty($smaskData)) {
0311             /*
0312              * Includes the Alpha transparency data as a Gray Image, then assigns the image as the Shadow Mask for the main image data.
0313              */
0314             $smaskStream = $this->_objectFactory->newStreamObject($smaskData);
0315             $smaskStream->dictionary->Type             = new Zend_Pdf_Element_Name('XObject');
0316             $smaskStream->dictionary->Subtype          = new Zend_Pdf_Element_Name('Image');
0317             $smaskStream->dictionary->Width            = new Zend_Pdf_Element_Numeric($width);
0318             $smaskStream->dictionary->Height           = new Zend_Pdf_Element_Numeric($height);
0319             $smaskStream->dictionary->ColorSpace       = new Zend_Pdf_Element_Name('DeviceGray');
0320             $smaskStream->dictionary->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
0321             $imageDictionary->SMask = $smaskStream;
0322 
0323             // Encode stream with FlateDecode filter
0324             $smaskStreamDecodeParms = array();
0325             $smaskStreamDecodeParms['Predictor']        = new Zend_Pdf_Element_Numeric(15);
0326             $smaskStreamDecodeParms['Columns']          = new Zend_Pdf_Element_Numeric($width);
0327             $smaskStreamDecodeParms['Colors']           = new Zend_Pdf_Element_Numeric(1);
0328             $smaskStreamDecodeParms['BitsPerComponent'] = new Zend_Pdf_Element_Numeric(8);
0329             $smaskStream->dictionary->DecodeParms  = new Zend_Pdf_Element_Dictionary($smaskStreamDecodeParms);
0330             $smaskStream->dictionary->Filter       = new Zend_Pdf_Element_Name('FlateDecode');
0331         }
0332 
0333         if(!empty($transparencyData)) {
0334             //This is experimental and not properly tested.
0335             $imageDictionary->Mask = new Zend_Pdf_Element_Array($transparencyData);
0336         }
0337 
0338         $imageDictionary->Width            = new Zend_Pdf_Element_Numeric($width);
0339         $imageDictionary->Height           = new Zend_Pdf_Element_Numeric($height);
0340         $imageDictionary->ColorSpace       = $colorSpace;
0341         $imageDictionary->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
0342         $imageDictionary->Filter       = new Zend_Pdf_Element_Name('FlateDecode');
0343 
0344         $decodeParms = array();
0345         $decodeParms['Predictor']        = new Zend_Pdf_Element_Numeric(15); // Optimal prediction
0346         $decodeParms['Columns']          = new Zend_Pdf_Element_Numeric($width);
0347         $decodeParms['Colors']           = new Zend_Pdf_Element_Numeric((($color==Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB || $color==Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB_ALPHA)?(3):(1)));
0348         $decodeParms['BitsPerComponent'] = new Zend_Pdf_Element_Numeric($bits);
0349         $imageDictionary->DecodeParms  = new Zend_Pdf_Element_Dictionary($decodeParms);
0350 
0351         //Include only the image IDAT section data.
0352         $this->_resource->value = $imageData;
0353 
0354         //Skip double compression
0355         if ($compressed) {
0356             $this->_resource->skipFilters();
0357         }
0358     }
0359 
0360     /**
0361      * Image width
0362      */
0363     public function getPixelWidth() {
0364     return $this->_width;
0365     }
0366 
0367     /**
0368      * Image height
0369      */
0370     public function getPixelHeight() {
0371         return $this->_height;
0372     }
0373 
0374     /**
0375      * Image properties
0376      */
0377     public function getProperties() {
0378         return $this->_imageProperties;
0379     }
0380 }