Source of “subroutines.txt”
<?php // Emacs settings: -*- mode: Fundamental; tab-width: 4; -*-
////////////////////////////////////////////////////////////////////////////
// //
// Andrew's Album Application: subroutines.txt //
// //
// Copyright (c) 2004-2005, Andrew Birrell //
// //
// PHP subroutines common to photos.php and noscript.php //
// //
////////////////////////////////////////////////////////////////////////////
require("settings.txt");
define("C_mainSize", "" . C_mainW . "x" . C_mainH . ">");
define("C_pdaSize", "" . C_pdaW . "x" . C_pdaH . ">");
define("C_thumbSize", "" . C_thumbW . "x" . C_thumbH);
define("C_miniSize", "" . C_miniW . "x" . C_miniH);
define("C_shadowW", 6); // shadow offset; image grows by twice this
function elapsed($startTime) {
// Return the number of milliseconds elapsed since $startTime, rounded
// to an integer.
$stopTime = microtime();
// The format is microseconds (as a decimal fraction of a second),
// space, seconds (as an integer)
list($startUsec, $startSec) = explode(" ", $startTime);
list($stopUsec, $stopSec) = explode(" ", $stopTime);
// Because usecs are float, the entire formula is computed as a float.
// Subtract the seconds first to avoid limited float precision.
return round( (($stopSec - $startSec) + ($stopUsec - $startUsec)) *
1000 );
}
//
// Image cache
//
function getCacheFileName($imagepath, $size) {
// Given the local file pathname of a raw image, return the local
// file pathname for the image scaled to $size
$cacheFile = preg_replace("#^" . preg_quote(C_images) . "#", C_cache,
$imagepath);
$cacheFile .= ($size == C_mainSize ? "-main" :
($size == C_pdaSize ? "-pda" :
($size == C_thumbSize ? "-thumb" : "-mini"))) .
".jpg";
return $cacheFile;
}
function imageSize($imagepath) {
// Returns an array with 0=width and 1=height of the image
// Element 2 is image type, "JPEG" for JPEG
// Returns false on non-image file
if (!file_exists($imagepath)) {
$res = false;
} else if (preg_match('#\\.(jpg|jpeg|gif|png|tiff|bmp)$#i',
$imagepath)) {
// Use built-in getimagesize for the easy cases, because it's faster
$res = getimagesize($imagepath);
if ($res && $res[2] == 2) $res[2] = "JPEG";
} else {
// Use "identify" from ImageMagick package, because it handles
// everything that "convert" can handle (in particular, PICT).
$line = exec(C_identify . " ". escapeshellarg($imagepath));
$matches = array();
if (preg_match("#" . preg_quote($imagepath) .
" ([^ ]*) ([0-9]*)x([0-9]*)#",
$line, $matches)) {
$res[0] = $matches[2];
$res[1] = $matches[3];
$res[2] = $matches[1];
} else {
$res = false;
}
}
return $res;
}
function orient($imagepath) {
// Return "convert" argument to adjust image orientation based on
// the image's EXIF "orientation" tag
$sizes = imageSize($imagepath);
$correction = "";
if ($sizes && $sizes[2] == "JPEG") {
$exif = @exif_read_data($imagepath, null, true);
if (isset($exif["IFD0"])) {
$exifIFD0 = $exif["IFD0"];
if (isset($exifIFD0["Orientation"])) {
$n = $exifIFD0["Orientation"];
if ($n == 2) {
$correction = " -flop";
} else if ($n == 3) {
$correction = " -rotate 180";
} else if ($n == 4) {
$correction = " -flop -rotate 180";
} else if ($n == 5) {
$correction = " -flop -rotate -90";
} else if ($n == 6) {
$correction = " -rotate 90";
} else if ($n == 7) {
$correction = " -flop -rotate 90";
} else if ($n == 8) {
$correction = " -rotate -90";
}
}
}
}
return $correction;
}
function ensureDirExists($dirpath) {
// Ensure given directory exists, creating if necessary
if (!file_exists($dirpath)) {
if ($dirpath == C_cache) die("Cache directory missing");
if ($dirpath == C_titles) die("Titles directory missing");
ensureDirExists(dirname($dirpath));
if (!mkdir($dirpath)) die("Directory creation failed");
}
}
function cacheScaledImage($imagepath, $size) {
// Scale the image and place it in the cache
// Returns the cache file name
$withShadow = ($size == C_miniSize || $size == C_thumbSize);
$finalFile = getCacheFileName($imagepath, $size);
$rawMtime = filemtime($imagepath);
if (!file_exists($finalFile) || $rawMtime != filemtime($finalFile)) {
ensureDirExists(dirname($finalFile));
$unshadowed = C_cache . "/aaa" . microtime() . ".jpg";
exec(C_convert .
" -size " . escapeshellarg($size) . // speeds up reading
" " . escapeshellarg($imagepath) .
orient($imagepath) .
" -resize " . escapeshellarg($size) .
" -quality " .
($size == C_mainSize ? C_mainQuality :
($size == C_pdaSize ? C_pdaQuality : 100)) .
" -unsharp 1x2" .
($withShadow ? " -bordercolor white -border 1x1" : "").
" " . escapeshellarg($unshadowed));
if (!file_exists($unshadowed)) {
// C_convert failed, use placeholder
copy("missing.jpg", $unshadowed);
}
$scaled = $unshadowed;
if ($withShadow && file_exists($unshadowed)) {
// create drop-shadow version, iff C_convert succeeded
$shadowOnly = C_cache . "/aab" . microtime() . ".png";
exec(C_convert .
" -depth 8 -threshold 0 -negate " .
escapeShellArg($unshadowed) .
" +profile '*'" .
" -bordercolor " . escapeShellArg(C_thumbBg) .
" -border 20x20 -gaussian 3x4" .
" -shave " . (20-C_shadowW) . "x" . (20-C_shadowW) .
" " . escapeShellArg($shadowOnly));
if (file_exists($shadowOnly)) {
// create composite, iff shadow construction succeeded
$shadowed = C_cache . "/aac" . microtime() . ".jpg";
exec(C_composite . " -gravity northwest -quality 85 " .
escapeShellArg($unshadowed) .
" " . escapeShellArg($shadowOnly) .
" " . escapeShellArg($shadowed)
);
unlink($shadowOnly);
$scaled = $shadowed;
}
unlink($unshadowed);
}
if (file_exists($scaled)) {
// If older PHP versions, touch($scaled, mtime) doesn't
// handle spaces in filenames, so optionally use the "touch"
// command instead
if (C_touch) {
exec(C_touch . " -r " . escapeshellarg($imagepath) .
" " . escapeshellarg($scaled));
} else {
touch($scaled, $rawMtime);
}
if (!@rename($scaled, $finalFile)) {
// Windows won't rename in place
if (file_exists($finalFile)) unlink($finalFile);
rename($scaled, $finalFile);
}
} // else thumbnail creation failed
}
return $finalFile;
}
//
// Conversion of path names into URLs
//
function encodeUrlPath($urlpath) {
// URL-encode and XML- (or HTML-) encode the given URL path, which
// can be host-relative or directory-relative.
// I.e. URL-encode everything except "/" separators, then
// XML- (or HTML-) encode everything.
$arcs = explode("/", $urlpath);
for ($i = 0; $i < sizeof($arcs); $i++) {
$arcs[$i] = rawurlencode($arcs[$i]);
}
return htmlspecialchars(implode("/", $arcs));
}
function urlForRawImage($imagepath) {
// Given the local file pathname of a raw image, return a URL for it.
// Result is URL- and XML- (or HTML-) encoded.
$rawUrl = preg_replace("#^" . preg_quote(C_images) . "#",
C_imagesUrl, $imagepath);
return encodeUrlPath($rawUrl);
}
function urlForCachedImage($imagepath, $size) {
// Given the local file pathname of a raw image, return a URL for
// its cached version at given size.
// Result is URL- and XML- (or HTML-) encoded.
$cacheFile = getCacheFileName($imagepath, $size);
$cacheUrl = preg_replace("#^" . preg_quote(C_cache) . "#",
C_cacheUrl, $cacheFile);
return encodeUrlPath($cacheUrl);
}
function urlPath($imagepath) {
// Given the local file pathname of a folder or raw image, return the
// value that we expect to receive back as the "path" CGI argument to
// identify that folder or image. Return "" for a null path.
// Result is URL-encoded and XML- (or HTML-) encoded.
//
// Anything that's consistent with our treatment of $path CGI arg is OK,
// subject to the facts that the XML and Javascript use the path "" to
// mean "absent", and that the Javascript assumes that child paths have
// their parents paths as a prefix.
//
// In practice, we use a clean path relative to C_images, since this
// produces the simplest URL's.
//
if (is_null($imagepath)) return "";
if ($imagepath == C_images) return ".";
if (strpos($imagepath, C_images . "/") !== 0) die("Bad path in urlPath");
$trimmed = preg_replace("#^" . preg_quote(C_images) . "/#",
"", $imagepath);
return htmlspecialchars(rawurlencode($trimmed));
}
//
// Titles "database"
//
// Note: the titles are in UTF-8, and IE fails on illegal UTF-8 in XML
function stripExtension($path) {
// Strip file name extension, if any
return preg_replace('#[.][^./]*$#i', '', $path);
}
function getTitleFileName($imagepath) {
// Given the local file pathname of a folder or raw image, return the
// local file pathname for its title.
// Replaces C_images with a canned string, in case C_images
// is non-trivial, e.g. an absolute path name.
$relpath = preg_replace("#^" . preg_quote(C_images) . "#", "images",
$imagepath);
return C_titles . "/$relpath.txt";
}
function readTitle($titlepath) {
// Attempt to read title from given file; return false on failure
$title = false;
if (file_exists($titlepath)) {
$fp = fopen($titlepath, "rb");
if ($fp) $title = fread($fp, 99);
fclose($fp);
}
return $title;
}
function writeFileTitle($imagepath, $title) {
$barepath = stripExtension($imagepath);
$base = basename($barepath);
$titlepath = getTitleFileName($barepath);
ensureDirExists(dirname($titlepath));
$fd = fopen($titlepath, "w");
if ($fd) {
fwrite($fd, $title);
fclose($fd);
}
if (file_exists(C_titles . "/links/$base.txt")) {
unlink(C_titles . "/links/$base.txt");
}
if (C_useSymlink) {
ensureDirExists(C_titles . "/links");
$tRel = preg_replace("#^" . preg_quote(C_titles) . "#", "..",
$titlepath);
symlink($tRel, C_titles . "/links/$base.txt");
}
// clean up older variants
$baseX = basename($imagepath);
if (file_exists(C_titles . "/links/$baseX.txt")) {
unlink(C_titles . "/links/$baseX.txt");
}
if (file_exists(C_titles . "/old/$baseX.txt")) {
unlink(C_titles . "/old/$baseX.txt");
}
}
function fileTitle($imagepath) {
// Return string suitable for sub-title line for this file or dir
// Searches for title in:
// titles . relativepath.txt
// imagepath.txt
// titles/links/basename.txt
// titles/old/basename.txt
//
$barepath = stripExtension($imagepath);
$base = basename($barepath);
$title = readTitle(getTitleFileName($barepath));
if ($title === false) $title = readTitle("$barepath.txt");
if ($title === false) $title = readTitle(C_titles . "/links/$base.txt");
// allow older variants for backwards compatability
$baseX = basename($imagepath);
if ($title === false) $title = readTitle(getTitleFileName($imagepath));
if ($title === false) $title = readTitle("$imagepath.txt");
if ($title === false) $title = readTitle(C_titles . "/links/$baseX.txt");
if ($title === false) $title = readTitle(C_titles . "/old/$baseX.txt");
// Default to $base
if ($title === false) {
$title = ($imagepath == C_images ? "Photos" : $base);
}
return $title;
}
//
// File system subroutines, mostly searching
//
$dirCache = array(); // cache of directory enumerations
function getEntries($thisD) {
// Return an object with ->dirs being this directory's sub-directories,
// and ->images being the contained images. Non-image files and
// irrelevancies are excluded.
if (isset($dirCache[$thisD])) return $dirCache[$thisD];
$dirEnum = opendir($thisD);
$dirs = array();
$images = array();
if ($dirEnum) {
while ($entry = readdir($dirEnum)) {
if ($entry != "." &&
$entry != ".." &&
$entry != C_cache &&
$entry != C_titles &&
$entry != "index.html") {
if (preg_match('#\\.jpg$#i', $entry)) {
$images[] = $entry;
} else
if (is_dir("$thisD/$entry")) {
$dirs[] = $entry;
} else if (!preg_match(C_exclusions, $entry)) {
$images[] = $entry;
}
}
}
closedir($dirEnum);
natcasesort($dirs);
natcasesort($images);
}
unset($dirEnum);
$entries->dirs = $dirs;
$entries->images = $images;
$dirCache[$thisD] = $entries;
return $entries;
}
function cleanPath($filepath) {
// Fixup up use of ".", ".." and empty arcs in $filepath
// Initial "/" will work like initial empty relative arc
// Result is always a non-empty relative path not ending in "/"
$arcs = explode("/", $filepath);
for ($i = 0; $i < sizeof($arcs); $i++) {
if ($arcs[$i] == "..") {
for ($j = $i-1; $j >= 0; $j--) {
if ($arcs[$j] != "") {
$arcs[$j] = "";
break;
}
}
$arcs[$i] = "";
} else if ($arcs[$i] == ".") {
$arcs[$i] = "";
}
}
$destArcs = array();
$foundOne = false;
for ($i = 0; $i < sizeof($arcs); $i++) {
if ($arcs[$i] != "") {
$destArcs[] = $arcs[$i];
$foundOne = true;
}
}
if (!$foundOne) $destArcs[] = ".";
$dest = implode("/", $destArcs);
return $dest;
}
function findPath($basename, $thisD = C_images) {
// Return full pathname for file or directory $basename within $thisD
// Returns null if not found. Result path is already clean.
// Similar to exec("find . -name '$basename'");
$dirEnum = opendir($thisD);
if ($dirEnum) {
while ($entry = readdir($dirEnum)) {
$thispath = ($thisD == "." ? $entry : "$thisD/$entry");
if ($entry == $basename) return $thispath;
if (is_dir($thispath) && $entry != "." && $entry != ".." &&
$entry != C_cache && $entry != C_titles) {
$sub = findPath($basename, $thispath);
if (!is_null($sub)) return $sub;
}
}
closedir($dirEnum);
}
return null;
}
// findFirst, findLast, findPrev and findNext enumerate the entire set
// of images in the album. The enumeration order is the obvious tree-walk,
// depth-first: if a folder has sub-folders as well as directly contained
// images, the contents of the sub-folders come first.
function findFirst($thisD) {
// Return first image file's pathname within $dirpath, recursively
// Return null if no such image
$entries = getEntries($thisD);
foreach ($entries->dirs as $entry) {
$thispath = "$thisD/$entry";
$dirFirst = findFirst($thispath);
if (!is_null($dirFirst)) return $dirFirst;
}
foreach ($entries->images as $entry) {
return "$thisD/$entry";
}
return null;
}
function findLast($thisD) {
// Return last image file's pathname within $dirpath, recursively
// Return null if no such image
$entries = getEntries($thisD);
foreach (array_reverse($entries->images) as $entry) {
return "$thisD/$entry";
}
foreach (array_reverse($entries->dirs) as $entry) {
$thispath = "$thisD/$entry";
$dirLast = findLast($thispath);
if (!is_null($dirLast)) return $dirLast;
}
return null;
}
function findPrev($imagepath) {
// Return previous image file to given $imagepath (which might be a
// directory path),within C_images, or null if there's no such item.
// Assumes that $imagepath actually exists and is within C_images.
if ($imagepath == C_images) {
return null;
} else {
$thisD = dirname($imagepath);
$entries = getEntries($thisD);
$found = false;
foreach (array_reverse($entries->images) as $entry) {
$thispath = "$thisD/$entry";
if ($thispath == $imagepath) {
$found = true;
} else if ($found) {
return $thispath;
}
}
foreach (array_reverse($entries->dirs) as $entry) {
$thispath = "$thisD/$entry";
if ($thispath == $imagepath) {
$found = true;
} else if ($found) {
$dirChild = findLast($thispath);
if (!is_null($dirChild)) return $dirChild;
}
}
// Didn't find it: this is first entry in our directory
return findPrev($thisD);
}
}
function findNext($imagepath) {
// Return next image file to given $imagepath (which might be a
// directory path), within C_images, or null if there's no such item.
// Assumes that $imagepath actually exists and is within C_images.
if ($imagepath == C_images) {
return null;
} else {
$thisD = dirname($imagepath);
$entries = getEntries($thisD);
$found = false;
foreach ($entries->dirs as $entry) {
$thispath = "$thisD/$entry";
if ($thispath == $imagepath) {
$found = true;
} else if ($found) {
$dirChild = findFirst($thispath);
if (!is_null($dirChild)) return $dirChild;
}
}
foreach ($entries->images as $entry) {
$thispath = "$thisD/$entry";
if ($thispath == $imagepath) {
$found = true;
} else if ($found) {
return $thispath;
}
}
// Didn't find it: this is last entry in our directory
return findNext($thisD);
}
}
// findNextDir, findLastDir and findPrevDir enumerate the entire set
// of directories in the album. The enumeration order is the obvious
// tree-walk, with a directory being placed before its sub-directories.
function findNextDir($thisD, $descend) {
// Return next directory to $thisD: if it has a non-empty child
// and $descend, then the first such child; otherwise next
// sibling or ancestor; otherwise null
// Only directories that contain images are eligible.
// Assumes that $thisD actually exists and is within C_images.
if ($descend) {
$entries = getEntries($thisD);
foreach ($entries->dirs as $entry) {
$thispath = "$thisD/$entry";
if (!is_null(findFirst($thispath))) return $thispath;
}
}
if ($thisD == C_images) return null;
$parent = dirname($thisD);
$entries = getEntries($parent);
$found = false;
foreach ($entries->dirs as $entry) {
$thispath = "$parent/$entry";
if ($thispath == $thisD) {
$found = true;
} else if ($found) {
if (!is_null(findFirst($thispath))) return $thispath;
}
}
return findNextDir($parent, false);
}
function findLastDir($thisD) {
// Return last non-empty child of $thisD, recursively; otherwise
// if $thisD is non-empty, return $thisD; otherwise return null
$entries = getEntries($thisD);
$nonEmpty = false;
foreach ($entries->images as $entry) {
$nonEmpty = true;
break;
}
foreach (array_reverse($entries->dirs) as $entry) {
$thispath = "$thisD/$entry";
$lastDir = findLastDir($thispath);
if (!is_null($lastDir)) return $lastDir;
$nonEmpty = true;
}
return ($nonEmpty ? $thisD : null);
}
function findPrevDir($thisD) {
// Return previous directory to $thisD, including children of
// earlier siblings or ancestors; otherwise return null
// Only directories that contain images are eligible.
// Assumes that $thisD actually exists and is within C_images.
if ($thisD == C_images) return null;
$parent = dirname($thisD);
$entries = getEntries($parent);
$found = false;
foreach (array_reverse($entries->dirs) as $entry) {
$thispath = "$parent/$entry";
if ($thispath == $thisD) {
$found = true;
} else if ($found) {
$lastDir = findLastDir($thispath);
if (!is_null($lastDir)) return $lastDir;
}
}
return $parent;
}
//
// XML subroutines
//
$xmlDataPatterns = array('#&#', '#<#', '#>#', '#"#');
$xmlDataEscapes = array('&', '<', '>', '"');
function xmlSpecials($str) {
// Return string minimally escaped to live inside XML char data.
// Note that Safari doesn't handle "&" inside attribute values,
// so calling "htmlspecials" instead doesn't work.
global $xmlDataPatterns, $xmlDataEscapes;
return preg_replace($xmlDataPatterns, $xmlDataEscapes, $str);
}
function putTitleXML($imagepath) {
// Put title tag for given image or folder
echo " <title>" . xmlSpecials(fileTitle($imagepath)) . "</title>\n";
}
function putParentXML($imagepath) {
// Put the "parent" tags for the given image or folder (recursively),
// each with its title and path attributes
if ($imagepath != C_images) {
$parent = dirname($imagepath);
putParentXML($parent);
echo " <parent path=\"" . urlPath($parent) . "\">\n";
putTitleXML($parent);
echo " </parent>\n";
}
}
function putImageXML($imagepath, $size) {
// Put XML attributes width, height, src for the image at given size
$cacheFile = cacheScaledImage($imagepath, $size); // for its size
$sizes = imageSize($cacheFile);
?>
width="<?php echo ($sizes ? $sizes[0] : 16) ?>"
height="<?php echo ($sizes ? $sizes[1] : 16) ?>"
src="<?php echo urlForCachedImage($imagepath, $size) ?>"
<?php
}
function putCommentsXML($imagepath) {
// Put comment attributes for the given image, for the "photo" tag
$sizes = imageSize($imagepath);
$fsize = (file_exists($imagepath) ? filesize($imagepath) : 0);
if ($fsize > 1048575) {
$fsize = sprintf("%0.1f MBytes", $fsize/1048576);
} else if ($fsize > 1023) {
$fsize = sprintf("%0.1f KBytes", $fsize/1024);
} else {
$fsize = "$fsize Bytes";
}
if ($sizes && $sizes[2] == "JPEG") {
$exif = @exif_read_data($imagepath, null, true);
if (isset($exif["EXIF"])) {
$exifExif = $exif["EXIF"];
if (isset($exifExif["DateTimeOriginal"])) {
$matches = array();
if (preg_match('#^(\\d\\d\\d\\d):(\\d\\d):(\\d\\d) ' .
'(\\d\\d):(\\d\\d):(\\d\\d)#',
$exifExif["DateTimeOriginal"], $matches)) {
$date = gmmktime($matches[4], $matches[5], $matches[6],
$matches[2], $matches[3], $matches[1]);
if ($date !== false && $date != -1) {
// $date = gmdate("D, j M Y, H:i:s", $date);
$date = gmdate("D, j M Y, H:i", $date);
echo " date=\"". xmlSpecials($date) . "\"\n";
}
}
}
$details = array();
if (isset($exifExif["ExposureTime"])) {
$details[] = $exifExif["ExposureTime"] . " sec";
}
if (isset($exifExif["FNumber"])) {
$numDenom = explode("/", $exifExif["FNumber"]);
if (isset($numDenom[1])) $details[] = "f/" .
round($numDenom[0] / $numDenom[1], 1);
}
if (isset($exifExif["ISOSpeedRatings"])) {
$details[] = "ISO " . $exifExif["ISOSpeedRatings"];
}
if (isset($exifExif["FocalLength"])) {
$numDenom = explode("/", $exifExif["FocalLength"]);
if (isset($numDenom[1])) $details[] = "" .
round($numDenom[0] / $numDenom[1], 1) . "mm";
}
$exp = implode(", ", $details);
echo " exposure=\"" . xmlSpecials($exp) . "\"\n";
}
if (false) {
foreach ($exif as $key => $section) {
foreach ($section as $name => $val) {
echo " exif.$key.$name=\"" . xmlSpecials($val) .
"\"\n";
}
}
}
}
echo " size=\"" .
($sizes ? "$sizes[0] x $sizes[1] pixels, " : "") .
"$fsize\"\n";
}
?>
End of listing