diff --git a/leaf.php b/leaf.php index 96052de..6cd1ec4 100644 --- a/leaf.php +++ b/leaf.php @@ -2,16 +2,25 @@ 2) { return chop($argv[count($argv)-1], "/")."/"; } elseif (isset($opt[$query])) { return $opt[$query]; @@ -89,29 +98,263 @@ if (!is_dir("scratch")) { //////////////////////////////////////////////////////////////////////////////////////////////// // Help ////////////////////// -if (!args("app")) { +if (!args("app") | args("app") == "help" | args("app") == "-help" | args("app") == "--help") { ///////////////////////////////////////////////////////////////////////// $help = "Leaf $version USAGE: leaf [mode] [-options] directory Modes: -review print a table of image statistics -dpiset set image dpi +desort remove image sequence prefix +divide wrapper for imagemagick Divide_Src + -map= specify brightness file + -adjust= levels adjustment (ex. \"0%,98%,.9\") + -q= quality out of 100 +dupes Find duplicate images using computed PHASH on thumbnails + -threshold= match threshold + -walk= comparison scope (compare n image to n, n+1, n+2, etc) +makepdf combine images into a pdf with img2pdf +profile apply xmp profile to images (requires exiv2 > 0.25) + -file= xmp profile +resort reorder image sequence by adding a new image + -file= file to insert + -x= position of inserted file +review print a table of image dimension statistics +setdpi set image dpi with exiftool -x= specify dpi -height= calculate dpi from specified height -makepdf combine images into a pdf -stripcrop strip exif crop values from images - +sort sort files as AAABBB -> ABABAB + -m= specify midpoint (cover image) +strip strip exif crop values from images with exiftool + "; echo $help; fin(); //////////////////////////////////////////////////////////////////////////////////////////////// -// Review: output a table of stats on source images +// Profile +////////////////////// +} elseif (args("app") == "profile") { +echo Welcome("Apply XMP profile to images"); +///////////////////////////////////////////////////////////////////////// + +$files = glob(args("dir")."*.*"); + +if (!file_exists(args("file"))) { + msg("Error reading xmp profile",1); + } else { + $profile = args("file"); + } + +echo $profile.": ".date("F d, Y", filemtime($profile))." (".count(file($profile))." lines)\n\n"; +$lines = file_get_contents($profile); +$check[] = "crs:RawFileName"; +$check[] = "crs:WhiteBalance"; +$check[] = "crs:Sharpness"; +$check[] = "crs:LensProfileName"; +$check[] = "crs:IncrementalTemperature"; +$check[] = "crs:IncrementalTint"; +foreach ($check as $query) { + preg_match("/^.*".$query.".*\$/m",$lines,$matches); + if ($matches) { + echo trim($matches[0])."\n"; + } + } +echo "\n"; + +msg("This operation will overwrite all existing metadata for target files. Continue?",2); + +foreach ($files as $file) { + msg("Applying ".$profile." to ".$file); + @exec("cat ".$profile." | exiv2 -iXX- ".$file); + } + +fin(); + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Sort +////////////////////// +} elseif (args("app") == "sort") { +echo Welcome("Sort files AAABBB -> ABABAB"); +///////////////////////////////////////////////////////////////////////// + +$files = glob(args("dir")."*.*"); + +if (args("m")) { + $check = glob(args("dir")."*".args("m")."*"); + if (!$check) { + msg("Cannot find midpoint file to match ".args("m"),1); + } + $midpoint_key = args("m"); + } else { + $mid = $files[ceil(count($files)/2)]; + echo "Guessing midpoint key from ".$mid."\n"; + $midpoint_key = filter_var($mid, FILTER_SANITIZE_NUMBER_INT); + } + + + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Desort +////////////////////// +} elseif (args("app") == "desort") { +echo Welcome("Remove image sequence"); +///////////////////////////////////////////////////////////////////////// + +$files = glob(args("dir")."[0-9][0-9][0-9]-*"); + +foreach ($files as $file) { + $ops[] = array($file, args("dir").substr(basename($file),4)); + } + +foreach ($ops as $parts) { + if (file_exists($parts[1])) { + msg("Meltdown! Renamed file would overwrite ".$parts[1],1); + } + } + +foreach ($ops as $parts) { + echo "Renaming ".$parts[0]." to ".$parts[1]."\n"; + rename($parts[0], $parts[1]); + } + +fin(); + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Dupes +////////////////////// +function dupes() {} +} elseif (args("app") == "dupes") { +echo Welcome("Find duplicate images"); +///////////////////////////////////////////////////////////////////////// + +if (args("threshold")) { + $threshold = args("threshold"); + } else { + $threshold = 10; + } + +if (args("walk") > 1) { + $walk = args("walk"); + } elseif (args("walk")) { + msg("Walk must be 2 or greater",1); + } else { + $walk = 5; + } + +$files = glob(args("dir")."*.*"); + +echo "Checking thumbnails: "; + +foreach ($files as $file) { + $tnfile = "scratch/".basename($file,".".pathinfo($file,PATHINFO_EXTENSION))."_thumb.".pathinfo($file, PATHINFO_EXTENSION); + if (!file_exists($tnfile)) { + echo "."; + exec("vipsthumbnail ".$file." --size 300x300 -o ../".$tnfile." 2>&1"); + } else { + echo "o"; + } + $tnfiles[] = $tnfile; + } + +echo "\n\n"; +echo "Comparing phash values: "; + +foreach ($tnfiles as $file) { + foreach ($tnfiles as $nfile) { + $anum = filter_var($file, FILTER_SANITIZE_NUMBER_INT); + $bnum = filter_var($nfile, FILTER_SANITIZE_NUMBER_INT); + $diff = abs($anum-$bnum); + $done[$file][$nfile] = 1; + if ($file != $nfile && $diff < $walk && isset($done[$nfile][$file])) { + echo "."; + $distance = shell_exec("compare -metric phash ".$file." ".$nfile." diffimage 2>&1")."\n"; + if ($distance < $threshold) { + $match[] = array($file, $nfile, $distance); + } + } + } + } + +echo "\n\n"; + +foreach ($match as $pair) { + echo $pair[0]." <> ".$pair[1]." = ".$pair[2]; + } + +echo "\n"; + +msg("Review duplicates?",2); + +foreach ($match as $pair) { + $afile = args("dir").basename(str_replace("_thumb", "", $pair[1])); + $bfile = args("dir").basename(str_replace("_thumb", "", $pair[0])); + ask("Press return to compare ".$afile." to ".$bfile); + //@exec("montage -label '%f' -font Helvetica -pointsize 20 -background '#000000' -fill 'white' -define jpeg:size=600x600 -geometry 600x600+2+2 -auto-orient ".$afile." ".$bfile." /tmp/contact_sheet.jpg"); + //@exec("open /tmp/contact_sheet.jpg -b com.apple.Preview"); + @exec("open ".$afile." ".$bfile." -b com.apple.Preview"); + } + +fin(); + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Resort +////////////////////// +} elseif (args("app") == "resort") { +echo Welcome("Insert image into numbered sequence"); +///////////////////////////////////////////////////////////////////////// + +$files = glob(args("dir")."[0-9][0-9][0-9]-*.jpg"); +$count = count($files); + +if (!$count) { + msg("Could not find any numbered files in source directory",1); + } +if (!args("x")) { + msg("Please specify a position for inserted file",1); + } else { + $x = sprintf('%03d',args("x")); + } +if (!file_exists(args("file"))) { + msg("Error reading target file",1); + } else { + $target = args("file"); + } + +foreach ($files as $file) { + $seq[substr(basename($file),0,3)] = $file; + } +if (!isset($seq[$x])) { + msg("Specified position does not exist: ".$x,1); + } + +foreach ($seq as $num => $file) { + if ($num == $x) { + $ops[] = array($target, args("dir").$x."-".basename($target)); + } + if ($num >= $x) { + $ops[] = array($file, args("dir").sprintf('%03d',$num+1).substr(basename($file),3)); + } + } + +foreach ($ops as $parts) { + if (file_exists($parts[1])) { + msg("Meltdown! Renamed file would overwrite ".$parts[1],1); + } + } + +foreach ($ops as $parts) { + echo "Renaming ".$parts[0]." to ".$parts[1]."\n"; + rename($parts[0], $parts[1]); + } + +fin("Starting file count: ".$count."; Ending file count: ".count(glob(args("dir")."[0-9][0-9][0-9]-*.jpg"))); + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Review ////////////////////// } elseif (args("app") == "review") { -echo Welcome("List image statistics"); +echo Welcome("List image dimension statistics"); ///////////////////////////////////////////////////////////////////////// $files = glob(args("dir")."*.*"); @@ -146,8 +389,12 @@ foreach ($files as $file) { } $print[] = bashcolor($file, $hilite[pathinfo($file, PATHINFO_EXTENSION)]); $size = $attr['File Size']; + if ($ext != "DNG") { $quality = @exec("identify -format '%Q' ".$file); } + if (isset($quality)) { + $size .= " [Q=".$quality."]"; + } if (isset($attr['Photoshop Quality'])) { - $size .= " (Quality = ".$attr['Photoshop Quality'].")"; + $size .= " [PQ=".$attr['Photoshop Quality']."]"; } $print[] = $size; if (isset($attr['Profile Description'])) { @@ -187,10 +434,10 @@ foreach ($files as $file) { fin(); //////////////////////////////////////////////////////////////////////////////////////////////// -// Makepdf: combine finished images into a pdf with python img2pdf +// Makepdf ////////////////////// } elseif (args("app") == "makepdf") { -echo Welcome("Combine images into pdf"); +echo Welcome("Combine finished images into a pdf with python img2pdf"); ///////////////////////////////////////////////////////////////////////// $input = args("dir")."*.*"; @@ -208,9 +455,73 @@ exec("open ".$dest." -b com.adobe.Acrobat.Pro"); fin(); //////////////////////////////////////////////////////////////////////////////////////////////// -// DPIset: batch set resolution tags +// Divide +// Note: we assume a .JPG file is an unmodified DCIM image and convert to a greyscale tif for overlay. +// A .jpg file is treated like a prepared brightness map file and is unmodified. ////////////////////// -} elseif (args("app") == "dpiset") { +} elseif (args("app") == "divide") { +echo Welcome("Composite image from brightness map"); +///////////////////////////////////////////////////////////////////////// + +$files = glob(args("dir")."*.{jpg,JPG,tif}", GLOB_BRACE); + +if (!args("map")) { + msg("No brightness map specified",1); + } elseif (!file_exists(args("map"))) { + msg("Can't open brightness map",1); + } elseif (substr(args("map"), -3, 3) == "JPG") { + //$method = "auto-level"; + $method = "normalize"; + $map = "scratch/".basename(args("map"),".JPG")."-divide_map.tif"; + if (!file_exists($map)) { + @exec("convert -".$method." -colorspace gray ".args("map")." ".$map); + } + } elseif (substr(args("map"), -3, 3) == "jpg") { + $map = args("map"); + } + +$dest = rtrim(args("dir"), '/')."_divided"; + +if (count(glob($dest."/*.*"))) { + msg("Files already exist in destination ".$dest,1); + } elseif (!is_dir($dest)) { + mkdir($dest); + } + +if (args("q")) { + $quality = args("q"); + } else { + $quality = 95; + } + +foreach ($files as $file) { + echo "Dividing ".$file." with ".$map.", Q=".$quality; + list ($width, $height) = getimagesize($map); + list ($twidth, $theight) = getimagesize($file); + if ($width != $twidth | $height != $theight) { + $tmap = $map."'[".$twidth."x".$theight."!]'"; + echo " (resize map) "; + } else { + $tmap = $map; + } + if (args("adjust")) { + echo " (".args("adjust").")"; + $cmd = "convert ".$file." ".$tmap." -compose Divide_Src -composite -level ".args("adjust")." -quality ".$quality." ".$dest."/".basename($file); + @exec($cmd); + } else { + $cmd = "convert ".$file." ".$tmap." -compose Divide_Src -composite -quality ".$quality." ".$dest."/".basename($file); + @exec($cmd); + } + echo "\n"; + } + + +fin(); + +//////////////////////////////////////////////////////////////////////////////////////////////// +// SetDPI +////////////////////// +} elseif (args("app") == "setdpi") { echo Welcome("Batch set EXIF resolution tags"); ///////////////////////////////////////////////////////////////////////// @@ -250,10 +561,39 @@ foreach ($files as $file) { fin(); //////////////////////////////////////////////////////////////////////////////////////////////// -// Deskew: detect skew angle and apply to rotation tag +// Strip +////////////////////// +} elseif (args("app") == "strip") { +echo Welcome("Strip crop values from images"); +///////////////////////////////////////////////////////////////////////// + +$files = glob(args("dir")."*.*"); + +foreach ($files as $file) { + echo "Processing ".$file.": "; + $ext = pathinfo($file, PATHINFO_EXTENSION); + if ($ext == "jpg" | $ext == "JPG" | $ext == "DNG") { + $parts = chop(shell_exec("exiftool -s -s -s -XMP-crs:CropTop -XMP-crs:CropRight -XMP-crs:CropLeft -XMP-crs:CropBottom ".$file." 2>&1")); + if (strlen($parts) > 1) { list($top, $right, $left, $bottom) = explode("\n", $parts); } + if (isset($top) | isset($right) | isset($left) | isset($bottom)) { + shell_exec("exiftool -overwrite_original -XMP-crs:CropTop= -XMP-crs:CropRight= -XMP-crs:CropLeft= -XMP-crs:CropBottom= ".$file." 2>&1"); + echo "removed"; + } else { + echo "no crop found"; + } + } else { + echo "cant handle ".$ext; + } + echo "\n"; + } + +fin(); + +//////////////////////////////////////////////////////////////////////////////////////////////// +// Deskew ////////////////////// } elseif (args("app") == "deskew") { -echo Welcome("Detect skew angles"); +echo Welcome("Detect skew angle and apply to EXIF tags"); ///////////////////////////////////////////////////////////////////////// $deskew_max_angle = ".4";