1 <?php
2
3 namespace ICanBoogie\Modules\Thumbnailer;
4
5 use ICanBoogie\ToArray;
6
7 /**
8 * A thumbnail version.
9 *
10 * @property string $background Thumbnail background. Defaults to "transparent".
11 * @property string $default Thumbnail fallback image. Defaults to `null`.
12 * @property string $format Thumbnail format. Defaults to "jpeg".
13 * @property string $filter Thumbnail filter. Defaults to `null`.
14 * @property int $height Thumbnail height. Defaults to `null`.
15 * @property string $method Thumbnail resizing method. Default to "fill".
16 * @property bool $no_interlace Should the thumbnail *not* be interlaced. Default to `false`.
17 * @property bool $no_upscale Should the thumbnail *not* be upscaled. Default to `false`.
18 * @property string $overlay Thumbnail overlay path. Defaults to `null`.
19 * @property string $path Path to the directory of the source image. Defaults to `null`.
20 * @property int $quality Quality of the compression. Only applicable to JPEG. Defaults to 85.
21 * @property string $src Path to the source image. Defaults to `null`.
22 * @property int $width The width of the image. Defaults to `null`.
23 *
24 * @property string $b Alias to {@link $background}.
25 * @property string $d Alias to {@link $default}.
26 * @property string $f Alias to {@link $format}.
27 * @property string $ft Alias to {@link $filter}.
28 * @property int $h Alias to {@link $height}.
29 * @property string $m Alias to {@link $method}.
30 * @property bool $ni Alias to {@link $no_interlace}.
31 * @property bool $nu Alias to {@link $no_upscale}.
32 * @property string $o Alias to {@link $overlay}.
33 * @property string $p Alias to {@link $path}.
34 * @property int $q Alias to {@link $quality}.
35 * @property string $s Alias to {@link $src}.
36 * @property string $w alias to {@link $width}.
37 */
38 class Version implements ToArray
39 {
40 /**
41 * Option for the {@link to_array()} method.
42 *
43 * @var int
44 */
45 const ARRAY_SHORTEN = 1;
46
47 /**
48 * Option for the {@link to_array()} method.
49 *
50 * @var int
51 */
52 const ARRAY_FILTER = 2;
53
54 /**
55 * Options and their default value.
56 *
57 * @var array[string]mixed
58 */
59 static public $defaults = array
60 (
61 'background' => 'transparent',
62 'default' => null,
63 'format' => null,
64 'filter' => null,
65 'height' => null,
66 'method' => 'fill',
67 'no-interlace' => false,
68 'no-upscale' => false,
69 'overlay' => null,
70 'path' => null,
71 'quality' => 90,
72 'src' => null,
73 'width' => null
74 );
75
76 /**
77 * Options shorthands.
78 *
79 * @var array[string]string
80 */
81 static public $shorthands = array
82 (
83 'b' => 'background',
84 'd' => 'default',
85 'f' => 'format',
86 'ft' => 'filter',
87 'h' => 'height',
88 'm' => 'method',
89 'ni' => 'no-interlace',
90 'nu' => 'no-upscale',
91 'o' => 'overlay',
92 'p' => 'path',
93 'q' => 'quality',
94 's' => 'src',
95 'v' => 'version', // FIXME-20130507: remove this
96 'w' => 'width'
97 );
98
99 /**
100 * Returns version options extracted from the URI.
101 *
102 * Options are extracted from the pathinfo (`width`, `height`, `method`, and `format`) as well
103 * as from the query string.
104 *
105 * @param string $uri The URI from which options should be extracted.
106 *
107 * @return \ICanBoogie\Modules\Thumbnailer\Version
108 */
109 static public function from_uri($uri)
110 {
111 $options = [];
112 $path = $uri;
113 $query_string_position = strpos($uri, '?');
114
115 if ($query_string_position)
116 {
117 $path = substr($uri, 0, $query_string_position);
118 $query_string = substr($uri, $query_string_position + 1);
119 parse_str($query_string, $options);
120 $options = Version::widen($options);
121 }
122
123 $options += [
124
125 'width' => null,
126 'height' => null,
127 'method' => null,
128 'format' => null
129
130 ];
131
132 preg_match('#/(\d+x\d+|\d+x|x\d+)(/([^/\.]+))?(\.([a-z]+))?#', $path, $matches);
133
134 if ($matches)
135 {
136 list($w, $h) = explode('x', $matches[1]);
137
138 if ($w)
139 {
140 $options['width'] = (int) $w;
141 }
142
143 if ($h)
144 {
145 $options['height'] = (int) $h;
146 }
147
148 if (isset($matches[3]))
149 {
150 $options['method'] = $matches[3];
151 }
152
153 if (isset($matches[5]))
154 {
155 $options['format'] = $matches[5];
156 }
157
158 if (!$options['method'] && (!$options['width'] || !$options['height']))
159 {
160 $options['method'] = $options['width'] ? 'fixed-width' : 'fixed-height';
161 }
162
163 if ($options['format'] && $options['format'] == 'jpg')
164 {
165 $options['format'] = 'jpeg';
166 }
167 }
168
169 $options = array_filter($options);
170
171 return new static($options);
172 }
173
174 /**
175 * Normalizes thumbnail options.
176 *
177 * @param array $options
178 *
179 * @return array
180 */
181 static public function normalize(array $options)
182 {
183 foreach (self::$shorthands as $shorthand => $full)
184 {
185 if (isset($options[$shorthand]))
186 {
187 $options[$full] = $options[$shorthand];
188 }
189 }
190
191 #
192 # add defaults so that all options are defined
193 #
194
195 $options += self::$defaults;
196
197 #
198 # The parameters are filtered and sorted, making extraneous parameters and parameters order
199 # non important.
200 #
201
202 $options = array_intersect_key($options, self::$defaults);
203
204 ksort($options);
205
206 return $options;
207 }
208
209 /**
210 * Filter thumbnail options.
211 *
212 * Options than match default values are removed. The options are normalized using
213 * {@link normalize_options()} before they are filtered.
214 *
215 * @param array $options
216 *
217 * @return array The filtered thumbnail options.
218 */
219 static public function filter(array $options)
220 {
221 return array_diff_assoc(self::normalize($options), self::$defaults);
222 }
223
224 /**
225 * Shorten option names.
226 *
227 * Note: Extraneous options are not filtered.
228 *
229 * @param array $options
230 *
231 * @return array
232 */
233 static public function shorten(array $options)
234 {
235 $rc = [];
236 $longhands = array_flip(self::$shorthands);
237
238 foreach ($options as $name => $value)
239 {
240 if (isset($longhands[$name]))
241 {
242 $name = $longhands[$name];
243 }
244
245 $rc[$name] = $value;
246 }
247
248 return $rc;
249 }
250
251 /**
252 * Widen option names.
253 *
254 * Note: Extraneous options are not filtered.
255 *
256 * @param array $options
257 *
258 * @return array
259 */
260 static public function widen(array $options)
261 {
262 $rc = [];
263 $shorthands = self::$shorthands;
264
265 foreach ($options as $name => $value)
266 {
267 if (isset($shorthands[$name]))
268 {
269 $name = $shorthands[$name];
270 }
271
272 $rc[$name] = $value;
273 }
274
275 return $rc;
276 }
277
278 /**
279 * Unserialize serialized options.
280 *
281 * @param string $serialized_options
282 *
283 * @return array
284 */
285 static public function unserialize($serialized_options)
286 {
287 preg_match_all('#([^:]+):\s*([^;]+);?#', $serialized_options, $matches, PREG_PATTERN_ORDER);
288
289 return self::filter(array_combine($matches[1], $matches[2]));
290 }
291
292 /**
293 * Serializes options.
294 *
295 * @param array $options
296 *
297 * @return string
298 */
299 static public function serialize(array $options)
300 {
301 $options = self::filter($options);
302 $options = self::shorten($options);
303 $serialized_options = '';
304
305 foreach ($options as $option => $value)
306 {
307 $serialized_options .= "$option:$value;";
308 }
309
310 return $serialized_options;
311 }
312
313 protected $options;
314
315 /**
316 * Initializes and normalizes options.
317 *
318 * @param string|array $options
319 */
320 public function __construct($options)
321 {
322 if (is_string($options))
323 {
324 $options = self::unserialize($options);
325 }
326
327 $this->options = self::normalize($options);
328 }
329
330 /**
331 * Translates a property name into an option name.
332 *
333 * @param string $property
334 *
335 * @return string
336 */
337 static private function property_name_to_option_name($property)
338 {
339 if (isset(self::$shorthands[$property]))
340 {
341 $property = self::$shorthands[$property];
342 }
343 else
344 {
345 if ($property == 'no_interlace')
346 {
347 $property = 'no-interlace';
348 }
349 else if ($property == 'no_upscale')
350 {
351 $property = 'no-upscale';
352 }
353 }
354
355 return $property;
356 }
357
358 /**
359 * Returns an option's value.
360 *
361 * @param string $property
362 *
363 * @return mixed
364 */
365 public function __get($property)
366 {
367 $option = self::property_name_to_option_name($property);
368 return $this->options[$option];
369 }
370
371 /**
372 * Sets an option's value.
373 *
374 * @param string $property
375 * @param mixed $value
376 */
377 public function __set($property, $value)
378 {
379 $option = self::property_name_to_option_name($property);
380 $this->options[$option] = $value;
381 }
382
383 /**
384 * Returns a string representation of the instance.
385 *
386 * @return string
387 *
388 * @see Version::serialize
389 */
390 public function __toString()
391 {
392 return self::serialize($this->options);
393 }
394
395 /**
396 * Returns the instance as an array.
397 *
398 * @param int $flags A bitmask of one or more of the following flags:
399 * - {@link ARRAY_FILTER} The options are filtered with {@link filter()}.
400 * - {@link ARRAY_SHORTEN} The options are shortened with {@link shorten()}.
401 *
402 * @return array
403 */
404 public function to_array($flags=0)
405 {
406 $array = self::normalize($this->options);
407
408 if ($flags & self::ARRAY_FILTER)
409 {
410 $array = self::filter($array);
411 }
412
413 if ($flags & self::ARRAY_SHORTEN)
414 {
415 $array = self::shorten($array);
416 }
417
418 return $array;
419 }
420 }