Mercurial > hg > mgallery
view mgtool.php @ 109:c8cfc6cc161a
Adjust image scaling to be delayed and not being done on each resize event
(for example Firefox fullscreen switching animates by default and triggers
LOTS of resize events, which makes things slow.)
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Sun, 30 Oct 2016 15:22:03 +0200 |
parents | 417cdd9f8864 |
children | 9da8bab49711 |
line wrap: on
line source
#!/usr/bin/php <?php // // Yet Another Image Gallery // -- Commandline tool for creating / updating gallery // Programmed and designed by Matti 'ccr' Hamalainen <ccr@tnsp.org> // (C) Copyright 2015-2016 Tecnic Software productions (TNSP) // require_once "mgallery.inc.php"; // // Array for specifying what will be copied and converted // from the image file's EXIF information tag(s). // $galExifConversions = [ [ MG_STR, "caption" , "ImageDescription" ], [ MG_STR, "copyright" , "Copyright" ], [ MG_STR, "model" , "Model" ], [ MG_INT, "width" , ["COMPUTED", "Width"] ], [ MG_INT, "height" , ["COMPUTED", "Height"] ], [ MG_DVA, "fnumber" , "FNumber" ], [ MG_DVA, "exposure" , "ExposureTime" ], [ MG_INT, "iso" , "ISOSpeedRatings" ], [ MG_STR, "lensmodel" , "UndefinedTag:0xA434" ], [ MG_DVA, "focallength" , "FocalLength" ], [ MG_STR, "datetime" , "DateTimeOriginal" ], [ MG_STR, "datetime" , "DateTimeDigitized" ], [ MG_INT, "filesize" , "FileSize" ], ]; define("GCMD_UPDATE" , 1); define("GCMD_RESCAN" , 2); define("GCMD_CLEAN" , 3); define("GCLEAN_CACHES" , 0x01); define("GCLEAN_THUMBNAILS", 0x02); define("GCLEAN_ALL" , 0x0f); define("GUPD_MED_IMAGE" , 0x01); define("GUPD_TN_IMAGE" , 0x02); define("GUPD_IMAGES" , 0x0f); define("GUPD_EXIF_INFO" , 0x10); define("GUPD_CAPTION" , 0x20); // // Convert and scale image file function, for generating // the intermediate size images and thumbnails. Uses the // PHP ImageMagick bindings. // function mgConvertImage($inFilename, $outFilename, $outDim, $outFormat, $outQuality, $thumb) { // Create conversion entity try { $img = new Imagick($inFilename); } catch (Exception $e) { return mgError("ImageMagick exception for file '".$inFilename."':\n".$e->getMessage()."\n"); } if ($img === FALSE) return mgError("ImageMagick could not digest the file '".$inFilename."'.\n"); $profiles = $img->getImageProfiles("icc", true); $img->setImageDepth(16); $img->transformImageColorspace(Imagick::COLORSPACE_SRGB); $img->setImageColorspace(Imagick::COLORSPACE_SRGB); if ($outDim !== FALSE) { // Get dimensions, setup background $dim = $img->getImageGeometry(); //$img->setImageBackgroundColor(imagick::COLOR_BLACK); $img->setGravity(imagick::GRAVITY_CENTER); if ($dim["width"] < $dim["height"]) { $stmp = $outDim[0]; $outDim[0] = $outDim[1]; $outDim[1] = $stmp; } if ($dim["width"] != $outDim[0] || $dim["height"] != $outDim[1]) { $outDim[1] = ($dim["height"] * $outDim[0]) / $dim["width"]; } // Act based on image size vs. desired size and $thumb mode if ($thumb || $dim["width"] > $outDim[0] || $dim["height"] > $outDim[1]) { // Image is larger $img->resizeImage($outDim[0], $outDim[1], Imagick::FILTER_LANCZOS, 1); $img->setImageExtent($outDim[0], $outDim[1]); // $img->normalizeImage(); $img->unsharpMaskImage(0, 0.5, 1, 0.05); } if ($dim["width"] < $outDim[0] || $dim["height"] < $outDim[1]) { // Image is smaller than requested dimension(s)? $img->resizeImage($outDim[0], $outDim[1], Imagick::FILTER_LANCZOS, 1); $img->setImageExtent($outDim[0], $outDim[1]); // $img->normalizeImage(); $img->unsharpMaskImage(0, 0.5, 1, 0.05); } } $img->setImageDepth(8); $img->setFormat($outFormat); $img->setImageCompression(Imagick::COMPRESSION_JPEG); $img->setImageCompressionQuality($outQuality); $img->stripImage(); if (!empty($profiles)) $img->profileImage("icc", $profiles["icc"]); $img->writeImage($outFilename); $img->removeImage(); return TRUE; } // // Converts one value (mainly from EXIF tag information) // by doing explicit type casting and special conversions. // function mgConvertExifData($val, $vtype) { switch ($vtype) { case MG_STR: return is_array($val) ? $val : (string) $val; case MG_INT: return intval($val); case MG_BOOL: return intval($val); case MG_DVA: if (sscanf($val, "%d/%d", $v1, $v2) == 2) { if ($v1 < $v2) return $val; else return sprintf("%1.1f", $v1 / $v2); } else return $val; default: return $val; } } // // Conditionally copies one "field" from an associated array/hash to another. // If destination is already SET, nothing will be done. If source does // not exist (e.g. one or more of the keys do not exist in source array), // a default value will be used, if provided. // Source may have multi-depth keys, destination has one key. // function mgCopyEntryData(&$dst, $src, $vtype, $dkey, $skeys, $default = NULL) { // Is destination already set? if (isset($dst[$dkey])) return FALSE; // If input key is not array, change it into one if (!is_array($skeys)) $skeys = array($skeys); // Traverse input array by using consequent keys $tmp = &$src; foreach ($skeys as $skey) { if (!array_key_exists($skey, $tmp)) { // Key didn't exist, try for default if ($default !== NULL) $dst[$dkey] = $default; return FALSE; } else $tmp = &$tmp[$skey]; } // Optionally convert the input value $dst[$dkey] = mgConvertExifData($tmp, $vtype); return TRUE; } // // Attempt to get gallery album data from various sources. // function mgGetAlbumData($galBasePath, $galPath) { // Check path permissions $galData = []; if (is_readable($galPath)) { // First, try to read gallery/album info file $filename = mgGetPath($galPath, "info_file"); if ($filename !== FALSE && file_exists($filename)) { mgDebug("Reading INFOFILE: ".$filename."\n"); if (($galData = parse_ini_file($filename, FALSE)) === FALSE) $galData = []; } // Read header file, if any, and we don't have "header" field set yet $filename = mgGetPath($galPath, "header_file"); if ($filename !== FALSE && file_exists($filename) && !isset($galData["header"])) { mgDebug("Reading HEADERFILE: ".$filename."\n"); $galData["header"] = file_get_contents($filename); } // Check for alternate key/values for album title/caption if (isset($galData["title"]) && !isset($galData["caption"])) { $galData["caption"] = $galData["title"]; unset($galData["title"]); } } else $galData["hide"] = TRUE; // If caption is not set, use last path component for // a fallback value in case we don't discover proper title // from other sources we can't check here yet. $path = explode("/", $galPath); $galData["fallback_caption"] = ucfirst(str_replace("_", " ", end($path))); // Last, store the current gallery path $len = strlen($galBasePath); if ($len < strlen($galPath) && substr($galPath, 0, $len) == $galBasePath) $galData["path"] = substr($galPath, $len); else $galData["path"] = ""; return $galData; } function mgReadCaptionsFile($galBasePath, $galPath) { $captions = []; $filename = mgGetPath($galPath, "captions_file"); if ($filename === FALSE || ($fp = @fopen($filename, "rb")) === FALSE) return $captions; mgDebug("Reading CAPTIONS: ".$filename."\n"); // Read and parse data while (!feof($fp)) { $str = trim(fgets($fp)); // Ignore comments and empty lines if ($str != "#" && $str != "") { if (preg_match("/^([#%]?)\s*(\S+?)\s+(.+)$/", $str, $m)) $captions[$m[2]] = ["caption" => $m[3], "hide" => ($m[1] == "#"), "hide_contents" => ($m[1] == "%"), "used" => FALSE]; else if (preg_match("/^([#%]?)\s*(\S+?)$/", $str, $m)) $captions[$m[2]] = ["hide" => ($m[1] == "#"), "hide_contents" => ($m[1] == "%"), "used" => FALSE]; } } fclose($fp); return $captions; } function mgMakeDir($path, $perm) { if (!file_exists($path)) { if (mkdir($path, $perm, TRUE) === false) return mgError("Could not create directory '".$path."'\n"); } return TRUE; } function mgYesNoPrompt($msg, $default = FALSE) { echo $msg." [".($default ? "Y/n" : "y/N")."]? "; $sprompt = strtolower(trim(fgets(STDIN))); if ($default) return ($sprompt == "n"); else return ($sprompt == "y"); } function mgDelete($path, $recurse) { global $flagDoDelete, $countDoDelete; if (is_dir($path)) { if (($dirHandle = @opendir($path)) === FALSE) return mgError("Could not read directory '".$path."'.\n"); while (($dirFile = @readdir($dirHandle)) !== FALSE) { if ($dirFile != "." && $dirFile != "..") mgDelete($path."/".$dirFile, $recurse); } closedir($dirHandle); echo " - ".$path." [DIR]\n"; $countDoDelete++; if ($flagDoDelete) rmdir($path); } else if (file_exists($path)) { if (!$recurse) echo " - ".$path."\n"; $countDoDelete++; if ($flagDoDelete) unlink($path); } } function mgCheckQuit($now = FALSE) { global $flagQuit; // Dispatch pending signals pcntl_signal_dispatch(); // Check result if ($now && $flagQuit) mgFatal("Quitting.\n"); return $flagQuit; } function mgNeedUpdate($entry, $field, $cvalue) { if (!array_key_exists($field, $entry)) return TRUE; return ($entry[$field] < $cvalue); } function mgSortFunc($a, $b) { if (isset($a["datetime"]) && isset($b["datetime"])) return strcmp($b["datetime"], $a["datetime"]); else if (isset($a["base"]) && isset($b["base"])) return strcmp($b["base"], $a["base"]); else return 0; } function mgWriteGalleryCache($cacheFilename, &$gallery, &$entries, &$parentEntry) { // Store gallery cache for this directory $images = []; $albums = []; $output = []; // If we are hiding contents of an album, generate no data if (!$parentEntry["hide_contents"]) { foreach ($entries as $ename => &$edata) { if ($edata["hide"]) continue; unset($edata["hide"]); if ($edata["type"] == 0) $images[$ename] = &$edata; else $albums[$ename] = &$edata; $output[$ename] = &$edata; } uasort($images, "mgSortFunc"); krsort($albums); // Choose gallery album image if (count($images) > 0) { if (isset($gallery["albumpic"]) && isset($images[$gallery["albumpic"]])) { $parentEntry["image"] = $gallery["albumpic"]; } else { end($images); $parentEntry["image"] = key($images); } } else { foreach ($albums as $aid => &$adata) if (isset($adata["image"])) { $parentEntry["image"] = &$adata; break; } } } $str = "<?\n". "\$galData = ".var_export($gallery, TRUE).";\n". "\$galAlbumsIndex = ".var_export(array_keys($albums), TRUE).";\n". "\$galImagesIndex = ".var_export(array_keys($images), TRUE).";\n". "\$galEntries = ".var_export($output, TRUE).";\n". "?>"; if (@file_put_contents($cacheFilename, $str, LOCK_EX) === FALSE) return mgError("Error writing '".$cacheFilename."'\n"); return TRUE; } function mgHandleDirectory($mode, $basepath, $path, &$parentData, &$parentEntry, $writeMode, $startAt) { global $galExifConversions, $galTNPath, $galCleanFlags; // Get cache file path if (($cacheFilename = mgGetPath($path, "cache_file")) === FALSE) return mgError("Cache filename / path not set.\n"); mgCheckQuit(TRUE); // Read directory contents $entries = []; if (($dirHandle = @opendir($path)) === FALSE) return mgError("Could not read directory '".$path."'.\n"); while (($dirFile = @readdir($dirHandle)) !== FALSE) { $realFile = $path."/".$dirFile; if (is_dir($realFile)) { if ($dirFile[0] != "." && $dirFile != $galTNPath) $entries[$dirFile] = ["type" => 1, "base" => $dirFile, "ext" => "", "mtime" => filemtime($realFile)]; } else if (preg_match("/^([^\/]+)(".mgGetSetting("format_exts").")$/i", $dirFile, $dirMatch)) $entries[$dirFile] = ["type" => 0, "base" => $dirMatch[1], "ext" => $dirMatch[2], "mtime" => filemtime($realFile), "hide" => false]; } closedir($dirHandle); mgCheckQuit(); // Cleanup mode if ($mode == GCMD_CLEAN) { $gallery = []; if ($writeMode) { if ($galCleanFlags & GCLEAN_CACHES) mgDelete($cacheFilename, FALSE); if ($galCleanFlags & GCLEAN_THUMBNAILS) mgDelete($path."/".$galTNPath, TRUE); } } else // Update modes if ($mode == GCMD_UPDATE || $mode == GCMD_RESCAN) { // Load current cache file, if it exists $galEntries = []; $cacheTime = -1; if ($mode == GCMD_UPDATE && file_exists($cacheFilename)) { $cacheTime = filemtime($cacheFilename); @include $cacheFilename; } // Read caption data $captions = mgReadCaptionsFile($basepath, $path); $gallery = mgGetAlbumData($basepath, $path); if ($parentData !== NULL && $parentEntry !== NULL) { $gallery["parent"] = &$parentData; mgCopyEntryData($gallery, $parentEntry, MG_STR, "caption", "caption"); } // Start actual processing $nentries = count($entries); $nentry = 0; echo $path." .. "; foreach ($entries as $ename => &$edata) { printf("\r%s (%1.1f%%) ..", $path, ($nentry * 100.0) / $nentries); $nentry++; $efilename = $path."/".$ename; if (array_key_exists($ename, $galEntries)) $galEntry = &$galEntries[$ename]; else $galEntry = []; mgCheckQuit(TRUE); // Update with captions file data, if any if (array_key_exists($ename, $captions)) { foreach ($captions[$ename] as $ckey => $cval) $edata[$ckey] = $cval; } // Handle entry based on type if ($edata["type"] == 0) { $updFlags = 0; $tnPath = $path."/".$galTNPath; $medFilename = $tnPath."/".$edata["base"].mgGetSetting("med_suffix").$edata["ext"]; $tnFilename = $tnPath."/".$ename; $capFilename = $path."/".$edata["base"].".txt"; // Check what we need to update .. if (!file_exists($medFilename) || filemtime($medFilename) < $edata["mtime"]) $updFlags |= GUPD_MED_IMAGE; if (!file_exists($tnFilename) || filemtime($tnFilename) < $edata["mtime"]) $updFlags |= GUPD_TN_IMAGE; if (mgNeedUpdate($galEntry, "mtime", $edata["mtime"])) $updFlags |= GUPD_EXIF_INFO; if (file_exists($capFilename) && mgNeedUpdate($galEntry, "mtime", filemtime($capFilename))) $updFlags |= GUPD_CAPTION; // Check for EXIF info if (($updFlags & GUPD_EXIF_INFO) && ($exif = @exif_read_data($efilename)) !== FALSE) { echo "%"; foreach ($galExifConversions as $conv) mgCopyEntryData($edata, $exif, $conv[0], $conv[1], $conv[2]); } else { // Copy old data that is not yet in new echo "*"; foreach ($galEntry as $okey => $odata) { if (!array_key_exists($okey, $edata)) $edata[$okey] = $odata; } } // Generate thumbnails, etc. if ($updFlags & GUPD_IMAGES) { mgMakeDir($tnPath, 0755); if ($updFlags & GUPD_MED_IMAGE) { echo "1"; mgConvertImage($efilename, $medFilename, [mgGetSetting("med_width"), mgGetSetting("med_height")], "JPEG", mgGetSetting("med_quality"), TRUE); } if ($updFlags & GUPD_TN_IMAGE) { echo "2"; mgConvertImage($efilename, $tnFilename, [mgGetSetting("tn_width"), mgGetSetting("tn_height")], "JPEG", mgGetSetting("tn_quality"), TRUE); } } // Check for .txt caption file if ($updFlags & GUPD_CAPTION) { echo "?"; if (($tmpData = @file_get_contents($capFilename)) !== FALSE) $edata["caption"] = $tmpData; } } else if ($edata["type"] == 1) { // Set some of the album data here $tmp = mgGetAlbumData($basepath, $efilename); mgCopyEntryData($edata, $tmp, MG_STR, "caption", "caption"); mgCopyEntryData($edata, $tmp, MG_STR, "caption", "fallback_caption"); mgCopyEntryData($edata, $tmp, MG_STR, "caption", "title"); mgCopyEntryData($edata, $tmp, MG_BOOL, "hide", "hide", FALSE); mgCopyEntryData($edata, $tmp, MG_BOOL, "hide_contents", "hide_contents", FALSE); } } echo "\r".$path." ..... DONE\n"; mgCheckQuit(TRUE); } else mgFatal("Invalid work mode '".$mode."'.\n"); mgCheckQuit(TRUE); // Recurse to subdirectories foreach ($entries as $ename => &$edata) if ($edata["type"] == 1) { $epath = $path."/".$ename."/"; $newWriteMode = ($writeMode === FALSE && $epath == $startAt) || $writeMode; if (!mgHandleDirectory($mode, $basepath, $epath, $gallery, $edata, $newWriteMode, $startAt)) return FALSE; } // Finish update modes if ($mode == GCMD_UPDATE || $mode == GCMD_RESCAN) { // Store gallery cache for this directory if ($writeMode && !mgWriteGalleryCache($cacheFilename, $gallery, $entries, $parentEntry)) return FALSE; } mgCheckQuit(TRUE); return TRUE; } function mgSigHandler($signo) { global $flagQuit; switch ($signo) { case SIGTERM: mgFatal("Received SIGTERM.\n"); break; case SIGQUIT: case SIGINT: $flagQuit = TRUE; break; } } function mgProcessGalleries($cmd, $path) { global $galTNPath; // Fetch the settings we need if (mgReadSettings() === FALSE) die("MGallery not configured.\n"); // Check validity of some settings $galPath = mgGetSetting("base_path"); $galTNPath = mgCleanPath(TRUE, mgGetSetting("tn_path")); if ($galTNPath != mgGetSetting("tn_path")) mgError("Invalid tn_path, using '".$galTNPath."'.\n"); if (strpos($galTNPath, "/") !== FALSE || $galTNPath == "") mgFatal("Invalid tn_path '".$galTNPath."'.\n"); $parentData = $parentEntry = NULL; $writeMode = TRUE; $startAt = NULL; // Check for path argument if ($path !== FALSE) { // Check the given path, needs to be "under" the gallery path $cmp = mgCleanPath(TRUE, mgRealPath($galPath))."/"; $tmp = mgCleanPath(TRUE, mgRealPath($path))."/"; if (substr($tmp, 0, strlen($cmp)) != $cmp) mgFatal("Path '".$path."' ('".$tmp."') does not reside under '".$galPath."' ('".$cmp."')!\n"); // Check if we need to bootstrap if ($cmp != $tmp) { $bpath = mgCleanPath(TRUE, mgRealPath($tmp."../")); if ($cmd != GCMD_CLEAN) { $cacheFile = mgGetPath($bpath, "cache_file"); if (!file_exists($cacheFile)) mgFatal("Can't start working from '".$path."', parent '".$cacheFile."' does not exist!\n"); @include($cacheFile); if (!isset($galEntries) || !isset($galData)) mgFatal("Cache file '".$cacheFile."' is broken or stale.\n"); } $writeMode = FALSE; $startAt = $tmp; $path = $bpath; echo "Starting: '".$startAt."' inside '".$path."'.\n"; } else $path = $tmp; } else $path = $galPath; // Start working echo "Gallery path: '".$galPath."', starting at '".$path."' ...\n"; mgHandleDirectory($cmd, $galPath, $path, $parentData, $parentEntry, $writeMode, $startAt); } function mgShowCopyright() { global $mgProgVersion, $mgProgCopyright, $mgProgInfo, $mgProgEmail; echo "MGTool ".$mgProgVersion." - MGallery management tool\n". $mgProgInfo." ".$mgProgEmail."\n". "(C) Copyright ".$mgProgCopyright."\n"; } function mgShowHelp() { global $argv; echo "Usage: ".basename($argv[0])." <command> [arguments]\n". "\n". " --help - Show this help.\n". " --version - Show version information.\n". "\n". " update [path]\n". " Update directories under <path> or all gallery dirs.\n". " Conditionally scans dirs for new or changed images and\n". " updates cache files as needed.\n". "\n". " rescan [path]\n". " Like 'update', but forces all cache files to be regenerated\n". " and EXIF/caption/etc information to be re-scanned even\n". " if the file timestamps do not justify re-scanning.\n". "\n". " clean <all|caches|thumbnails> [path]\n". " Delete all generated files or selectively cache files or\n". " everything inside thumbnail directories.\n". "\n". " config\n". " Display configuration values.\n". "\n"; } // // Main code starts // if (php_sapi_name() != "cli" || !empty($_SERVER["REMOTE_ADDR"])) { header("Status: 404 Not Found"); die(); } pcntl_signal(SIGTERM, "mgSigHandler"); pcntl_signal(SIGHUP, "mgSigHandler"); pcntl_signal(SIGQUIT, "mgSigHandler"); pcntl_signal(SIGINT, "mgSigHandler"); $cmd = mgCArgLC(1); switch ($cmd) { case "--version": case "version": case "ver": mgShowCopyright(); break; case FALSE: // No arguments mgError("Nothing to do. Showing help:\n"); case "--help": case "help": if ($cmd !== FALSE) mgShowCopyright(); mgShowHelp(); break; case "update": case "up": case "upd": case "upda": mgProcessGalleries(GCMD_UPDATE, mgCArg(2)); break; case "rescan": case "re": case "res": mgProcessGalleries(GCMD_RESCAN, mgCArg(2)); break; case "clean": case "cl": case "cle": $cmode = mgCArgLC(2, 2); switch ($cmode) { case "al": $galCleanFlags = GCLEAN_ALL; break; case "ca": $galCleanFlags = GCLEAN_CACHES; break; case "th": $galCleanFlags = GCLEAN_THUMBNAILS; break; case FALSE: mgFatal("Cleaning requires a mode argument.\n"); default: mgFatal("Invalid clean mode '".mgCArg(2)."'.\n"); } $countDoDelete = 0; $flagDoDelete = FALSE; mgProcessGalleries(GCMD_CLEAN, mgCArg(3)); echo "--\n"; if ($countDoDelete == 0) { echo "Nothing to clean!\n"; } else if (mgYesNoPrompt("Really delete the above files and directories?")) { echo "Deleting ...\n"; $flagDoDelete = TRUE; mgProcessGalleries(GCMD_CLEAN, mgCArg(3)); } else { echo "Okay, canceling operation.\n"; } break; case "config": case "dump": if (mgReadSettings() === FALSE) die("MGallery not configured.\n"); foreach ($mgDefaults as $key => $dval) { $sval = mgGetSetting($key); if ($cmd == "dump") { printf("%-20s = %s\n", $key, mgGetDValStr($dval[0], $sval)); } else { printf("%-20s = %s%s\n", $key, mgGetDValStr($dval[0], $sval), ($dval[1] !== NULL && $sval !== $dval[1]) ? " (default: ".mgGetDValStr($dval[0], $dval[1]).")" : ""); } } break; default: mgError("Unknown option/command '".$cmd."'.\n"); break; } ?>