add support for OpenID, prepare 0.2.0
[surrogator.git] / surrogator.php
1 #!/usr/bin/env php
2 <?php
3 /**
4  * Tool to create avatar images in different sizes.
5  *
6  * Part of Surrogator - a simple libravatar avatar image server
7  *
8  * PHP version 5
9  *
10  * @category Tools
11  * @package  Surrogator
12  * @author   Christian Weiske <cweiske@cweiske.de>
13  * @license  http://www.gnu.org/licenses/agpl.html AGPLv3 or later
14  * @link     https://sourceforge.net/p/surrogator/
15  */
16 namespace surrogator;
17 $cfgFile = __DIR__ . '/data/surrogator.config.php';
18 if (!file_exists($cfgFile)) {
19     $cfgFile = '/etc/surrogator.config.php';
20     if (!file_exists($cfgFile)) {
21         logErr(
22             "Configuration file does not exist.\n"
23             . "Copy data/surrogator.config.php.dist to data/surrogator.config.php"
24         );
25         exit(2);
26     }
27 }
28 require $cfgFile;
29
30 array_shift($argv);
31 $files = array();
32 foreach ($argv as $arg) {
33     if ($arg == '-v' || $arg == '--verbose') {
34         ++$logLevel;
35     } else if ($arg == '-vv') {
36         $logLevel += 2;
37     } else if ($arg == '-q' || $arg == '--quiet') {
38         $logLevel = 0;
39     } else if ($arg == '-f' || $arg == '--force') {
40         $forceUpdate = true;
41     } else if ($arg == '-h' || $arg == '--help') {
42         showHelp();
43         exit(4);
44     } else if ($arg == '--version') {
45         echo "surrogator 0.0.1\n";
46         exit();
47     } else if (file_exists($arg)) {
48         $files[] = $arg;
49     } else {
50         logErr('Unknown argument: ' . $arg);
51         exit(3);
52     }
53 }
54
55 /**
56  * Echos the --help screen.
57  *
58  * @return void
59  */
60 function showHelp()
61 {
62     echo <<<HLP
63 Usage: php surrogator.php [options] [filename(s)]
64
65 surrogator - a simple libravatar server
66  Put files in raw/ dir and run surrogator.php to generate different sizes
67
68 Options:
69
70  -h, --help     Show help
71  -v, --verbose  Be verbose (more log messages, also -vv)
72  -q, --quiet    Be quiet (no log messages)
73  -f, --force    Force update of all files
74      --version  Show program version
75
76 filenames       One or several files whose small images shall get generated.
77                 If none given, all will be checked
78
79 HLP;
80 }
81
82 if (!isset($rawDir)) {
83     logErr('$rawDir not set');
84     exit(1);
85 }
86 if (!isset($varDir)) {
87     logErr('$varDir not set');
88     exit(1);
89 }
90 if (!isset($sizes)) {
91     logErr('$sizes not set');
92     exit(1);
93 }
94 if (!isset($maxSize)) {
95     logErr('$maxSize not set');
96     exit(1);
97 }
98 if (!isset($logLevel)) {
99     logErr('$logLevel not set');
100     exit(1);
101 }
102
103
104 if (!is_dir($varDir . '/square')) {
105     log('creating square dir: ' . $varDir . '/square');
106     if (!mkdir($varDir . '/square', 0755, true)) {
107         logErr('cannot create square dir');
108         exit(5);
109     }
110 }
111 log('sizes: ' . implode(', ', $sizes), 2);
112 foreach ($sizes as $size) {
113     if (!is_dir($varDir . '/' . $size)) {
114         log('creating size dir: ' . $varDir . '/' . $size);
115         mkdir($varDir . '/' . $size, 0755);
116     }
117 }
118 foreach (array('mm.png', 'default.png') as $resFile) {
119     if (!file_exists($rawDir . '/' . $resFile)) {
120         log($resFile . ' missing, copying it from res/', 2);
121         copy($resDir . '/' . $resFile, $rawDir . '/' . $resFile);
122     }
123 }
124 foreach (array('index.html', 'robots.txt', 'favicon.ico') as $resFile) {
125     if (!file_exists($wwwDir . '/' . $resFile) && is_writable($wwwDir)) {
126         log('no www/' . $resFile . ' found, copying default over', 1);
127         copy($resDir . '/www/' . $resFile, $wwwDir . '/' . $resFile);
128     }
129 }
130
131 if (count($files)) {
132     $fileInfos = array();
133     foreach ($files as $file) {
134         $fileInfos[] = new \SplFileInfo($file);
135     }
136 } else {
137     $fileInfos = new \RegexIterator(
138         new \DirectoryIterator($rawDir),
139         '#^.+\.(png|jpg)$#'
140     );
141 }
142 foreach ($fileInfos as $fileInfo) {
143     $origPath   = $fileInfo->getPathname();
144     $fileName   = $fileInfo->getFilename();
145     $ext        = strtolower(substr($fileName, strrpos($fileName, '.') + 1));
146     $squarePath = $varDir . '/square/'
147         . substr($fileName, 0, -strlen($ext)) . 'png';
148
149     log('processing ' . $fileName, 1);
150     if (imageUptodate($origPath, $squarePath)) {
151         log(' image up to date', 2);
152         continue;
153     }
154
155     if (!createSquare($origPath, $ext, $squarePath, $maxSize)) {
156         continue;
157     }
158
159     if ($fileName == 'default.png') {
160         $md5 = $sha256 = 'default';
161     } else if ($fileName == 'mm.png') {
162         $md5 = $sha256 = 'mm';
163     } else {
164         list($md5, $sha256) = getHashes($fileName);
165     }
166
167     log(' creating sizes for ' . $fileName, 2);
168     log(' md5:    ' . $md5, 3);
169     log(' sha256: ' . $sha256, 3);
170     $imgSquare = imagecreatefrompng($squarePath);
171     foreach ($sizes as $size) {
172         log(' size ' . $size, 3);
173         $sizePathMd5    = $varDir . '/' . $size . '/' . $md5 . '.png';
174         $sizePathSha256 = $varDir . '/' . $size . '/' . $sha256 . '.png';
175
176         $imgSize = imagecreatetruecolor($size, $size);
177         imagealphablending($imgSize, false);
178         imagefilledrectangle(
179             $imgSize, 0, 0, $size - 1, $size - 1,
180             imagecolorallocatealpha($imgSize, 0, 0, 0, 127)
181         );
182         imagecopyresampled(
183             $imgSize, $imgSquare,
184             0, 0, 0, 0,
185             $size, $size, $maxSize, $maxSize
186         );
187         imagesavealpha($imgSize, true);
188         imagepng($imgSize, $sizePathMd5);
189         imagepng($imgSize, $sizePathSha256);
190         imagedestroy($imgSize);
191
192     }
193     imagedestroy($imgSquare);
194 }
195
196 /**
197  * Create and return md5 and sha256 hashes from a filename.
198  *
199  * @param string $fileName filename without path, e.g. "foo@example.org.png"
200  *
201  * @return array Array with 2 values: md5 and sha256 hash
202  */
203 function getHashes($fileName)
204 {
205     //OpenIDs have their slashes "/" url-encoded
206     $fileName = rawurldecode($fileName);
207
208     $fileNameNoExt = substr($fileName, 0, -strlen(strrpos($fileName, '.')) - 2);
209     $emailAddress  = trim(strtolower($fileNameNoExt));
210
211     return array(
212         md5($emailAddress), hash('sha256', $emailAddress)
213     );
214 }
215
216 /**
217  * Creates the square image from the given image in maximum size.
218  * Scales the image up or down and makes the non-covered parts transparent.
219  *
220  * @param string  $origPath   Full path to original image
221  * @param string  $ext        File extension ("jpg" or "png")
222  * @param string  $targetPath Full path to target image file
223  * @param integer $maxSize    Maxium image size the server supports
224  *
225  * @return boolean True if all went well, false if there was an error
226  */
227 function createSquare($origPath, $ext, $targetPath, $maxSize)
228 {
229     if ($ext == 'png') {
230         $imgOrig = imagecreatefrompng($origPath);
231     } else if ($ext == 'jpg' || $ext == 'jpeg') {
232         $imgOrig = imagecreatefromjpeg($origPath);
233     } else {
234         //unsupported format
235         return false;
236     }
237
238     if ($imgOrig === false) {
239         logErr('Error loading image file: ' . $origPath);
240         return false;
241     }
242
243     $imgSquare = imagecreatetruecolor($maxSize, $maxSize);
244     imagealphablending($imgSquare, false);
245     imagefilledrectangle(
246         $imgSquare, 0, 0, $maxSize - 1, $maxSize - 1,
247         imagecolorallocatealpha($imgSquare, 0, 0, 0, 127)
248     );
249     imagealphablending($imgSquare, true);
250
251     $oWidth    = imagesx($imgOrig);
252     $oHeight   = imagesy($imgOrig);
253     if ($oWidth > $oHeight) {
254         $flScale = $maxSize / $oWidth;
255     } else {
256         $flScale = $maxSize / $oHeight;
257     }
258     $nWidth  = (int)($oWidth * $flScale);
259     $nHeight = (int)($oHeight * $flScale);
260
261     imagecopyresampled(
262         $imgSquare, $imgOrig,
263         ($maxSize - $nWidth) / 2, ($maxSize - $nHeight) / 2,
264         0, 0,
265         $nWidth, $nHeight,
266         $oWidth, $oHeight
267     );
268
269     imagesavealpha($imgSquare, true);
270     imagepng($imgSquare, $targetPath);
271
272     imagedestroy($imgSquare);
273     imagedestroy($imgOrig);
274     return true;
275 }
276
277 /**
278  * Check if the target file is newer than the source file.
279  *
280  * @param string $sourcePath Full source file path
281  * @param string $targetPath Full target file path
282  *
283  * @return boolean True if target file is newer than the source file
284  */
285 function imageUptodate($sourcePath, $targetPath)
286 {
287     global $forceUpdate;
288     if ($forceUpdate) {
289         return false;
290     }
291     if (!file_exists($targetPath)) {
292         return false;
293     }
294     if (filemtime($sourcePath) > filemtime($targetPath)) {
295         //source newer
296         return false;
297     }
298
299     return true;
300 }
301
302 /**
303  * Write a log message to stdout
304  *
305  * @param string  $msg   Message to write
306  * @param integer $level Log level - 1 is important, 3 is unimportant
307  *
308  * @return void
309  */
310 function log($msg, $level = 1)
311 {
312     global $logLevel;
313     if ($level <= $logLevel) {
314         echo $msg . "\n";
315     }
316 }
317
318 /**
319  * Write an error message to stderr
320  *
321  * @param string $msg Message to write
322  *
323  * @return void
324  */
325 function logErr($msg)
326 {
327     file_put_contents('php://stderr', $msg . "\n");
328 }
329
330 ?>