1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie\Modules\Thumbnailer;
13
14 use ICanBoogie\Exception;
15 use ICanBoogie\FileCache;
16 use ICanBoogie\HTTP\HTTPError;
17 use ICanBoogie\HTTP\NotFound;
18 use ICanBoogie\HTTP\Request;
19 use ICanBoogie\I18n;
20 use ICanBoogie\Image;
21 use ICanBoogie\Operation;
22
23 24 25 26 27
28 class GetOperation extends Operation
29 {
30 const VERSION = '2.1';
31
32 static public $background;
33
34 35 36 37 38 39 40
41 protected function resolve_version(Request $request)
42 {
43 global $core;
44
45 $version_name = $request['version'] ?: $request['v'];
46
47 if ($version_name)
48 {
49 $version = $core->thumbnailer_versions[$version_name];
50 }
51 else
52 {
53 $version = Version::from_uri($request->uri);
54
55 if (!$version)
56 {
57 $version = new Version($request->params);
58 }
59 }
60
61 Image::assert_sizes($version->method, $version->width, $version->height);
62
63 return $version;
64 }
65
66 67 68 69 70 71 72 73 74
75 public function get()
76 {
77 $version = clone $this->resolve_version($this->request);
78
79
80
81
82
83 $src = $version->src;
84 $path = $version->path;
85
86 if (!$src)
87 {
88 throw new NotFound('Missing thumbnail source.');
89 }
90
91 $src = $path . $src;
92 $location = \ICanBoogie\DOCUMENT_ROOT . DIRECTORY_SEPARATOR . $src;
93
94 if (!is_file($location))
95 {
96 $default = $version->default;
97
98
99
100
101
102 if (!$default)
103 {
104 throw new NotFound(I18n\t('Thumbnail source not found: %src', array('%src' => $src)));
105 }
106
107 $src = $path . $default;
108 $location = \ICanBoogie\DOCUMENT_ROOT . DIRECTORY_SEPARATOR . $src;
109
110 if (!is_file($location))
111 {
112 throw new NotFound(I18n\t('Thumbnail source (default) not found: %src', array('%src' => $src)));
113 }
114 }
115
116 if (!$version->format)
117 {
118 $info = getimagesize($location);
119 $version->format = substr($info['mime'], 6);
120 }
121
122 if ($version->format == 'jpeg' && $version->background == 'transparent')
123 {
124 $version->background = 'white';
125 }
126
127
128
129
130
131
132 $key = filemtime($location) . '#' . filesize($location) . '#' . json_encode($version->to_array());
133 $key = sha1($key) . '.' . $version->format;
134
135
136
137
138
139 $cache = new CacheManager;
140
141 return $cache->retrieve($key, array($this, 'get_construct'), array($location, $version));
142 }
143
144 145 146 147 148 149 150 151 152
153 public function get_construct(FileCache $cache, $destination, $userdata)
154 {
155 list($path, $version) = $userdata;
156
157 $callback = null;
158
159 if ($version->background != 'transparent')
160 {
161 self::$background = self::decode_background($version->background);
162
163 $callback = __CLASS__ . '::fill_callback';
164 }
165
166 $image = Image::load($path, $info);
167
168 if (!$image)
169 {
170 throw new Exception('Unable to load image from file %path', array('%path' => $path));
171 }
172
173
174
175
176
177 $w = $version->width;
178 $h = $version->height;
179
180 list($ow, $oh) = $info;
181
182 $method = $version->method;
183
184 if ($version->no_upscale)
185 {
186 if ($method == Image::RESIZE_SURFACE)
187 {
188 if ($w * $h > $ow * $oh)
189 {
190 $w = $ow;
191 $h = $oh;
192 }
193 }
194 else
195 {
196 if ($w > $ow)
197 {
198 $w = $ow;
199 }
200
201 if ($h > $oh)
202 {
203 $h = $ow;
204 }
205 }
206 }
207
208 $image = Image::resize($image, $w, $h, $method, $callback);
209
210 if (!$image)
211 {
212 throw new Exception
213 (
214 'Unable to resize image for file %path with version: !version', array
215 (
216 '%path' => $path,
217 '!version' => $version
218 )
219 );
220 }
221
222
223
224
225
226 $filter = $version->filter;
227
228 if ($filter)
229 {
230 $this->apply_filter($image, $filter);
231 }
232
233
234
235
236
237 if ($version->overlay)
238 {
239 $overlay_file = \ICanBoogie\DOCUMENT_ROOT . $version->overlay;
240
241 list($o_w, $o_h) = getimagesize($overlay_file);
242
243 $overlay_source = imagecreatefrompng($overlay_file);
244
245 imagecopyresampled($image, $overlay_source, 0, 0, 0, 0, $w, $h, $o_w, $o_h);
246 }
247
248
249
250
251
252 if (!$version->no_interlace)
253 {
254 imageinterlace($image, true);
255 }
256
257
258
259
260
261 $format = $version->format;
262
263 static $functions = array
264 (
265 'gif' => 'imagegif',
266 'jpeg' => 'imagejpeg',
267 'png' => 'imagepng'
268 );
269
270 $function = $functions[$format];
271 $args = array($image, $destination);
272
273 if ($format == 'jpeg')
274 {
275
276
277
278
279 $args[] = $version->quality;
280 }
281 else if ($format == 'png' && !$callback)
282 {
283
284
285
286
287
288 imagealphablending($image, false);
289 imagesavealpha($image, true);
290 }
291
292 $rc = call_user_func_array($function, $args);
293
294 imagedestroy($image);
295
296 if (!$rc)
297 {
298 throw new Exception('Unable to save thumbnail');
299 }
300
301 return $destination;
302 }
303
304 protected function apply_filter($image, $filter)
305 {
306 if ($filter != 'grayscale')
307 {
308 return;
309 }
310
311 imagefilter($image, IMG_FILTER_GRAYSCALE);
312 }
313
314 protected function validate(\ICanBoogie\Errors $errors)
315 {
316 return true;
317 }
318
319 320 321 322 323 324 325 326 327 328 329
330 protected function process()
331 {
332 $this->rescue_uri();
333
334 $path = $this->get();
335
336 if (!$path)
337 {
338 throw new HTTPError(\ICanBoogie\format('Unable to create thumbnail for: %src', array('%src' => $this->request['src'])), 404);
339 }
340
341 $request = $this->request;
342 $response = $this->response;
343
344 $server_location = \ICanBoogie\DOCUMENT_ROOT . $path;
345 $stat = stat($server_location);
346 $etag = md5($path);
347
348
349
350
351
352 session_cache_limiter('public');
353
354 $response->cache_control->cacheable = 'public';
355 $response->etag = $etag;
356 $response->expires = '+1 week';
357 $response->headers['X-Generated-By'] = 'Icybee/Thumbnailer';
358
359 if ($request->cache_control->cacheable != 'no-cache')
360 {
361 $if_none_match = $request->headers['If-None-Match'];
362 $if_modified_since = $request->headers['If-Modified-Since'];
363
364 if ($if_modified_since && $if_modified_since->timestamp >= $stat['mtime']
365 && $if_none_match && trim($if_none_match) == $etag)
366 {
367 $response->status = 304;
368
369
370
371
372
373 return true;
374 }
375 }
376
377 $pos = strrpos($path, '.');
378 $type = substr($path, $pos + 1);
379
380 $response->last_modified = $stat['mtime'];
381 $response->content_type = "image/$type";
382 $response->content_length = $stat['size'];
383
384 return function() use ($server_location)
385 {
386 $fh = fopen($server_location, 'rb');
387
388 fpassthru($fh);
389
390 fclose($fh);
391 };
392 }
393
394 static private function decode_background($background)
395 {
396 $parts = explode(',', $background);
397
398 $parts[0] = Image::decode_color($parts[0]);
399
400 if (count($parts) == 1)
401 {
402 return array($parts[0], null, 0);
403 }
404
405 $parts[1] = Image::decode_color($parts[1]);
406
407 return $parts;
408 }
409
410 static public function fill_callback($image, $w, $h)
411 {
412
413
414
415
416
417 $args = (array) self::$background;
418
419 array_unshift($args, $image, 0, 0, $w - 1, $h - 1);
420
421 call_user_func_array('ICanBoogie\Image::draw_grid', $args);
422 }
423
424 425 426 427 428 429
430 private function rescue_uri()
431 {
432 $query = $this->request->query_string;
433
434 if (strpos($query, '&') === false)
435 {
436 return;
437 }
438
439 $query = html_entity_decode($query);
440
441 $rc = parse_str($query, $this->request->params);
442 }
443 }