PluginProbe
AMP WP – Google AMP For WordPress / trunk
AMP WP – Google AMP For WordPress vtrunk
1.8.3 1.8.2 1.8.1 1.8.0 1.7.11 1.7.10 1.7.9 1.7.7 1.7.8 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.2.0 1.2.1 1.2.2 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.3.1 All 56 releases
amp-wp / includes / Fastimage.php

Fastimage.php in AMP WP – Google AMP For WordPress trunk, at includes/Fastimage.php

344 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * FastImage — Because sometimes you just want the size!
4 *
5 * Vendored third-party library that determines image dimensions (width / height)
6 * by reading only the first few bytes of an image file header over HTTP, avoiding
7 * a full download. Supports BMP, GIF, JPEG, and PNG.
8 *
9 * Upstream PHP port : https://github.com/tommoor/fastimage (v 0.1, last commit 2015-05-16)
10 * Original Ruby lib : https://github.com/sdsykes/fastimage (by Steven Sykes)
11 *
12 * The PHP port has been vendored and adapted for WordPress / AMP WP:
13 * - ABSPATH guard added.
14 * - Class-existence guard to avoid re-declaration.
15 * - Method names converted to snake_case for WPCS compliance.
16 * - PHPDoc blocks added for every method and property.
17 * - Suppressed fopen warning replaced with set_error_handler pattern for PHP 8+.
18 *
19 * @author Tom Moor <tom@tommoor.com>
20 * @license MIT
21 * @version 0.1
22 * @link https://github.com/tommoor/fastimage
23 *
24 * @package Amp_WP
25 * @subpackage Amp_WP/includes
26 */
27
28 if ( ! defined( 'ABSPATH' ) ) {
29 exit;
30 }
31
32 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
33 // phpcs:disable WordPress.Files.FileName
34
35 if ( class_exists( 'FastImage' ) ) {
36 return;
37 }
38
39 /**
40 * Reads image headers over HTTP to quickly determine dimensions without
41 * downloading the full file.
42 *
43 * @since 1.0.0
44 */
45 class FastImage {
46
47 /**
48 * Current read position in the buffered string.
49 *
50 * @var int
51 */
52 private $strpos = 0;
53
54 /**
55 * Buffered bytes read from the stream so far.
56 *
57 * @var string|null
58 */
59 private $str;
60
61 /**
62 * Detected image type (bmp, gif, jpeg, png).
63 *
64 * @var string|null
65 */
66 private $type;
67
68 /**
69 * File stream handle.
70 *
71 * @var resource|null
72 */
73 private $handle;
74
75 /**
76 * Constructor.
77 *
78 * @param string|null $uri Optional image URI to load immediately.
79 */
80 public function __construct( $uri = null ) {
81 if ( $uri ) {
82 $this->load( $uri );
83 }
84 }
85
86 /**
87 * Open a stream to the given URI.
88 *
89 * @param string $uri Image URI to open.
90 */
91 public function load( $uri ) {
92 if ( $this->handle ) {
93 $this->close();
94 }
95
96 // Use a temporary error handler instead of @ so failures are
97 // recoverable and PHP 8+ does not emit suppressed warnings to logs.
98 set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
99 static function (): bool {
100 return true;
101 },
102 E_WARNING
103 );
104 $this->handle = fopen( $uri, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
105 restore_error_handler();
106 }
107
108 /**
109 * Close the stream and reset internal state.
110 */
111 public function close() {
112 if ( $this->handle ) {
113 fclose( $this->handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
114 $this->handle = null;
115 $this->type = null;
116 $this->str = null;
117 }
118 }
119
120 /**
121 * Get the image dimensions.
122 *
123 * @return array|false Array of [ width, height ] on success, false on failure.
124 */
125 public function get_size() {
126 $this->strpos = 0;
127 if ( $this->get_type() ) {
128 return array_values( $this->parse_size() );
129 }
130
131 return false;
132 }
133
134 /**
135 * Detect the image type from the first two bytes.
136 *
137 * @return string|false Image type string or false if unrecognised.
138 */
139 public function get_type() {
140 $this->strpos = 0;
141
142 if ( ! $this->type ) {
143 switch ( $this->get_chars( 2 ) ) {
144 case 'BM':
145 $this->type = 'bmp';
146 return $this->type;
147 case 'GI':
148 $this->type = 'gif';
149 return $this->type;
150 case chr( 0xFF ) . chr( 0xd8 ):
151 $this->type = 'jpeg';
152 return $this->type;
153 case chr( 0x89 ) . 'P':
154 $this->type = 'png';
155 return $this->type;
156 default:
157 return false;
158 }
159 }
160
161 return $this->type;
162 }
163
164 /**
165 * Dispatch to the correct format-specific parser.
166 *
167 * @return array|null Parsed dimensions or null.
168 */
169 private function parse_size() {
170 $this->strpos = 0;
171
172 switch ( $this->type ) {
173 case 'png':
174 return $this->parse_size_for_png();
175 case 'gif':
176 return $this->parse_size_for_gif();
177 case 'bmp':
178 return $this->parse_size_for_bmp();
179 case 'jpeg':
180 return $this->parse_size_for_jpeg();
181 }
182
183 return null;
184 }
185
186 /**
187 * Parse PNG dimensions from the IHDR chunk.
188 *
189 * @return array Width and height.
190 */
191 private function parse_size_for_png() {
192 $chars = $this->get_chars( 25 );
193
194 return unpack( 'N*', substr( $chars, 16, 8 ) );
195 }
196
197 /**
198 * Parse GIF dimensions from the logical screen descriptor.
199 *
200 * @return array Width and height.
201 */
202 private function parse_size_for_gif() {
203 $chars = $this->get_chars( 11 );
204
205 return unpack( 'S*', substr( $chars, 6, 4 ) );
206 }
207
208 /**
209 * Parse BMP dimensions from the DIB header.
210 *
211 * @return array Width and height.
212 */
213 private function parse_size_for_bmp() {
214 $chars = $this->get_chars( 29 );
215 $chars = substr( $chars, 14, 14 );
216 $type = unpack( 'C', $chars );
217
218 return ( 40 === reset( $type ) ) ? unpack( 'L*', substr( $chars, 4 ) ) : unpack( 'L*', substr( $chars, 4, 8 ) );
219 }
220
221 /**
222 * Parse JPEG dimensions by walking SOF markers.
223 *
224 * @return array|false Width and height, or false on failure.
225 */
226 private function parse_size_for_jpeg() {
227 $state = null;
228 $skip = 0;
229
230 while ( true ) {
231 switch ( $state ) {
232 default:
233 $this->get_chars( 2 );
234 $state = 'started';
235 break;
236
237 case 'started':
238 $b = $this->get_byte();
239 if ( false === $b ) {
240 return false;
241 }
242
243 $state = 0xFF === $b ? 'sof' : 'started';
244 break;
245
246 case 'sof':
247 $b = $this->get_byte();
248 if ( in_array( $b, range( 0xe0, 0xef ), true ) ) {
249 $state = 'skipframe';
250 } elseif ( in_array( $b, array_merge( range( 0xC0, 0xC3 ), range( 0xC5, 0xC7 ), range( 0xC9, 0xCB ), range( 0xCD, 0xCF ) ), true ) ) {
251 $state = 'readsize';
252 } elseif ( 0xFF === $b ) {
253 $state = 'sof';
254 } else {
255 $state = 'skipframe';
256 }
257 break;
258
259 case 'skipframe':
260 $skip = $this->read_int( $this->get_chars( 2 ) ) - 2;
261 $state = 'doskip';
262 break;
263
264 case 'doskip':
265 $this->get_chars( $skip );
266 $state = 'started';
267 break;
268
269 case 'readsize':
270 $c = $this->get_chars( 7 );
271
272 return array( $this->read_int( substr( $c, 5, 2 ) ), $this->read_int( substr( $c, 3, 2 ) ) );
273 }
274 }
275 }
276
277 /**
278 * Read N bytes from the stream, buffering as needed.
279 *
280 * @param int $n Number of bytes to read.
281 * @return string|false The bytes, or false on failure.
282 */
283 private function get_chars( $n ) {
284 $response = null;
285
286 if ( ! $this->handle ) {
287 return false;
288 }
289
290 // Read more data from the stream if the buffer is too short.
291 $str_len = strlen( $this->str );
292 if ( $this->strpos + $n - 1 >= $str_len ) {
293 $end = ( $this->strpos + $n );
294
295 while ( $str_len < $end && false !== $response ) {
296 $need = $end - ftell( $this->handle );
297 $response = fread( $this->handle, $need ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
298 if ( $response ) {
299 $this->str .= $response;
300 $str_len = strlen( $this->str );
301 } else {
302 return false;
303 }
304 }
305 }
306
307 $result = substr( $this->str, $this->strpos, $n );
308 $this->strpos += $n;
309
310 return $result;
311 }
312
313 /**
314 * Read a single byte from the stream.
315 *
316 * @return int|false Byte value or false on failure.
317 */
318 private function get_byte() {
319 $c = $this->get_chars( 1 );
320 $b = unpack( 'C', $c );
321
322 return reset( $b );
323 }
324
325 /**
326 * Read a two-byte big-endian unsigned integer.
327 *
328 * @param string $str Two-byte string.
329 * @return int
330 */
331 private function read_int( $str ) {
332 $size = unpack( 'C*', $str );
333
334 return ( $size[1] << 8 ) + $size[2];
335 }
336
337 /**
338 * Destructor. Closes the stream handle.
339 */
340 public function __destruct() {
341 $this->close();
342 }
343 }
344