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