Complex classes like Ico often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Ico, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
8 | class Ico |
||
9 | { |
||
10 | /** |
||
11 | * Background color on icon extraction. |
||
12 | * @var array(R, G, B) = array(255, 255, 255) |
||
13 | */ |
||
14 | public $bgcolor = [255, 255, 255]; |
||
15 | |||
16 | /** |
||
17 | * @var bool Is background color transparent? |
||
18 | */ |
||
19 | public $bgcolorTransparent = false; |
||
20 | |||
21 | private $filename; |
||
22 | private $ico; |
||
23 | private $formats; |
||
24 | |||
25 | /** |
||
26 | * Constructor |
||
27 | * |
||
28 | * @param string $path optional path to ICO file |
||
29 | */ |
||
30 | 5 | public function __construct($path = '') |
|
31 | { |
||
32 | 5 | if (strlen($path) > 0) { |
|
33 | 4 | $this->loadFile($path); |
|
34 | } |
||
35 | 5 | } |
|
36 | |||
37 | /** |
||
38 | * Load an ICO file (don't need to call this is if fill the |
||
39 | * parameter in the class constructor). |
||
40 | * |
||
41 | * @param string $path Path to ICO file |
||
42 | * |
||
43 | * @return bool Success |
||
44 | */ |
||
45 | 5 | public function loadFile($path) |
|
46 | { |
||
47 | 5 | $this->filename = $path; |
|
48 | 5 | if (($fp = @fopen($path, 'rb')) !== false) { |
|
49 | 5 | $data = ''; |
|
50 | 5 | while (!feof($fp)) { |
|
51 | 5 | $data .= fread($fp, 4096); |
|
52 | } |
||
53 | 5 | fclose($fp); |
|
54 | |||
55 | 5 | return $this->loadData($data); |
|
56 | } |
||
57 | |||
58 | return false; |
||
59 | } |
||
60 | |||
61 | /** |
||
62 | * Load an ICO data. If you prefer to open the file |
||
63 | * and return the binary data you can use this function |
||
64 | * directly. Otherwise use loadFile() instead. |
||
65 | * |
||
66 | * @param string $data Binary data of ICO file |
||
67 | * |
||
68 | * @return bool Success |
||
69 | */ |
||
70 | 5 | private function loadData($data) |
|
71 | { |
||
72 | 5 | $this->formats = []; |
|
73 | |||
74 | /** |
||
75 | * ICO header. |
||
76 | **/ |
||
77 | 5 | $icodata = unpack('SReserved/SType/SCount', $data); |
|
78 | 5 | $this->ico = $icodata; |
|
79 | 5 | $data = substr($data, 6); |
|
80 | |||
81 | /* |
||
82 | * Extract each icon header |
||
83 | **/ |
||
84 | 5 | for ($i = 0; $i < $this->ico['Count']; ++$i) { |
|
85 | 5 | $icodata = unpack('CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset', $data); |
|
86 | 5 | $icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6; |
|
87 | 5 | if ($icodata['ColorCount'] == 0) { |
|
88 | 3 | $icodata['ColorCount'] = 256; |
|
89 | } |
||
90 | 5 | $this->formats[] = $icodata; |
|
91 | |||
92 | 5 | $data = substr($data, 16); |
|
93 | } |
||
94 | |||
95 | /* |
||
96 | * Extract aditional headers for each extracted icon header |
||
97 | **/ |
||
98 | 5 | $formatCount = count($this->formats); |
|
99 | 5 | for ($i = 0; $i < $formatCount; ++$i) { |
|
100 | 5 | $icodata = unpack( |
|
101 | 'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/' . |
||
102 | 5 | 'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant', |
|
103 | 5 | substr($data, $this->formats[$i]['FileOffset']) |
|
104 | ); |
||
105 | |||
106 | 5 | $this->formats[$i]['header'] = $icodata; |
|
107 | 5 | $this->formats[$i]['colors'] = []; |
|
108 | |||
109 | 5 | $this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount']; |
|
110 | |||
111 | 5 | switch ($this->formats[$i]['BitCount']) { |
|
112 | 5 | case 32: |
|
113 | 4 | case 24: |
|
114 | 2 | $length = $this->formats[$i]['header']['Width'] * |
|
115 | 2 | $this->formats[$i]['header']['Height'] * |
|
116 | 2 | ($this->formats[$i]['BitCount'] / 8); |
|
117 | 2 | $this->formats[$i]['data'] = substr( |
|
118 | $data, |
||
119 | 2 | $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'], |
|
120 | $length |
||
121 | ); |
||
122 | 2 | break; |
|
123 | 3 | case 8: |
|
124 | 3 | case 4: |
|
125 | 2 | $icodata = substr( |
|
126 | $data, |
||
127 | 2 | $this->formats[$i]['FileOffset'] + $icodata['Size'], |
|
128 | 2 | $this->formats[$i]['ColorCount'] * 4 |
|
129 | ); |
||
130 | 2 | $offset = 0; |
|
131 | 2 | for ($j = 0; $j < $this->formats[$i]['ColorCount']; ++$j) { |
|
132 | 2 | $this->formats[$i]['colors'][] = [ |
|
133 | 2 | 'blue' => ord($icodata[$offset]), |
|
134 | 2 | 'green' => ord($icodata[$offset + 1]), |
|
135 | 2 | 'red' => ord($icodata[$offset + 2]), |
|
136 | 2 | 'reserved' => ord($icodata[$offset + 3]), |
|
137 | ]; |
||
138 | 2 | $offset += 4; |
|
139 | } |
||
140 | 2 | $length = $this->formats[$i]['header']['Width'] * |
|
141 | 2 | $this->formats[$i]['header']['Height'] * |
|
142 | 2 | (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount']; |
|
143 | 2 | $this->formats[$i]['data'] = substr( |
|
144 | $data, |
||
145 | 2 | $this->formats[$i]['FileOffset'] + |
|
146 | 2 | ($this->formats[$i]['ColorCount'] * 4) + |
|
147 | 2 | $this->formats[$i]['header']['Size'], |
|
148 | $length |
||
149 | ); |
||
150 | 2 | break; |
|
151 | 1 | case 1: |
|
152 | 1 | $icodata = substr( |
|
153 | $data, |
||
154 | 1 | $this->formats[$i]['FileOffset'] + $icodata['Size'], |
|
155 | 1 | $this->formats[$i]['ColorCount'] * 4 |
|
156 | ); |
||
157 | |||
158 | 1 | $this->formats[$i]['colors'][] = [ |
|
159 | 1 | 'blue' => ord($icodata[0]), |
|
160 | 1 | 'green' => ord($icodata[1]), |
|
161 | 1 | 'red' => ord($icodata[2]), |
|
162 | 1 | 'reserved' => ord($icodata[3]), |
|
163 | ]; |
||
164 | 1 | $this->formats[$i]['colors'][] = [ |
|
165 | 1 | 'blue' => ord($icodata[4]), |
|
166 | 1 | 'green' => ord($icodata[5]), |
|
167 | 1 | 'red' => ord($icodata[6]), |
|
168 | 1 | 'reserved' => ord($icodata[7]), |
|
169 | ]; |
||
170 | |||
171 | 1 | $length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] / 8; |
|
172 | 1 | $this->formats[$i]['data'] = substr( |
|
173 | $data, |
||
174 | 1 | $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'] + 8, |
|
175 | $length |
||
176 | ); |
||
177 | 1 | break; |
|
178 | } |
||
179 | 5 | $this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']); |
|
180 | } |
||
181 | |||
182 | 5 | return true; |
|
183 | } |
||
184 | |||
185 | /** |
||
186 | * Return the total icons extracted at the moment. |
||
187 | * |
||
188 | * @return int Total icons |
||
189 | */ |
||
190 | 5 | public function getTotalIcons() |
|
194 | |||
195 | /** |
||
196 | * Return the icon header corresponding to that index. |
||
197 | * |
||
198 | * @param int $index Icon index |
||
199 | * |
||
200 | * @return resource|bool Icon header or false |
||
201 | */ |
||
202 | 5 | public function getIconInfo($index) |
|
210 | |||
211 | /** |
||
212 | * Changes background color of extraction. You can set |
||
213 | * the 3 color components or set $red = '#xxxxxx' (HTML format) |
||
214 | * and leave all other blanks. |
||
215 | * |
||
216 | * @param int $red Red component |
||
217 | * @param int $green Green component |
||
218 | * @param int $blue Blue component |
||
219 | */ |
||
220 | 5 | public function setBackground($red = 255, $green = 255, $blue = 255) |
|
221 | { |
||
222 | 5 | if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) { |
|
223 | 5 | $green = hexdec($red[3] . $red[4]); |
|
224 | 5 | $blue = hexdec($red[5] . $red[6]); |
|
225 | 5 | $red = hexdec($red[1] . $red[2]); |
|
226 | } |
||
227 | |||
228 | 5 | $this->bgcolor = [$red, $green, $blue]; |
|
229 | 5 | } |
|
230 | |||
231 | /** |
||
232 | * Set background color to be saved as transparent. |
||
233 | * |
||
234 | * @param bool $transparent Is Transparent or not |
||
235 | * |
||
236 | * @return bool Is Transparent or not |
||
237 | */ |
||
238 | public function setBackgroundTransparent($transparent = true) |
||
242 | |||
243 | /** |
||
244 | * Return an image resource with the icon stored |
||
245 | * on the $index position of the ICO file. |
||
246 | * |
||
247 | * @param int $index Position of the icon inside ICO |
||
248 | * |
||
249 | * @return resource|bool Image resource |
||
250 | **/ |
||
251 | 5 | public function getImage($index) |
|
252 | { |
||
253 | 5 | if (!isset($this->formats[$index])) { |
|
254 | return false; |
||
255 | } |
||
256 | |||
257 | // create image filled with desired background color |
||
258 | 5 | $im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']); |
|
259 | 5 | $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]); |
|
260 | 5 | imagefilledrectangle($im, 0, 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor); |
|
261 | |||
262 | 5 | if ($this->bgcolorTransparent) { |
|
263 | imagecolortransparent($im, $bgcolor); |
||
264 | } |
||
265 | |||
266 | //we may build a string of 1/0 to represent the XOR mask |
||
267 | 5 | $maskBits = ''; |
|
268 | //we may build a palette for 8 bit images |
||
269 | 5 | $palette = []; |
|
270 | |||
271 | // allocate palette and get XOR image |
||
272 | 5 | if (in_array($this->formats[$index]['BitCount'], [1, 4, 8, 24])) { |
|
273 | 4 | if ($this->formats[$index]['BitCount'] != 24) { |
|
274 | 3 | $palette = []; |
|
275 | 3 | for ($i = 0; $i < $this->formats[$index]['ColorCount']; ++$i) { |
|
276 | 3 | $palette[$i] = $this->allocateColor( |
|
277 | $im, |
||
278 | 3 | $this->formats[$index]['colors'][$i]['red'], |
|
279 | 3 | $this->formats[$index]['colors'][$i]['green'], |
|
280 | 3 | $this->formats[$index]['colors'][$i]['blue'], |
|
281 | 3 | round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127) |
|
282 | ); |
||
283 | } |
||
284 | } |
||
285 | |||
286 | // build XOR mask bits |
||
287 | 4 | $width = $this->formats[$index]['Width']; |
|
288 | 4 | if (($width % 32) > 0) { |
|
289 | $width += (32 - ($this->formats[$index]['Width'] % 32)); |
||
290 | } |
||
291 | 4 | $offset = $this->formats[$index]['Width'] * |
|
292 | 4 | $this->formats[$index]['Height'] * |
|
293 | 4 | $this->formats[$index]['BitCount'] / 8; |
|
294 | 4 | $total_bytes = ($width * $this->formats[$index]['Height']) / 8; |
|
295 | 4 | $maskBits = ''; |
|
296 | 4 | $bytes = 0; |
|
297 | 4 | $bytes_per_line = ($this->formats[$index]['Width'] / 8); |
|
298 | 4 | $bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8); |
|
299 | 4 | for ($i = 0; $i < $total_bytes; ++$i) { |
|
300 | 4 | $maskBits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT); |
|
301 | 4 | ++$bytes; |
|
302 | 4 | if ($bytes == $bytes_per_line) { |
|
303 | 4 | $i += $bytes_to_remove; |
|
304 | 4 | $bytes = 0; |
|
305 | } |
||
306 | } |
||
307 | } |
||
308 | |||
309 | // now paint pixels based on bit count |
||
310 | 5 | switch ($this->formats[$index]['BitCount']) { |
|
311 | 5 | case 32: |
|
312 | /** |
||
313 | * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ]. |
||
314 | **/ |
||
315 | 1 | $offset = 0; |
|
316 | 1 | for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
|
317 | 1 | for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
|
318 | 1 | $color = substr($this->formats[$index]['data'], $offset, 4); |
|
319 | 1 | if (ord($color[3]) > 0) { |
|
320 | 1 | $palette = $this->allocateColor( |
|
321 | $im, |
||
322 | 1 | ord($color[2]), |
|
323 | 1 | ord($color[1]), |
|
324 | 1 | ord($color[0]), |
|
325 | 1 | 127 - round(ord($color[3]) / 255 * 127) |
|
326 | ); |
||
327 | 1 | imagesetpixel($im, $j, $i, $palette); |
|
328 | } |
||
329 | 1 | $offset += 4; |
|
330 | } |
||
331 | } |
||
332 | 1 | break; |
|
333 | 4 | case 24: |
|
334 | /** |
||
335 | * 24 bits: 3 bytes per pixel [ B | G | R ]. |
||
336 | **/ |
||
337 | 1 | $offset = 0; |
|
338 | 1 | $bitoffset = 0; |
|
339 | 1 | for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
|
340 | 1 | for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
|
341 | 1 | if ($maskBits[$bitoffset] == 0) { |
|
342 | 1 | $color = substr($this->formats[$index]['data'], $offset, 3); |
|
343 | 1 | $palette = $this->allocateColor($im, ord($color[2]), ord($color[1]), ord($color[0])); |
|
344 | 1 | imagesetpixel($im, $j, $i, $palette); |
|
345 | } |
||
346 | 1 | $offset += 3; |
|
347 | 1 | ++$bitoffset; |
|
348 | } |
||
349 | } |
||
350 | 1 | break; |
|
351 | 3 | case 8: |
|
352 | /** |
||
353 | * 8 bits: 1 byte per pixel [ COLOR INDEX ]. |
||
354 | **/ |
||
355 | 1 | $offset = 0; |
|
356 | 1 | for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
|
357 | 1 | for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
|
358 | 1 | if ($maskBits[$offset] == 0) { |
|
359 | 1 | $color = ord(substr($this->formats[$index]['data'], $offset, 1)); |
|
360 | 1 | imagesetpixel($im, $j, $i, $palette[$color]); |
|
361 | } |
||
362 | 1 | ++$offset; |
|
363 | } |
||
364 | } |
||
365 | 1 | break; |
|
366 | 2 | case 4: |
|
367 | /** |
||
368 | * 4 bits: half byte/nibble per pixel [ COLOR INDEX ]. |
||
369 | **/ |
||
370 | 1 | $offset = 0; |
|
371 | 1 | $maskoffset = 0; |
|
372 | 1 | for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
|
373 | 1 | for ($j = 0; $j < $this->formats[$index]['Width']; $j += 2) { |
|
374 | 1 | $colorByte = ord($this->formats[$index]['data'][$offset]); |
|
375 | 1 | $lowNibble = $colorByte & 0x0f; |
|
376 | 1 | $highNibble = ($colorByte & 0xf0) >> 4; |
|
377 | |||
378 | 1 | if ($maskBits[$maskoffset++] == 0) { |
|
379 | 1 | imagesetpixel($im, $j, $i, $palette[$highNibble]); |
|
380 | } |
||
381 | |||
382 | 1 | if ($maskBits[$maskoffset++] == 0) { |
|
383 | 1 | imagesetpixel($im, $j + 1, $i, $palette[$lowNibble]); |
|
384 | } |
||
385 | 1 | $offset++; |
|
386 | } |
||
387 | } |
||
388 | 1 | break; |
|
389 | 1 | case 1: |
|
390 | /** |
||
391 | * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ]. |
||
392 | **/ |
||
393 | 1 | $colorbits = ''; |
|
394 | 1 | $total = strlen($this->formats[$index]['data']); |
|
395 | 1 | for ($i = 0; $i < $total; ++$i) { |
|
396 | 1 | $colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT); |
|
397 | } |
||
398 | |||
399 | 1 | $offset = 0; |
|
400 | 1 | for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
|
401 | 1 | for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
|
402 | 1 | if ($maskBits[$offset] == 0) { |
|
403 | 1 | imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]); |
|
404 | } |
||
405 | 1 | ++$offset; |
|
406 | } |
||
407 | } |
||
408 | 1 | break; |
|
409 | } |
||
410 | |||
411 | 5 | return $im; |
|
412 | } |
||
413 | |||
414 | /** |
||
415 | * Allocate a color on $im resource. This function prevents |
||
416 | * from allocating same colors on the same pallete. Instead |
||
417 | * if it finds that the color is already allocated, it only |
||
418 | * returns the index to that color. |
||
419 | * It supports alpha channel. |
||
420 | * |
||
421 | * @param resource $im Image resource |
||
422 | * @param int $red Red component |
||
423 | * @param int $green Green component |
||
424 | * @param int $blue Blue component |
||
425 | * @param int $alpha Alpha channel |
||
426 | * |
||
427 | * @return int Color index |
||
428 | */ |
||
429 | 5 | private function allocateColor(&$im, $red, $green, $blue, $alpha = 0) |
|
438 | } |
||
439 |