]> git.wh0rd.org Git - tt-rss.git/blob - lib/jimIcon.php
reinstate wrongfully renamed archived feed; properly fix prefs filtertree labels...
[tt-rss.git] / lib / jimIcon.php
1 <?php
2
3 // Simple .ICO parsing.  The ICO format is insanely complex and this may
4 // fail to correctly handle some technically valid files, but it works
5 // on the majority I've found.
6 //
7 // jimIcon was written in 2013 by Jim Paris <jim@jtan.com> and is
8 // released under the terms of the CC0:
9 //
10 // To the extent possible under law, the author(s) have dedicated all
11 // copyright and related and neighboring rights to this software to
12 // the public domain worldwide. This software is distributed without
13 // any arranty.
14 //
15 // You may have received a copy of the CC0 Public Domain Dedication
16 // along with this software.  If not, see
17 //   http://creativecommons.org/publicdomain/zero/1.0/
18
19 class jimIcon {
20         // Get an image color from a string
21         function get_color($str, $img) {
22                 $b = ord($str[0]);
23                 $g = ord($str[1]);
24                 $r = ord($str[2]);
25                 if (strlen($str) > 3) {
26                         $a = 127 - (ord($str[3]) / 2);
27                         if ($a != 0 && $a != 127)
28                                 $this->had_alpha = 1;
29                 } else {
30                         $a = 0;
31                 }
32                 if ($a != 127)
33                         $this->all_transaprent = 0;
34                 return imagecolorallocatealpha($img, $r, $g, $b, $a);
35         }
36
37         // Given a string with the contents of an .ICO,
38         // return a GD image of the icon, or false on error.
39         function fromiconstring($ico) {
40                 $this->error = "(unknown error)";
41                 $this->had_alpha = 0;
42
43                 // Read header
44                 if (strlen($ico) < 6) {
45                         $this->error = "too short";
46                         return false;
47                 }
48                 $h = unpack("vzero/vtype/vnum", $ico);
49
50                 // Must be ICO format with at least one image
51                 if ($h["zero"] != 0 || $h["type"] != 1 || $h["num"] == 0) {
52                         // See if we can just parse it with GD directly
53                         // if it's not ICO format; maybe it was a mislabeled
54                         // PNG or something.
55                         $i = @imagecreatefromstring($ico);
56                         if ($i) {
57                                 imagesavealpha($i, true);
58                                 return $i;
59                         }
60                         $this->error = "not ICO or other image";
61                         return false;
62                 }
63
64                 // Read directory entries to find the biggest image
65                 $most_pixels = 0;
66                 for ($i = 0; $i < $h["num"]; $i++) {
67                         $entry = substr($ico, 6 + 16 * $i, 16);
68                         if (!$entry || strlen($entry) < 16)
69                                 continue;
70                         $e = unpack("Cwidth/" .
71                                     "Cheight/" .
72                                     "Ccolors/" .
73                                     "Czero/" .
74                                     "vplanes/" .
75                                     "vbpp/" .
76                                     "Vsize/" .
77                                     "Voffset/",
78                                     $entry);
79                         if ($e["width"] == 0)
80                                 $e["width"] = 256;
81                         if ($e["height"] == 0)
82                                 $e["height"] = 256;
83                         if ($e["zero"] != 0) {
84                                 $this->error = "nonzero reserved field";
85                                 return false;
86                         }
87                         $pixels = $e["width"] * $e["height"];
88                         if ($pixels > $most_pixels) {
89                                 $most_pixels = $pixels;
90                                 $most = $e;
91                         }
92                 }
93                 if ($most_pixels == 0) {
94                         $this->error = "no pixels";
95                         return false;
96                 }
97                 $e = $most;
98
99                 // Extract image data
100                 $data = substr($ico, $e["offset"], $e["size"]);
101                 if (!$data || strlen($data) != $e["size"]) {
102                         $this->error = "bad image data";
103                         return false;
104                 }
105
106                 // See if we can parse it (might be PNG format here)
107                 $i = @imagecreatefromstring($data);
108                 if ($i) {
109                         imagesavealpha($img, true);
110                         return $i;
111                 }
112
113                 // Must be a BMP.  Parse it ourselves.
114                 $img = imagecreatetruecolor($e["width"], $e["height"]);
115                 imagesavealpha($img, true);
116                 $bg = imagecolorallocatealpha($img, 255, 0, 0, 127);
117                 imagefill($img, 0, 0, $bg);
118
119                 // Skip over the BITMAPCOREHEADER or BITMAPINFOHEADER;
120                 // we'll just assume the palette and pixel data follow
121                 // in the most obvious format as described by the icon
122                 // directory entry.
123                 $bitmapinfo = unpack("Vsize", $data);
124                 if ($bitmapinfo["size"] == 40) {
125                         $info = unpack("Vsize/" .
126                                        "Vwidth/" .
127                                        "Vheight/" .
128                                        "vplanes/" .
129                                        "vbpp/" .
130                                        "Vcompress/" .
131                                        "Vsize/" .
132                                        "Vxres/" .
133                                        "Vyres/" .
134                                        "Vpalcolors/" .
135                                        "Vimpcolors/", $data);
136                         if ($e["bpp"] == 0) {
137                                 $e["bpp"] = $info["bpp"];
138                         }
139                 }
140                 $data = substr($data, $bitmapinfo["size"]);
141
142                 $height = $e["height"];
143                 $width = $e["width"];
144                 $bpp = $e["bpp"];
145
146                 // For indexed images, we only support 1, 4, or 8 BPP
147                 switch ($bpp) {
148                 case 1:
149                 case 4:
150                 case 8:
151                         $indexed = 1;
152                         break;
153                 case 24:
154                 case 32:
155                         $indexed = 0;
156                         break;
157                 default:
158                         $this->error = "bad BPP $bpp";
159                         return false;
160                 }
161
162                 $offset = 0;
163                 if ($indexed) {
164                         $palette = array();
165                         $this->all_transparent = 1;
166                         for ($i = 0; $i < (1 << $bpp); $i++) {
167                                 $entry = substr($data, $i * 4, 4);
168                                 $palette[$i] = $this->get_color($entry, $img);
169                         }
170                         $offset = $i * 4;
171
172                         // Hack for some icons: if everything was transparent,
173                         // discard alpha channel.
174                         if ($this->all_transparent) {
175                                 for ($i = 0; $i < (1 << $bpp); $i++) {
176                                         $palette[$i] &= 0xffffff;
177                                 }
178                         }
179                 }
180
181                 // Assume image data follows in bottom-up order.
182                 // First the "XOR" image
183                 if ((strlen($data) - $offset) < ($bpp * $height * $width / 8)) {
184                         $this->error = "short data";
185                         return false;
186                 }
187                 $XOR = array();
188                 for ($y = $height - 1; $y >= 0; $y--) {
189                         $x = 0;
190                         while ($x < $width) {
191                                 if (!$indexed) {
192                                         $bytes = $bpp / 8;
193                                         $entry = substr($data, $offset, $bytes);
194                                         $pixel = $this->get_color($entry, $img);
195                                         $XOR[$y][$x] = $pixel;
196                                         $x++;
197                                         $offset += $bytes;
198                                 } elseif ($bpp == 1) {
199                                         $p = ord($data[$offset]);
200                                         for ($b = 0x80; $b > 0; $b >>= 1) {
201                                                 if ($p & $b) {
202                                                         $pixel = $palette[1];
203                                                 } else {
204                                                         $pixel = $palette[0];
205                                                 }
206                                                 $XOR[$y][$x] = $pixel;
207                                                 $x++;
208                                         }
209                                         $offset++;
210                                 } elseif ($bpp == 4) {
211                                         $p = ord($data[$offset]);
212                                         $pixel1 = $palette[$p >> 4];
213                                         $pixel2 = $palette[$p & 0x0f];
214                                         $XOR[$y][$x] = $pixel1;
215                                         $XOR[$y][$x+1] = $pixel2;
216                                         $x += 2;
217                                         $offset++;
218                                 } elseif ($bpp == 8) {
219                                         $pixel = $palette[ord($data[$offset])];
220                                         $XOR[$y][$x] = $pixel;
221                                         $x += 1;
222                                         $offset++;
223                                 } else {
224                                         $this->error = "bad BPP";
225                                         return false;
226                                 }
227                         }
228                         // End of row padding
229                         while ($offset & 3)
230                                 $offset++;
231                 }
232
233                 // Now the "AND" image, which is 1 bit per pixel.  Ignore
234                 // if some of our image data already had alpha values,
235                 // or if there isn't enough data left.
236                 if ($this->had_alpha ||
237                     ((strlen($data) - $offset) < ($height * $width / 8))) {
238                         // Just return what we've got
239                         for ($y = 0; $y < $height; $y++) {
240                                 for ($x = 0; $x < $width; $x++) {
241                                         imagesetpixel($img, $x, $y,
242                                                       $XOR[$y][$x]);
243                                 }
244                         }
245                         return $img;
246                 }
247
248                 // Mask what we have with the "AND" image
249                 for ($y = $height - 1; $y >= 0; $y--) {
250                         $x = 0;
251                         while ($x < $width) {
252                                 for ($b = 0x80;
253                                      $b > 0 && $x < $width; $b >>= 1) {
254                                         if (!(ord($data[$offset]) & $b)) {
255                                                 imagesetpixel($img, $x, $y,
256                                                               $XOR[$y][$x]);
257                                         }
258                                         $x++;
259                                 }
260                                 $offset++;
261                         }
262
263                         // End of row padding
264                         while ($offset & 3)
265                                 $offset++;
266                 }
267                 return $img;
268         }
269 }
270 ?>