Keith Vetter 2016-07-22 : This started out small weekend project, as a way to help organize photographs from a recent trip. I'm old fashion, and like to get actual prints and put them into albums.
So I thought I'd write a short little program that lets you drag photos from a gallery of thumbnails and drop them in a replica of a real photo album--the kind with two vertical and one horizontal pocket. You could add more pages, add more photos, delete and rearrange photos, etc.
Well that simple little weekend project grew a bit: as I would be using it I realized it would be nice to have this new feature and that new feature, and so on. For example, I thought it would be useful to be able to add tags the thumbnails such as family or friends, and to rearrange the thumbnails based on the tags. Or for it to produce a list of all the photographs to be printed for the final product.
The result is something I'm kind of proud of--essentially a virtual photo album, that lets you flip the pages and experience what the final album would look like.
Couple of technical notes.
First, it requires ImageMagick to run--I needed to resize images to an arbitrary size and tk is lacking in that area.
Second, I had to implement my own drag-and-drop technology that lets you drag a thumbnail from one window and drop it into another. This was fairly complicated, requiring a new toplevel to hold the dragged item and converting between screen coordinates and window coordinates.
Third, if you want to change the album title or add text to be displayed on each page you must edit the file called _photo_album.cfg. (The page text is for display only.)
Fourth, this photo album works well with my previous program Photo Crop. One section of the manifest lists all the photos that need to be cropped. Plus, this tool will recognize when you have both a cropped and non-cropped version of the same photo.
##+########################################################################## # # photoAlbum.tcl -- Simulates laying out photos in a photo album # by Keith Vetter 2016-06-19 package require Tk package require Img package require tooltip set P(pixel,inch) 72 set P(album,width,inch) 9 set P(album,height,inch) 11 set P(album,gutter,inch) .5 set P(full,width,inch) [expr {2 * $P(album,width,inch) + $P(album,gutter,inch)}] set P(thumbs,display,rows) COMPUTED_LATER set P(thumbs,display,cols) COMPUTED_LATER set P(thumbs,image,pixels) 200 set P(thumbs,margin,pixels) 25 set P(thumbs,gutter,pixels) 0 set P(thumbs,box,pixels) [expr {$P(thumbs,image,pixels) + 2*$P(thumbs,margin,pixels) + $P(thumbs,gutter,pixels)}] set P(thumbs,qview,pixels) 600 set P(thumbs,width) COMPUTED_LATER set P(thumbs,height) COMPUTED_LATER # Layout coordinates (in inches) set P(gutter) {9 0 9.5 11} set P(recto,top) {12.25 .25 18.25 4.25} set P(recto,message) {9.75 .25 12 4.25} set P(recto,left) {9.75 4.75 13.75 10.75} set P(recto,right) {14.25 4.75 18.25 10.75} set P(verso,top) {.25 .25 6.25 4.25} set P(verso,message) {6.5 .25 8.75 4.25} set P(verso,left) {.25 4.75 4.25 10.75} set P(verso,right) {4.75 4.75 8.75 10.75} # Note: S(marks) require images with names ::img::XXX, e.g. ::img::Family set S(marks) {"Best" "Family" "Friends" "Animal" "Trash" "Other" "Underwater"} set S(marks,accel) {"B" "F" "N" "A" "T" "O" "U"} set S(noWrite) false set S(title,font) {Helvetica 24 bold} set S(text,font) {Helvetica 16 bold} proc DoDisplay {} { global P S set left [expr {int([winfo screenwidth .] - $P(width) - 10)}] wm geom . +$left+100 wm resizable . 0 0 ::ttk::label .title -textvariable S(title) -font $S(title,font) -anchor c pack .title -side top -fill x ::tooltip::clear ::ttk::frame .bbar pack .bbar -side top -fill x foreach {key text cmd} {thumbs "Open gallery" ::Gallery::MakeWindow manifest "Show manifest" ::Manifest::Show undo "Undo" ::Undo::Undo open "Open album" ::Album::Open prevpage "Previous page" {ChangePage 1} nextpage "Next page" {ChangePage -1} info "About" About} { ::ttk::button .bbar.$key -image ::img::$key -compound none -style Toolbutton -command $cmd ::tooltip::tooltip .bbar.$key $text pack .bbar.$key -side [expr {$key eq "info" ? "right" : "left"}] } canvas .c -width $P(width) -height $P(height) -bd 0 -highlightthickness 0 -bg white pack .c -side top foreach {key action} {"t" ::Gallery::MakeWindow "m" ::Manifest::Show "Key-Next" {ChangePage -1} "Key-Prior" {ChangePage 1} "Key-Right" {ChangePage -1} "Key-Left" {ChangePage 1} "Control-z" ::Undo::Undo} { bind . "<$key>" $action } menu .popup -tearoff 0 menu .popup.marks -tearoff 0 .popup add command -label Info -command ::Popup::Info -accel I .popup add command -label "Quick view" -command ::Popup::QuickView -under 0 -accel Q .popup add command -label "External Viewer" -command ::Popup::Viewer -under 9 \ -state [expr {[CanViewImage] ? "normal" : "disabled"}] -accel V .popup add cascade -label "Annotate" -menu .popup.marks .popup add separator .popup add command -label Delete -command ::Popup::Delete -accel D .popup add command -label "Rotate right" -command {::Popup::Rotate right} -accel R .popup add command -label "Rotate left" -command {::Popup::Rotate left} -accel L foreach mark $S(marks) accel $S(marks,accel) { .popup.marks add checkbutton -label $mark -command [list ::Popup::Annotate $mark] \ -variable ::M(mark,$mark) -accel $accel } if {[string equal $::tcl_platform(os) "Darwin"]} { event add <<MenuMousePress>> <Control-Button-1> event add <<MenuMousePress>> <Button-2> } else { event add <<MenuMousePress>> <Button-3> } } proc DrawPage {} { global P .c delete all .c create rect [ToCanvas $P(gutter)] -fill gray50 -width 0 foreach side {verso recto} { foreach pocket {message top left right} { set tag "$side,$pocket" set itag "img,$tag" lassign [ToCanvas $P($side,$pocket)] x0 y0 x1 y1 .c create rect $x0 $y0 $x1 $y1 -tag [list $side $tag] -fill {} -outline black -width 2 -fill white if {$pocket in {message top}} { .c create image $x0 $y0 -tag [list image $itag] -anchor nw } else { .c create image $x0 $y1 -tag [list image $itag] -anchor sw } .c bind $itag <<MenuMousePress>> [list DoPopup $itag album %X %Y] } CreateTextBox $side } } proc DoPopup {tag who x y} { global M S ALBUM set M(popup,tag) $tag if {[string match "thumb_*" $tag]} { scan $M(popup,tag) "thumb_%d_%d" row col set M(popup,idx) [::Gallery::Pos2Index $row $col] } else { lassign [split $tag ","] . side pocket set pageNo [expr {$S(current,page) + ($side eq "recto")}] set M(popup,idx) [Image2Index $ALBUM($pageNo,$pocket)] } # Disable Delete and the Rotate entries depending on context for {set idx 0} {$idx < [.popup index last]} {incr idx} { if {[.popup type $idx] ne "command"} continue set txt [.popup entrycget $idx -label] if {$txt eq "Delete"} { .popup entryconfig $idx -state [expr {$who eq "thumbs" ? "disabled" : "normal"}] } elseif {[string match "Rotate *" $txt]} { .popup entryconfig $idx -state [expr {$who eq "thumbs" ? "normal" : "disabled"}] } } ::Popup::Annotate -populate set focus [focus] tk_popup .popup $x $y if {[tk windowingsystem] eq "aqua" && $focus ne ""} { # Aqua's help window steals focus on display after idle [list focus -force $focus] focus -force $focus } } proc BestSize {} { global P set width [expr {$P(full,width,inch) * $P(pixel,inch)}] set height [expr {$P(album,height,inch) * $P(pixel,inch)}] set sw [winfo screenwidth .] set sh [winfo screenheight .] set scaleW [expr {($sw - 200.) / $width}] set scaleH [expr {($sh - 300.) / $height}] set scale [expr {min($scaleW, $scaleH)}] set P(scale) [expr {int($scale * 10) / 10.}] set P(width) [expr {$P(scale) * $width}] set P(height) [expr {$P(scale) * $height}] set thumbW [expr {($sw / 3) / $P(thumbs,box,pixels)}] set thumbH [expr {round(($sh - 300.) / $P(thumbs,box,pixels))}] set P(thumbs,display,cols,raw) [expr {max(3, min(5, $thumbW))}] set P(thumbs,display,rows,raw) [expr {min(5, $thumbH)}] } proc ToCanvas {xy4} { global P set xy {} foreach pt $xy4 { lappend xy [expr {round($P(scale) * $P(pixel,inch) * $pt)}] } return $xy } ################################################################ namespace eval ::Pocket {} proc ::Pocket::InsertImage {side pocket iname} { if {$iname eq ""} { .c itemconfig img,$side,$pocket -image {} } else { set fname [FullName $iname] set tag img,$side,$pocket set sizedFname [::Pocket::ResizeImageToFit $pocket $fname] image create photo ::album::${side}::$pocket -file $sizedFname .c itemconfig $tag -image ::album::${side}::$pocket } } proc ::Pocket::ResizeImageToFit {pocket fullName} { set cacheName [GetCacheName $pocket $fullName] if {[file exists $cacheName]} { return $cacheName } lassign [GetImageSize $fullName] iwidth iheight lassign [::Pocket::GetSize $pocket] pwidth pheight set imageVertical [expr {$iwidth < $iheight}] set pocketVertical [expr {$pocket ne "top"}] set cmd [list "convert"] if {$imageVertical ne $pocketVertical} { lappend cmd "-rotate" "-90" } lappend cmd "-resize" "${pwidth}x${pheight}" lappend cmd "--" $fullName lappend cmd $cacheName MyExec $cmd return $cacheName } proc ::Pocket::Highlight {pocket onoff} { if {$onoff} { .c itemconfig $pocket -outline magenta -width 15 } else { .c itemconfig $pocket -outline black -width 2 } } proc ::Pocket::XY2Pocket {x y} { foreach side {recto verso} { foreach pocket {top left right} { lassign [.c bbox $side,$pocket] x0 y0 x1 y1 if {$x >= $x0 && $x <= $x1 && $y >= $y0 && $y <= $y1} { return "$side,$pocket" } } } return "" } proc ::Pocket::GetSize {pocket} { lassign [ToCanvas $::P(verso,$pocket)] x0 y0 x1 y1 return [list [expr {$x1 - $x0}] [expr {$y1 - $y0}]] } ################################################################ proc RotateImageInPlace {dir fullName} { set backupName "[file rootname $fullName]_org[file extension $fullName]" if {! [file exists $backupName]} { file copy $fullName $backupName } close [file tempfile tempfileName "photo_album_"] file rename -force $fullName $tempfileName set degrees [expr {$dir eq "left" ? -90 : 90}] set cmd [list "convert" "-rotate" $degrees "--" $tempfileName $fullName] MyExec $cmd file delete $tempfileName } proc MyExec {cmd} { set oldFocus [focus] set result [exec {*}$cmd] focus $oldFocus return $result } proc GetImageSize {fullName} { return [exec identify -format "%w %h" -- $fullName] } proc GetCacheName {type iname} { if {$type eq "right"} { set type left } if {$type eq "qview"} { set size $::P(thumbs,qview,pixels) } elseif {$type eq "thumb"} { set size $::P(thumbs,image,pixels) } else { set size $::P(scale) } set fullName "${type}_${size}_[file tail $iname]" return [file join $::ALBUM(cache) $fullName] } ################################################################ namespace eval ::Popup {} proc ::Popup::Info {} { global M S ALBUM if {! [info exists M(popup,idx)]} return set idx $M(popup,idx) set fullName [FullName [Index2Image $idx]] lassign [GetImageSize $fullName] iwidth iheight set tail [file tail $fullName] set dateTime [::Popup::GetImageDateTime $fullName] set location [::Popup::GetImageLocation $fullName] set ratio [expr {max($iwidth,$iheight) / double(min($iwidth,$iheight))}] set is4x6 [expr {abs($ratio - 1.5) < .01}] set msg "File: $tail\n" append msg "Index: [comma [expr {$idx+1}]] / [comma [llength $ALBUM(files)]]\n" append msg "Size: [comma $iwidth] x [comma $iheight]\n" append msg "Date/Time: $dateTime\n" append msg "Location: $location\n" append msg "4x6: [expr {$is4x6 ? {yes} : {no}}]\n" set marks [join [::Gallery::GetAnnotations $idx] ", "] if {$marks eq {}} { set marks "none" } append msg "Annotations: [string map {Check {Used in album}} $marks]\n" tk_messageBox -message "Image Information" -detail $msg } proc ::Popup::Annotate {how} { global M S ALBUM if {! [info exists M(popup,idx)]} return set iname [Index2Image $M(popup,idx)] if {! [info exists ALBUM(mark,$iname)]} {set ALBUM(mark,$iname) {}} if {$how eq "-populate"} { foreach key $S(marks) { set M(mark,$key) 0 } foreach mark $ALBUM(mark,$iname) { set M(mark,$mark) 1 } return } # Ignore $how, use M(mark,*) to determine annotations set old $ALBUM(mark,$iname) set ALBUM(mark,$iname) {} foreach key $S(marks) { if {$M(mark,$key)} { lappend ALBUM(mark,$iname) $key }} if {$old eq $ALBUM(mark,$iname)} return ::Undo::RegisterAnnotationEvent $iname $old ::Gallery::RedrawAll ::Album::Write focus -force .thumbs.c } proc ::Popup::AnnotateDirect {accelKey idx} { global M set n [lsearch -exact $::S(marks,accel) $accelKey] if {$n == -1} return set mark [lindex $::S(marks) $n] set M(popup,idx) $idx ::Popup::Annotate -populate set M(mark,$mark) [expr {! $::M(mark,$mark)}] ::Popup::Annotate $mark } proc ::Popup::Delete {} { global M S ALBUM if {! [info exists M(popup,tag)]} return lassign [split $M(popup,tag) ","] . side pocket set pageNo [expr {$S(current,page) + ($side eq "recto")}] set currentValue [expr {[info exists ALBUM($pageNo,$pocket)] ? $ALBUM($pageNo,$pocket) : ""}] ::Undo::RegisterDragAndDropEvent $pageNo $pocket "" ::Pocket::InsertImage $side $pocket "" ::Gallery::RedrawAll } proc ::Popup::Rotate {dir} { global M ALBUM if {! [info exists M(popup,idx)]} return set idx $M(popup,idx) set iname [Index2Image $idx] lassign [::Gallery::Index2Pos $idx] row col lassign [::Gallery::Pos2XY [expr {$row+.3}] [expr {$col + .3}]] x y Busy 1 .thumbs.c $x $y RotateImageInPlace $dir [FullName $iname] Busy 0 .thumbs.c 0 0 ::Undo::RegisterRotateEvent $iname ClearCache $iname ::Gallery::ClearImage $iname ::Gallery::RedrawAll } proc ::Popup::QuickView {} { global M S ALBUM if {! [info exists M(popup,idx)]} return ::Gallery::DisplayQView $M(popup,idx) } proc ::Popup::Viewer {} { global M S ALBUM if {! [info exists M(popup,idx)]} return set idx $M(popup,idx) ViewImage [FullName [Index2Image $idx]] } proc ::Popup::GetImageDateTime {fullName} { set exif [MyExec [list "identify" "-format" {%[EXIF:*]} $fullName]] set n [regexp -line {DateTimeOriginal=(.*)$} $exif . dateTime] if {! $n} {return ""} set ticks [clock scan $dateTime -format "%Y:%m:%d %k:%M:%S"] return [clock format $ticks] } proc ::Popup::GetImageLocation {fullName} { set exif [MyExec [list "identify" "-format" {%[EXIF:*]} $fullName]] if {$exif eq ""} { return "" } set n1 [regexp {GPSLatitude=([0-9/]+), *([0-9/]+), *([0-9/]+)} $exif . lat1 lat2 lat3] set n2 [regexp {GPSLatitudeRef=(.)} $exif . latRef] set n3 [regexp {GPSLongitude=([0-9/]+), *([0-9/]+), *([0-9/]+)} $exif . lon1 lon2 lon3] set n4 [regexp {GPSLongitudeRef=(.)} $exif . lonRef] if {!$n1 || !$n2 || !$n3 || !$n4} { return "" } proc FixNum {ll} { lassign [split $ll "/"] num den if {$den eq "" || $den eq "1"} { return $num} return [expr {$num / double($den)}] } foreach var {lat1 lat2 lat3 lon1 lon2 lon3} {set $var [FixNum [set $var]]} set lat [expr {($lat1 + $lat2 / 60.0 + $lat3 / 3600.0) * ($latRef eq "N" ? 1 : -1)}] set lon [expr {($lon1 + $lon2 / 60.0 + $lon3 / 3600.0) * ($lonRef eq "E" ? 1 : -1)}] return [format "%.3f %.3f" $lat $lon] } proc comma { num } { while {[regsub {^([-+]?[0-9]+)([0-9][0-9][0-9])} $num {\1,\2} num]} {} return $num } ################################################################ # # Displaying in album # proc ShowPages {pageNo} { WindowTitle $pageNo set lo [expr {int($pageNo/2) * 2}] set ::S(current,page) $lo .c itemconfig image -image {} ShowOnePage $lo ShowOnePage [expr {$lo + 1}] } proc ShowOnePage {pageNo} { global ALBUM set side [expr {($pageNo & 1) ? "recto" : "verso"}] foreach pocket {top left right} { set fullName [FindAlbumImage $pageNo $pocket] ::Pocket::InsertImage $side $pocket $fullName update } set text [expr {[info exist ALBUM($pageNo,text)] ? $ALBUM($pageNo,text) : ""}] set text [string map {\\n \n} $text] .c itemconfig $side,text -text $text } proc ChangePage {dir} { global S ALBUM set dir [expr {$dir == 0 ? 0 : -$dir/abs($dir)}] set newPage [expr {$S(current,page) + 2 * $dir}] set newPage [expr {int($newPage/2) * 2}] if {$newPage < 0} return set highestVerso [expr {int($ALBUM(pages)/2) * 2}] if {$newPage > 2 + $highestVerso} return lappend ::pages $newPage ShowPages $newPage } proc FindAlbumImage {page pocket} { global ALBUM if {! [info exists ALBUM($page,$pocket)]} { return "" } set fullName [FullName $ALBUM($page,$pocket)] if {[file exists $fullName]} { return $fullName } return "" } proc RemoveCroppedDuplicates {inames} { set result {} foreach item $inames { if {[string first "_org." $item] > -1} continue if {[string first "_cropped." $item] == -1} { set cropName [CroppedName $item] if {$cropName in $inames} continue } lappend result $item } return $result } proc CroppedName {iname} { set cropName "[file rootname $iname]_cropped[file extension $iname]" return $cropName } proc WindowTitle {page} { wm title . $::ALBUM(title) if {$page <= 1} { set ::S(title) "$::ALBUM(title) -- Page 1" } else { set lo [expr {int($page/2) * 2}] set ::S(title) "$::ALBUM(title) -- Page $lo & [expr {$lo+1}]" } append ::S(title) " of [expr {max(1,$::ALBUM(pages))}]" } ################################################################ namespace eval ::Album {} proc ::Album::Open {} { set newDir [tk_chooseDirectory -mustexist true -initialdir $::ALBUM(dir)] if {$newDir eq "" || $newDir eq $::ALBUM(dir)} return if {[::Album::GetImages $newDir] eq {}} { tk_messageBox -icon error \ -message "Error: directory must contain the images to put into the album" return } ::Gallery::ClearAllImages destroy .thumbs ::Album::Read $newDir ShowPages 1 ::Indexer::IndexAll ::Gallery::MakeWindow } proc ::Album::Read {dir} { global ALBUM ::Undo::Reset ::Album::DefaultAlbum $dir if {[::Album::ReadAndParse]} { ::Album::CheckForMissingOrCropped return false } if {$ALBUM(files) eq {}} { set msg "Error: cannot create photo album for directory $dir." set detail "There are no image files in there." tk_messageBox -message $msg -detail $detail -icon error if {$::tcl_interactive} { return -level 999 } exit } ::Album::Write 1 return true } proc ::Album::DefaultAlbum {dir} { global ALBUM unset -nocomplain ALBUM set ALBUM(dir) [file normalize $dir] set ALBUM(cache) [file join $ALBUM(dir) _photo_album.cache] set ALBUM(files) [::Album::GetImages $ALBUM(dir)] if {$ALBUM(files) ne {}} { file mkdir $ALBUM(cache) } set shortDir [file join [file tail [file dirname $ALBUM(dir)]] [file tail $ALBUM(dir)]] set ALBUM(title) "Photo Album for $shortDir" set ALBUM(pages) 0 set ALBUM(sortLast) "Name" } proc ::Album::GetImages {dir} { return [lsort -dictionary [RemoveCroppedDuplicates \ [glob -nocomplain -tail -directory $dir \ *.jpg *.png *.gif]]] } proc ::Album::Write {{force 0}} { global ALBUM if {$::S(noWrite) && ! $force} return set cfgFile [FullName "_photo_album.cfg"] set fout [open $cfgFile w] puts $fout [format "%-10s %s" title $ALBUM(title)] if {[array names ALBUM *,text] eq {}} { set ALBUM(1,text) "# you can actually have text on each page" } foreach key [lsort -dictionary [array names ALBUM {[0-9]*,*}]] { if {$ALBUM($key) ne ""} { puts $fout [format "%-10s %s" $key $ALBUM($key)] } } foreach key [lsort -dictionary [array names ALBUM mark,*]] { if {$ALBUM($key) eq {}} continue puts $fout "$key $ALBUM($key)" } close $fout } proc ::Album::ReadAndParse {} { global ALBUM set cfgFile [FullName "_photo_album.cfg"] if {! [file exists $cfgFile]} { return false } set fin [open $cfgFile r] set lines [split [string trim [read $fin]] \n] close $fin array unset ALBUM {[0-9]*,*} foreach line $lines { set line [string trim $line] if {[string match "#*" $line]} continue set n [regexp {^ *([^ ]+) +(.*)$} $line . name value] if {! $n} { error "mal-formed config line: $line" } if {[string match "#*" $value]} continue set ALBUM($name) $value if {[string first "," $name] > -1} { lassign [split $name ","] pageNo pocket if {[string is integer -strict $pageNo]} { if {$pageNo > $ALBUM(pages)} { set ALBUM(pages) $pageNo } } } } return true } proc ::Album::CheckForMissingOrCropped {} { global ALBUM set needUpdate 0 set missing {} for {set pageNo 0} {$pageNo <= $ALBUM(pages)} {incr pageNo} { foreach key [array names ALBUM $pageNo,*] { if {[string match "*,text" $key]} continue set iname $ALBUM($key) if {$iname in $ALBUM(files)} continue set cropName [CroppedName $iname] if {$cropName in $ALBUM(files)} { set ALBUM($key) $cropName set needUpdate 1 } else { lappend missing $iname set ALBUM($key) "" set needUpdate 1 } } } if {$missing ne {}} { tk_messageBox -icon error -title "Missing images" \ -message "Cannot find the following images for the album" \ -detail [join $missing \n] } if {$needUpdate} { ::Album::Write } } ################################################################ proc Image2Index {iname} { set tail [file tail $iname] set idx [lsearch -exact $::ALBUM(files) $tail] if {$idx == -1} { error "cannot find $iname in ALBUM(files)" } return $idx } proc Index2Image {idx} { return [lindex $::ALBUM(files) $idx] } proc IncrIndex {idx incr} { return [expr {($idx + $incr) % [llength $::ALBUM(files)]}] } proc ImageInAlbum {iname} { global ALBUM set tail [file tail $iname] foreach key [array name ALBUM {[0-9]*,*}] { if {$ALBUM($key) eq $tail} { return true } } return false } proc FullName {fname} { if {$fname eq ""} { return "" } return [file join $::ALBUM(dir) $fname] } proc ClearCache {iname} { set glob "*[file tail $iname]" set staleFiles [glob -nocomplain -directory $::ALBUM(cache) $glob] file delete -- {*}$staleFiles } ################################################################ # # Thumbnail gallery # namespace eval ::Gallery {} proc ::Gallery::MakeWindow {} { global S P ALBUM if {[winfo exists .thumbs]} { raise .thumbs ::Gallery::RedrawAll return } set P(thumbs,display,cols) $P(thumbs,display,cols,raw) set S(thumb,row,index) 0 set S(thumb,total,rows) [expr {int(ceil([llength $ALBUM(files)] / double($P(thumbs,display,cols))))}] set P(thumbs,display,rows) [expr {min($P(thumbs,display,rows,raw), 1 + $S(thumb,total,rows))}] set P(thumbs,width) [expr {$P(thumbs,box,pixels) * $P(thumbs,display,cols)}] set P(thumbs,height) [expr {$P(thumbs,box,pixels) * $P(thumbs,display,rows)}] destroy .thumbs toplevel .thumbs wm title .thumbs "Gallery for '$ALBUM(title)'" wm resizable .thumbs 0 0 wm geom .thumbs +10+10 pack [::ttk::frame .thumbs.top -padding {.1i 0}] -side top -fill x pack [::ttk::scrollbar .thumbs.sb -orient v] -side right -fill y ;# NB. no -command pack [canvas .thumbs.c -width $P(thumbs,width) -height $P(thumbs,height) \ -bd 0 -highlightthickness 0 -bg white] pack [::ttk::label .thumbs.top.count -textvariable S(thumb,count)] -side left tk_optionMenu .thumbs.top.sort ALBUM(sortCriteria) "Name" "In album" {*}$S(marks) pack .thumbs.top.sort -side right pack [::ttk::label .thumbs.top.lbl -text "Sort by:"] -side right set w [.thumbs.top.sort cget -menu] for {set i 0} {$i < [$w index last]} {incr i} { $w entryconfig $i -command [list ::Gallery::SortBy [$w entrycget $i -value]] } for {set row 0} {$row < $P(thumbs,display,rows)} {incr row} { for {set col 0} {$col < $P(thumbs,display,cols)} {incr col} { set tag "thumb_${row}_${col}" set idx [::Gallery::Pos2Index $row $col] set xy [::Gallery::Pos2XY $row $col] .thumbs.c create image $xy -anchor nw -tag [list image $tag] .thumbs.c bind $tag <1> [list ::Gallery::Click down %x %y $row $col] .thumbs.c bind $tag <B1-Motion> [list ::Gallery::Click move %x %y $row $col] .thumbs.c bind $tag <ButtonRelease-1> [list ::Gallery::Click up %x %y $row $col] .thumbs.c bind $tag <<MenuMousePress>> [list DoPopup $tag thumbs %X %Y] } } bind .thumbs <Key> [list ::Gallery::KeyPress %K] if {"x11" eq [tk windowingsystem]} { bind .thumbs.c <Button-4> {::Gallery::Scroller move 1} bind .thumbs.c <Button-5> {::Gallery::Scroller move -1} } else { bind .thumbs.c <MouseWheel> {::Gallery::Scroller move %D} } foreach {key action} {"m" ::Manifest::Show "Key-Next" {::Gallery::Scroller move -1} "Key-Down" {::Gallery::Scroller move -1} "Key-space" {::Gallery::Scroller move -1} "Key-Prior" {::Gallery::Scroller move 1} "Key-Up" {::Gallery::Scroller move 1} "Shift-Key-space" {::Gallery::Scroller move 1} "Control-z" ::Undo::Undo} { bind .thumbs "<$key>" $action } ::Gallery::RedrawAll } proc ::Gallery::ClearImage {iname} { set idx [Image2Index $iname] foreach prefix {thumb qview} { set img ::${prefix}::$idx if {$img in [image names]} { image delete $img } } } proc ::Gallery::ClearAllImages {} { # We link thumbnail to index into ALBUM(files), so if that changes we # must delete all the images foreach prefix {thumb qview} { foreach img [info commands ::${prefix}::*] { image delete $img } } } proc ::Gallery::RedrawAll {} { global S P ALBUM if {! [winfo exists .thumbs.c]} return .thumbs.c itemconfig image -image {} .thumbs.c delete checks for {set row 0} {$row < $P(thumbs,display,rows)} {incr row} { for {set col 0} {$col < $P(thumbs,display,cols)} {incr col} { set tag "thumb_${row}_${col}" set idx [::Gallery::Pos2Index $row $col] set fname [FullName [Index2Image $idx]] if {$fname eq {}} { .thumbs.c itemconfig $tag -image {} continue } set thumbImg ::thumb::$idx if {$thumbImg ni [image names]} { lassign [::Gallery::MakeThumbnail $fname] thumbName image create photo $thumbImg -file $thumbName ::ShadowBorder::MakeShadowPhoto $thumbImg $thumbImg .thumbs.c itemconfig $tag -image $thumbImg update } else { .thumbs.c itemconfig $tag -image $thumbImg } set qviewImg ::qview::$idx if {$qviewImg ni [image names]} { lassign [::Gallery::MakeQViewImage $fname] qviewName # image create photo $qviewImg -file $qviewName # ::ShadowBorder::MakeShadowPhoto $qviewImg $qviewImg } # NB. requires custom version of tooltip # ::tooltip::tooltip .thumbs.c -items $tag $qviewImg ::Gallery::ShowAnnotations $row $col } } set firstVisibleRow $S(thumb,row,index) set lastVisibleRow [expr {$S(thumb,row,index) + $P(thumbs,display,rows)}] set lo [expr {double($firstVisibleRow) / $S(thumb,total,rows)}] set hi [expr {double($lastVisibleRow) / $S(thumb,total,rows)}] .thumbs.sb set $lo $hi set len [llength $ALBUM(files)] set S(thumb,count) " $len image[expr {$len == 1 ? {} : {s}}]" } proc ::Gallery::ShowAnnotations {row col} { set marks [::Gallery::GetAnnotations [::Gallery::Pos2Index $row $col]] set tag "thumb_${row}_${col}" lassign [.thumbs.c bbox $tag] x0 y0 x1 y1 if {$x0 eq ""} return set x [expr {$x1 - $::P(thumbs,margin,pixels)}] set y [expr {$y0 + $::P(thumbs,margin,pixels)}] foreach mark $marks { set id [.thumbs.c create image $x $y -anchor ne -tag checks \ -image ::img::$mark] incr y [image height ::img::$mark] incr y -2 if {$mark eq "Check"} { set mark "In album" } ::tooltip::tooltip .thumbs.c -items $id $mark } } proc ::Gallery::GetAnnotations {idx} { set iname [Index2Image $idx] set marks {} if {[ImageInAlbum $iname]} { lappend marks "Check" } if {[info exists ::ALBUM(mark,$iname)]} { foreach mark $::ALBUM(mark,$iname) { lappend marks $mark } } return $marks } proc ::Gallery::Scroller {how value args} { global S P ALBUM if {$how eq "move"} { if {$value > 0} { if {$S(thumb,row,index) > 0} { incr S(thumb,row,index) -1 ::Gallery::RedrawAll } } elseif {$value < 0} { if {$S(thumb,row,index) + $P(thumbs,display,rows) < $S(thumb,total,rows)} { incr S(thumb,row,index) ::Gallery::RedrawAll } } } } proc ::Gallery::Pos2Index {row col} { return [expr {($::S(thumb,row,index) + $row) * $::P(thumbs,display,cols) + $col}] } proc ::Gallery::Index2Pos {idx} { set row [expr {$idx / $::P(thumbs,display,cols) - $::S(thumb,row,index)}] set col [expr {$idx % $::P(thumbs,display,cols)}] return [list $row $col] } proc ::Gallery::Pos2XY {row col} { global P set y [expr {$P(thumbs,gutter,pixels) / 2 + $P(thumbs,box,pixels) * $row}] set x [expr {$P(thumbs,gutter,pixels) / 2 + $P(thumbs,box,pixels) * $col}] return [list $x $y] } proc ::Gallery::Click {how x y row col} { global M S P set tag "thumb_${row}_$col" # Use window pointer position to track drag and drop outside the containing window lassign [winfo pointerxy .thumbs] px py if {$how eq "down"} { lassign [.thumbs.c coords $tag] x0 y0 set dx [expr {$x - $x0}] set dy [expr {$y - $y0}] set M(left) [expr {int($px - $dx + 5)}] set M(top) [expr {int($py - $dy + 5)}] set M(px) $px set M(py) $py set M(pocket) "" destroy .d_and_d toplevel .d_and_d wm withdraw .d_and_d wm overrideredirect .d_and_d 1 set thumbImg [.thumbs.c itemcget $tag -image] pack [label .d_and_d.l -image $thumbImg -anchor nw -bd 2 -relief solid -bg red] wm geom .d_and_d +$M(left)+$M(top) wm deiconify .d_and_d raise .d_and_d return } if {$how eq "move"} { if {! [winfo exists .d_and_d]} return raise . raise .d_and_d set dx [expr {$px - $M(px)}] set dy [expr {$py - $M(py)}] set M(px) $px set M(py) $py incr M(left) $dx incr M(top) $dy wm geom .d_and_d +$M(left)+$M(top) lassign [::Gallery::Pointer2Canvas .c $px $py] cx cy set pocket [::Pocket::XY2Pocket $cx $cy] if {$pocket ne $M(pocket)} { ::Pocket::Highlight $M(pocket) 0 set M(pocket) $pocket ::Pocket::Highlight $M(pocket) 1 } return } if {$how eq "up"} { ::Pocket::Highlight $M(pocket) 0 destroy .d_and_d if {$M(pocket) ne ""} { DragAndDrop $M(pocket) $row $col } return } } proc ::Gallery::Pointer2Canvas {canvas px py} { set x [expr {$px - [winfo rootx $canvas]}] set y [expr {$py - [winfo rooty $canvas]}] return [list $x $y] } proc ::Gallery::KeyPress {K} { set K [string toupper $K] if {$K ni $::S(marks,accel)} return lassign [winfo pointerxy .thumbs] px py lassign [::Gallery::Pointer2Canvas .thumbs.c $px $py] cx cy lassign [::Gallery::XY2Thumbnail $cx $cy] isFound row col if {! $isFound} return set idx [::Gallery::Pos2Index $row $col] ::Popup::AnnotateDirect $K $idx } proc ::Gallery::XY2Thumbnail {x y} { foreach id [.thumbs.c find overlapping $x $y $x $y] { set tags [.thumbs.c itemcget $id -tags] if {"image" in $tags} { set tag [lsearch -inline -glob $tags "thumb_*"] set n [scan $tag "thumb_%d_%d" row col] if {$n != 2} { error "cannot parse $tag for thumb_##_##" } return [list true $row $col] } } return false } proc ::Gallery::SortBy {criteria} { global ALBUM set last $ALBUM(sortLast) set ALBUM(sortLast) $criteria if {$criteria eq $last} { set ALBUM(files) [lreverse $ALBUM(files)] } elseif {$criteria eq "Name"} { set ALBUM(files) [lsort -dictionary $ALBUM(files)] } else { if {$criteria eq "In album"} {set criteria "Check"} set matching {} set nonMatching {} for {set idx 0} {$idx < [llength $ALBUM(files)]} {incr idx} { set iname [Index2Image $idx] set annotations [::Gallery::GetAnnotations $idx] if {$criteria in $annotations} { lappend matching $iname } else { lappend nonMatching $iname } } set ALBUM(files) [concat $matching $nonMatching] } ::Album::Write set ::S(thumb,row,index) 0 ::Gallery::ClearAllImages ::Gallery::RedrawAll } proc ::Gallery::MakeThumbnail {fname {inBackground 0}} { set thumbName [GetCacheName thumb $fname] if {[file exists $thumbName]} { return [list $thumbName 0] } set size $::P(thumbs,image,pixels) set cmd [list "convert" "-thumbnail" "${size}x$size" "--" $fname $thumbName] if {$inBackground} { lappend $cmd "&" } MyExec $cmd return [list $thumbName 1] } proc ::Gallery::MakeQViewImage {fname} { set qviewName [GetCacheName qview $fname] if {[file exists $qviewName]} { return [list $qviewName 0] } set size $::P(thumbs,qview,pixels) set cmd [list "convert" "-thumbnail" "${size}x$size" "--" $fname $qviewName] MyExec $cmd return [list $qviewName 1] } set S(after,delay) 1000 proc ::Gallery::BackgroundThumbnails {files} { while {1} { if {$files eq {}} return set files [lassign $files iname] set iname [FullName $iname] lassign [::Gallery::MakeThumbnail $iname 1] . didConvert if {$didConvert} {lappend ::BG $iname} if {$didConvert} break } after $::S(after,delay) ::Gallery::BackgroundThumbnails [list $files] } proc ::Gallery::DisplayQView {idx} { set ::Gallery::qviewIndex $idx set qviewImg ::qview::$idx if {$qviewImg ni [image names]} { set fname [FullName [Index2Image $idx]] lassign [::Gallery::MakeQViewImage $fname] qviewName image create photo $qviewImg -file $qviewName ::ShadowBorder::MakeShadowPhoto $qviewImg $qviewImg } ::Gallery::ShowQViewImage $qviewImg } proc ::Gallery::NextQView {dir} { set idx [IncrIndex $::Gallery::qviewIndex $dir] ::Gallery::DisplayQView $idx } proc ::Gallery::ShowQViewImage {img} { if {! [winfo exists .quick]} { toplevel .quick pack [frame .quick.f] -fill both -expand 1 #wm attribute .quick -topmost 1 wm transient .quick .thumbs label .quick.l -image $img button .quick.prev -image ::img::previmage -command {::Gallery::NextQView -1} -width 40 -height 64 button .quick.next -image ::img::nextimage -command {::Gallery::NextQView 1} -width 40 -height 64 ::tooltip::tooltip .quick.prev "Previous quick view" ::tooltip::tooltip .quick.next "Next quick view" pack .quick.prev .quick.l .quick.next -side left -in .quick.f foreach {key action} { "Key-Next" {::Gallery::NextQView 1} "Key-Prior" {::Gallery::NextQView -1} "Key-Right" {::Gallery::NextQView 1} "Key-Left" {::Gallery::NextQView -1}} { bind .quick "<$key>" $action } } else { raise .quick .quick.l config -image $img } } ################################################################ proc Busy {onoff w x y} { $w delete busy if {! $onoff} return set id [$w create text $x $y -tag busy -fill red -anchor nw -text " Please wait... "] foreach xy {x0 y0 x1 y1} value [$w bbox $id] delta {-2 -2 2 2} { set $xy [expr {$value + $delta}] } $w create rect $x0 $y0 $x1 $y1 -tag busy -fill yellow -outline black -width 2 $w raise $id update } ################################################################ # # Drag and drop # proc DragAndDrop {slot row col} { global ALBUM S set idx [::Gallery::Pos2Index $row $col] lassign [split $slot ","] side pocket set iname [Index2Image $idx] set pageNo [expr {$S(current,page) + ($side eq "verso" ? 0 : 1)}] ::Undo::RegisterDragAndDropEvent $pageNo $pocket $iname ::Pocket::InsertImage $side $pocket $iname ::Gallery::RedrawAll } ################################################################ # # Undo # namespace eval ::Undo {} proc ::Undo::Reset {} { set ::S(undo) {} catch {.bbar.undo config -state disabled} } proc ::Undo::RegisterDragAndDropEvent {pageNo pocket newIname} { global S ALBUM set oldIname [expr {[info exists ALBUM($pageNo,$pocket)] ? $ALBUM($pageNo,$pocket) : ""}] lappend ::S(undo) [list drop $pageNo $pocket $oldIname] set ALBUM($pageNo,$pocket) [file tail $newIname] set ALBUM(pages) [expr {max($ALBUM(pages), $pageNo)}] .bbar.undo config -state normal ::Album::Write } proc ::Undo::RegisterRotateEvent {iname} { lappend ::S(undo) [list rotate $iname] .bbar.undo config -state normal } proc ::Undo::RegisterAnnotationEvent {iname oldMarks} { lappend ::S(undo) [list annotation $iname $oldMarks] .bbar.undo config -state normal } proc ::Undo::Undo {} { global S ALBUM if {$S(undo) eq {}} return set event [lindex $S(undo) end] set S(undo) [lrange $S(undo) 0 end-1] set type [lindex $event 0] if {$type eq "drop"} { ::Undo::UndoDragAndDrop $event } elseif {$type eq "rotate"} { ::Undo::UndoRotate $event } elseif {$type eq "annotation"} { ::Undo::UndoAnnotation $event } if {$S(undo) eq {}} { .bbar.undo config -state disabled } } proc ::Undo::UndoRotate {event} { lassign $event . iname dir set fname [FullName $iname] # We undo by copying back the original set backupName "[file rootname $fname]_org[file extension $fname]" if {! [file exists $backupName]} { tk_messageBox -icon error -message "Error: cannot undo rotation, cannot locate original image" return } file copy -force $backupName $fname ClearCache $fname ::Gallery::ClearImage $iname ::Gallery::RedrawAll } proc ::Undo::UndoDragAndDrop {event} { global ALBUM lassign $event action pageNo pocket oldValue set currentValue [expr {[info exists ALBUM($pageNo,$pocket)] ? $ALBUM($pageNo,$pocket) : ""}] set ALBUM($pageNo,$pocket) $oldValue if {$oldValue eq ""} { unset ALBUM($pageNo,$pocket) } set side [expr {($pageNo & 1) ? "recto" : "verso"}] ::Pocket::InsertImage $side $pocket $oldValue ::Gallery::RedrawAll ::Album::Write } proc ::Undo::UndoAnnotation {event} { lassign $event . iname oldMarks set ::ALBUM(mark,$iname) $oldMarks ::Gallery::RedrawAll } ################################################################ proc CanViewImage {} { global S if {[info exists S(viewer)]} { return [expr {$S(viewer) ne ""}] } foreach cmd {iview open gnome-open} { if {$cmd eq "open" && $::tcl_platform(os) ne "Darwin"} continue set S(viewer) [auto_execok $cmd] if {$S(viewer) ne ""} { return true } } return false } proc ViewImage {fname} { global S if {$S(viewer) eq ""} return MyExec [list $S(viewer) $fname &] } proc About {{isNewAlbum false}} { set msg "Photo Album\nby Keith Vetter\nMay 2016" set detail "" if {$isNewAlbum} { append detail "\nCreated an empty photo album '$::ALBUM(title)'\n\n" } append detail "This tool lets you design a photo album from pictures in " append detail "a directory. It simulates the look of physical photo album " append detail "with two vertical pockets and one horizontal pocket for pictures " append detail "(plus a smaller pocket for a description).\n\n" append detail "You populate the photo album by dragging " append detail "thumbnails of the desired pictures and dropping them on " append detail "the simulacrum of a photo album. As you add more pictures " append detail "you can see how the finished album will look. " append detail "If you change your mind, you can delete a picture or replace one with another.\n\n" append detail "When you're satisfied with the layout, you can get a manifest " append detail "listing all the pictures used in the album and on which page. " append detail "It will also lists those images which still need to be cropped to " append detail "a 4x6 size ratio (see https://wiki.tcl-lang.org/PhotoCrop)." tk_messageBox -parent . -message $msg -detail $detail focus . } ################################################################ namespace eval ::Manifest {} proc ::Manifest::Show {} { global ALBUM destroy .manifest toplevel .manifest wm title .manifest "Manifest for $ALBUM(title)" set manifest [string trim [::Manifest::Create]] set lines [llength [split $manifest "\n"]] set height [expr {min(30, $lines)}] ::ttk::scrollbar .manifest.sb_y -command {.manifest.t yview} -orient vertical text .manifest.t -height $height -width 80 -yscroll {.manifest.sb_y set} -wrap word grid .manifest.t .manifest.sb_y -sticky news grid [::ttk::frame .manifest.f] - -sticky ew grid rowconfigure .manifest 0 -weight 1 grid columnconfigure .manifest 0 -weight 1 ::ttk::button .manifest.f.save -text Save -command ::Manifest::Write ::ttk::button .manifest.f.close -text Close -command [list destroy .manifest] pack .manifest.f.save .manifest.f.close -pady .25i -expand 1 -side left .manifest.t insert end $manifest .manifest.t config -state disabled } proc ::Manifest::Write {} { set manifest [::Manifest::Create] set manifestFile [FullName "Photo_album.manifest"] set fout [open $manifestFile "w"] puts $fout $manifest close $fout tk_messageBox -message "Write Photo_album.manifest" -detail $manifestFile focus . } proc ::Manifest::Create {} { global ALBUM set manifest "Manifest for $ALBUM(title)\n\n" append manifest "Album directory: $ALBUM(dir)\n" append manifest "Pages: $ALBUM(pages)\n\n" set unCropped {} set allImages {} for {set pageNo 0} {$pageNo <= $ALBUM(pages)} {incr pageNo} { set onThisPage "" foreach pocket {top left right} { if {! [info exists ALBUM($pageNo,$pocket)]} continue set fname [FullName $ALBUM($pageNo,$pocket)] if {! [file exists $fname]} continue lassign [GetImageSize $fname] iwidth iheight set ratio [expr {max($iwidth,$iheight) / double(min($iwidth,$iheight))}] set is4x6 [expr {abs($ratio - 1.5) < .01}] if {! $is4x6} { lappend unCropped $ALBUM($pageNo,$pocket) set 4x6Marker " *" } else { set 4x6Marker "" } append onThisPage " $pocket: $ALBUM($pageNo,$pocket)$4x6Marker\n" lappend allImages $ALBUM($pageNo,$pocket) } if {$onThisPage ne ""} { append manifest "Page $pageNo\n" append manifest $onThisPage } } append manifest "\n" if {$unCropped ne {}} { set uniq [lsort -dictionary -unique $unCropped] append manifest "Uncropped images ([llength $uniq]):\n" foreach fname $uniq { append manifest " $fname\n" } append manifest "\n" } append manifest "All images ([llength $allImages]):\n" unset -nocomplain cnts foreach fname $allImages {incr cnts($fname)} foreach fname [lsort -dictionary -unique $allImages] { if {$cnts($fname) > 1} { append manifest " ($cnts($fname)) $fname\n" } else { append manifest " $fname\n" } } append manifest "\n" unset -nocomplain MARKS foreach key [lsort -dictionary [array names ALBUM mark,*]] { set iname [lindex [split $key ","] 1] foreach mark $ALBUM($key) { lappend MARKS($mark) $iname } } foreach mark $::S(marks) { if {[info exists MARKS($mark)]} { append manifest "Images marked '$mark':\n" foreach iname [lsort -dictionary -unique $MARKS($mark)] { append manifest " $iname\n" } append manifest "\n" } } append manifest [::Manifest::CopyScript $allImages] return $manifest } proc ::Manifest::CopyScript {allImages} { if {$allImages eq {}} { return "" } set script "\n# bash script to copy images used in the album to ./toBuy\n" append script "mkdir -p toBuy\n" append script "rm toBuy/\[1-9]*.jpg\n\n" set longest 0 foreach iname $allImages { set longest [expr {min(45,max($longest,[string length $iname]))}] } set fmt "cp %-${longest}s %s\n" set idx 0 foreach iname $allImages { incr idx set dst [file join toBuy "${idx}_$iname"] append script [format $fmt $iname $dst] } return $script } ##+########################################################################## # # ::ShadowBorder::MakeShadowPhoto -- creates an image with a shadow border # see https://wiki.tcl-lang.org/ShadowPhoto # namespace eval ::ShadowBorder {} proc ::ShadowBorder::MakeShadowPhoto {imgSrc imgDst} { ::ShadowBorder::_MakeBorderImages set w [image width $imgSrc] set h [image height $imgSrc] set w1 [expr {$w + 25}] set w2 [expr {$w + 50}] set h1 [expr {$h + 25}] set h2 [expr {$h + 50}] set imgTmp [image create photo -width $w2 -height $h2] $imgTmp copy ::img::border::TL $imgTmp copy ::img::border::T -to 25 0 $w1 25 $imgTmp copy ::img::border::TR -to $w1 0 $imgTmp copy ::img::border::L -to 0 25 25 $h1 $imgTmp copy ::img::border::R -to $w1 25 $w2 $h1 $imgTmp copy ::img::border::BL -to 0 $h1 $imgTmp copy ::img::border::B -to 25 $h1 $w1 $h2 $imgTmp copy ::img::border::BR -to $w1 $h1 $imgTmp copy $imgSrc -to 25 25 if {$imgDst in [image names]} { image delete $imgDst } image create photo $imgDst -width $w2 -height $h2 $imgDst copy $imgTmp image delete $imgTmp return $imgDst } ##+########################################################################## # # ::ShadowBorder::_MakeBorderImages -- makes 8 images which forming the shadow # gradient for the four sides and four corners. # proc ::ShadowBorder::_MakeBorderImages {} { if {[info commands ::img::border::T] ne ""} return set gradient {\#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#8d8d8d \#999999 \#a6a6a6 \#b2b2b2 \#bebebe \#c8c8c8 \#d0d0d0 \#dadada \#e2e2e2 \#e8e8e8 \#eeeeee \#f2f2f2 \#f7f7f7 \#fcfcfc \#fdfdfd \#fdfdfd \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff \#ffffff} image create photo ::img::border::T -width 1 -height 25 image create photo ::img::border::B -width 1 -height 25 image create photo ::img::border::L -width 25 -height 1 image create photo ::img::border::R -width 25 -height 1 image create photo ::img::border::TR -width 25 -height 25 image create photo ::img::border::TL -width 25 -height 25 image create photo ::img::border::BR -width 25 -height 25 image create photo ::img::border::BL -width 25 -height 25 for {set x 0} {$x < 25} {incr x} { ::img::border::B put [lindex $gradient $x] -to 0 $x ::img::border::R put [lindex $gradient $x] -to $x 0 for {set y 0} {$y < 25} {incr y} { set idx [expr {$x<5&& $y<5 ? 0 : round(hypot($x,$y))}] ::img::border::BR put [lindex $gradient $idx] -to $x $y } } ::img::border::TL copy ::img::border::BR -subsample -1 -1 ::img::border::TR copy ::img::border::BR -subsample 1 -1 ::img::border::BL copy ::img::border::BR -subsample -1 1 ::img::border::L copy ::img::border::R -subsample -1 1 ::img::border::T copy ::img::border::B -subsample 1 -1 } ################################################################ # # Text boxes # proc CreateTextBox {side} { lassign [.c bbox $side,message] x0 y0 x1 y1 set w [expr {$x1 - $x0 - 5}] set tag $side,text .c create text $x0 $y0 -tag $tag -width $w -anchor nw -font $::S(text,font) .c move $tag 3 2 return } ################################################################ # # Thumbnails and quick view generation # namespace eval ::Indexer { variable fileList {} variable done "" variable status "" } proc ::Indexer::DoDisplay {} { ::Indexer::WhoNeedsIndexing destroy .indexer ::ttk::frame .indexer ::ttk::label .indexer.title -text "Indexing pictures in\n$::ALBUM(title)" \ -font $::S(title,font) -anchor c -justify c ::ttk::label .indexer.title2 -textvariable ::Indexer::status -font $::S(text,font) -anchor c ::ttk::scrollbar .indexer.sb -command {.indexer.lb yview} listbox .indexer.lb -listvariable ::Indexer::fileList -yscrollcommand {.indexer.sb set} \ -width 50 -height 5 ::ttk::button .indexer.cancel -text "Cancel" -command {set ::Indexer::done cancelled} pack .indexer.title -side top pack .indexer.title2 -side top pack .indexer.cancel -side bottom -pady .2i pack .indexer.sb -side right -fill y pack .indexer.lb -side left -fill both -expand 1 place .indexer -relx .5 -rely .3 -anchor c } proc ::Indexer::WhoNeedsIndexing {} { set ::Indexer::fileList {} for {set idx 0} {$idx < [llength $::ALBUM(files)]} {incr idx} { set iname [Index2Image $idx] set thumbName [GetCacheName thumb $iname] set qviewName [GetCacheName qview $iname] if {! [file exists $thumbName] || ! [file exists $qviewName]} { lappend ::Indexer::fileList " $iname" } } } proc ::Indexer::IndexAll {} { ::Indexer::WhoNeedsIndexing if {$::Indexer::fileList eq {}} { destroy .indexer ; return } ::Indexer::DoDisplay update set ::Indexer::done "" after idle [list ::Indexer::IndexOne 0] tkwait variable ::Indexer::done ::Indexer::Done } proc ::Indexer::IndexOne {idx} { variable fileList variable done variable status if {$done ne ""} return while {$idx < [llength $fileList]} { set iname [string trim [lindex $fileList $idx] " \u2713"] lassign [::Gallery::MakeThumbnail [FullName $iname]] . didThumb lassign [::Gallery::MakeQViewImage [FullName $iname]] . didQView lset fileList $idx "\u2713 $iname" incr idx if {$didThumb || $didQView} break } set status "[expr {$idx+1}] of [llength $fileList]" if {$idx >= [llength $fileList]} { set ::Indexer::done done return } .indexer.lb see [expr {$idx + 1}] after 100 [list ::Indexer::IndexOne $idx] } proc ::Indexer::Done {} { destroy .indexer.done set txt "Indexing $::Indexer::done" label .indexer.done -text $txt -font $::S(title,font) \ -bd 2 -relief solid -padx .25i -pady .25i place .indexer.done -relx .5 -rely .5 -anchor c after 2000 [list destroy .indexer] return if {[catch {set alpha [wm attributes .indexer -alpha]}]} { after 2000 [list destroy .indexer] } else { wm attributes .indexer -alpha .99 for {set i 0} {1} {incr i} { set when [expr {2000 + $i * 50}] if {$alpha <= 0} { after $when [list destroy .indexer] break } after $when [list wm attributes .indexer -alpha $alpha] set alpha [expr {$alpha - .2}] } } } # See http://plainicon.com/ image create photo ::img::manifest -data { iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABb0lEQVRYheXXsStFYRjH8Q8ZJOmuFhlMBg wWJbNk8G8oMvoDTJRBYuGPUKKsRiEZTAYZXJMkycZwDvfKdd733ve63fjW2+mp9/ec33ne5zydw3+no469 45iJ3HuI01QDq+iriocxFWngGFdV8ROWI7WflPHWpFX+6SadARMPmMjXWh3m16p0D0Ubi47gw3V/fu1FKd LAI55/yPOFrsiE8oTPwV11EmughCHc5WtIuBrXsko0TFmlfLOyZlrJ433hxputkecbsRW4xS7O8vgI9xGa JAqdNytPbAVGsIgD7GEBowHNJi4j89ekJT0QOwdS3oLCOVBES3ogNIo/SDmCQmKb8AkXKk9yk8chTRJt9R oOyD5GznGCaQwGNIcSh1FbjeJLzMsqAFuyoRTSJNFWPTCFDexgWzZmJwOaJdm3YSGxBvowpjLNBvM4pEmi rSbhrxE6gm7MJd6jG6+NCFvyX1BUgXX0NOK8Bi9NyvMHeQe4aac35w1N9QAAAABJRU5ErkJggg==} image create photo ::img::thumbs -data { iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA6ElEQVRYhe2VMQ6CQBBFXywtjUeh4kCewc SSYG9hYSg4CDW1ByAcgpoCi4G4EHCJw7LN/mSqZfPe7gwA2ycCMqAG2r7ewMkBa5QjkAPdpBog3gNe+oKD x5OD9NwbHGTgpgLXlXs3Gcx6RqBYsS9G3g512hmBFjhb4E3/nBOBDrhY4IOoOtWCQNVLmDdhwjukfeq8Fg TMdhTIYDaTtWwLgadF4FdFWniqgOdaeKKAl8jn2ws8dw1/IINZ8f0d18jAqXtugydagAaeBniAB3iAB/g/ OVjW78DNpcCQuVtwenKbxO5wU2JX+Ac2hgXUUMkWNAAAAABJRU5ErkJggg==} image create photo ::img::undo -data { iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABGklEQVRYhe3VvytGURzH8ZcnGWQyGJ4kSQ aDZPBHmGQwGSSzyVP+AZPBX2CUwWgy+wNsSgZRKHqSDBJ6DPe5deN6nnN/bM67vnVPnXM/784593uJRCL/ nYGS6xqYwTRG8YlHXOC+rMxUwJxFHOAJnT/qCruYKCpw3g3IYxInPULz6h37GAkVuMZzjsQyXgqGZ+sSs6 ECnR8S6/jqE3CHPSxJ7sU45rGGI7yhjYVQgVRiGx89gl+xhaE+723iEA9duSCBfnUjcFszbOC0DoG25PMr wyZWqwqslAxP2akicFYxnKQ/zKWDRsHFxzUI3Eo65y9CdiCvT5RhrKxAXRLDVQTqkMg9+iICtR3HYOa5pc CPo0uzqkAkEol8A6tbq0l7zHtVAAAAAElFTkSuQmCC} image create photo ::img::nextpage -data { R0lGODlhIAAgAPEDAAAAAMDAwICAgAAAACH5BAUAAAMALAAAAAAgACAAQAKCnI+pywz9gAgL2IuzFnE2pY WW95Xm+RgXIrQSdQ6RmlqyTMb6zutp9er9QKrcQYTEHI2HgPMJdV5agVUwNrNptUxT9AvticfkcikpGUey 2e75ZrXhYFh4TYZxO+wrfJWJFjin0FJoaJjRskRn8jPilHCFgqJXQclocqhpiGnm+VlWAAA7} image create photo ::img::prevpage -data { R0lGODlhIAAgAPEDAAAAAMDAwICAgAAAACH5BAUAAAMALAAAAAAgACAAQAKAnI+pyxMCRmSzoRcBBLz7/y UYSFbWiZ6YwCLdYaqQJE2ckaX6zvfL2IjJbMEdBlZKvi6zzmMDCEinVOmvecvmhI2ql+oLi8dkH7TUO2a2 OeOstt66Qza5DhO4wbO0O1YJKPI2wPLBcoiIqHB0AYQzl8LFwHgieZWIeVi2ydl5UgAAOw==} image create photo ::img::info -data { iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB4ElEQVRYheWXsUscQRSHv1v28R7WcqTJEY KFiII2wUKIRdpAiE2KgDYpIkQrG8Fa9DguEEgaQa3CpU0RyJ9gmUJCIApGRIKFpNBZV9Ri9+A8uMOdPTfF /WBh5rHzft/Ozr6ZhX5XCUDUAuAJ8KAg32NgJ47cVUnUysBXoALsAVf3bB4Aj4ED4HlJ1BrAJTAbR+7yns 0BELUQ2AZCRO1c1IaKMG6DGBK18wAw4NQjwbSo/U6vaQ+GU8BCj4FN1YEPLe0JnyR5AM6Ahy1tLwU5AN4A o8BI2vaS1wykn24ZWE1DZVE7iSP3txAAYAmYIykokBSwrTSeSXlewUYcubE4cmPAhm+SPAAvRK2eY3wugE 3gM/D6vwDEkdsFvuQ19wYQtUlgsS08JWoLhQAAMyTbdzXtfwN+AWtFAQD8AL6L2jjwD2j4JPGtA3+AV8Cz tvhu5kyidi1qg54g3hK1QVG7DoAjkppetEaBo5BkIW2K2jvgZ5cBh3HkXLMjapAcrXzW0TDJVl4NgffARQ rS6VA6AKwA6y3mNWAecB3GdNNx6vfxTneL2idRW26ai1pN1PZFreJhfkuZpq/lyV8CT+PIHRQJEPTaHLLV gbcka6Vn5pBtBnpufmeJ2iNRK+q3rc90A9iKayTqe99XAAAAAElFTkSuQmCC} image create photo ::img::open -data { iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACHElEQVR42u3XTUgVURjG8Xf6Iij6WNSiTU iLFm0qgoiSFn3sQiiUoMCwWhRRQhDlJpRSIqhFlJuyXIQoERFBuCqpKBKLdoEbaRERklgZopT9H+a9NEyj 3ns517u5L/zweObeO885Z+YwE1lcu3EJm7HQ/q8pfEMPzuO7BarIT/4UC/L8zmtU43eoAG+wtcDvHcDDUA EmLHvaZyotxf0izvcLby2xhAowFWIkBdQPnMa9cgVQ/cFOvEwG+IBjnrBUtRbdWGHxEh5OBtDtdWUORv8Y +zR6VCcDnMPVOQjwCDV4hR2VAJUAlQDlCHAHDeUMoDqIOuwvZYAtGiFWWby9v8MzTPrxxRgvRYDtuIFNGc c+owmduY7QAepx22Z/umrHydABNPLnqZP/xDjmY2Xq82dxLWQArXF62o+iA+vxMXVsDFWhAuiC68/onymA qlEBJn3aLqKlyACNuO5tTfsZb7/AIJZb/CCrOpWYqQcK8B4b8RWX/QfyrQ7/24oL3h62+NabrnI7oaov8n /0iD2viJEvs/ge19NUm/eNYJu3v2AUi1DlfTexy9tPIm/U+g+sKzCA1n4Ae9GbcXy2a6AlSnVorfJ9QzIf fe69YghrCgigJ+MNkYWrI7ib6tP19QlL7d+050ob1vGQAVS3cCKPz+n9cg/GQgdQaYdrxpKMY5p2LYluW2 1EVooAKt2Ghyzenldb/C6onbLLUtfCXz4DlGzydueYAAAAAElFTkSuQmCC} image create photo ::img::nextimage -data { iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6Jg AAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfgBxMPLxew j2/UAAACeUlEQVRIx6WVTUhUURTHf/fNmzHTzD6GRPIjB6pFEuXGwBZCJIZCMBCBlGGLCrLVRPSh46YIjE A3tTNbmKskghZFZFFQ0JeSVAoqNeRoilAizNh4WsyzYd67bxzo/Vf3/s/5n4977n1K+L/P1G8v8UbG+WSt 8tnJXsqVzljZM4jxWQYY5JfDtIQgNQRURoEFumSQny7peiijhTrldRN4IV2MrVKzQS0XKFIagVfSznRWja slRJmyCTyXjizdAXZzSxVaGVm13ySK2NBAhWMviY/0ShywjjFGp3yxxVjHRYJEOcY3bQ69BKRBWRmMyFNH DA9+oIh2SrQ5LHAnVUI/UyRsmCXEEMvsp5NCB5sgwQeeCRgQ54k2xiyXGAb2EGKj1uIxfzDgpcyzrMUIp5 kEjtCGV8MPEREDxlw6LQhTtDEBHOIMOZocZzHgvUv8JAZpYQKTs7SibNwUUQwQbYtS+MpVZjA5yWEHJxhx YhlKWBmbCJBLHXk2BkwfXlZ7VBrZAczRzW8HZ0I+yxmcc2kiDEQ4xzsbl4MXEyozZtDEeSDOZV477PxswY R9JFycvTRyDZjnCg81fDFbMWG7KpdxrcABwgBc5762zCo2KQNMmjEcnTeop4cSFrnBbRY1Z+PjBEbyMh0k 4BggP634gLt0u4xYPdsUWGp9UiB5aSiQ4zIq/bbdFKpkWARZedLiNMsDR427mHZ9oXsIJv8TKxV9l6DkZI n10iH/ZjHVlDGpEV9WCMucTkCYk7BsEDMjKqVP0m5D+tHEuCfV4nHBWjkqQ7KU5qF0Y/xIBnjLDD+s9RqK KKWaU1Qou63S34MlJiUl4KOYUjYrj8byL5AmGwIJZMc9AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTA3LT E5VDE1OjQ3OjIzLTA3OjAw8g23QwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0wNy0xOVQxNTo0NzoyMy0w NzowMINQD/8AAAAASUVORK5CYII=} image create photo ::img::previmage ::img::previmage copy ::img::nextimage -subsample -1 1 # Icons for thumbnail markings: http://www.iconsdownload.net # Names must match S(marks) image create photo ::img::Check -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAX1JREFUOI2tky 2uAkEQhL/ZbAICFG5vACfgAFg84QAgkFwChcJikGgSBA4LAonAo8CQLAlhqSdgh/0Zfl5CJSN2pru6urrX AOIBH0C6f3sAYRgCYOIwL/Us6f4U3wJcLhd4xKtcLiuGJUnCT6YaY57kANPplPl8/lQXR8RZKcmWJivJBW PMs04WQRDYUilBk8mE6/VqGYrFYq68bbhWqwnQarVSEoDbDKfObwOdVuTY3nXd7XZpNBqpO2VxOBwEqN1u p5vJTSEzqlTp0WgEQLPZBOB0OuXkaDweC1CpVBKgfr+f89Fq3Gw2AuT7fk7zS40ue34/mW9h3ZH09YmiiP V6TRRF9i4J4RjiK8xms9hVAdput0mn9S/CVqtlE4Mg0PF4zI7uO8L9fq9KpWKTOp1OLsZJuFgs7EO9Xtdu t7NLBcjzPC2XS2dRJ+HtdtNwOFShUEj5BKharep8Pr/s4mPLYRiq1+vJ8zwNBoO3liQJ7R7qw3Z/QvzH/n yx/wA9Vzt+ahTfrwAAAABJRU5ErkJggg==} image create photo ::img::Animal -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAWxJREFUOI29k7 HOKVEUhb8tEhGFZp5AIhKJ8A4imknEA5jCG6i0XoN4CgodpU4rGoVEpZiKCMW6BeaaMX7j5s9d1WTvddY6 6+w9Bog7UgCtVgtJGCDp1rQHLS2JyWSCPXohXkgJwMxIpVJhkWeko0fN7HbccRwGg8HfjiR5nqf7pwBZ1N rM4q8UReDzjGazyXq95ng8xucB2O/3nM9nyuUyvV4vrJrUOvYxYslJiWlu3h+tX1K/wwvR8zxc1/2Z2G63 yWazzGYzut3uC1kP7HY7ARoOh+p0OkGde+BoQYBWq1Wono5IJw/zDr8/maQIsnya4ieYGZAg8+l04nK5fC Ue/H/P2Gw28n1f+XxepVJJ/X5fgJbL5Qs3MtJ4wWKxqHq9Lt/3NRqNtFgsNJ/P5bquAF2v1+8Et9utarVa aJkymYzG4/G/3XA6ncpxHBUKBTUaDVUqFeVyOVWrVR0Oh7eCwdr8tyl/LcwvL/Yf949rQErZxW0AAAAASU VORK5CYII=} image create photo ::img::Family -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAqZJREFUOI2tVD 1LK1EQPbNczDVg1i9itUL2iWKhorXiDxB2QbAWRSy1iLV2BgtF7QTBKgQstbKy1FIUIkIkiiBYCUGjkI3n FXFvsjx98cE7sMXcmXvOzJ2ZFQDEJywAyOVyEBEIAJI1p4RhCiJo1RoolUoM/czlcmwEANBxHF5dXZEkLy 8vKY1aRpMkFhcXQRLxeLx2WqlUCBGSpOM4BEBLKQVLBGJZmJ6erkWSZLlcZqVSCZUZSf07iAhUaKysrKCt rQ1ra2sAgMfHR2xsbGBiYsJc4MvLCyFCiHBmZoZbW1ssFouECI+PjxmWR5L0fZ+xWIwkOTAwQJJcXl5mPp +v5/jXBMM8fxqoPrWbVm2Fxvz8fMR5e3uLdDptbAsAgiBAKpXC6empcaTTaZRKpchluq7L7u5u07dCocCR kRFChGdnZ/XnOTo6IkQ4NTXFzc1NJpNJ+r5PiPDt7a3WbAA4Pz/H2OgotNY4PDzEzc2NkdNa16VJMpPJsF gscnJyks/Pz4bxj6H4+PiAZVkIggBKKVSrVZCEUqo++M2f+x8681OYxpAESby/v2Nvbw/5fN6cffWVy+WI 3Qh+1k2SvLu74+vrKwuFAr/D09MTh4aGuLu727jP9f6FhPv7+4QIOzs7KSLs7+9ntVqNkC0sLBAijMfjhA hd12UQBIbQlHxycoLV1VW0ag3btkMVjI+P4+HhAQcHB5idnQVJ+J6HRCIBoDae2Wz265KXlpbMxkKEWmve 398zmUwSItze3jbL6nkeIcKLi4tIyVYj887ODnzPQ2Z9HbGWFlxfX6O3txeDg4OwEwnMzc2Z2FQqBQBQSj VSIEIIALZto729HT09PeYP2NHRgeHhYVhWLdxxHPT19eGX66Krqyty38xhs+VvBhGJEv4v/AbE0d1CK1hV vAAAAABJRU5ErkJggg==} image create photo ::img::Friends -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAATNJREFUOI2tk7 HNgzAQhb9DSHQULJAlGMJdZsgWdKTMDFCwQkoqtjEjkPL+4o8d2xCCojzJsu7u+fx8vhNAeSIjgDdUFXG0 LAr7UNd1AOR5zuVyefEdC4idPo+Lru5OAxnA/X6PnKKqKiKx8//0Om9EEiF3xvV6pWkabrcbAMYYxnHEGB NnFBHcvoWohCHJydktxqbOo8Q8TP82m8hLo4gwDAMiQt/3fnfIAKZpom1bTqcTbdtireXxeGCt/awxLVm2 RdpC1AHho9IGeMZVwz1cznf46t//zFFEI6eqzPNMXdfUdc08zx9td26VMMWyLJRlufKXZcmyLLsqfVWrqt J3tltFUUS/ktq+hulQ7Q3ZHjd68vl8Zs8+wokUwnqqU5Vb6sJzhxvxKFYKv070VPjzxv4D/C7b293NwLMA AAAASUVORK5CYII=} image create photo ::img::Best -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAWtJREFUOI2tkj GugkAQhr/dkGhhQ4KJd/AG9lYeQCruoZUVidELYMkdrEzUws64nZ2dHYm9MYZ9xQN1YUHfi38Fwz/fzDAj AE0mCXC9XtFaIwCt9e9HkducPCLzJMNnkADO5zNhGJqQVznFVCHEM300GpWZrVbr6S6WFkLYWyrKqPOoIS Wz2YzNZmOfJ9fhcKDX67Hf70tk/arT6aTn87kRA7T1Z1j7/NToZLXrabapq2QY1+s1YRgipT3fmHA4HOqi yOZ4BKbTqd7tdlopVTIaNe73O9vtFs/z6nscj8c0m01ut1t5cj48iu9v5lM5+cO7Nt9JCAFYzjZXmqYEQU AQBKRpWnqvUiVQSkkcx7TbbRqNBovFgk6nQxzHlRdTCwRIkgTP85hMJlwuF1zXJUmSupR6YBRF+L6PUgql FL7vE0VRLfCxZdtSVqsVx+ORwWAAwHK5pNvt0u/3y6BsKbXAv+jtlv8N5suH/QMpe+ZfvAITCwAAAABJRU 5ErkJggg==} image create photo ::img::Trash -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAiJJREFUOI2tUz Fr8lAUPTdoq4NulVZcslh0ce/UQSgt7g4uguJfcPYvtHXqIIhdXLoILiUupUORQilkjVDQDoUMDtFAcjrU vDatpeHjO/BI3svJOe/ed54AIDbQAGBnZwfkxxoXiwVJUgKaRhIiAm02m8H3/Q8eSVqWxQACgIZhAABWqx VOT08hX72UJ0mQRCaTUb5a8LVer6NarQIAYsFiPp/H7u4ulPXd3R11XWer1WLgwe8YjUah+a9EEaGI0HEc RVQbAgDXdQEAjuMAABKJxGc5wcvNzQ0sy8J4PEYqlYLv+9C0T52tzdiGyERtU40aw+EwNA+6+6PqdDpN27 b/bk+326WIsFKphIih9pyfnyObzQIAyuUy1ut1aJ/qz9fXV5JkMpmkrushRdkQwxVu+hfETkQ+U/Hy8gLH cbC3t4d2u42DgwOYpolisRi2HgwGvLy8ZKPRoO/7JMlarRa2jtLwyMSoUG38fmrBMAwDIgIRgWmav/K+gt iS8a9YLpfs9/s8Pj7m1dXVVk6g86fgxcUF4/E4M5mMuki5XI5nZ2f0PC+64HQ6ZSKRUNf29vZWCTabzX/f YYC3tzfGYjEl+vT0FE3w+vo6RAzmJycnPDw8JEkeHR2xUChEE7y/v+dkMqFt25xOp3x8fGSv16OI8OHhgS Q5n8+paRo7nc4PQZVDbo7e8zw8Pz+rCJRKJdi2Ddd1sb+//2v+ROTjif8c7HdjU8NBxfiKuAAAAABJRU5E rkJggg==} image create photo ::img::Other -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAWpJREFUOI2tlK GuwlAQRM/CDZBgyh/gEVgcCQJQSD4Kh6rkMzAkQMCB5wNwIEg9yTxBb/Pa3sLLC2Oabmfnzt2d1ACRogaQ JAmSMEDS66N5Ws1Xary+kz6lwWCg0+kk3yRJSpJEHvb7rOxMSUii3W5nekjSfr/PWgHhX+bzeVa0zGIKM8 tbr4KZvS7hMRqNGA6H3G43zKzUoCIAxXGcN1kkHg4HFWtAeBhBn38luvTs92rFW282G5rNJtPpNNiQM/14 PNTv9zWbzXL1nOJ2uyWKIsbjMdfrtVpRkuI4VqPRKI2nRJxMJsEFlIiALpdLqeYCPsIj4tub+Ssyi5+2+A k+vrUqwm63wzmHmeGco16vY2a0Wi3O5/Nb8VKcJSmKIgFar9e53QLqdrtVq1OlYBHL5VJmJkCr1er/gs/n U51OR4COx2Mlz+uUclPE/X5nsVgA0Ov1PtGrZ+jhfxkEkhxymOVQX4rN14P9Ax1sKGnJY+6aAAAAAElFTk SuQmCC} image create photo ::img::Underwater -data { iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAAYagMeiWXwAAAXJJREFUOI21k7 GqwkAQRc+KgiCksFFsLQQ7P8HSXxDt8kPRz7AWQ2r7FCmsLBWxEURQwTiv0OzLxo0Gnu/Cwk5mdubO3IkC hCdKpFAGEHk4VRKmQ8qGKzHSkOQEQSCO44hOYpTJPlVKwWKxkHQKQAx2y+XyEZ0trZTKp5QNLCU9DodDNp sN/X6f1WrFbDZjMpkQRdFvP1lSSikZj8fieZ40m83HN0COxyPr9Tq3dK/XwzoMK8+igYZgudmSrougBBBF EdPplPl8ThiGDAYDttsto9HIqPaiV71e13ff9wWQsq3M9XolCAJ833+fsdvtiojIbrfTYihAwjDMbaLdbu M4TvGl+L4yRaGn84nmJyilAF6Vvlwuhn0+n3N9WVuT4/lPY5GoyEm/1QzjOH7bUqPRoFarWX33+13fCy/3 fr/H8zwOhwMiQhzHnE4nKpWKMX/rytogIriui+u6dDodWq0Wt9uNarVqxOm1+TeV/4qvL/YPdVjl/2tjzW 0AAAAASUVORK5CYII=} ################################################################ if {[auto_execok convert] eq "" || [auto_execok identify] eq ""} { wm withdraw . tk_messageBox -icon error -message "ERROR: Photo Album require Image Magick to run" exit 1 } if {$argv eq {}} { set argv [pwd] } set dir [lindex $argv 0] BestSize DoDisplay DrawPage update set isNewAlbum [::Album::Read $dir] if {$isNewAlbum} { About $isNewAlbum } ::Indexer::IndexAll ShowPages 1 ::Gallery::MakeWindow return