You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

380 lines
18 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Pdf
  17. * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
  18. * @license http://framework.zend.com/license/new-bsd New BSD License
  19. * @version $Id$
  20. */
  21. /** Internally used classes */
  22. // require_once 'Zend/Pdf/Element/Array.php';
  23. // require_once 'Zend/Pdf/Element/Dictionary.php';
  24. // require_once 'Zend/Pdf/Element/Name.php';
  25. // require_once 'Zend/Pdf/Element/Numeric.php';
  26. // require_once 'Zend/Pdf/Element/String/Binary.php';
  27. /** Zend_Pdf_Resource_Image */
  28. // require_once 'Zend/Pdf/Resource/Image.php';
  29. /**
  30. * PNG image
  31. *
  32. * @package Zend_Pdf
  33. * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
  34. * @license http://framework.zend.com/license/new-bsd New BSD License
  35. */
  36. class Zend_Pdf_Resource_Image_Png extends Zend_Pdf_Resource_Image
  37. {
  38. const PNG_COMPRESSION_DEFAULT_STRATEGY = 0;
  39. const PNG_COMPRESSION_FILTERED = 1;
  40. const PNG_COMPRESSION_HUFFMAN_ONLY = 2;
  41. const PNG_COMPRESSION_RLE = 3;
  42. const PNG_FILTER_NONE = 0;
  43. const PNG_FILTER_SUB = 1;
  44. const PNG_FILTER_UP = 2;
  45. const PNG_FILTER_AVERAGE = 3;
  46. const PNG_FILTER_PAETH = 4;
  47. const PNG_INTERLACING_DISABLED = 0;
  48. const PNG_INTERLACING_ENABLED = 1;
  49. const PNG_CHANNEL_GRAY = 0;
  50. const PNG_CHANNEL_RGB = 2;
  51. const PNG_CHANNEL_INDEXED = 3;
  52. const PNG_CHANNEL_GRAY_ALPHA = 4;
  53. const PNG_CHANNEL_RGB_ALPHA = 6;
  54. protected $_width;
  55. protected $_height;
  56. protected $_imageProperties;
  57. /**
  58. * Object constructor
  59. *
  60. * @param string $imageFileName
  61. * @throws Zend_Pdf_Exception
  62. * @todo Add compression conversions to support compression strategys other than PNG_COMPRESSION_DEFAULT_STRATEGY.
  63. * @todo Add pre-compression filtering.
  64. * @todo Add interlaced image handling.
  65. * @todo Add support for 16-bit images. Requires PDF version bump to 1.5 at least.
  66. * @todo Add processing for all PNG chunks defined in the spec. gAMA etc.
  67. * @todo Fix tRNS chunk support for Indexed Images to a SMask.
  68. */
  69. public function __construct($imageFileName)
  70. {
  71. if (($imageFile = @fopen($imageFileName, 'rb')) === false ) {
  72. // require_once 'Zend/Pdf/Exception.php';
  73. throw new Zend_Pdf_Exception( "Can not open '$imageFileName' file for reading." );
  74. }
  75. parent::__construct();
  76. //Check if the file is a PNG
  77. fseek($imageFile, 1, SEEK_CUR); //First signature byte (%)
  78. if ('PNG' != fread($imageFile, 3)) {
  79. // require_once 'Zend/Pdf/Exception.php';
  80. throw new Zend_Pdf_Exception('Image is not a PNG');
  81. }
  82. fseek($imageFile, 12, SEEK_CUR); //Signature bytes (Includes the IHDR chunk) IHDR processed linerarly because it doesnt contain a variable chunk length
  83. $wtmp = unpack('Ni',fread($imageFile, 4)); //Unpack a 4-Byte Long
  84. $width = $wtmp['i'];
  85. $htmp = unpack('Ni',fread($imageFile, 4));
  86. $height = $htmp['i'];
  87. $bits = ord(fread($imageFile, 1)); //Higher than 8 bit depths are only supported in later versions of PDF.
  88. $color = ord(fread($imageFile, 1));
  89. $compression = ord(fread($imageFile, 1));
  90. $prefilter = ord(fread($imageFile,1));
  91. if (($interlacing = ord(fread($imageFile,1))) != Zend_Pdf_Resource_Image_Png::PNG_INTERLACING_DISABLED) {
  92. // require_once 'Zend/Pdf/Exception.php';
  93. throw new Zend_Pdf_Exception( "Only non-interlaced images are currently supported." );
  94. }
  95. $this->_width = $width;
  96. $this->_height = $height;
  97. $this->_imageProperties = array();
  98. $this->_imageProperties['bitDepth'] = $bits;
  99. $this->_imageProperties['pngColorType'] = $color;
  100. $this->_imageProperties['pngFilterType'] = $prefilter;
  101. $this->_imageProperties['pngCompressionType'] = $compression;
  102. $this->_imageProperties['pngInterlacingType'] = $interlacing;
  103. fseek($imageFile, 4, SEEK_CUR); //4 Byte Ending Sequence
  104. $imageData = '';
  105. /*
  106. * The following loop processes PNG chunks. 4 Byte Longs are packed first give the chunk length
  107. * followed by the chunk signature, a four byte code. IDAT and IEND are manditory in any PNG.
  108. */
  109. while (!feof($imageFile)) {
  110. $chunkLengthBytes = fread($imageFile, 4);
  111. if ($chunkLengthBytes === false) {
  112. // require_once 'Zend/Pdf/Exception.php';
  113. throw new Zend_Pdf_Exception('Error ocuured while image file reading.');
  114. }
  115. $chunkLengthtmp = unpack('Ni', $chunkLengthBytes);
  116. $chunkLength = $chunkLengthtmp['i'];
  117. $chunkType = fread($imageFile, 4);
  118. switch($chunkType) {
  119. case 'IDAT': //Image Data
  120. /*
  121. * Reads the actual image data from the PNG file. Since we know at this point that the compression
  122. * strategy is the default strategy, we also know that this data is Zip compressed. We will either copy
  123. * the data directly to the PDF and provide the correct FlateDecode predictor, or decompress the data
  124. * decode the filters and output the data as a raw pixel map.
  125. */
  126. $imageData .= fread($imageFile, $chunkLength);
  127. fseek($imageFile, 4, SEEK_CUR);
  128. break;
  129. case 'PLTE': //Palette
  130. $paletteData = fread($imageFile, $chunkLength);
  131. fseek($imageFile, 4, SEEK_CUR);
  132. break;
  133. case 'tRNS': //Basic (non-alpha channel) transparency.
  134. $trnsData = fread($imageFile, $chunkLength);
  135. switch ($color) {
  136. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY:
  137. $baseColor = ord(substr($trnsData, 1, 1));
  138. $transparencyData = array(new Zend_Pdf_Element_Numeric($baseColor),
  139. new Zend_Pdf_Element_Numeric($baseColor));
  140. break;
  141. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB:
  142. $red = ord(substr($trnsData,1,1));
  143. $green = ord(substr($trnsData,3,1));
  144. $blue = ord(substr($trnsData,5,1));
  145. $transparencyData = array(new Zend_Pdf_Element_Numeric($red),
  146. new Zend_Pdf_Element_Numeric($red),
  147. new Zend_Pdf_Element_Numeric($green),
  148. new Zend_Pdf_Element_Numeric($green),
  149. new Zend_Pdf_Element_Numeric($blue),
  150. new Zend_Pdf_Element_Numeric($blue));
  151. break;
  152. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_INDEXED:
  153. //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).
  154. if(($trnsIdx = strpos($trnsData, "\0")) !== false) {
  155. $transparencyData = array(new Zend_Pdf_Element_Numeric($trnsIdx),
  156. new Zend_Pdf_Element_Numeric($trnsIdx));
  157. }
  158. break;
  159. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY_ALPHA:
  160. // Fall through to the next case
  161. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB_ALPHA:
  162. // require_once 'Zend/Pdf/Exception.php';
  163. throw new Zend_Pdf_Exception( "tRNS chunk illegal for Alpha Channel Images" );
  164. break;
  165. }
  166. fseek($imageFile, 4, SEEK_CUR); //4 Byte Ending Sequence
  167. break;
  168. case 'IEND';
  169. break 2; //End the loop too
  170. default:
  171. fseek($imageFile, $chunkLength + 4, SEEK_CUR); //Skip the section
  172. break;
  173. }
  174. }
  175. fclose($imageFile);
  176. $compressed = true;
  177. $imageDataTmp = '';
  178. $smaskData = '';
  179. switch ($color) {
  180. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB:
  181. $colorSpace = new Zend_Pdf_Element_Name('DeviceRGB');
  182. break;
  183. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY:
  184. $colorSpace = new Zend_Pdf_Element_Name('DeviceGray');
  185. break;
  186. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_INDEXED:
  187. if(empty($paletteData)) {
  188. // require_once 'Zend/Pdf/Exception.php';
  189. throw new Zend_Pdf_Exception( "PNG Corruption: No palette data read for indexed type PNG." );
  190. }
  191. $colorSpace = new Zend_Pdf_Element_Array();
  192. $colorSpace->items[] = new Zend_Pdf_Element_Name('Indexed');
  193. $colorSpace->items[] = new Zend_Pdf_Element_Name('DeviceRGB');
  194. $colorSpace->items[] = new Zend_Pdf_Element_Numeric((strlen($paletteData)/3-1));
  195. $paletteObject = $this->_objectFactory->newObject(new Zend_Pdf_Element_String_Binary($paletteData));
  196. $colorSpace->items[] = $paletteObject;
  197. break;
  198. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_GRAY_ALPHA:
  199. /*
  200. * To decode PNG's with alpha data we must create two images from one. One image will contain the Gray data
  201. * the other will contain the Gray transparency overlay data. The former will become the object data and the latter
  202. * will become the Shadow Mask (SMask).
  203. */
  204. if($bits > 8) {
  205. // require_once 'Zend/Pdf/Exception.php';
  206. throw new Zend_Pdf_Exception("Alpha PNGs with bit depth > 8 are not yet supported");
  207. }
  208. $colorSpace = new Zend_Pdf_Element_Name('DeviceGray');
  209. // require_once 'Zend/Pdf/ElementFactory.php';
  210. $decodingObjFactory = Zend_Pdf_ElementFactory::createFactory(1);
  211. $decodingStream = $decodingObjFactory->newStreamObject($imageData);
  212. $decodingStream->dictionary->Filter = new Zend_Pdf_Element_Name('FlateDecode');
  213. $decodingStream->dictionary->DecodeParms = new Zend_Pdf_Element_Dictionary();
  214. $decodingStream->dictionary->DecodeParms->Predictor = new Zend_Pdf_Element_Numeric(15);
  215. $decodingStream->dictionary->DecodeParms->Columns = new Zend_Pdf_Element_Numeric($width);
  216. $decodingStream->dictionary->DecodeParms->Colors = new Zend_Pdf_Element_Numeric(2); //GreyAlpha
  217. $decodingStream->dictionary->DecodeParms->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
  218. $decodingStream->skipFilters();
  219. $pngDataRawDecoded = $decodingStream->value;
  220. //Iterate every pixel and copy out gray data and alpha channel (this will be slow)
  221. for($pixel = 0, $pixelcount = ($width * $height); $pixel < $pixelcount; $pixel++) {
  222. $imageDataTmp .= $pngDataRawDecoded[($pixel*2)];
  223. $smaskData .= $pngDataRawDecoded[($pixel*2)+1];
  224. }
  225. $compressed = false;
  226. $imageData = $imageDataTmp; //Overwrite image data with the gray channel without alpha
  227. break;
  228. case Zend_Pdf_Resource_Image_Png::PNG_CHANNEL_RGB_ALPHA:
  229. /*
  230. * To decode PNG's with alpha data we must create two images from one. One image will contain the RGB data
  231. * the other will contain the Gray transparency overlay data. The former will become the object data and the latter
  232. * will become the Shadow Mask (SMask).
  233. */
  234. if($bits > 8) {
  235. // require_once 'Zend/Pdf/Exception.php';
  236. throw new Zend_Pdf_Exception("Alpha PNGs with bit depth > 8 are not yet supported");
  237. }
  238. $colorSpace = new Zend_Pdf_Element_Name('DeviceRGB');
  239. // require_once 'Zend/Pdf/ElementFactory.php';
  240. $decodingObjFactory = Zend_Pdf_ElementFactory::createFactory(1);
  241. $decodingStream = $decodingObjFactory->newStreamObject($imageData);
  242. $decodingStream->dictionary->Filter = new Zend_Pdf_Element_Name('FlateDecode');
  243. $decodingStream->dictionary->DecodeParms = new Zend_Pdf_Element_Dictionary();
  244. $decodingStream->dictionary->DecodeParms->Predictor = new Zend_Pdf_Element_Numeric(15);
  245. $decodingStream->dictionary->DecodeParms->Columns = new Zend_Pdf_Element_Numeric($width);
  246. $decodingStream->dictionary->DecodeParms->Colors = new Zend_Pdf_Element_Numeric(4); //RGBA
  247. $decodingStream->dictionary->DecodeParms->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
  248. $decodingStream->skipFilters();
  249. $pngDataRawDecoded = $decodingStream->value;
  250. //Iterate every pixel and copy out rgb data and alpha channel (this will be slow)
  251. for($pixel = 0, $pixelcount = ($width * $height); $pixel < $pixelcount; $pixel++) {
  252. $imageDataTmp .= $pngDataRawDecoded[($pixel*4)+0] . $pngDataRawDecoded[($pixel*4)+1] . $pngDataRawDecoded[($pixel*4)+2];
  253. $smaskData .= $pngDataRawDecoded[($pixel*4)+3];
  254. }
  255. $compressed = false;
  256. $imageData = $imageDataTmp; //Overwrite image data with the RGB channel without alpha
  257. break;
  258. default:
  259. // require_once 'Zend/Pdf/Exception.php';
  260. throw new Zend_Pdf_Exception( "PNG Corruption: Invalid color space." );
  261. }
  262. if(empty($imageData)) {
  263. // require_once 'Zend/Pdf/Exception.php';
  264. throw new Zend_Pdf_Exception( "Corrupt PNG Image. Mandatory IDAT chunk not found." );
  265. }
  266. $imageDictionary = $this->_resource->dictionary;
  267. if(!empty($smaskData)) {
  268. /*
  269. * Includes the Alpha transparency data as a Gray Image, then assigns the image as the Shadow Mask for the main image data.
  270. */
  271. $smaskStream = $this->_objectFactory->newStreamObject($smaskData);
  272. $smaskStream->dictionary->Type = new Zend_Pdf_Element_Name('XObject');
  273. $smaskStream->dictionary->Subtype = new Zend_Pdf_Element_Name('Image');
  274. $smaskStream->dictionary->Width = new Zend_Pdf_Element_Numeric($width);
  275. $smaskStream->dictionary->Height = new Zend_Pdf_Element_Numeric($height);
  276. $smaskStream->dictionary->ColorSpace = new Zend_Pdf_Element_Name('DeviceGray');
  277. $smaskStream->dictionary->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
  278. $imageDictionary->SMask = $smaskStream;
  279. // Encode stream with FlateDecode filter
  280. $smaskStreamDecodeParms = array();
  281. $smaskStreamDecodeParms['Predictor'] = new Zend_Pdf_Element_Numeric(15);
  282. $smaskStreamDecodeParms['Columns'] = new Zend_Pdf_Element_Numeric($width);
  283. $smaskStreamDecodeParms['Colors'] = new Zend_Pdf_Element_Numeric(1);
  284. $smaskStreamDecodeParms['BitsPerComponent'] = new Zend_Pdf_Element_Numeric(8);
  285. $smaskStream->dictionary->DecodeParms = new Zend_Pdf_Element_Dictionary($smaskStreamDecodeParms);
  286. $smaskStream->dictionary->Filter = new Zend_Pdf_Element_Name('FlateDecode');
  287. }
  288. if(!empty($transparencyData)) {
  289. //This is experimental and not properly tested.
  290. $imageDictionary->Mask = new Zend_Pdf_Element_Array($transparencyData);
  291. }
  292. $imageDictionary->Width = new Zend_Pdf_Element_Numeric($width);
  293. $imageDictionary->Height = new Zend_Pdf_Element_Numeric($height);
  294. $imageDictionary->ColorSpace = $colorSpace;
  295. $imageDictionary->BitsPerComponent = new Zend_Pdf_Element_Numeric($bits);
  296. $imageDictionary->Filter = new Zend_Pdf_Element_Name('FlateDecode');
  297. $decodeParms = array();
  298. $decodeParms['Predictor'] = new Zend_Pdf_Element_Numeric(15); // Optimal prediction
  299. $decodeParms['Columns'] = new Zend_Pdf_Element_Numeric($width);
  300. $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)));
  301. $decodeParms['BitsPerComponent'] = new Zend_Pdf_Element_Numeric($bits);
  302. $imageDictionary->DecodeParms = new Zend_Pdf_Element_Dictionary($decodeParms);
  303. //Include only the image IDAT section data.
  304. $this->_resource->value = $imageData;
  305. //Skip double compression
  306. if ($compressed) {
  307. $this->_resource->skipFilters();
  308. }
  309. }
  310. /**
  311. * Image width
  312. */
  313. public function getPixelWidth() {
  314. return $this->_width;
  315. }
  316. /**
  317. * Image height
  318. */
  319. public function getPixelHeight() {
  320. return $this->_height;
  321. }
  322. /**
  323. * Image properties
  324. */
  325. public function getProperties() {
  326. return $this->_imageProperties;
  327. }
  328. }