1<?php
2
3/**
4 * Php library to Bake the PNG Images
5 *
6 */
7class PNGImageBaker
8{
9    private $_contents;
10    private $_size;
11    private $_chunks;
12
13    /**
14     * Prepares file for handling metadata.
15     * Verifies that this file is a valid PNG file.
16     * Unpacks file chunks and reads them into an array.
17     *
18     * @param string $contents File content as a string
19     */
20    public function __construct($contents)
21    {
22        $this->_contents = $contents;
23        $png_signature = pack("C8", 137, 80, 78, 71, 13, 10, 26, 10);
24        // Read 8 bytes of PNG header and verify.
25        $header = substr($this->_contents, 0, 8);
26        if ($header != $png_signature) {
27            echo 'This is not a valid PNG image';
28        }
29        $this->_size = strlen($this->_contents);
30        $this->_chunks = array();
31        // Skip 8 bytes of IHDR image header.
32        $position = 8;
33        do {
34            $chunk = @unpack('Nsize/a4type', substr($this->_contents, $position, 8));
35            $this->_chunks[$chunk['type']][] = substr($this->_contents, $position + 8, $chunk['size']);
36            // Skip 12 bytes chunk overhead.
37            $position += $chunk['size'] + 12;
38        } while ($position < $this->_size);
39    }
40
41    /**
42     * Checks if a key already exists in the chunk of said type.
43     * We need to avoid writing same keyword into file chunks.
44     *
45     * @param string $type Chunk type, like iTXt, tEXt, etc.
46     * @param string $check Keyword that needs to be checked.
47     *
48     * @return boolean (true|false) True if file is safe to write this keyword, false otherwise.
49     */
50    public function checkChunks($type, $check)
51    {
52        if (array_key_exists($type, $this->_chunks)) {
53            foreach (array_keys($this->_chunks[$type]) as $typekey) {
54                list($key, $data) = explode("\0", $this->_chunks[$type][$typekey]);
55                if (strcmp($key, $check) == 0) {
56                    echo 'Key "' . $check . '" already exists in "' . $type . '" chunk.';
57                    return false;
58                }
59            }
60        }
61        return true;
62    }
63
64    /**
65     * Add a chunk by type with given key and text
66     *
67     * @param string $chunkType Chunk type, like iTXt, tEXt, etc.
68     * @param string $key Keyword that needs to be added.
69     * @param string $value Currently an assertion URL that is added to an image metadata.
70     *
71     * @return string $result File content with a new chunk as a string.
72     */
73    public function addChunk($chunkType, $key, $value)
74    {
75        $chunkData = $key . "\0" . $value;
76        $crc = pack("N", crc32($chunkType . $chunkData));
77        $len = pack("N", strlen($chunkData));
78
79        $newChunk = $len . $chunkType . $chunkData . $crc;
80        $result = substr($this->_contents, 0, $this->_size - 12)
81                . $newChunk
82                . substr($this->_contents, $this->_size - 12, 12);
83        return $result;
84    }
85
86    /**
87     * removes a chunk by type with given key and text
88     *
89     * @param string $chunkType Chunk type, like iTXt, tEXt, etc.
90     * @param string $key Keyword that needs to be deleted.
91     * @param string $png the png image.
92     *
93     * @return string $result New File content.
94     */
95    public function removeChunks($chunkType, $key, $png)
96    {
97        // Read the magic bytes and verify
98        $retval = substr($png, 0, 8);
99        $ipos = 8;
100        if ($retval != "\x89PNG\x0d\x0a\x1a\x0a") {
101            throw new Exception('Is not a valid PNG image');
102        }
103        // Loop through the chunks. Byte 0-3 is length, Byte 4-7 is type
104        $chunkHeader = substr($png, $ipos, 8);
105        $ipos = $ipos + 8;
106        while ($chunkHeader) {
107            // Extract length and type from binary data
108            $chunk = @unpack('Nsize/a4type', $chunkHeader);
109            $skip = false;
110            if ($chunk['type'] == $chunkType) {
111                $data = substr($png, $ipos, $chunk['size']);
112                $sections = explode("\0", $data);
113                print_r($sections);
114                if ($sections[0] == $key) {
115                    $skip = true;
116                }
117            }
118            // Extract the data and the CRC
119            $data = substr($png, $ipos, $chunk['size'] + 4);
120            $ipos = $ipos + $chunk['size'] + 4;
121            // Add in the header, data, and CRC
122            if (!$skip) {
123                $retval = $retval . $chunkHeader . $data;
124            }
125            // Read next chunk header
126            $chunkHeader = substr($png, $ipos, 8);
127            $ipos = $ipos + 8;
128        }
129        return $retval;
130    }
131
132    /**
133     * Extracts the baked PNG info by the Key
134     *
135     * @param string $png the png image
136     * @param string $key Keyword that needs to be searched.
137     *
138     * @return mixed - If there is an error - boolean false is returned
139     * If there is PNG information that matches the key an array is returned
140     *
141     */
142    public function extractBadgeInfo($png, $key = 'openbadges')
143    {
144        // Read the magic bytes and verify
145        $retval = substr($png, 0, 8);
146        $ipos = 8;
147        if ($retval != "\x89PNG\x0d\x0a\x1a\x0a") {
148            return false;
149        }
150
151        // Loop through the chunks. Byte 0-3 is length, Byte 4-7 is type
152        $chunkHeader = substr($png, $ipos, 8);
153        $ipos = $ipos + 8;
154        while ($chunkHeader) {
155            // Extract length and type from binary data
156            $chunk = @unpack('Nsize/a4type', $chunkHeader);
157            $skip = false;
158            if ($chunk['type'] == 'tEXt') {
159                $data = substr($png, $ipos, $chunk['size']);
160                $sections = explode("\0", $data);
161                if ($sections[0] == $key) {
162                    return $sections;
163                }
164            }
165            // Extract the data and the CRC
166            $data = substr($png, $ipos, $chunk['size'] + 4);
167            $ipos = $ipos + $chunk['size'] + 4;
168
169            // Read next chunk header
170            $chunkHeader = substr($png, $ipos, 8);
171            $ipos = $ipos + 8;
172        }
173    }
174}
175