Welcome to the PHP Album Tutorial! Before going ahead with the tutorial, you should make sure you feel comfortable with basic PHP programming, the GD image library, Perl-compatible regular expressions (PCRE). These topics, in addition to advanced Program Execution and functions used to access Directories will be explained briefly. The tutorial also relies on the FFmpeg package for video files. Good luck!
The basic method the script will use is as follows (in pseudocode):
for each ( file in the current directory ) { if ( extension indicates a picture ) { get image information using libgd if needed, generate and cache a thumbnail } else if ( extension indicates a video ) { launch a FFmpeg child process to get video information and, if needed, to output the first frame parse the text output of FFmpeg and, if needed, generate and cache a thumbnail of the first frame } append generic information (eg. file size) } for each ( summary ) { output HTML code }
Specifically, the "summary" is an associative array returned by the file processing functions (prefixed by "image_build_") that include the following:
Key | Description | Example 1 (VV 007.jpg) | Example 2 (VV 004.avi) |
---|---|---|---|
file | File name (including extension) | "VV 007.jpg" | "VV 004.avi" |
thumb | Thumbnail file path | "/~cat/thumbs/VV 007.jpg" | "/~cat/thumbs/VV 004.jpg" |
type | File type | "JPEG" | "AVI" |
cx | Width (in pixels) | 1024 | 320 |
cy | Height (in pixels) | 768 | 240 |
size | File size (in bytes) | 130365 | 4289380 |
duration | Video length (in seconds, video only) | N/A | 13.8 |
fps | Frames per second (video only) | N/A | 15 |
This function is very basic yet very important. It is used by picture and video processing functions to generate a thumbnail. The procedure is fairly simple, it operates on a GD image handle ($im) and outputs a JPEG thumbnail directly to file (named in $thumb):
2 function image_thumbnail($im, $thumb)
3 {
4 $ratio = max($cx = imagesx($im), $cy = imagesy($im)) / 200;
5 $new_cx = $cx / $ratio;
6 $new_cy = $cy / $ratio;
8 $ret = false;
9 if(($th = imagecreatetruecolor($new_cx, $new_cy)))
10 {
11 if(imagecopyresampled($th, $im, 0, 0, 0, 0, $new_cx, $new_cy, $cx, $cy))
12 if(imagejpeg($th, $thumb))
13 $ret = true;
14
15 imagedestroy($th);
16 }
18 return $ret;
19 }
This function, just like the other image processing function, receives the file name to be processed ($file), the path where a thumbnail should exist or be generated ($thumbfile), and the URL where the thumbnail should be accessible if it already exists or will exist ($thumburl). It also returns the summary array which contains all kinds of juicy facts about the image.
The role of $picture_types is to facilitate the conversion of getimagesize's return value to a string representing the image type.
21 $picture_types = array(null, 'GIF', 'JPEG', 'PNG');
22 function image_build_picture($file, $thumbfile, $thumburl)
23 {
25 $old_limit = ini_set('memory_limit', '32M');
27 if((list($ret['cx'], $ret['cy'], $type, ) = @getimagesize($file)) === false)
28 return;
30 if(file_exists($thumbfile))
31 {
32 $ret['thumb'] = $thumburl;
33 }
34 else
35 {
37 switch($type)
38 {
39 case 1:
40 $im = imagecreatefromgif($file);
41 break;
42 case 2:
43 $im = imagecreatefromjpeg($file);
44 break;
45 case 3:
46 $im = imagecreatefrompng($file);
47 break;
48 default:
49 $im = null;
50 break;
51 }
52
53 if($im)
54 {
55 if(image_thumbnail($im, $thumbfile))
56 $ret['thumb'] = $thumburl;
57
58 imagedestroy($im);
59 }
60 }
63 ini_set('memory_limit', $old_limit);
65 if($type)
66 {
67 global $picture_types;
68 $ret['type'] = $picture_types[$type];
69 }
70 return $ret;
71 }
I've already described the arguments and return value of these two functions. However, the focus of explanation for image_build_video will need to be the actual processing. In my opinion, it's the most complex but also the neatest (dare I say, most beautiful?) part of this script.
Before continuing, please take a look at FFmpeg. In fact, you'll need to compile it and make sure it's executable by the script. Also, familiarize yourself with pipes and how to control processes using proc_open.
Two main cases are handled: when the thumbnail exists, and when it doesn't.
73 function image_build_video($file, $thumbfile, $thumburl)
74 {
75 $cmd = '/home/vv_staff/cat/proj/ffmpeg/ffmpeg -i '.escapeshellarg($file);
76 $fds = array(
77 0 => array('file', '/dev/null', 'r'),
78 2 => array('pipe', 'w')
79 );
80 $ret = array();
82 if(file_exists($thumbfile))
83 {
84 $fds[1] = array('file', '/dev/null', 'w');
85 $ret['thumb'] = $thumburl;
86 }
-an
),
to limit processing to as little frames as possible (-t .01
),
to output to a pipe (-f imagepipe
), to output in JPEG
format (-img jpeg
) and to direct the data to stdout
(-
). The standard output pipe is also specified, in order
to allow the script to capture the image data from FFmpeg.
87 else
88 {
89 $cmd .= ' -an -t .01 -f imagepipe -img jpeg -';
90 $fds[1] = array('pipe', 'w');
91 }
93 $proc = proc_open($cmd, $fds, $pipes);
94 if(is_resource($proc))
95 {
96 if(isset($pipes[1])) stream_set_blocking($pipes[1], false);
97 stream_set_blocking($pipes[2], false);
98 }
100 $image_data = '';
101 $image_info = '';
102 for(;;)
103 {
104 $read = array($pipes[2]);
105 if(isset($pipes[1])) $read[] = $pipes[1];
106
107 $r = stream_select($read, $write = null, $except = null, null);
108 if($r === false) // error
109 break;
110 else if($r == 1 || $r == 2)
111 {
112 if(isset($pipes[1]))
113 if(in_array($pipes[1], $read))
114 $image_data .= fread($pipes[1], 4096);
115
116 if(in_array($pipes[2], $read))
117 $image_info .= fread($pipes[2], 4096);
118 }
119 else // no data or unexpected return value
120 continue;
122 if((isset($pipes[1]) ? feof($pipes[1]) : true) && feof($pipes[2]))
123 break;
124 }
126 if(isset($pipes[1])) fclose($pipes[1]);
127 fclose($pipes[2]);
128 proc_close($proc);
129
130 if($image_data && ($im = imagecreatefromstring($image_data)))
131 {
132 image_thumbnail($im, $thumbfile);
133 imagedestroy($im);
134 $ret['thumb'] = $thumburl;
135 }
137 if(!$image_info)
138 return;
141 $lines = explode("\n", $image_info);
142 foreach($lines as $line)
143 {
144 if(preg_match('/^Input #\d+, ([^,]+), from /', $line, $matches))
145 {
146 $ret['type'] = strtoupper($matches[1]);
147 }
148 else if(preg_match('/^ Duration: (\d\d):(\d\d):(\d\d)\.(\d)/', $line, $matches))
149 {
150 // add up whole part first
151 $duration = intval($matches[3]);
152 $duration += 60 * intval($matches[2]);
153 $duration += 3600 * intval($matches[1]);
154
155 // build entire duration, including deciseconds
156 $ret['duration'] = (double)$duration + (double)intval($matches[4]) / 10;
157 }
158 else if(preg_match('/^ Stream #\d+\.\d+: Video: [^,]+, (\d+)x(\d+), (\d+)\.(\d+) fps$/', $line, $matches))
159 {
160 $ret['cx'] = intval($matches[1]);
161 $ret['cy'] = intval($matches[2]);
162 $ret['fps'] = floatval($matches[3].'.'.$matches[4]);
163 }
164 }
165 return $ret;
166 }
All right, well the truth is that you don't really have to include video support. If you're certain you don't need this ridiculously cool feature, you can omit the image_build_video function along with lines 186 through 189.
I will mainly explain here the image_build function and most of the top-level code that binds the parts I've described, up to generating the summary array.
171 set_time_limit(5);
173 if(!preg_match('/^(.+)\.([^.]{3,4})$/', $file, $matches))
174 return;
176 $name = $matches[1];
177 $ext = $matches[2];
178 $thumbfile = '/home/vv_staff/cat/public_html/thumbs/'.$name.'.jpg';
179 $thumburl = '/~cat/thumbs/'.$name.'.jpg';
181 if( !strcasecmp($ext, 'gif') ||
182 !strcasecmp($ext, 'jpg') ||
183 !strcasecmp($ext, 'jpeg') ||
184 !strcasecmp($ext, 'png'))
185 $ret = image_build_picture($file, $thumbfile, $thumburl);
186 else if(!strcasecmp($ext, 'avi') ||
187 !strcasecmp($ext, 'mpg') ||
188 !strcasecmp($ext, 'mpeg'))
189 $ret = image_build_video($file, $thumbfile, $thumburl);
190 else
191 return;
194 $ret['file'] = $file;
195 $ret['size'] = filesize($file);
196 return $ret;
197 }
211 $excl = explode(',', $_GET['excl']);
213 $files = array();
214 if($dir = opendir('.'))
215 {
216 while(false !== ($file = readdir($dir)))
217 {
218 if(in_array($file, $excl))
219 continue;
220
221 if(($info = image_build($file)))
222 $files[] = $info;
223 }
224 closedir($dir);
225 }
228 function compare_files($a, $b)
229 {
230 return strcasecmp($a['file'], $b['file']);
231 }
232
233 usort($files, compare_files);
Everything you need is in the $files array, so it really shouldn't be too hard to write this second part. I have, however, some neat size and time formatting utility functions that I'd like to show off, so here's what I did:
HEADER.html
, since it would allow for
customization through CSS without editing the PHP script.
236 @include "HEADER.html";
for
blocks without any code
body. ;)
199 function human_size($size)
200 {
201 for($si = 0; $size >= 1024; $size /= 1024, $si++);
202 return ($si ? sprintf('%.1f', $size) : $size).substr(' kMG', $si, 1);
203 }
204
205 function human_time($time)
206 {
207 for($ti = 0; $size >= 60; $size /= 60, $si++);
208 return ($ti ? sprintf('%.1f', $time) : $time).' '.substr('smh', $si, 1);
209 }
237 echo '<table width="100%" border="0" cellspacing="10">';
238
239 $per_row = 3;
240 $col_width = round(100.0 / $per_row);
241 for($i = 0; $i < count($files); $i++)
242 {
243 $f = $files[$i];
244
245 if($i % $per_row == 0)
246 echo '<tr valign="middle">';
247
248 echo "<td align=\"center\" width=\"{$col_width}%\"><a href=\"{$f['file']}\">";
249
250 if($f['thumb'])
251 echo "<img src=\"{$f['thumb']}\" border=\"0\" />";
252 else
253 echo "<br />Thumbnail not available";
254
255 echo "<br />{$f['file']}</a><br />";
256
257 if(isset($f['type']))
258 echo $f['type'];
259
260 if(isset($f['cx']) && isset($f['cy']))
261 echo " ~ {$f['cx']}x{$f['cy']}";
262
263 echo ' ~ '.human_size($f['size']).'<br />';
264
265 if(isset($f['duration']))
266 echo human_time($f['duration']);
267
268 if(isset($f['fps']))
269 echo " @ {$f['fps']} fps";
270
271 echo '</td>';
272
273 if($i % $per_row == $per_row - 1)
274 echo '</tr>';
275 }
276 echo '</table>';
277
278 ?>
HEADER.html
:
1 <html>
2 <head>
3 <title>Farm Experiments</title>
4 <style>
5 body {
6 font-family: Verdana;
7 font-size: x-small;
8 background-color: #99ccff;
9 color: green;
10 }
11 a {
12 text-decoration: none;
13 }
14 table {
15 font-size: 100%;
16 }
17 </style>
18 </head>
19 <body>
I hope this tutorial was productive for you, and that you actually learned something by following it. Please feel free to send any comments and suggestions to cat@vv.carleton.ca. Make sure to add the word "NOTSPAM" in the subject line.