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('&#38;', '&#60;', '&#62;', '&#34;');

function xmlSpecials($str) {
    // Return string minimally escaped to live inside XML char data.
    // Note that Safari doesn't handle "&amp;" 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