We are working on a product to help developers in their struggle to develop and maintain a mobile product. Since we have a open mindset, we release some key parts that can be useful for the community as open source. In this first release we have a iOS optimized PNG decoder in PHP.
To understand what this piece of code does, you need to understand what the problem is in the first place.
If you make iOS applications you know the result of a build is a .app file (packed into a .ipa file, which actually is a ZIP file with a .app inside). If you examin the package content of that file you’ll notice that the png-24 images are broken, and can’t be viewed.
So what’s up with that? Why does Apple break them? That’s what we thought too. As it turns out, GPU’s don’t like the order of the colors in a PNG file (Red, Green and Blue a.k.a. RGB) and flips some colors around so it becomes a blue, green and red (BGR) order that can be copied right tot the video memory. But… that would mean you would see flipped colors if you viewed the images in a regular viewer, but that’s not the case.
We dug a little deeper and found out that Apple also places an extra header in the file, but even if we strip that out, it still doesn’t work.
In the data part of the file is a gziped string that contains the image. But, you can’t decode it with the regular gzuncompress() function because it misses some essential parts (header, crc code). We’ve used a alternative Zlib decompressor (in PHP!!!) that fixes this part.
Anyhow, here is an example code of how you can implement it, and extract all the images of a iOS application.
1 2 3 4 5 6 7 8 9 10 | <?php // Include the class include 'Peperzaken/Ios/DecodeImage.php'; // Initialize the class an set the source $processor = new Peperzaken_Ios_DecodeImage('Icon@2x.png'); // Process the image en write it to this path $processor->decode('Icon@2x.regular.png'); ?> |
So how does it work, well i’ve posted the class we’ve written here (with comments). You can download the full package on GitHub.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | <?php class Peperzaken_Ios_DecodeImage { private $_imagePath; public function __construct($path = null) { if ($path !== null) { $this->setSource($path); } } public function setSource ($path) { $this->_imagePath = realpath($path); } public function decode ($outPath = null) { // Open the file $fh = fopen($this->_imagePath, 'rb'); // Get the header $headerData = fread ($fh, 8); // Split the header $header = unpack ("C1highbit/A3signature/C2lineendings/C1eof/C1eol", $headerData); // check if it's a PNG image if (! is_array ($header) && ! $header['highbit'] == 0x89 && ! $header['signature'] == "PNG") { return false; } $chunks = array(); $isIphoneCompressed = false; while (! feof($fh)) { $data = fread ($fh, 8); if (strlen($data) > 0) { // Fix for empty parts // Unpack the chunk $chunk = unpack ("N1length/A4type", $data); // get the type and length of the chunk $data = @fread ($fh, $chunk['length']); // can be 0... $dataCrc = fread ($fh, 4); // get the crc $crc = unpack ("N1crc", $dataCrc); $chunk['crc'] = $crc['crc']; // This chunk is first when it's a iPhone compressed image if ($chunk['type'] == 'CgBI') { $isIphoneCompressed = true; } // Extract the header if needed if($chunk['type'] == 'IHDR' && $isIphoneCompressed) { $width = unpack('N*', substr($data, 0, 4)); $height = unpack('N*', substr($data, 4, 4)); $width = $width[1]; $height = $height[1]; } // Extract and mutate the data chunk if needed (can be multiple) if ($chunk['type'] == 'IDAT' && $isIphoneCompressed) { $bufSize = $height * $width * 4 + $height; $orgData = $data; $uncompressed = @gzuncompress($orgData, $bufSize); // Supress the warning if it can't be extracted // Try extracting via the amazing ZlibDecompress class, because it probably misses the gzip header, footer and crc parts. if ($uncompressed == false) { include 'ZlibDecompress/ZlibDecompress.php'; $zlib = new ZlibDecompress(); $uncompressed = $zlib->inflate($orgData); // we assume it works, this might need some work } // Lets swop some colors $newData = ''; for ($y=0; $y < $height; $y++) { $i = strlen($newData); // setting the offset $newData .= $uncompressed[$i]; // inject the first pixel, don't know why... for ($x=0; $x < $width; $x++) { $i = strlen($newData); // setting the offset // Now we need to swap the BGRA to RGBA $newData .= $uncompressed[$i+2]; // Place the Red pixel $newData .= $uncompressed[$i+1]; // Place the Green pixel $newData .= $uncompressed[$i+0]; // Place the Blue pixel $newData .= $uncompressed[$i+3]; // Place the Aplha byte } } // Compress the data again after swopping (this time with headers and crc and so on) $data = gzcompress($newData, 8); $chunk['length'] = strlen($data); $chunk['crc'] = crc32($chunk['type'] . $data); } $chunk['data'] = $data; // Add the chunk to the chunks array so we can rebuild the thing $chunks[] = $chunk; } } $out = $headerData; foreach ($chunks as $chunk) { // rebuild the PNG image without the CgBI chunk if ($chunk['type'] !== 'CgBI') { $out .= pack('N', $chunk['length']); $out .= $chunk['type']; $out .= $chunk['data']; $out .= pack('N', $chunk['crc']); } } if ($outPath !== null) { return file_put_contents($outPath, $out); } return $out; } } ?> |
I wrote this script in colaboration with my collegue Eltjo Veninga, and we’ve used the following sources for our research:
* iphonedevwiki.net – What apple does to the PNG
* ipin.py – How to decode it in Python
* Handling binary data in php with pack and unpack/ – How to unpack a PNG in PHP
And many thanks to Emanuelle for reporting this PHP bug and write the fix for it.
The GitHub link: iPhone optimized PNG reverse script.



