mapivi.pl0000775000000000000000000555151613007661042011424 0ustar rootroot#!/usr/bin/perl # the line above could be the first line for a typical UNIX systems # you can find perl on your system by using "which perl" in the shell # to build an exectuable for windows use this PAR call: # pp -M Tk::DragDrop::Win32Site -o mapivi.exe mapivi.pl # to build an exectuable for Linux use this PAR call: # pp -M Tk::DragDrop::XDNDSite -M Tk::DragDrop::SunSite -M PerlIO -M Image::Info -o mapivi.out mapivi.pl # to check the code against Perl coding standards # install Perl::Critic module # execute: "perlcritic -5 mapivi.pl" or: "perlcritic -4 -profile test/perlcriticrc mapivi.pl" # instead of -5 (list only most severe warnings) you may also use -4, -3, -2, or -1 (list all warnings), see also "perldoc Perl::Critic" or "perlcritic -man" # include perl packages use strict; use Encode::Unicode; # needed according to the PAR FAQ (for perl apps on Microsoft Windows) use warnings; #use diagnostics; # pod (to view the formated document try "perldoc mapivi" in the shell =head1 NAME Mapivi - Picture Viewer and Organizer Mapivi means Martin's Picture Viewer =head1 DESCRIPTION JPEG picture viewer / image management system with meta info support written in Perl/Tk for UNIX, Mac OS X and Windows. I wrote mapivi just for me, because I needed a image viewer which is also able to display and edit meta infos of JPEG pictures, like EXIF, JPEG comments and IPTC/IIM infos. As hobby photographer I am mostly interested in the EXIF infos (like timestamp, camera model, focal length, exposure time, aperture, etc.) and the possibility to add and edit IPTC infos and JPEG comments. But I also want to rename pictures according to their internal date/time and to do lossless rotation, lossless cropping and other stuff. mapivi can be found here: http://mapivi.de.vu (link to the mapivi site) or if this won't work: http://herrmanns-stern.de (real site) http://sourceforge.net/projects/mapivi (download) I would be happy to receive some feedback (e.g. on which os mapivi works), bugfixes, patches or suggestions about mapivi. Copyright (c) 2002 - 2016 Martin Herrmann All rights reserved. Feel free to redistribute. Enjoy! =head1 USAGE mapivi [-i ] [file|folder] to display a certain picture use: mapivi picture.jpg mapivi will generate and display all pictures in the folder as thumbnails. The given picture will be displayed in original size or zoomed to fit the window (picture frame). to view a folder containing pictures use: mapivi ~/pics/ mapivi will generate and display all pictures in the given folder as thumbnails. to start mapivi with the import wizard mapivi -i =head1 KEYS mapivi is controlled by the following keys: see also menu Help->Keys (the list is generated from the source code and is always actual.) =over 4 =item Space, Page-Down Show the next picture in folder =item BackSpace, Page-Up Show the previous picture in folder =item Escape Iconify Mapivi (Boss-Key :) =item Cursor-up, -down, -left, -right Scroll the picture, if it's bigger than the Canvas =item Shift-Cursor-up, -down, -left, -right Move to the border of the picture, if it's bigger than the Canvas =item q Quit Mapivi For all other key bindings, see the menu Help->Keys =back =head1 MOUSE Try the right mouse button in the thumbnail picture list for a popup menu to copy, move, rename, rotate or delete pictures, to open a new folder, to add or remove comments or to exit Mapivi. Use the buttons to add, edit or remove JPG comments, or to display all EXIF infos. If you hold the mouse over the buttons or labels a help message will pop up (or at least at most of them :). =cut ############################################################## # load basic modules use Env; use File::Spec; # determine full path to mapivi script or executable, should be done before e.g. a chdir() takes place my $mapivi_file = File::Spec->rel2abs($0); # boolean, if we run on Windows this variable is set to 1 my $EvilOS = 0; $EvilOS = 1 if ($^O =~ m/win/i); my $MacOSX = 0; # boolean, if we run on Mac OS X this is 1 if ($^O =~ m/darwin/i) { # Mac OS X is not evil, but unfortunately contains the string "win"! $MacOSX = 1; $EvilOS = 0; } my $home = get_home_path(); die lang('Mapivi can not find a home directory') if (!-d $home); # this path is used for user specific data, like the search database, the keyword tree, the configuration, trash, etc. my $user_data_path = get_user_data_path($home); my $conf_file = "$user_data_path/mapivi_conf"; # the configuration file my $configFile = "$user_data_path/mapivirc"; # the old configuration file my $trashdir = "$user_data_path/trash"; # the Mapivi trashcan my $iptcdir = "$user_data_path/IPTC_templates"; # the IPTC templates folder my $plugin_user_path = "$user_data_path/PlugIns"; # the mapivi plugin user dir my $file_Entry_values = "$user_data_path/Entry_values"; # needed to store completions for Tk::MatchEntry my $thumbDB = "$user_data_path/thumbDB"; # path to thumbnail database (when thumbnails are stored in a central place) my $searchDBfile = "$user_data_path/SearchDataBase"; # path to the search database file my $collectionsFile = "$user_data_path/slideshows"; # path to picture collections/slideshows file # this path is used for Mapivi icons, languages, plugins, html templates etc. # for Debian and Ubuntu we use /usr/share/mapivi, other UNIX distributions may need other paths my $program_data_path = '/usr/share/mapivi'; if ($EvilOS) { # in Windows we store the programm data in the same folder as mapivi $program_data_path = dirname($mapivi_file); $program_data_path =~ s!\\!\/!g; # replace Windows path delimiter with UNIX style \ -> / } my $icon_path = "$program_data_path/icons"; # the icon dir my $thumbExample = "$icon_path/EmptyThumb.jpg"; my $lang_path = "$program_data_path/languages"; # the localization / languages dir my $plugin_sys_path = "$program_data_path/PlugIns"; # the mapivi plugin system dir my $logo = "$program_data_path/pics/logo.jpg"; my $exifdirname = ".exif"; # the subdir to store exif infos my $thumbdirname = ".thumbs"; # the subdir to store thumbnails my $xvpicsdirname = ".xvpics"; # a subdir from GIMP we usualy ignore ############################################################## # load optional module (as soon as possible) my $splashAvail = (eval {require Tk::Splash}) ? 1 : 0 ; my $splash; if ($splashAvail and -f $logo) { # Splash->Show parameters: $image, $width, $height, $title, $overrideredirect $splash = Tk::Splash->Show($logo, undef, undef, '', 1); } ############################################################## # load modules use File::Basename; use POSIX qw(ceil); use Cwd qw(cwd abs_path); my $verbose = 0; # boolean (1 = print debug infos, 0 = be quiet) # set version my $version = '1.2'; # get version and date from subversion (SVN) # this works only if you enable subversion's keyword substitution on your machine for this file: # svn propset svn:keywords "Rev Date" mapivi.pl my @svnversion = split / /, '$Rev: 356 $'; my $svnrevision = ''; $svnrevision = "($svnversion[1])" if defined $svnversion[1]; my @svndate = split / /, '$Date: 2016-11-06 18:02:25 +0100 (So, 06. Nov 2016) $'; my $version_date = '2012-03-23'; $version_date = $svndate[1] if defined $svndate[1]; $main::VERSION = $version; my $mapiviURL = "http://mapivi.de.vu"; my $mapiviInfo = "mapivi"; showCopyright(); ############################################################## # load modules #use Encode qw(is_utf8 encode decode); use Encode; #use encoding "utf8" #use utf8; use Getopt::Std; our($opt_i, $opt_v); $Getopt::Std::STANDARD_HELP_VERSION = 1; use File::Copy; use File::Find; use File::Path; # for rmtree, mkpath use Text::Wrap; use List::Util qw/max min/; # core module since Perl 5.8.0 use Tk 800.025; # minimum version >= 800.025 use Tk::JPEG; use Tk::PNG; use Tk::HList; use Tk::ItemStyle; use Tk::ROText; use Tk::ProgressBar; use Tk::IO; use Tk::ErrorDialog; use Tk::Balloon; use Tk::DirTree; use Tk::Font; use Tk::Pane; use Tk::Tiler; use Tk::NoteBook; use Tk::FileSelect; use Storable qw(nstore retrieve dclone); use Tk::Adjuster; use Tk::DragDrop; use Tk::DropSite; use Tk::Compound; # for icons in the menues use MIME::Base64; # for get_encode_file(); a workaround for Tk::Photo which can't handle non-ASCII characters # for debugging # use Data::Dumper; # and then in the code e.g. print Dumper(\%conf); ############################################################## # load mapivi specific modules # the mapivi specific modules may be located in the same dir as mapivi itself, so we add this path to @INC use FindBin; use lib "$FindBin::RealBin"; # load mapivi specific modules use Tk::MhConfig qw(configuration_edit configuration_store configuration_restore); # this will be used in future to provide a multilanguage mapivi # keywords: i18n, gettext #use Locale::TextDomain ('mapivi', $user_data_path."/locale"); #use POSIX qw(locale_h); #setlocale (LC_MESSAGES, ''); ############################################################## # load non standard modules, they may be located below the mapivi program folder use Image::ExifTool; use Image::Info qw(image_info dim); use Image::MetaData::JPEG; # disable warnings from this module $Image::MetaData::JPEG::show_warnings = 0; # todo: use metadatawarn to switch this my $metadataVersionNeeded = 0.14; my $metadataVersion = $Image::MetaData::JPEG::VERSION; $metadataVersion =~ s/[a-zA-Z]//g; die langf("Aborting, because Mapivi needs at least version $metadataVersionNeeded of perl module Image::MetaData::JPEG!\n(installed version: $metadataVersion)\n") if ($metadataVersion < $metadataVersionNeeded); use Time::Local; # timelocal() #use Tk::Date; # not in the Tk distro # This should prevent opening DOS boxes on windows when doing background tasks, but does not work. ToDo #my $win32Avail = (eval "require Win32") ? 1 : 0; #SetChildShowWindow() if ($EvilOS and $win32Avail); ############################################################## # load optional modules # seems not to work so I comment it out for a future test #my $win32FOAvail = (eval "require Win32::FileOp") ? 1 : 0; my $win32FOAvail = 0; my $resizeAvail = (eval {require Tk::ResizeButton}) ? 1 : 0; use constant Win32ProcAvail => eval { require Win32::Process; 1 }; #use constant Win32DriveInfoAvail => eval { require Win32::DriveInfo; 1 }; use constant MatchEntryAvail => eval { require Tk::MatchEntry; 1 }; use constant ProcBackgroundAvail => eval { require Proc::Background; 1 }; # 2009-12-22: the next lines may be used to enable the Gtk2 FileChooserDialog # this works in a small example, but here mapivi dies with a X Window System error #my $gtk2_avail = (eval {require Gtk2}) ? 1 : 0 ; my $gtk2_avail = 0; Gtk2->init if ($gtk2_avail); #use Time::HiRes qw(gettimeofday tv_interval); # needed just for debugging/optimizing #my $hiresstart; ############################################################## # constants use constant WITH_PATH => 1; use constant JUST_FILE => 0; use constant LONG => 1; use constant SHORT => 0; use constant MICRO => 2; use constant WRAP => 1; use constant NO_WRAP => 0; use constant FORMAT => 1; use constant NO_FORMAT => 0; use constant NUMERIC => 1; use constant STRING => 0; use constant WAIT => 1; use constant NO_WAIT => 0; use constant TOUCH => 1; use constant NO_TOUCH => 0; use constant OVERWRITE => 1; use constant OVERWRITEALL => 2; use constant ASK_OVERWRITE => 0; use constant ASK => 1; use constant NO_ASK => 0; use constant PREVIEW => 1; use constant NO_PREVIEW => 0; use constant SHOW => 1; use constant NO_SHOW => 0; use constant COPY => 0; use constant MOVE => 1; use constant RENAME => 2; use constant BACKUP => 3; use constant TRASH => 0; use constant REMOVE => 1; use constant OK => 1; use constant CANCEL => 0; use constant CANCELALL => -1; use constant ADD => 1; use constant RESET => 0; use constant PIXEL => 0; use constant ASPECT_RATIO => 1; use constant RELATIVE => 2; use constant SINGLE => 0; use constant MULTIPLE => 1; use constant UPDATE => 1; use constant FOLDER => 0; # value for $act_modus use constant LOCATION => 1; # value for $act_modus use constant DATE => 2; # value for $act_modus use constant KEYWORDCLOUD => 3; # value for $act_modus use constant KEYWORD => 4; # value for $act_modus use constant SEARCH => 5; # value for $act_modus use constant COLLECTION => 6; # value for $act_modus use constant NO_CHECK_JPEG => 0; use constant CHECK_JPEG => 1; use constant OPEN => 1; # tree collapse (fold) use constant CLOSE => 0; # tree unfold use constant FLAG_RESET => 0b00000000; use constant FLAG_RED => 0b00000001; use constant FLAG_GREEN => 0b00000010; use constant FLAG_BLUE => 0b00000100; use constant START => 0; use constant SETTINGS => 1; use constant FIT => 0; # pic zoom use constant FILL => 1; # pic zoom use constant NORMAL => 0; # window size use constant FULLSCREEN => 1; # window fullscreen ############################################################## # globals (yes, I know there are to many globals, I'm working on it ...) my @dirHist; # folder history - stores the last folders visited my @cachedPics; # a list of all cached pictures my @savedselection; my @savedselection2; # search database: hash to store all the data of all pictures in the visited folders (comments, EXIF, IPTC) my %searchDB; # location hash, will be filled on demand and should only be accessed from function get_locations my %locations; # flag indicating that the %locations hash needs to be filled (updated) from the %searchDB my $locations_need_update = 1; # date/time hash, will be filled on demand and should only be accessed from function get_dates my %dates; # flag indicating that the %dates hash needs to be filled (updated) from the %searchDB my $dates_need_update = 1; # folder checklist: hash to store properties of folders (key: dir value: hash SORT, META, PRIO, COMM) my %dirProperties; # hash to store all loaded photo objects (real size or zoomed) key = path/file name, value = photo object my %photos; # hash to store all loaded thumbnail photo objects key = path/file name, value = photo object my %thumbs; my %searchthumbs;# hash containing all thumbnails of the search dialog, for memory clean up my %thumbDBhash; # store the thumb dirs for one session: dir -> thumbdir my %dirHotlist; # often visited dirs # minimum set of the hot dirs foreach my $dir ("/", $home, cwd()) { $dirHotlist{$dir} = 1 unless (defined $dirHotlist{$dir}); } # hash of all slideshows # is stored on exit and retrieved on start # A slideshow (collection, album) is a manual sorted list of pictures, # like a music playlist # Perl data structure: hash of hash of scalars and lists (HoHoL :) # key1=Folder, key2=Slideshow key3=file, picturelist, my %slideshows = ( 'Vacation' => { '2013-Paris' => { 'file' => 'C:/_data/Bilder/temp/slideshow-paris.gqv', 'pics' => ['C:/_data/Bilder/test/a.png', 'C:/_data/Bilder/test/b.png', 'C:/_data/Bilder/test/c.jpg', 'C:/_data/Bilder/test/d.jpg'], }, '2014-Berlin' => { 'file' => undef, 'pics' => ['C:/_data/Bilder/test/a.png', 'C:/_data/Bilder/test/b.png', 'C:/_data/Bilder/test/c.jpg', 'C:/_data/Bilder/test/d.jpg'], }, }, 'Family' => { 'Simpsons' => { 'file' => 'C:/_data/Bilder/temp/slideshowSimpson.gqv', 'pics' => ['C:/_data/Bilder/test/a.png', 'C:/_data/Bilder/test/b.png', 'C:/_data/Bilder/test/c.jpg', 'C:/_data/Bilder/test/d.jpg'], }, 'Einsteins' => { 'file' => 'C:/_data/Bilder/temp/slideshow-OneStone.gqv', 'pics' => ['C:/_data/Bilder/test/a.png', 'C:/_data/Bilder/test/b.png', 'C:/_data/Bilder/test/c.jpg', 'C:/_data/Bilder/test/d.jpg'], }, }, 'BestOf' => { '2014' => { 'file' => 'C:/_data/Bilder/temp/slideshowSimpson.gqv', 'pics' => ['C:/_data/Bilder/test/a.png', 'C:/_data/Bilder/test/b.png', 'C:/_data/Bilder/test/c.jpg', 'C:/_data/Bilder/test/d.jpg'], }, }, 'Other' => { }, ); # hash of hash example #my %modules = ( #'Carp' => { 'version' => $Carp::VERSION, # 'license' => 'Perl Artistic License or GNU GPL' }, #'File::Basename' => { 'version' => $File::Basename::VERSION, my %quickSortHash; my %quickSortHashSize; my %quickSortHashPixel; my %quickSortHashBitsPixel; my $quickSortSwitch = 0; my $actpic = ''; # the path and file name of the actual picture my $act_modus = FOLDER; # FOLDER, LOCATION, DATE, KEYWORDCLOUD, KEYWORD, SEARCH or COLLECTION my $actdir = ''; # the actual folder - valid if $act_modus == FOLDER my @act_location = (); # the actual location (Country, Privince/State, City, Sublocation) - valid if $act_modus == LOCATION my @act_date = (); # the actual location (Year, Month, Day, Hour, Minute) - valid if $act_modus == DATE my @act_keywords = (); # the actual keywords (any number) - valid if $act_modus == KEYWORDCLOUD my @act_keywords_ex = (); # the actual exclude keywords (any number) - valid if $act_modus == KEYWORDCLOUD my @act_collection = (); # the actual collection (folder and name of %slideshows) - valid if $act_modus == COLLECTION; todo: hierarchy still unclear my $widthheight = ''; my $loadtime = ''; my $size = ''; my $zoomFactorStr = ''; my $nrof = ''; my $userinfo = ''; my $otherFiles = ''; my $proccount = 0; my $nrToConvert = 0; my $maxCommentLength = 2**16 - 3; # a comment block may have max 64kB my $global_log = "Mapivi $version log file:"; # file suffixes my @video_suffix = qw(.avi .mp4 .mpg .mpeg .mov); my @xmp_suffix = qw(.XMP); my @wav_suffix = qw(.WAV); my @raw_suffix = qw(.NEF .CRW .CR2 .DNG .NRW); # update also sub getPics()!! my @raw_suffix_lc; # lower case raw suffixes push @raw_suffix_lc, lc($_) foreach (@raw_suffix); # picture formats supported by Tk::Photo (standard Tk distribution) # xbm works, but takes ages (who needs xbm???) and tiff doesn't work # xcf works for thumbs, but makes problems with layers my $nonJPEGsuffixes = "gif|png|xpm|bmp"; my $cameraJunkSuffixes = "ctg"; # uninteresting files created by cameras my $copyright_year = (localtime(time()))[5] + 1900; # the actual year, for the copyright notice my $HTMLPicDir = "pics"; # this is the name of the subdir for pics when building html pages my $HTMLThumbDir = "thumbs"; # this is the name of the subdir for thumbs when building HTML pages my $slideshow = 0; # start/stop flag for slideshow my $showPicInAction = 0; # bool = 1 while loading picture my %winapps; # used for sub findApp() my $clocktimer; my $time; # used to show the clock or memory usage in the top bar my $date; # the date is shown as balloon info my $clockL; # clock/memory label widget my $scsw; my $wizW; my $impW; my $interpW; my $fuzzybw; # fuzzy border dialod window my $ll_b_w; # lossless border dialog window my $ll_r_w; # lossless relative border dialog window my $ll_a_w; # lossless aspect ratio border dialog window my $ll_w_w; # lossless drop picture (watermark) dialog window my $bpw; # border preview window my $indexW; # montage /index print dialog window my $passportW; # passport print dialog window my $ow; # options window, see sub options() my $sw; # the search window, see searchMetaInfo() my $dpw; # the dir properties window, see showDirProperties() my $dsw; # the dir size window my $ltw; # the light table window for slideshows my $ddw; # dirDiffWindow widget my $catw; # the IPTC categories window, see editIPTCCategories() my $locw; # the location window, see search_by_location() my $keycw; # the comment keywords window, see editCommentKeywords() my $dupw; # the duplicate search window, see sub finddups() my $filterW; # the filter window my $menubar; # handle for menubar of main window my $balloon; # balloon handle my $dirMenu; # context menu for dirs my $thumbMenu; # context menu for thumbnails my $picMenu; # context menu for picture my $copyCommentSource; # global variable of sub copyComment() my ($idx, $idy); # coordinates of actual item when clicked on or moved my ($width, $height); my %nonJPEGdirNoAskAgain; # hash to store the dirs with non-JPEG files not to convert (valid for one session) my $cleanDirNoAsk = 0; # needed in sub cleanDir() my $cleanDirLevel = 0; # needed in sub cleanDir() #my $loc_search = 1; # location window: search or add location on double click # some example hierarchical categories my @precats = sort qw(Nature Nature/Flower Nature/Landscape Nature/Macro Nature/Animal Nature/Animal/Fish Nature/Animal/Cat Nature/Animal/Insect Nature/Animal/Insect/Ant People People/Portrait People/Wedding Architecture Architecture/Tower Architecture/Bridge Architecture/Church Technology Technology/Car Technology/Train Technology/Computer); # overwrite them, when some stored categories are available @precats = readArrayFromFile("$user_data_path/categories") if (-f "$user_data_path/categories"); uniqueArray(\@precats); # remove double entries foreach (@precats) { $_ =~ s|^/||; } # cut leading slash @precats = qw(Nature) unless (@precats); # add a starting point if array is empty # some example hierarchical keywords my @prekeys = qw(Family Family/Einstein Family/Einstein/Albert Family/Einstein/Hermann Family/Einstein/Pauline Family/Planck Family/Planck/Max Family/Planck/Johann Family/Planck/Marie Family/Planck/Karl Family/Planck/Grete Family/Planck/Emma Family/Planck/Erwin Family/Planck/Hermann Friend Friend/Bundy Friend/Bundy/Al Friend/Bundy/Bud Friend/Bundy/Kelly Friend/Bundy/Peggy); # overwrite them, when some stored keywords are available @prekeys = readArrayFromFile("$user_data_path/keywords") if (-f "$user_data_path/keywords"); uniqueArray(\@prekeys); # remove double entries foreach (@prekeys) { $_ =~ s|^/||; } # cut leading slash @prekeys = qw(Family) unless (@prekeys); # add a starting point if array is empty # global hash for new keywords found in displayed pictures my %new_keywords; # global hash to store keywords, which should be ignored (e.g. nature.animal.dog) my %ignore_keywords; # hot keywords, list of keywords which are often used, maintained by the user my %hot_keywords = ('Family/Einstein/Pauline' => 1, 'Family/Planck/Max' => 1, 'Family/Planck/Johann' => 1); # try to get the saved hot keywords if (-f "$user_data_path/keywords_hot") { my $hashRef = retrieve("$user_data_path/keywords_hot"); warn langf("could not retrieve %s",'keywords_hot') unless defined $hashRef; %hot_keywords = %{$hashRef}; } # add mapivi program path to PATH # this allows to locate external programs like jpegtran in the mapivi folder # which makes packaging easier $ENV{PATH} .= ";$FindBin::Bin"; # external programs used by mapivi my %exprogs = qw/convert 0 composite 0 jhead 0 jpegtran 0 mogrify 0 montage 0 xwd 0 identify 0 thunderbird 0 mozilla-thunderbird 0 exiftool 0/; # short comment about the usage of the external programs my %exprogscom = ( 'convert' => lang('Build thumbnails'), 'composite' => lang('Combine pictures e.g. thumbnails with a background'), 'jhead' => lang('Handle EXIF infos and embedded thumbnail pictures'), 'jpegtran' => lang('Do lossless rotation of pictures'), 'mogrify' => lang('Change the size/quality of pictures'), 'montage' => lang("Combine pictures to e.g. index prints"), 'xwd' => lang("Make a screenshot of a window or desktop"), 'identify' => lang("Describe the format and characteristics of a picture"), 'thunderbird' => lang("Send pictures via email"), 'mozilla-thunderbird' => lang("Send pictures via email"), 'exiftool' => lang("Read/write meta information in image files"), ); # where to find the external programs (resources) my %exprogsres = ( "convert" => "Image Magick http://www.imagemagick.org", "composite" => "Image Magick http://www.imagemagick.org", "jhead" => "http://www.sentex.net/~mwandel/jhead/", "jpegtran" => "libjpeg http://www.ijg.org", "mogrify" => "Image Magick http://www.imagemagick.org", "montage" => "Image Magick http://www.imagemagick.org", "identify" => "Image Magick http://www.imagemagick.org", "thunderbird" => "http://www.mozilla.org/projects/thunderbird/", "mozilla-thunderbird" => "http://www.mozilla.org/projects/thunderbird/", "exiftool" => "http://owl.phy.queensu.ca/~phil/exiftool/", ); # hash to replace (german) umlaute by corresponding letters my %umlaute = qw(ä ae Ä Ae ö oe Ö Oe ü ue Ü Ue ß ss); my $umlaute = join '', keys(%umlaute); # stolen from Image::ExifTool (thanks to Phil Harvey) my %iptcCharset = ( "\x1b%G" => 'UTF8', # don't translate these (at least until we handle ISO 2022 shift codes) # because the sets are only designated and not invoked # "\x1b,A" => 'Latin', # G0 = ISO 8859-1 (similar to Latin1, but codes 0x80-0x9f are missing) # "\x1b-A" => 'Latin', # G1 " # "\x1b.A" => 'Latin', # G2 # "\x1b/A" => 'Latin', # G3 ); # hash to replace (german) umlaute by corresponding HTML-tags my %umlauteHTML = qw(ä ä Ä Ä ö ö Ö Ö ü ü Ü Ü ß ß); my $umlauteHTML = join '', keys(%umlauteHTML); # hash to escape special HTML characters my %htmlChars = ( "<" => "<", ">" => ">", "&" => "&", "\"" => """, "'" => "'", ); my $htmlChars = join '', keys(%htmlChars); # mapivi configuration hash / tool options my %conf; my @conf_tab_order; # defines the order of the tabs in the configuration_edit dialog configuration_set_default(); # preset for channel mixer for black and white conversion # hash of lists HoL; list is red, green , blue = RGB my %channel_mixer = ( 'Filter Yellow' => [30, 70, 20], 'Filter Orange' => [78, 22, 0], 'Filter Red' => [75, 0, 25], 'Filter Red II' => [150,-25,-25], 'Filter Red 25a' => [200, 0,-100], 'Filter Green' => [20, 60, 40], 'Normal 1' => [30, 59, 11], 'Normal 2' => [80, 15, 5], 'Normal 3' => [70, 20, 10], 'Normal 4' => [80, 20,-20], 'Normal 5' => [65, 25, 10], 'Contrast High' => [40, 34, 60], 'Contrast Normal' => [43, 33, 30], ); # old config hash -> new solution see configuration_set_default() # insert here all default configurations # these configurations will be overwritten by $configFile # at startup my %config = ( "Geometry" => "790x560+1+1", # fit on a 800x600 screen "SearchGeometry" => "790x560+1+1", # fit on a 800x600 screen "KeyGeometry" => "250x500+50+50", # fit on a 800x600 screen "LocGeometry" => "250x500+50+50", # fit on a 800x600 screen "LtwGeometry" => "700x500+10+10", # fit on a 800x600 screen "FontSize" => 12, "FontFamily" => "itc avant garde", "PropFontSize" => 12, "PropFontFamily" => "helvetica", "ColorSel" => "gray40", "ColorSelBut" => "red4", "ColorSelFG" => "gray85", "ColorName" => "gray85", "ColorComm" => "gray85", "ColorIPTC" => "gray85", "ColorEXIF" => "gray85", "ColorFile" => "gray85", "ColorDir" => "gray85", "ColorThumbBG" => "gray60", "ColorProgress" => "gray85", "ColorPicker" => "#efefef", # last color selected with color picker "Copyright" => "copyright (c) $copyright_year Herrmann", "Comment" => "This picture was taken in south africa ...", "MaxProcs" => 1, "MaxCachedPics" => 3, "NrOfRuns" => 0, # count how often mapivi was started "ShowPic" => 1, # boolean (1 = show pic, 0 = do not show pic) "ThumbCapt" => "none", # thumbnail caption "ThumbCaptFontSize" => 10, "ShowNavFrame" => 1, # boolean (1 = show naviagtion frame, 0 = hide) "ShowInfoFrame" => 1, # boolean (1 = show info frame, 0 = hide) "ShowThumbFrame" => 1, # boolean (1 = show thumb frame, 0 = hide) "ShowPicFrame" => 1, # boolean (1 = show pic frame, 0 = hide) "ShowComment" => 1, # boolean (1 = show comment, 0 = hide comment in thumbnail view) "ShowCommentField"=> 0, # boolean (1 = show comment, 0 = hide comment in picture view) "ShowIPTCFrame" => 0, # boolean (1 = show IPTC headline, caption, 0 = hide IPTC frame in picture view) "ShowEXIF" => 1, # boolean (1 = show EXIF, 0 = hide EXIF in thumbnail view) "ShowIPTC" => 1, # boolean (1 = show IPTC, 0 = hide IPTC in thumbnail view) "ShowFile" => 1, # boolean (1 = show Size, 0 = hide Size in thumbnail view) "ShowDirectory" => 1, # boolean (1 = show directory, 0 = hide dir in thumbnail view) "ShowMenu" => 1, # boolean (1 = show menu, 0 = hide the menu bar) "ShowHiddenDirs" => 0, # boolean (1 = show hidden dirs (starting with .), 0 = hide them) "PicQuality" => 95, # quality of jpg picture (in %) "PicSharpen" => 5, # sharpness of picture "PicBlur" => 0, # blur the pictur "PicGamma" => 1.0,# gamma value of picture "PicBrightness" => 100,# Brightnes of picture (in %) "PicSaturation" => 100,# Saturation of picture (in %) "PicHue" => 100,# Hue of picture (in %) "PicStrip" => 0, # boolean (1 = strip all meta info when resizing pic) "ThumbQuality" => 85, # quality of thumbnail jpg picture "SortBy" => "name", "SortReverse" => 0, "LastDir" => $home, "FileNameFormat" => "%y%m%d-%h%M%s", # the actual file name format when renaming "FileNameFormatDef"=> "%y%m%d-%h%M%s", # the default file name format when renaming "ThumbSharpen" => 1, "ThumbSize" => 100, "ThumbBorder" => 4, "HTMLaddComment" => 1, "HTMLaddEXIF" => 1, "HTMLaddIPTC" => 1, "HTMLcols" => 2, "HTMLTargetDir" => $home, "HTMLGalleryIndex"=> "../galleries.html", "HTMLGalleryTitle"=> "My gallery", "HTMLHomepage" => "../../index.shtml", "HTMLTemplate" => "$program_data_path/html/light.html", "HTMLFooter" => "© Martin Herrmann <Martin-Herrmann\@gmx.de>", "HTMLBGcolor" => "white", "HTMLPicSize" => 600, "HTMLPicSharpen" => 1, "HTMLPicCopyright"=> 0, # bool - add a visible copyright info into the picture "HTMLPicQuality" => 80, # quality of html jpg pictures "HTMLPicEXIF" => 1, # bool - 1 = copy the EXIF infos to the converted HTML pics "HTMLnoPicChange" => 0, # bool - 1 = no pic changes (no resize etc ...) "AutoZoom" => 1, # boolean - zoom big pictures to fill the canvas "UseEXIFThumb" => 0, # boolean - use EXIF Thumbnails if available "AskGenerateThumb"=> 1, # ask before generating thumbnails "AskDeleteThumb" => 1, # ask before deleting thumbnails "AskMakeDir" => 1, # ask before makeing a directory (e.g. .thumbs or .exif) "MaxTrashSize" => 50, # MB - a warning will appear if the trash contains more than this "BitsPixel" => 0, # boolean - show bits per pixel info "AspectRatio" => 1, # boolean - show image aspect ratio e.g. 4:3 or 3:2 "NameComment" => 0, # boolean - 1 = add file name to comment, when importing pics "NameComRmSuffix" => 1, # boolean - 1 = remove file suffix when adding filename to comment "SaveDatabase" => 1, # boolean - 1 = save dir info to a file "UseThumbShadow" => 0, "MakeBackup" => 1, # make a backup of the original file, before appling a filter "PicListFile" => "$home/filelist", "XMLFile" => "$home/IPTCinfo.xml", "indexRows" => 2, # indexPrint "indexCols" => 2, # indexPrint "indexPicX" => 500, # indexPrint "indexPicY" => 500, # indexPrint "indexDisX" => 10, # indexPrint "indexDisY" => 10, # indexPrint "indexBG" => "white", # indexPrint background color "indexLabel" => 1, # indexPrint "indexLabelStr" => "%f (%wx%h, %b)", # indexPrint "WarnBeforeResize"=> 1, # warn before using mogrify in resize "IPTCoverwrite" => 0, # overwrite IPTC attributes, when editing multiple pictures "IPTCmergeCatKey" => 1, # merge categories and keywords, when editing multiple pictures "IPTCdateEXIF" => 0, # use EXIF date as creation date "IPTCtimeEXIF" => 0, # use EXIF time as creation time "IPTCbylineEXIF" => 0, # use EXIF owner as ByLine "IPTCaddMapivi" => 0, # add Mapivi infos to IPTC "IPTC_action" => 'UPDATE', # ADD UPDATE or REPLACE "CheckForNonJPEGs"=> 0, # check if there are non JPEGs in the dir and ask to convert them "ShowPicInfo" => 1, # show a balloon info box with EXIF, comment, ... for the actual picture "SearchPattern" => '', # the search pattern "SearchExPattern" => '', # the search exclude pattern "SearchCom" => 1, # search in the picture comments "SearchExif" => 1, # search in the picture EXIF info "SearchIptc" => 1, # search in the picture IPTC info "SearchKeys" => 1, # search in the picture keywords "SearchName" => 1, # search in the picture file name "SearchDir" => 1, # search in the picture path "SearchCase" => 0, # search case sensitive "SearchWord" => 0, # 1 = search only complete words 0 = match also parts "SearchType" => 'exactly', # search type: "exactly", "all" or "any" "SearchOnlyInDir" => 0, # search only in dirs matching the actual/selected dir "SearchPixelOn" => 0, # search for pictures with a certain pixel size "SearchPixel" => 0, # "SearchPixelRel" => '<=', # <=, ==, >= "SearchPopOn" => 0, # search for pic with a certain number of views "SearchPopRel" => 0, # <=, ==, >= "SearchPop" => 0, # search for pic with a certein numer of views "SearchJoin" => 0, # join comment, EXIF, IPTC and filename before searching "SearchDate" => 0, # search pics by date "SearchDateStart" => "01.01.1970", # start date "SearchDateEnd" => "25.08.2012", # end date "SearchMore" => 0, # show more search options in search window "SearchDBOnlyNew" => 0, # add only new pics when building DB "CopyPosition" => 'SouthEast', # position of the visible copyright info "CopyX" => 20, # x offset of the visible copyright info "CopyY" => 20, # Y offset of the visible copyright info "CopyAdd" => 0, # bool - add a visible copyright info "CopyFontFamily" => "Courier", # font family of the embedded copyright info "CopyFontSize" => 12, # font size of the embedded copyright info "CopyFontColFG" => "white", # foreground color of the embedded copyright info font "CopyFontColBG" => "black", # background color of the embedded copyright info font "CopyFontShadow" => 1, # bool - add a shadow to the copyright text "CopyrightLogo" => "$program_data_path/icons/MapiviIcon.gif", "CopyTextOrLogo" => "text", "BorderWidth1x" => 10, # border 1 width in x direction "BorderWidth1y" => 10, # border 1 width in y direction "BorderColor1" => "white", # border 1 color "BorderWidth2x" => 0, # border 2 width in x direction "BorderWidth2y" => 0, # border 2 width in y direction "BorderColor2" => "black", # border 2 color "BorderWidth3x" => 0, # border 3 width in x direction "BorderWidth3y" => 0, # border 3 width in y direction "BorderColor3" => "white", # border 3 color "BorderWidth4x" => 0, # border 4 width in x direction "BorderWidth4y" => 0, # border 4 width in y direction "BorderColor4" => "gray80",# border 4 color "BorderAdd" => 0, # bool - add a border "DropShadow" => 0, # bool - add a drop shadow "DropShadowWidth" => 5, # the width of the drop shadow "DropShadowBlur" => 3, # the blur sigma factor of the drop shadow "DropShadowBGColor" => "white", # the background color of the drop shadow "jpegtranTrim" => 0, # bool - use the -trim switch of jpegtran "SlideShowTime" => 4, # pause between picture loading im sec "CropAspect" => 3/2, # 0 for no aspect ratio, 3/2 for 3:2 1 for 1:1 4/3 for 4:3 "CropGrid" => 1, # bool show 1/3 crop grid "AspectSloppyFactor" => 2.0, # delta factor for aspect ratio calculation in % "FilterDeco" => 0, # add a border or a text to the pictures when filtering "FilterPrevSize" => 200, # filter preview size (100% zoom crop of the picture) "EXIFshowApp" => 1, # show App*-Info and MakerNotes and ColorComponents in EXIF info "Layout" => 0, # layout of the dir, thumb and picture frame "Layout0dirX" => 25, # default percentual width of the different layouts "Layout0thumbX" => 30, # "" "Layout1dirX" => 20, # "" "Layout3thumbX" => 20, # "" "Layout5dirX" => 20, # "" "CommentHeight" => 2, # height of the comment text frame above the picture "Gamma" => 1.0, # the gamma value, when displaying pictures "ShowFileDate" => 0, # show the file date in the size coloumn "Unsharp" => 0, # bool unsharp mask operation on/off "UnsharpRadius" => 0, # unsharp mask radius (blur) "UnsharpSigma" => 1.0, # unsharp mask sigma (blur) "UnsharpAmount" => 1.0, # unsharp mask amount "UnsharpThreshold"=> 0.05,# unsharp mask threshold "ResizeFilter" => "Lanczos", "RenameBackup" => 1, # bool, if 1 a backup file will be renamed if the file is renamed "ThumbMaxLimit" => 200, # maximum number of displayed thumbnails "Level" => 0, # level a picture "LevelBlack" => 8, # level a picture black point (%) "LevelWhite" => 92, # level a picture white point (%) "LevelGamma" => 1.0, # level a picture mid point (gamma value) "indexBorder" => 0, # bool add a border around the index print "indexBorderWidth"=> 50, "indexBorderColor"=> 'white', "indexInnerBorder" => 0, # bool add a border around the each picture "indexInnerBorderWidth"=> 2, "indexInnerBorderColor"=> 'black', "indexFontSize" => 10, # the font size of the index labels (0 = automatic) "CheckForLinks" => 1, # bool - check if a file is a link before processing it "ColorAdj" => 0, # bool - do some color adjustments when filtering a pic "LineLimit" => 8, # max nr of lines in the thumbnail table e.g. for comments "LineLength" => 30, # length of one line in the thumbnail table e.g. for comments "ExtBGApp" => "wmsetbg -a", # name of external app to set desktop background (with options) "ConvertUmlaut" => 1, # convert german umlaute (e.g. ä -> ae etc.) "onlyASCII" => 1, # convert Umlaute and remove non printable (non-ASCII) chars "ImportSource" => "/mnt/usb/DCIM/DIMG", "ImportSubdirs" => 0, # bool - import also from all subdirs "ImportTargetFix" => "$home/pictures", "ImportTargetVar" => "2012/02/14_Birthday_Sam", "ImportRotate" => 1, "ImportRename" => 1, "ImportDeleteCameraJunk" => 0, "ImportDelete" => 1, "ImportShowPics" => 1, "ImportAddCom" => 0, "ImportAddComment"=> "(c) $copyright_year Martin Herrmann", "ImportAddIPTC" => 0, # bool "ImportAddIPTCDateTime" => 0, # bool "ImportAddIPTCByLine" => 0, # bool "ImportIPTCTempl" => 'template.iptc2', "ImportMore" => 0, # bool - show additional import options in wizard "ImportMarkLocked"=> 0, # bool - add a high rating to locked (= write protected) pictures during import "Borderwidth" => 1, # border width of GUI elements (widgets) "PrintBaseDir" => "$home/pictures/print", "PrintVarDir" => "3_times_13x18", "PrintTimes" => "1", "PrintTimesStr" => "times", "PrintSize" => "10x15", "CenterThumb" => 0, # move the thumbnails up or down, so that the next e.g. previous thumb is also visible "BeepWhenLooping" => 1, # play a beep when looping to the first e.g. last picture "SlowButMoreFeatures" => 0, # enable some features slowing down mapivi "setEXIFDateAskAgain" => 0, # show/don't show ask dialog "EXIFDateAbs" => "2012:03:20-18:51:45", "EXIFPlusMin" => "+", # used in setEXIFdate "EXIFAbsRel" => 'abs', # used in setEXIFdate "EXIFyears" => 0, # used in setEXIFdate "EXIFdays" => 0, # used in setEXIFdate "EXIFhours" => 0, # used in setEXIFdate "EXIFmin" => 0, # used in setEXIFdate "EXIFsec" => 0, # used in setEXIFdate "RotateThumb" => 1, # bool - rotate thumb when rotating the pic "ToggleBorder" => 0, # bool - switch window decoration on/off in fullscreen mode "CentralThumbDB" => 0, # bool - 1 = central thumb DB, 0 = decentral .thumbs dirs "IPTCLastPad" => "cap", # remember the NoteBook page on the IPTC dialog "OptionsLastPad" => "gen", # remember the NoteBook page on the IPTC dialog "MetadataWarn" => 0, # print a warning to stdout if some strange metadata is found (e.g. in EXIF) "dirDiffDirA" => $home, "dirDiffDirB" => $home, "dirDiffSize" => 1, "dirDiffPixel" => 1, "dirDiffComment" => 1, "dirDiffEXIF" => 1, "dirDiffIPTC" => 1, "MailPicNoChange" => 0, "MailPicMaxLength"=> 800, "MailPicQuality" => 75, "winDirRequesterAskAgain" => 1, "FuzzyBorderRelative"=> 1, # 1 = Border width in %, 0 = Absolute in pixels "FuzzyBorderWidth"=> 10, # % or pixels depending on FuzzyBorderRelative "FuzzyBorderBlur" => 10, "FuzzyBorderColor"=> 'black', "ShowInfoInCanvas"=> 1, "llBorderWidthX" => 16, "llBorderWidthY" => 16, "llBorderWidthIX" => 1, "llBorderWidthIY" => 1, "llBorderColor" => 'white', "llBorderColorI" => 'black', "supportOtherPictureFormats" => 0, "CategoriesAll" => 2, # category mode 0= last, 1=all, 2=join "KeywordsAll" => 2, # keyword mode 0= last, 1=all, 2=join "Version" => '000', "ShowUnfinishedDirs" => 1, "ShowFinishedDirs" => 1, "trackPopularity" => 1, "ChannelRed" => 100, "ChannelGreen" => 100, "ChannelBlue" => 100, "ChannelDeco" => 0, "ChannelBright" => 1, 'SlideShowDir' => $home, # settings for slideshows 'relative_path' => 1, # settings for xnview slideshows 'xnview_loop' => 1, # settings for xnview slideshows 'xnview_fullscreen' => 1, # settings for xnview slideshows 'xnview_filename' => 0, # settings for xnview slideshows 'xnview_random' => 0, # settings for xnview slideshows 'xnview_mouse' => 0, # settings for xnview slideshows 'xnview_title' => 0, # settings for xnview slideshows 'PicWinBalloon' => 1, # boolean -1 show balloon info in pic window 'IPTCProfessional'=> 1, # boolean - 1 = professional IPTC, 0 = simple dialog 'CheckNewKeywords'=> 1, 'KeywordMore' => 0, # boolean 1 = show more options in keyword search window 'KeywordExclude' => '', # space separated list of keywords to exclude 'KeywordLimit' => 0, # boolean 1 = limit number of displayed keywords 'KeywordDate' => 0, # boolean 1 = limit to a date range 'KeywordStart' => 1070254800, # start date (UNIX time) 'KeywordEnd' => 1170254800, # end date (UNIX time) 'UrgencyChangeWarning' => 1, # boolean 1 = show a warning when urgency changed 'ActPic' => '', # the last picture shown 'SelectLastPic' => 1, # Select last shown pic after startup 'AutoImport' => 1, # boolean = 1 start import at Mapivi wizard if memory card is inserted (ImportSource) 'llWatermarkX' => 16, # lossless watermark x position 'llWatermarkY' => -16, # lossless watermark y position 'llWatermarkFile' => "$icon_path/EmptyThumb.jpg", # lossless watermark file name 'AspectBorderN' => 3, # lossless aspect ratio border 'AspectBorderM' => 2, # lossless aspect ratio border 'RelativeBorderX' => 10, # lossless relative border 'RelativeBorderY' => 10, # lossless relative border 'RelativeBorderIX' => 0.1, # lossless relative border 'RelativeBorderIY' => 0.1, # lossless relative border 'RelativeBorderEqual'=> 1, # boolean lossless relative border 'XMP_file_operations'=> 1, # boolean XMP sidecar files follow picture file operations 'WAV_file_operations'=> 1, # boolean WAV audio files follow picture file operations 'RAW_file_operations'=> 0, # boolean RAW files follow picture file operations 'LocationMode' => 'UPDATE', # UPDATE or REPLACE - mode for writing IPTC location info 'AskDeleteHighRating'=> 1, # boolean - ask before deleting high rated pictures 'AskDeleteHighRatingLevel'=> 3, # Urgency (Rating) - ask before deleting high rated pictures 'Language' => 'en', # language localization, needs a corresponding translation file e.g. mapivi-lang-de for german (de) 'ColorCloud' => 'red', # color of the most prominent keywords in the keyword cloud ); # some platform specific default settings # for windows #if ($EvilOS) { # $config{ExtViewer} = 'C:\Program Files\IrfanView\iview_32.exe'; # $config{ExtEdior} = 'gimp-win-remote gimp-2.2.exe'; #} # for Mac OS X #if ($MacOSX) { # $config{ExtViewer} = "macosx-preview"; # $config{ExtViewerMulti} = 1; #} my @IPTCAttributes = ( "Urgency", "Keywords", "Headline", "Caption/Abstract", "Country/PrimaryLocationName", "Country/PrimaryLocationCode", "Province/State", "City", "SubLocation", "Writer/Editor", "ObjectName", "CopyrightNotice", "Category", "Source", "EditStatus", "OriginatingProgram", "ProgramVersion", "EditorialUpdate", "ObjectCycle", "ByLine", "ByLineTitle", "FixtureIdentifier", "ContentLocationName", "ContentLocationCode", "ReleaseDate", "ReleaseTime", "OriginalTransmissionReference", "ExpirationDate", "ExpirationTime", "Credit", "SpecialInstructions", "ActionAdvised", "Contact", #"ReferenceService", # only usefull for multiple objects #"ReferenceDate", # only usefull for multiple objects #"ReferenceNumber", # only usefull for multiple objects "DateCreated", "TimeCreated", "ImageType", "ImageOrientation", "DigitalCreationDate", "DigitalCreationTime", "LanguageIdentifier", #"RecordVersion", # binary "ObjectTypeReference", "ObjectAttributeReference", "SubjectReference", "SupplementalCategory", #"RasterizedCaption", # binary # Audio... and ObjDataPreview... left out by now ... ); my %iptcHelp = ( 'ByLine' => lang("Contains name of the creator of the objectdata, e.g. writer, photographer or graphic artist. Examples: Robert Capa, Ernest Hemingway, Pablo Picasso (max. 32 chars)"), 'ByLineTitle' => lang("A ByLineTitle is the title of the creator or creators of an objectdata. Where used, a by-line title should follow the ByLine it modifies. Examples: Staff Photographer, Corresponsal, Envoye Special (max. 32 chars)"), 'Caption/Abstract' => lang("The Caption field is the text that accompanies the photo, containing the who, what, when, where and why information (max. 2000 chars)"), 'CaptionWriter' => lang("The Caption Writer field lists the initials of all the people who wrote or edited the caption, header fields or image file. This includes toning and pixel editing"), 'Category' => lang("Identifies the subject of the objectdata in the opinion of the provider. A list of categories will be maintained by a regional registry, where available, otherwise by the provider. Note: Use of this DataSet is Deprecated. It is likely that this DataSet will not be included in further versions of the IIM. The Category field lists codes that aid in a more detailed search. (max. 3 chars)"), 'SubLocation' => lang("Identifies the location within a city. Examples: Capitol Hill, Maple Leaf Gardens, Strandgateparken (max. 32 chars)"), 'City' => lang("The City field lists where the photo was originally made. For file photos, do not use the transmission point's city (max. 32 chars)"), 'Country/PrimaryLocationCode' => lang("The Country field lists the three-letter country code where the photo was originally made. For file photos, do not put the transmission points country. Examples: USA (United States), GER (Germany), FRA (France), XUN (United Nations) (max. 3 chars)"), 'Country/PrimaryLocationName' => lang("Provides full, publishable, name of the country/primary location where the intellectual property of the objectdata was created, according to guidelines of the provider. (max. 64 chars)"), 'DateCreated' => lang("The Create Date field is the date the photo was originally made. For file photos, use the date the photo was originally made, if known. If the complete date is not known, leaf the field blank. The field will not accept a partial date. (8 chars CCYYMMDD)"), 'TimeCreated' => lang("Represented in the form HHMMSS+HHMM (or -HHMM) to designate the time the intellectual content of the objectdata current source material was created rather than the creation of the physical representation. Follows ISO 8601 standard. Where the time cannot be precisely determined, the closest approximation should be used. Example: 133015+0100 indicates that the object intellectual content was created at 1:30 p.m. and 15 seconds Frankfurt time, one hour ahead of UTC. (11 chars HHMMSS+HHMM)"), 'Credit' => lang("Identifies the provider of the objectdata, not necessarily the owner/creator. (The Credit field is the name of the service transmitting the photo) (max. 32 chars)"), 'Headline' => lang("The Headline field lists keywords to aid in a more detailed search for a photo. Example: Lindbergh Lands In Paris (max. 256 chars)"), 'SpecialInstructions' => lang("The Instructions field lists special notations that apply uniquely to a photo, such as file photo, correction, advance or outs Examples: SECOND OF FOUR STORIES, 3 Pictures follow, Argentina OUT (max. 256 chars)"), 'ObjectName' => lang("Used as a shorthand reference for the object. Changes to existing data, such as updated stories or new crops on photos, should be identified in Edit Status. Examples: Wall St., Ferry Sinks. The Object Name field lists the story slug associated with a photo. For photos without a story, Associated Press photographers or photo editors will make up a logical slug to aid in a search and note it as a stand alone photo in the INSTRUCTIONS field of the NAA/IPTC header. If a related story moves on Data-Stream, the photo will be retransmitted with the appropriate OBJECT NAME to match the story. (max. 64 chars)"), 'Source' => lang("Identifies the original owner of the intellectual content of the objectdata. This could be an agency, a member of an agency or an individual. (The Source field lists who is the original provider of a photo, such as: AP, an AP member, pool photo provider or handout photo provider.) (max. 32 chars)"), 'Province/State' => lang("The State field lists the state where the photo was originally made. Use U.S. postal code abbreviations. For file photos, do not use the transmission point's state. Examples: WA, Sussex, Baden-Wuerttenberg (max. 32 chars)"), 'SupplementalCategory' => lang("The Supplemental Categories field lists codes that aid in a more detailed search for a photo."), 'OriginalTransmissionReference' => lang("A code representing the location of original transmission according to practices of the provider. Examples: BER-5, PAR-12-11-01. (The Trans Reference field lists a call letter/number combination associated with a photo. It includes an originating transmit points call letters and picture number from that point's sequence of offerings for a given day. Example: NY105.) (max. 32 chars)"), 'Urgency' => lang("Priority 0 meaning None, 1 meaning High to 8 meaning Low"), 'CopyrightNotice' => lang("Contains any necessary copyright notice. (max. 128 chars)"), 'ExpirationTime' => lang("Designates in the form HHMMSS+HHMM (or -HHMM) the latest time the provider or owner intends the objectdata to be used. Follows ISO 8601 standard. Example: 090000-0500 indicates an objectdata that should not be used after 0900 in New York (five hours behind UTC)."), 'ExpirationDate' => lang("Designates in the form CCYYMMDD the latest date the provider or owner intends the objectdata to be used. Follows ISO 8601 standard. Example: 19940317 indicates an objectdata that should not be used after 17 March 1994."), 'ReleaseTime' => lang("Designates in the form HHMMSS+HHMM (or -HHMM) the earliest time the provider intends the object to be used. Follows ISO 8601 standard. Example: 090000-0500 indicates object for use after 0900 in New York (five hours behind UTC)"), 'ReleaseDate' => lang("Designates in the form CCYYMMDD the earliest date the provider intends the object to be used. Follows ISO 8601 standard. Example: 19890317 indicates data for release on 17 March 1989. (8 chars)"), 'FixtureIdentifier' => lang("Identifies objectdata that recurs often and predictably. Enables users to immediately find or recall such an object. Example: EUROWEATHER"), 'EditStatus' => lang("Status of the objectdata, according to the practice of the provider. Examples: Lead, CORRECTION (max. 64 chars)"), 'Writer/Editor' => lang("Identification of the name of the person involved in the writing, editing or correcting the objectdata or caption/abstract. (max. 32 chars)"), 'LanguageIdentifier' => lang("Describes the major national language of the object, according to the 2-letter codes of ISO 639:1988. Does not define or imply any coded character set, but is used for internal routing, e.g. to various editorial desks. Example: en (english), de (german) (2 or 3 chars)"), 'ObjectCycle' => lang("Where: a = morning, p = evening, b = both. Virtually only used in North America. (1 char)"), 'Contact' => lang("Identifies the person or organisation which can provide further background information on the objectdata. (max. 128 chars)") ); # store all values which were entered in the labeled entry widgets # key = label of entry, value = reference to array containing all unique values my %entryHistory; my @allcolors = qw/black gray10 gray20 gray30 gray40 gray50 gray60 gray70 gray80 gray85 gray90 gray95 white snow2 snow3 snow4 seashell1 seashell2 seashell3 seashell4 AntiqueWhite1 AntiqueWhite2 AntiqueWhite3 AntiqueWhite4 bisque1 bisque2 bisque3 bisque4 PeachPuff1 PeachPuff2 PeachPuff3 PeachPuff4 NavajoWhite1 NavajoWhite2 NavajoWhite3 NavajoWhite4 LemonChiffon1 LemonChiffon2 LemonChiffon3 LemonChiffon4 cornsilk1 cornsilk2 cornsilk3 cornsilk4 ivory1 ivory2 ivory3 ivory4 honeydew1 honeydew2 honeydew3 honeydew4 LavenderBlush1 LavenderBlush2 LavenderBlush3 LavenderBlush4 MistyRose1 MistyRose2 MistyRose3 MistyRose4 azure1 azure2 azure3 azure4 SlateBlue1 SlateBlue2 SlateBlue3 SlateBlue4 RoyalBlue1 RoyalBlue2 RoyalBlue3 RoyalBlue4 blue1 blue2 blue3 blue4 DodgerBlue1 DodgerBlue2 DodgerBlue3 DodgerBlue4 SteelBlue1 SteelBlue2 SteelBlue3 SteelBlue4 DeepSkyBlue1 DeepSkyBlue2 DeepSkyBlue3 DeepSkyBlue4 SkyBlue1 SkyBlue2 SkyBlue3 SkyBlue4 LightSkyBlue1 LightSkyBlue2 LightSkyBlue3 LightSkyBlue4 SlateGray1 SlateGray2 SlateGray3 SlateGray4 LightSteelBlue1 LightSteelBlue2 LightSteelBlue3 LightSteelBlue4 LightBlue1 LightBlue2 LightBlue3 LightBlue4 LightCyan1 LightCyan2 LightCyan3 LightCyan4 PaleTurquoise1 PaleTurquoise2 PaleTurquoise3 PaleTurquoise4 CadetBlue1 CadetBlue2 CadetBlue3 CadetBlue4 turquoise1 turquoise2 turquoise3 turquoise4 cyan1 cyan2 cyan3 cyan4 DarkSlateGray1 DarkSlateGray2 DarkSlateGray3 DarkSlateGray4 aquamarine1 aquamarine2 aquamarine3 aquamarine4 DarkSeaGreen1 DarkSeaGreen2 DarkSeaGreen3 DarkSeaGreen4 SeaGreen1 SeaGreen2 SeaGreen3 SeaGreen4 PaleGreen1 PaleGreen2 PaleGreen3 PaleGreen4 SpringGreen1 SpringGreen2 SpringGreen3 SpringGreen4 green1 green2 green3 green4 chartreuse1 chartreuse2 chartreuse3 chartreuse4 OliveDrab1 OliveDrab2 OliveDrab3 OliveDrab4 DarkOliveGreen1 DarkOliveGreen2 DarkOliveGreen3 DarkOliveGreen4 khaki1 khaki2 khaki3 khaki4 LightGoldenrod1 LightGoldenrod2 LightGoldenrod3 LightGoldenrod4 LightYellow1 LightYellow2 LightYellow3 LightYellow4 yellow1 yellow2 yellow3 yellow4 gold1 gold2 gold3 gold4 goldenrod1 goldenrod2 goldenrod3 goldenrod4 DarkGoldenrod1 DarkGoldenrod2 DarkGoldenrod3 DarkGoldenrod4 RosyBrown1 RosyBrown2 RosyBrown3 RosyBrown4 IndianRed1 IndianRed2 IndianRed3 IndianRed4 sienna1 sienna2 sienna3 sienna4 burlywood1 burlywood2 burlywood3 burlywood4 wheat1 wheat2 wheat3 wheat4 tan1 tan2 tan3 tan4 chocolate1 chocolate2 chocolate3 chocolate4 firebrick1 firebrick2 firebrick3 firebrick4 brown1 brown2 brown3 brown4 salmon1 salmon2 salmon3 salmon4 LightSalmon1 LightSalmon2 LightSalmon3 LightSalmon4 orange1 orange2 orange3 orange4 DarkOrange1 DarkOrange2 DarkOrange3 DarkOrange4 coral1 coral2 coral3 coral4 tomato1 tomato2 tomato3 tomato4 OrangeRed1 OrangeRed2 OrangeRed3 OrangeRed4 red1 red2 red3 red4 DeepPink1 DeepPink2 DeepPink3 DeepPink4 HotPink1 HotPink2 HotPink3 HotPink4 pink1 pink2 pink3 pink4 LightPink1 LightPink2 LightPink3 LightPink4 PaleVioletRed1 PaleVioletRed2 PaleVioletRed3 PaleVioletRed4 maroon1 maroon2 maroon3 maroon4 VioletRed1 VioletRed2 VioletRed3 VioletRed4 magenta1 magenta2 magenta3 magenta4 orchid1 orchid2 orchid3 orchid4 plum1 plum2 plum3 plum4 MediumOrchid1 MediumOrchid2 MediumOrchid3 MediumOrchid4 DarkOrchid1 DarkOrchid2 DarkOrchid3 DarkOrchid4 purple1 purple2 purple3 purple4 MediumPurple1 MediumPurple2 MediumPurple3 MediumPurple4 thistle1 thistle2 thistle3 thistle4/; # get the configurations from the rc file if the configdir exists (old configuration) readConfig($configFile, \%config) if (-d $user_data_path); # get the configurations from file (new configuration) { my ($ok, $err) = configuration_restore($conf_file, \%conf); warn $err if (not $ok); } $actpic = $config{ActPic}; # todo: generate a template from all lang() and langf() calls within mapivi my %messages; # Warning: no lang() or langf() call before the language_load call! (They will be useless) language_load($config{Language}); # Warning! The keys of the %statistic_data_longnames hash have to be same as in the %searchDB hash!!! my %statistic_data_longnames = ( 'COM' => lang("comment"), 'EXIF' => lang("EXIF data"), 'IPTC' => lang("IPTC data"), 'URG' => lang("rating"), 'KEYS' => lang("keywords"), ); # used as '[empty]' string in IPTC location info my $empty_str = '['.lang("empty").']'; # At startup the menu should always be visible $config{ShowMenu} = 1; # I consider it safer to reset this option after a restart, else the user may lose an orignial picture $config{MakeBackup} = 1; # check at startup if a new Mapivi version is available check_version($version); # check if this is the first start of a new Mapivi version mapiviUpdate() if (($config{Version} eq '000') or ($version ne $config{Version})); $config{Version} = $version; processARGV(); # process the command line arguments as early as possible to give a fast feedback my $layoutOld = $config{Layout}; # this must be done after readConfig! # for zoom and subsample of Tk::Photo objects # the higher the zoom value the longer the time to zoom # subsample is quite fast, so the first number (zoom) should not be bigger than 4 # the second (subsample) may be bigger my @frac; if ($config{SlowButMoreFeatures}) { @frac = (10,1, 6,1, 4,1, 3,1, 2,1, 3,2, 4,3, 1,1, 4,5, 3,4, 2,3, 3,5, 4,7, 5,9, 1,2, 3,7, 2,5, 3,8, 4,11, 1,3, 2,7, 1,4, 2,9, 1,5, 1,6, 1,7, 1,8, 1,10, 1,12, 1,16, 1,25, 1,50); } else { @frac = (10,1, 6,1, 4,1, 3,1, 2,1, 3,2, 4,3, 1,1, 4,5, 3,4, 2,3, 1,2, 1,3, 1,4, 1,5, 1,6, 1,7, 1,8, 1,10, 1,12, 1,16, 1,25, 1,50); } # open main window my $top = MainWindow->new; # hide it, while building up $top->withdraw; # store session start time $top->{sessioninfo}{starttime} = time(); # store name in top hash (used in Tk::MhConfig.pm) $top->{tool_name} = 'Mapivi'; # process Gtk2 events if Gtk2 is available if ($gtk2_avail) { $top->repeat(10, sub{ Gtk2->main_iteration while Gtk2->events_pending; }); } # set the window size checkGeometry(\$config{Geometry}); $top->geometry($config{Geometry}); # add a window and icon picture my $icon_data = <Photo(-data => $icon_data); #my $mapiviiconfile = "$program_data_path/icons/MapiviIcon.gif"; #$mapiviiconfile = "$program_data_path/icons/MapiviIcon32.gif" if $EvilOS; $top->idletasks if $EvilOS; # this line is crucial (at least on windows) $top->iconimage($mapiviicon) if $mapiviicon; my $dragAndDrop1 = "$program_data_path/icons/MiniPic.jpg"; my $dragAndDrop2 = "$program_data_path/icons/MiniPicMulti.jpg"; my $dragAndDropIcon1; $dragAndDropIcon1 = $top->Photo(-file => $dragAndDrop1) if (-f $dragAndDrop1); my $dragAndDropIcon2; $dragAndDropIcon2 = $top->Photo(-file => $dragAndDrop2) if (-f $dragAndDrop2); # button bitmap needed for color buttons my $mcbut = pack("b8" x 8, ".......", ".......", ".......", ".......", ".......", ".......", ".......", "......."); $top->DefineBitmap('mcbut' => 8, 8, $mcbut); # button bitmap needed for + buttons my $plusbut = pack("b5" x 5, "..1..", "..1..", "11111", "..1..", "..1..",); $top->DefineBitmap('plusbut' => 5, 5, $plusbut); # button bitmap needed for - buttons my $minusbut = pack("b5" x 5, ".....", ".....", "11111", ".....", ".....",); $top->DefineBitmap('minusbut' => 5, 5, $minusbut); # pseudo transpartent bitmap for cropDialog my $transbits = pack("b4" x 4, "11..", "11..", "..11", "..11"); $top->DefineBitmap('transp' => 4, 4, $transbits); # pseudo transpartent bitmap for cropDialog my $transbits2 = pack("b1" x 3, "1", "1", "."); $top->DefineBitmap('transp2' => 1, 3, $transbits2); # pseudo transpartent bitmap for cropDialog my $transbits3 = pack("b1" x 3, "1", ".", "1"); $top->DefineBitmap('transp3' => 1, 3, $transbits3); # set title and icon $top->title("Mapivi $version $svnrevision"); $top->iconname("Mapivi"); # set options my $ScW = 10; $ScW = 14 if $EvilOS; # the small scrollbars look ugly under windows for (qw(Scale Scrollbar)) { $top->optionAdd("*$_.width", $ScW, 'userDefault'); } # override -takefocus for frames and scrollbars $top->optionAdd('*Frame.TakeFocus','0'); $top->optionAdd('*Scrollbar.TakeFocus','0'); $top->optionAdd('*ResizeButton.TakeFocus','0'); # change menu style to compact $top->optionAdd('*Menu.borderWidth' => 1); $top->optionAdd('*Menu.activeBorderWidth' => 0); $top->optionAdd('*Menu.borderWidth' => 1); $top->optionAdd('*selectForeground', $config{ColorSelFG}, 'userDefault'); $top->optionAdd('*selectBackground', $config{ColorSel}, 'userDefault'); $top->optionAdd("*highlightColor", $config{ColorSel}, 'userDefault'); $top->optionAdd("*highlightBackground", $conf{color_hl_bg}{value}, 'userDefault'); $top->optionAdd("*background", $conf{color_bg}{value}, 'userDefault'); $top->optionAdd("*activeBackground", $conf{color_act_bg}{value},'userDefault'); # must be after the *background optionAdd call $top->optionAdd("*Menu.background", $conf{color_menu_bg}{value}, 'userDefault'); for (qw(foreground)) { $top->optionAdd("*$_", $conf{color_fg}{value}, 'userDefault'); } $top->optionAdd('*Button.foreground' => $conf{color_fg}{value}); $top->optionAdd('*Button.padY' => 0); $top->optionAdd('*Radiobutton.padY' => 0); $top->optionAdd('*ROText.foreground' => $conf{color_fg}{value}); $top->optionAdd('*Optionmenu.foreground' => $conf{color_fg}{value}); $top->optionAdd('*DirTree.foreground' => $conf{color_fg}{value}); $top->optionAdd('*HList.foreground' => $conf{color_fg}{value}); # must be after the *foreground and *background optionAdd call $top->optionAdd("*Menu.background", $conf{color_menu_bg}{value}, 'userDefault'); $top->optionAdd("*Menu.foreground", $conf{color_menu_fg}{value}, 'userDefault'); for (qw(Scale Scrollbar Adjuster)) { $top->optionAdd("*$_.troughColor", $conf{color_entry}{value}, 'userDefault'); } $top->optionAdd("*ProgressBar.troughColor", $conf{color_bg}{value}, 'userDefault'); $top->optionAdd("*Label.background", $conf{color_bg}{value}, 'userDefault'); for (qw(Entry NumEntry Listbox KListbox K2Listbox TixHList HList Text BrowseEntry.Entry NoteBook)) { $top->optionAdd("*$_.background", $conf{color_entry}{value}, 'userDefault'); } for (qw(Button Checkbutton Radiobutton Menubutton FlatCheckbox FireButton Menu)) { $top->optionAdd("*$_.cursor", "hand2", 'userDefault'); } $top->optionAdd("*Radiobutton.selectColor", $config{ColorSelBut}, 'userDefault'); $top->optionAdd("*Checkbutton.selectColor", $config{ColorSelBut}, 'userDefault'); $top->optionAdd("*Menu.selectColor", $config{ColorSelBut}, 'userDefault'); my $font = $top->Font(-family => $config{FontFamily}, -size => $config{FontSize}, #-weight => "normal,-slant,roman,-underline,0,-overstrike,0 ); my $small_font = $top->Font(-family => $config{FontFamily}, -size => 8); my $font_big = $top->Font(-family => $config{FontFamily}, -size => 20); $top->optionAdd("*font", $font, 'userDefault'); # slick scrollbars $top->optionAdd('*Scrollbar.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Adjuster.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Button.borderWidth' => $config{Borderwidth}); $top->optionAdd('*ResizeButton.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Entry.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Scale.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Slider.borderWidth' => $config{Borderwidth}); $top->optionAdd('*NoteBook.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Frame.borderWidth' => $config{Borderwidth}); $top->optionAdd('*NoteBook.Frame.borderWidth' => 0); $top->optionAdd('*checkbutton.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Checkbutton.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Radiobutton.borderWidth' => $config{Borderwidth}); $top->optionAdd('*radiobutton.borderWidth' => $config{Borderwidth}); $top->optionAdd('*separator.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Menu.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Cascade.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Label.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Canvas.borderWidth' => $config{Borderwidth}); $top->optionAdd('*ROText.borderWidth' => $config{Borderwidth}); $top->optionAdd('*Optionmenu.borderWidth' => $config{Borderwidth}); $top->optionAdd('*DirTree.borderWidth' => $config{Borderwidth}); $top->optionAdd('*HList.borderWidth' => $config{Borderwidth}); # call quitMain when the window is closed by the window manager $top->protocol("WM_DELETE_WINDOW" => sub { quitMain(); }); # init stuff $balloon = $top->Balloon(-bg => $config{ColorSel}, -initwait => 1000); $balloon->Subwidget('message')->configure(-justify => 'left'); $top->fontCreate(qw/C_big -family courier -size 14 -weight bold/); #createMenubar(); my $infoF = $top->Frame(-relief => 'raised'); # $subF contains the 3 frames: navigation frame ($nav_F), thumbnails ($thumbF) and picture ($mainF) my $subF = $top->Frame(); my $nav_F = $subF->Frame(); my $dirA = $subF->Adjuster(); my $thumbF = $subF->Frame(); my $thumbA = $subF->Adjuster(); my $mainF = $subF->Frame(); my $dirtree; # is defined in add_nav_frame() my $comF = $mainF->Frame(-relief => 'raised'); my $comBF = $comF->Frame()->pack(-side => 'left', -expand => 1, -fill => 'both', -anchor=> 'nw', -padx => 0, -pady => 0); my $iptcF = $mainF->Frame(-relief => 'raised'); my $nrofL = $infoF->Label(-justify => 'left', -textvariable => \$nrof, -relief => 'sunken', -anchor => 'w', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -expand => 0, -fill => 'y'); $balloon->attach($nrofL, -msg => lang("x/y (z, s) = first selected picture is number x\nfrom y pictures in the actual folder\nz pictures are selected\ns is the file size of all selected pictures")); my $dirtreedir; my $actdirF = $thumbF->Frame()->pack(-expand => 1, -fill => 'x', -padx => 2, -pady => 1); if ($conf{filter_pics_button}{value}) { $actdirF->{Filter} = $actdirF->Checkbutton(-variable => \$conf{filter_pics}{value})->pack(-side => 'left', -anchor=>'w', -padx => 0); $actdirF->{Filter}->configure(-textvariable => \$actdirF->{Filter}->{excluded_pics}); $actdirF->{Filter}->{excluded_pics} = 0; $balloon->attach($actdirF->{Filter}, -postcommand => sub {$actdirF->{Filter}->{msg} = lang("If enabled this function will filter the pictures using a keyword list.\nPictures containing the following keywords are not shown:\n$conf{filter_pics_keywords}{value}\nUse the right mouse button over the check button to edit this list.\nThe displayed number (currently $actdirF->{Filter}->{excluded_pics}) indicates the number of excluded pictures.")}, -msg => \$actdirF->{Filter}->{msg}); $actdirF->{Filter}->bind('', sub { my $exclude_keys = $conf{filter_pics_keywords}{value}; my $rc = myEntryDialog(lang("Edit keywords"), lang("Please edit the list of keywords to exclude.\nSeparate different keywords with a space."), \$exclude_keys); if ($rc eq 'OK') { $conf{filter_pics_keywords}{value} = $exclude_keys; } }); } else { # if the button is not enabled we should not filter $conf{filter_pics}{value} = 0; } my $act_nav_label = ''; my $act_nav_L = $actdirF->Label(-textvariable => \$act_nav_label, -width => 10, -anchor => 'e', -relief => 'sunken', -bd => $config{Borderwidth}, -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -expand => 1, -fill => 'x'); $balloon->attach($act_nav_L, -msg => lang("Actual view as chosen in the navigation frame.\nClick opens folder selection dialog.")); $act_nav_L->bind('', sub { openDir(); } ); # add three folder check buttons to mark the folder state (sorted, meta-info, prio/ratings) $actdirF->{folder_check_buttons} = $actdirF->Frame()->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 2, -pady => 1); my $otherFilesL = $actdirF->{folder_check_buttons}->Button(-textvariable => \$otherFiles, -command => sub {showNonJPEGS();}, -relief => 'sunken', -bd => $config{Borderwidth}, -foreground => $conf{color_menu_fg}{value}, -padx => 1, -pady => 0)->pack(-side => 'left'); $balloon->attach($otherFilesL, -msg => lang('Number of hidden files in actual folder')."\n".lang('Click to see hidden files')); my $dirPropSORT = 0; my $dirPropMETA = 0; my $dirPropPRIO = 0; $actdirF->{cbSORT} = $actdirF->{folder_check_buttons}->Checkbutton(-variable => \$dirPropSORT, -command => sub { $dirProperties{$actdir}{SORT} = $dirPropSORT; })->pack(-side => 'left', -anchor=>'w', -padx => 0); $actdirF->{cbMETA} = $actdirF->{folder_check_buttons}->Checkbutton(-variable => \$dirPropMETA, -command => sub { $dirProperties{$actdir}{META} = $dirPropMETA; })->pack(-side => 'left', -anchor=>'w', -padx => 0); $actdirF->{cbPRIO} = $actdirF->{folder_check_buttons}->Checkbutton(-variable => \$dirPropPRIO, -command => sub { $dirProperties{$actdir}{PRIO} = $dirPropPRIO; })->pack(-side => 'left', -anchor=>'w', -padx => 0); { my $common_info = lang("\n\nThis button is intended to be used as\npersonal markers for the folder status.\nSee also Menu: ").lang("File")."->".lang("Folder checklist ..."); $balloon->attach($actdirF->{cbSORT}, -msg => langf("You may use this marker e.g. to\nmark this folder as sorted out.%s",$common_info)); $balloon->attach($actdirF->{cbMETA}, -msg => langf("You may use this marker e.g. to\nmark this folder, if all meta information is added.%s",$common_info)); $balloon->attach($actdirF->{cbPRIO}, -msg => langf("You may use this marker e.g. to\nmark this folder, if all pictures are rated.%s",$common_info)); } my %mapivi_icons = define_icons(); my $picLB; # main canvas used to show pictures my $c = $mainF->Scrolled('Canvas', -scrollbars => 'osoe', -width => 2000, -height => 2000, -relief => 'flat', -borderwidth => 0, -highlightthickness => 0, -bg => $conf{color_bg_canvas}{value}, ); $c->configure(-scrollregion => [0, 0, 100, 100]); # some canvas settings $c->{thumb_distance} = 5; # in pixels $c->{thumb_size} = $config{'ThumbSize'}; # in pixels my $whL = $infoF->Label(-textvariable => \$widthheight, -relief => 'sunken', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -expand => 0, -fill => 'y'); $balloon->attach($whL, -msg => lang("Width and height of actual picture in pixels")); my $sizeL = $infoF->Label(-textvariable => \$size, -relief => 'sunken', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -fill => 'y'); $balloon->attach($sizeL, -msg => lang("File size of actual picture in kByte")); # zoom info my $zoomL = $infoF->Label(-textvariable => \$zoomFactorStr, -relief => 'sunken', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -fill => 'y'); $balloon->attach($zoomL, -msg => lang("Zoom factor of actual picture")); # picture rating my $rating_but; { my $rating = 3; $rating_but = rating_button($infoF, sub {setIPTCurgency($picLB, $rating);}, lang("Rating (IPTC urgency) of actual picture\nTo change click on stars or use keys Ctrl-5 .. -1 or Ctrl-F1, -F2, ... -F8"), 'left', 'y', \$rating); } # log info my $userInfoL = $infoF->Label(-textvariable => \$userinfo, -relief => 'sunken', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -fill => 'both', -expand => 1); $balloon->attach($userInfoL, -msg => lang("Mapivi log information\nHint: Click to see complete log history.")); $userInfoL->bind('', sub { showText(lang("Mapivi log"), $global_log, NO_WAIT); } ); # color picker my $colorPickerInfo = $infoF->Label(-text => ' ', -background => $config{ColorPicker}, -relief => 'sunken')->pack(-side => 'left', -fill => 'both', -expand => 0); $balloon->attach($colorPickerInfo, -msg => lang("Color picker: last color picked by clicking\non the picture in the main window.\nPlease click to clear.")); $colorPickerInfo->bind('', sub { $config{ColorPicker} = $conf{color_bg}{value}; $colorPickerInfo->configure(-background => $config{ColorPicker}); }); # thumbnail generator my $nrTCL = $infoF->Label(-textvariable => \$nrToConvert, -relief => 'sunken', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -expand => 0, -fill => 'y'); $balloon->attach($nrTCL, -msg => lang("Number of thumbnails to generate/refresh")); my $progressBar = $infoF->ProgressBar(-takefocus => 0, -borderwidth => 1, -relief => 'sunken', -width => (2*$config{FontSize}), # try to guess the height of the labels -length => 30, -padx => 0, -pady => 0, -variable => \$proccount, -colors => [0 => $config{ColorProgress}], -resolution => 1, -blocks => $config{MaxProcs}, -anchor => 'w', -from => 0, -to => $config{MaxProcs} )->pack(-side => 'left', -fill => 'both', -expand => 0, -padx => 0, -pady => 0); $balloon->attach($progressBar, -msg => lang("Number of background processes\n(generating thumbnail pictures)")); # clock or memory usage label $clockL = $infoF->Label(-textvariable => \$time, -relief => 'sunken', -foreground => $conf{color_menu_fg}{value})->pack(-side => 'left', -fill => 'y'); $balloon->attach($clockL, -msg => \$date); $clockL->bind('', sub { toggle(\$conf{clock_or_memory}{value}); showTimeOrMemory(); } ); # show thumbnails button my $thumbscanvasbut = $infoF->Button(-image => $mapivi_icons{'Image'}, -command => sub {my @pics = $picLB->info('children');show_canvas_thumbs($c, \@pics);})->pack(-side => 'left', -fill => 'both', -expand => 0); $balloon->attach($thumbscanvasbut, -msg => lang('Show thumbnails in picture frame')); # JPEG comment box my $commentText = $comF->Scrolled('ROText', -scrollbars => 'oe', -wrap => 'word', -width => 200, -height => $config{CommentHeight}, )->pack(-side => 'left', -fill => 'both', -expand => 1, -padx => 0, -pady => 0); $balloon->attach($commentText, -msg => lang("Comment(s) of displayed picture")); $picLB = makeThumbListbox($thumbF); focus_on_enter($picLB); # IPTC headline and caption edit box my $titleF = $iptcF->Frame()->pack(-fill => 'both', -expand => 1); my $capF = $iptcF->Frame()->pack(-fill => 'both', -expand => 1); my $titleText; my $captionText; $titleF->Label(-text => 'Headline')->pack(-side => 'left', -fill => 'both'); $titleText = $titleF->Scrolled('Text', -scrollbars => '', -wrap => 'word', -width => 20, -height => 1, )->pack(-side => 'left', -fill => 'both', -expand => 1); $capF->Label(-text => 'Caption ')->pack(-side => 'left', -fill => 'both'); $captionText = $capF->Scrolled('Text', -scrollbars => 'oe', -wrap => 'word', -width => 20, -height => $config{CommentHeight}, )->pack(-side => 'left', -fill => 'both', -expand => 1); { my $common_text = lang("Add or edit text and then save it by pressing the save button.\nPress F4 to show or hide this box."); $balloon->attach($titleText, -msg => lang("IPTC headline (title) of displayed picture.\n").$common_text); $balloon->attach($captionText, -msg => lang("IPTC caption of displayed picture.\n").$common_text); } my $saveB = $capF->Button(-image => compound_menu($top, lang('Save'), 'media-floppy.png', 0), -command => sub { my $title = $titleText->get(0.1, 'end'); $title =~ s/\n+$//; # cut off trailing newline(s) my $caption = $captionText->get(0.1, 'end'); $caption =~ s/\n+$//; # cut off trailing newline(s) my $iptc = { 'Headline' => $title, 'Caption/Abstract' => $caption }; my @list; my @sellist = getSelection($picLB); if (!isInList($actpic, \@sellist)) { my $selp = langf("%d pic(s)", scalar(@sellist)); my $actp = lang('actual'); my $cancel = lang('Cancel'); my $rc = $top->Dialog( -text => langf("The selection (%s) does not contain the actual picture (%s).",$selp, basename($actpic))."\n".lang("Add headline and caption to:"), -title => lang("Add to selection or to actual picture?"), -width => 50, -buttons => [$selp, $actp, $cancel])->Show(); return if ($rc eq $cancel); @list = @sellist if ($rc eq $selp); @list = ($actpic) if ($rc eq $actp); } elsif (scalar(@sellist) > 1) { my $selp = scalar(@sellist).' selected'; my $actp = 'actual'; my $cancel = 'Cancel'; my $rc = $top->Dialog( -text => lang('Add headline and caption to:'), -title => lang('Add to selection or to actual picture?'), -width => 50, -buttons => [$selp, $actp, $cancel])->Show(); return if ($rc eq $cancel); @list = @sellist if ($rc eq $selp); @list = ($actpic) if ($rc eq $actp); } else { @list = ($actpic); } applyIPTC($picLB, $iptc, \@list); } )->pack(-side => 'left', -fill => 'both'); $balloon->attach($saveB, -msg => lang("Save the IPTC headline and caption to the file and database.\nPlease press this button after adding or editing.")); #$captionText->Subwidget("scrolled")->bindtags([]); #$captionText->Subwidget("scrolled")->bind('', sub {}); #->Subwidget("scrolled") # item styles for the thumbnail view my $thumbCaptionFont = $top->Font(-family => $config{FontFamily}, -size => $config{ThumbCaptFontSize}); # if changes are made here, other places may need an update too (see e.g. line containg "my $fileS2") my $thumbS = $picLB->ItemStyle('imagetext', -anchor => 'w', -textanchor => 's', -foreground=>$conf{color_fg}{value}, -background=>$conf{color_bg}{value}, -font => $thumbCaptionFont); my $fileS = $picLB->ItemStyle('image', -anchor=>'w', -foreground=>$config{ColorFile}, -background=>$conf{color_bg}{value}); my $iptcS = $picLB->ItemStyle('text', -anchor=>'nw', -foreground=>$config{ColorIPTC}, -background=>$conf{color_bg}{value}); my $comS = $picLB->ItemStyle('text', -anchor=>'nw', -foreground=>$config{ColorComm}, -background=>$conf{color_bg2}{value}); my $exifS = $picLB->ItemStyle('text', -anchor=>'nw', -foreground=>$config{ColorEXIF}, -background=>$conf{color_bg2}{value}); my $dirS = $picLB->ItemStyle('text', -anchor=>'nw', -foreground=>$config{ColorDir}, -background=>$conf{color_bg2}{value}); toggleHeaders(); # mouse and button bindings # key-desc,double click,show picture in own window $picLB->bind('', sub { return if (!$picLB->info('children')); showPicInOwnWin(getNearestItem($picLB)); } ); # does not always work ??? # key-desc,MiddleMouseButton,show picture in own window $picLB->bind('', sub { return if (!$picLB->info('children')); showPicInOwnWin(getNearestItem($picLB)); } ); # experimental stuff #$top->bind('', sub { print "Mouse Press But 4\n"; } ); #$top->bind('', sub { print "Mouse Press But 5\n"; } ); # this has to be done after the %keywords and %hot_keywords have been read in add_nav_frame($nav_F); # Drag-and-drop # Define the source for drags. # Picture drags are started while pressing left mouse button and moving the mouse my $token; # key-desc,LeftBut,(LeftMouseButton) drag and drop pictures to a folder $token = $picLB->DragDrop (-event => '', # drawback: no selection by dragging possible, but intuitive #(-event => '', # drawback: difficult to use -sitetypes => 'Local', -startcommand => sub { dragFromPicLB($token) }, ); # Define the target for picture drops. $dirtree->DropSite (-droptypes => 'Local', -dropcommand => sub { dropToDirTree(); }, ); # keyword tree drag and drop { my $keyword_token; # key-desc,LeftBut,(LeftMouseButton) drag and drop keywords to picture # drag from keyword tree $keyword_token = $nav_F->{key_frame}->{tree}->DragDrop (-event => '', -sitetypes => 'Local', -startcommand => sub { drag_keyword($nav_F->{key_frame}->{tree}, $keyword_token) }, ); # keyword clipboard (hotlist) drag and drop $keyword_token = $nav_F->{key_frame}->{hot}->DragDrop (-event => '', -sitetypes => 'Local', -startcommand => sub { drag_keyword($nav_F->{key_frame}->{hot}, $keyword_token); }, ); # Define the targets for keyword drops # drop to canvas - actual picture $c->DropSite (-droptypes => 'Local', -dropcommand => sub { print "drop site canvas tree\n"; drop_keyword($c, $keyword_token); }, ); # # drop to picLB - selected pictures in thumbnail list $picLB->DropSite (-droptypes => 'Local', -dropcommand => sub { drop_keyword($picLB, $keyword_token); }, ); # drop to the keyword hotlist $nav_F->{key_frame}->{hot}->DropSite (-droptypes => 'Local', -dropcommand => sub { drop_keyword($nav_F->{key_frame}->{hot}, $keyword_token); }, ); } $picLB->bind('', sub { # save button press coordinates to distinguish picture selection from picture dragging ($picLB->{lastx}, $picLB->{lasty}) = ($Tk::event->x(), $Tk::event->y()); #print "press: $picLB->{lastx}, $picLB->{lasty}\n"; # if mouse is pressed over an already selected item it may be a drag, so we ignore the press #my @selection = $picLB->info('selection'); #if (isInList(getNearestItem($picLB), \@selection)) { # print "pic is in list ignoring press\n"; # Tk->break; # return; #} # saved here for undo function @savedselection2 = @savedselection; @savedselection = $picLB->info('selection'); } ); $picLB->bind('', sub { showSelectedPic(); } ); $picLB->bind('', sub { if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off $thumbMenu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); # key-desc,Return,display the selected picture $picLB->bind('', sub { showSelectedPic(); } ); $c->CanvasBind('', sub { if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off $picMenu->Popup(-popover => "cursor", -popanchor => "nw"); } ); # we can't bind all keys to the complete window ($top) as we have e.g. the IPTC Caption entry which should get all key events addWindowKeyBindings($dirtree, $picLB); addWindowKeyBindings($nav_F->{loc_frame}, $picLB); addWindowKeyBindings($nav_F->{date_frame}, $picLB); addWindowKeyBindings($nav_F->{key_frame}, $picLB); addWindowKeyBindings($nav_F->{cloud_frame}, $picLB); addWindowKeyBindings($nav_F->{search_frame}, $picLB); addWindowKeyBindings($picLB, $picLB); addWindowKeyBindings($c, $picLB); # 2011-03-30: attempt to remove problem that mapivi sometimes doesn't respond to key input in fullscreen- or only-picture-mode # but this doesn't solve the problem, binding to $mainF doesn't help either #addWindowKeyBindings($c->Subwidget('canvas'), $picLB); addCommonKeyBindings($dirtree, $picLB); addCommonKeyBindings($nav_F->{loc_frame}, $picLB); addCommonKeyBindings($nav_F->{date_frame}, $picLB); addCommonKeyBindings($nav_F->{key_frame}, $picLB); addCommonKeyBindings($nav_F->{cloud_frame}, $picLB); addCommonKeyBindings($nav_F->{search_frame}, $picLB); addCommonKeyBindings($picLB, $picLB); addCommonKeyBindings($c, $picLB); $c->CanvasBind('', sub { return unless defined $actpic; return unless (-f $actpic); log_it("Opening $actpic in new window"); showPicInOwnWin($actpic); } ); # key-desc,d,display picture in own window $picLB->bind('', sub { my @sellist = getSelection($picLB); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); show_multiple_pics(\@sellist, 0); } ); $dirtree->bind('', sub { my $dir = getRightDir(); my @list = getPics($dir, WITH_PATH, NO_CHECK_JPEG); sortPics($config{SortBy}, $config{SortReverse}, \@list); showThumbList(\@list, $dir); }); $dirtree->bind('', sub { $dirtree->selectionClear(); $dirtree->selectionSet(getNearestItem($dirtree)); my $dir = getRightDir(); my @list = getPics($dir, WITH_PATH, NO_CHECK_JPEG); sortPics($config{SortBy}, $config{SortReverse}, \@list); showThumbList(\@list, $dir); }); # window resize event #~ $top->bind("" => sub { print "top:Expose\n"; }); #~ $top->bind("" => sub { #~ print "top:Configure\n"; # only if dock is selected #return unless ($config{KeywordDialogDock}); # and the keyword dialog is open #return unless (Exists($keyw)); #dock_keyword_dialog(); #}); # just a test for autosave (2012-01): #$top->bind("" => sub { print "top:FocusIn\n"; }); #$top->bind("" => sub { print "top:FocusOut\n"; }); # support drag and drop from extern # this enables dropping pictures and folders on the mapivi window # 2009-10-22: Drag and drops from extern to Mapivi works under Windows XP but crashes under Ubuntu # so it is now diabled for non-windows systems if ($EvilOS) { $dirtree->DropSite (#-entercommand => sub { print "DragAndDrop - Entercommand\n";}, -dropcommand => [\&dragAndDropExtern, $dirtree], -droptypes => 'Win32' ); $picLB->DropSite (-dropcommand => [\&dragAndDropExtern, $picLB], -droptypes => 'Win32' ); $c->DropSite (-dropcommand => [\&dragAndDropExtern, $c], -droptypes => 'Win32' ); } startup(); # show all types of images supported by Tk::Image #my @types = $top->imageTypes;printlist(@types); # Perl/Tk-Mainloop $top->MainLoop; # override the Motion sub of listbox (extended selection mode) # seems not to help with the drag and drop problem #sub Tk::HList::Motion { #sub Tk::Listbox::Motion { # return; #} ############################################################## # fill the conf hash with default values ############################################################## sub configuration_set_default { # this defines the order of the tabs in the configuration_edit dialog: @conf_tab_order = qw(Main Metadata Thumbnails Tools Files Colors Extra); # if an option should not been shown in the edit dialog use 'tab' => 'no' # HINT!!!!!! # when you change something here it may be overwritten with the content of the user setting # so you may need to delete $conf_file ($user_data_path/mapivi_conf) first! # or press the reset all options button in the options window %conf = ( # Main tab ######### 'folder_preview' => { 'value' => 1, 'kind' => 'bool', 'long' => lang('Folder preview'), 'tab' => 'Main', 'frame' => lang('Navigation'), 'info' => lang('Shows an animated folder preview using a thumbnail slideshow'), 'ord' => 2}, 'show_canvas_thumbs' => { 'value' => 1, 'kind' => 'bool', 'long' => lang('Show thumbnails in picture frame'), 'tab' => 'Main', 'frame' => lang('Navigation'), 'info' => lang('Shows thumbnails in picture frame in square layout'), 'ord' => 4}, 'filter_pics_button' => { 'value' => 0, 'kind' => 'bool', 'long' => 'Filter Pictures Button', 'tab' => 'Main', 'frame' => 'Navigation', 'info' => lang('Show filter by keywords button in thumbnail frame (needs restart).'), 'ord' => 6}, 'show_clock' => { 'value' => 1, 'kind' => 'bool', 'long' => lang('Display a clock in the status bar'), 'tab' => 'Main', 'frame' => 'Display', 'ord' => 7}, 'show_coordinates' => { 'value' => 0, 'kind' => 'bool', 'long' => "Display the coordinates of the mouse cursor in the status bar", 'tab' => 'Main', 'frame' => 'Display', 'ord' => 8}, 'check_version_online' => { 'value' => 1, 'kind' => 'bool', 'long' => lang('Check for Mapivi updates'), 'tab' => 'Main', 'frame' => 'Behavior', 'info' => lang("Check at startup if a new Mapivi version is available.\nNeeds a internet connection."), 'ord' => 10}, 'show_statistic' => { 'value' => 0, 'kind' => 'bool', 'long' => lang('Show database statistic on exit'), 'tab' => 'Main', 'frame' => 'Behavior', 'info' => lang("Show a statistic about the search database when Mapivi is closed."), 'ord' => 15}, # Metadata tab ######### 'xmp_rating' => { 'value' => 1, 'kind' => 'bool', 'long' => 'Store rating in XMP', 'tab' => 'Metadata', 'info' => lang("Store picture rating not only in IPTC urgency, but also in XMP rating tag")."\n".convert_iptc_to_xmp_text(), 'ord' => 2}, 'exif_plus' => { 'value' => 1, 'kind' => 'bool', 'long' => 'Display additional EXIF data', 'tab' => 'Metadata', 'info' => lang("Display detailed EXIF data like contrast, artist, white balance, focus distance, picture number, lens, ..."), 'ord' => 4}, 'add_tool_info' => { 'value' => 0, 'kind' => 'bool', 'long' => 'Add tool information to processed pictures as JPEG comment', 'tab' => 'Metadata', 'info' => langf("If this is enabled Mapivi will add a JPEG comment\nto pictures which are created or processed by Mapivi.\nExample comment: \"Picture lossless cropped by Mapivi $version\""), 'ord' => 6}, # Thumbnails tab ######### # Tools tab ######### 'external_pic_viewer' => { 'value' => 'display', 'kind' => 'file', 'long' => 'Picture viewer', 'tab' => 'Tools', 'frame' => 'Picture viewer', 'info' => lang("Enter the command to start the external picture viewer here.\nYou may also add options.\nExamles: \"gqview -f\", \"gthumb --fullscreen\", \"C:\\Program Files\\IrfanView\\iview_32.exe\""), 'ord' => 2}, 'external_pic_viewer_multi' => { 'value' => 0, 'kind' => 'bool', 'long' => 'Viewer can handle multiple files', 'tab' => 'Tools', 'frame' => 'Picture viewer', 'info' => 'If the external picture viewer is able to handle multiple files enable this. Example: You have selected 3 pictures. If this option is enabled one viewer will be started like this: "viewer pic1.jpg pic2.jpg pic3.jpg", if not 3 viewers will be started like this: "viewer pic1.jpg" "viewer pic2.jpg" "viewer pic3.jpg".', 'ord' => 3}, 'external_pic_editor' => { 'value' => 'gimp-remote', 'kind' => 'file', 'long' => 'Picture editor', 'tab' => 'Tools', 'info' => lang("Enter the command to start the external picture editor here.\nYou may also add options.\nExamples: \"gimp-remote\" for UNIX\n, \"gimp-win-remote gimp-2.6.exe\" for Windows and GIMP > 2.0\n, \"gimp-win-remote\" for Windows and GIMP <= 2.0"), 'ord' => 4}, 'external_raw_editor' => { 'value' => 'darktable', 'kind' => 'file', 'long' => 'RAW editor', 'tab' => 'Tools', 'info' => lang("Enter the command to start the external RAW picture editor here.\nYou may also add options.\nExamples: \"darktable\" for UNIX.\nLeave field empty if the picture editor should be used for all kind of pictures."), 'ord' => 6}, 'external_mail_tool' => { 'value' => 'thunderbird', 'kind' => 'file', 'long' => 'Mail tool', 'tab' => 'Tools', 'info' => lang("Enter the command to start the external mail tool here.\nExamles: \"thunderbird\", \"mozilla-thunderbird\", \"evolution\", \"icedove\", or\n\"C:\\Program Files\\Microsoft Office\\OFFICE11\\OUTLOOK.EXE\""), 'ord' => 7}, 'web_browser' => { 'value' => 'firefox', 'kind' => 'file', 'long' => 'External web browser', 'tab' => 'Tools', 'info' => lang("Enter the command to start the external web browser here.\nExamples: \"firefox\""), 'ord' => 8}, 'video_player' => { 'value' => 'vlc', 'kind' => 'file', 'long' => 'Video player', 'tab' => 'Tools', 'info' => lang("Enter the command to start an external video player.\nExamples: \"vlc\""), 'ord' => 10}, # Files tab ######### 'media_folder_path' => { 'value' => '/media', 'kind' => 'dir', 'long' => 'Path to base folder for removable devices', 'tab' => 'Files', 'info' => lang("Enter the path to the folder used by the OS to mount removable devices,\nlike USB sticks or external HDD etc.\nFor Ubuntu use \"/media\"."), 'ord' => 2}, # Colors tab ######### 'color_fg' => { 'value' => 'gray85', 'kind' => 'color', 'long' => 'Font color', 'tab' => 'Colors', 'ord' => 1}, 'color_bg' => { 'value' => 'gray30', 'kind' => 'color', 'long' => 'Background color', 'tab' => 'Colors', 'ord' => 2}, 'color_bg2' => { 'value' => 'gray30', 'kind' => 'color', 'long' => 'Background color 2', 'tab' => 'Colors', 'ord' => 3}, 'color_bg_canvas' => { 'value' => 'gray30', 'kind' => 'color', 'long' => 'Canvas background color', 'tab' => 'Colors', 'ord' => 4}, 'color_menu_bg' => { 'value' => 'gray40', 'kind' => 'color', 'long' => 'Menu background color', 'tab' => 'Colors', 'ord' => 10}, 'color_hl_bg' => { 'value' => 'gray60', 'kind' => 'color', 'long' => 'Highlight background color', 'tab' => 'Colors', 'ord' => 15}, 'color_act_bg' => { 'value' => 'gray60', 'kind' => 'color', 'long' => 'Active background color', 'tab' => 'Colors', 'ord' => 15}, 'color_menu_fg' => { 'value' => 'gray90', 'kind' => 'color', 'long' => 'Menu font color', 'tab' => 'Colors', 'ord' => 20}, 'color_entry' => { 'value' => 'gray60', 'kind' => 'color', 'long' => 'Entry color', 'tab' => 'Colors', 'ord' => 30}, # Extra tab ######### 'origs_folder_name' => { 'value' => 'originals', 'kind' => 'string', 'long' => 'Folder name for originals', 'tab' => 'Extra', 'info' => lang("Name of sub folder to store original pictures"), 'ord' => 8}, # no tab - options not shown in the configuration editor ######### 'filter_pics' => { 'value' => 0, 'kind' => 'bool', 'long' => 'Filter Pictures', 'tab' => 'no', 'info' => lang("If enabled this function will filter the pictures using a keyword list.")}, 'filter_pics_keywords' => { 'value' => "Person.Family Bird Flower", 'kind' => 'string', 'tab' => 'no', 'info' => lang("filter pics by exclude keyword list (space separated string)"), }, 'import_source' => { 'value' => "/mnt/usb/DCIM/DIMG", 'kind' => 'dir', 'long' => lang('Import source'), 'tab' => 'no', 'info' => lang("Path to picture import folder")}, 'iptc_geometry' => { 'value' => '800x600+1+1', 'kind' => 'string', 'long' => 'IPTC dialog window geometry', 'tab' => 'no'}, 'search_rating_on' => { 'value' => 0, 'kind' => 'bool', 'long' => lang('Search rating constraint on/off'), 'tab' => lang('no')}, 'search_rating_max' => { 'value' => 1, 'kind' => 'int', 'long' => lang('Search rating constraint maximum (also used for navigation rating)'), 'tab' => 'no'}, 'search_rating_min' => { 'value' => 4, 'kind' => 'int', 'long' => lang('Search rating constraint minimum (also used for navigation rating)'), 'tab' => 'no'}, 'nav_rating_on' => { 'value' => 0, 'kind' => 'bool', 'long' => lang('Navigation rating constraint on/off'), 'tab' => 'no'}, 'search_format_on' => { 'value' => 0, 'kind' => 'bool', 'long' => lang('Search for pictures with a certain aspect ratio on/off'), 'tab' => 'no'}, 'search_format' => { 'value' => 'landscape', 'kind' => 'string', 'long' => lang('Search for pictures with a certain aspect ratio'), 'tab' => 'no'}, 'search_format_pano' => { 'value' => 0, 'kind' => 'bool', 'long' => lang('Search for pictures with a certain aspect ratio of 2 >= 1 (panorama format)'), 'tab' => 'no'}, 'logo_text' => { 'value' => 'Mapivi', 'kind' => 'string', 'tab' => 'no'}, 'logo_font' => { 'value' => 'Times_New-Roman', 'kind' => 'string', 'tab' => 'no'}, 'logo_font_size' => { 'value' => 72, 'kind' => 'int', 'tab' => 'no'}, 'logo_font_color' => { 'value' => 'black', 'kind' => 'string', 'tab' => 'no'}, 'logo_shadow' => { 'value' => 1, 'kind' => 'bool', 'tab' => 'no'}, 'logo_shadow_color' => { 'value' => 'gray50', 'kind' => 'string', 'tab' => 'no'}, 'slideshow_random' => { 'value' => 1, 'kind' => 'bool', 'tab' => 'no'}, 'slideshow_number_limit' => { 'value' => 0, 'kind' => 'bool', 'tab' => 'no'}, 'slideshow_number' => { 'value' => 2, 'kind' => 'int', 'tab' => 'no'}, 'slideshow_keywords_exclude' => { 'value' => 'dog cat landscape', 'kind' => 'string', 'tab' => 'no'}, 'slideshow_keywords_include' => { 'value' => 'portrait', 'kind' => 'string', 'tab' => 'no'}, 'slideshow_folders_exclude' => { 'value' => 'originals backup', 'kind' => 'string', 'tab' => 'no'}, 'slideshow_pop_exclude' => { 'value' => 1, 'kind' => 'bool', 'tab' => 'no'}, 'slideshow_pop_level' => { 'value' => 10, 'kind' => 'int', 'tab' => 'no'}, 'slideshow_norating_exclude' => { 'value' => 0, 'kind' => 'bool', 'tab' => 'no'}, 'slideshow_rating_exclude' => { 'value' => 1, 'kind' => 'bool', 'tab' => 'no'}, 'slideshow_rating_level' => { 'value' => 4, 'kind' => 'int', 'tab' => 'no'}, 'zoom_fit_fill' => { 'value' => FIT, 'kind' => 'int', 'tab' => 'no'}, 'show_micro_meta' => { 'value' => 1, 'kind' => 'bool', 'tab' => 'no'}, 'font_size_big' => { 'value' => 24, 'kind' => 'int', 'tab' => 'no'}, 'animation' # move picture on canvas in show_multiple_pics() => { 'value' => 1, 'kind' => 'bool', 'tab' => 'no'}, 'animation_steps' => { 'value' => 20, 'kind' => 'int', 'tab' => 'no'}, 'animation_duration' => { 'value' => 0.25, 'kind' => 'float', 'tab' => 'no'}, 'import_rotate_deg' # rotate pics when importing by this value => { 'value' => 'auto', 'kind' => 'string', 'tab' => 'no'}, 'import_iptc_headline' # add IPTC headline when importing pics => { 'value' => 0, 'kind' => 'bool', 'tab' => 'no'}, 'import_iptc_headline_content' # IPTC headline string => { 'value' => 'Event Headline', 'kind' => 'string', 'tab' => 'no'}, 'clock_or_memory' # show 0=clock or 1=memory usage in top bar => { 'value' => 0, 'kind' => 'bool', 'tab' => 'no'}, ); # some platform specific default settings # for windows if ($EvilOS) { $conf{external_pic_viewer}{value} = 'C:\Program Files\IrfanView\iview_32.exe'; $conf{external_pic_editor}{value} = 'gimp-win-remote gimp-2.6.exe'; $conf{web_browser}{tab} = 'no'; # not needed because we use "start" } # for Mac OS X if ($MacOSX) { $conf{external_pic_viewer}{value} = 'macosx-preview'; $conf{external_pic_viewer_multi}{value} = 1; } } ############################################################## # search and return a list of existing languages (e.g. ('de', 'en', 'fr')) # in the given directory ############################################################## sub languages_find { my $path = shift; my @languages; foreach my $file (getFiles($path)) { # check if the filename matches mapivi-lang-XX (XX is in ISO 639-1 format, see e.g. http://en.wikipedia.org/wiki/Iso_639-1 or http://de.wiktionary.org/wiki/Hilfe:Sprachcodes) if ($file =~ m|^mapivi-lang-(.+)$|) { # store the language push @languages, $1; } } return @languages; } ############################################################## # the language files can be edited with any text editor and # define the hash %messages. In this hash the keys are the # English strings as written in the mapivi.pl code to be translated # and the values are the translated strings ############################################################## sub language_load { my $fh; my $language = shift; # special treatment for en = english if ($language eq 'en') { # as Mapivi is written in english we need no language file and reset the messages hash undef %messages; return; } my $file = "$lang_path/mapivi-lang-$language"; #use utf8; if (!open($fh, '<', $file)) { warn langf("Open language file: Couldn't open $file: $!"); return; } my @lines = <$fh>; close($file); # execute language file to define %messages hash # executing code is always dangerous, but we trust it here. # "no critic" disables perlcritic for this line eval "@lines"; ## no critic (ProhibitStringyEval); warn langf("Failed to evaluate language file $file:\n$@\n") if ($@); return; } ############################################################## # check at startup if a new Mapivi version is available # this is done by trying to open a text file on the mapivi web page and comparing the version number ############################################################## # todo: this could maybe also be used to track the number of users/session??? sub check_version { return if (not $conf{check_version_online}{value}); my $version = shift; use LWP::Simple; my $actual_version = get('http://mapivi.sourceforge.net/actual_version.txt'); if (defined $actual_version) { if ($actual_version+0 > $version+0) { # force numeric context (+0) print langf(" A newer version of Mapivi is available (V%s), see http://mapivi.sourceforge.net/\n", $actual_version); } elsif ($actual_version+0 == $version+0) { # force numeric context (+0) print langf(" Mapivi %s is up-to-date!\n", $version); } } else { print lang(" Could not check actual Mapivi version. No internet connection.\n"); } } ############################################################## # Scan Mapivi code for lang() and langf() calls ############################################################## sub language_scan { my $file; # open the file mapivi if (!open($file, '<', $0)) { warn langf("Could not open $0 for read access!: $!"); return; } my @lines = <$file>; # read the complete file into the array lines close $file; # make a copy of the existing messages to find out unused (outdated) messages my %tmp_messages = %{ dclone(\%messages) }; my $found = "%messages = (\n"; my $not_found = "%messages = (\n"; #my @messages; my $line_nr = 0; foreach my $line (@lines) { $line_nr++; $line =~ s/\s+$//; # cut trailing whitespace $line =~ s/^\s+//; # cut leading whitespace # look for lines containing "lang()" .*? -> the question mark switches to ungreedy matching if ($line =~ m/.*lang\(["'](.*?)["']\).*/) { #push @messages, $line; print langf("Found: $line\n $1\n"); if (exists $messages{$1}) { print langf(" Found in hash: $messages{$1}\n"); $found .= "\"$1\" => \"$messages{$1}\",\t\t\t# line $line_nr\n"; # delete each key in the temp hash when it is used at least once delete $tmp_messages{$1} if (exists $tmp_messages{$1}); } else { print lang(" Not found in hash\n"); $not_found .= "\"$1\" => \"\",\t\t\t# line $line_nr\n"; } } } my $unused = "%messages = (\n"; foreach (keys %tmp_messages) { $unused .= "\"$_\" => \"$tmp_messages{$_}\",\n"; } showText(langf("Existing and used tranlations in language: $config{Language}"), $found, NO_WAIT); showText(langf("Missing translations for language: $config{Language}"), $not_found, NO_WAIT); showText(langf("Existing but unused translations in language: $config{Language}"), $unused, NO_WAIT); return; } ############################################################## # Return a language dependent version of $msg. # based on M() from Msg.pm from Slaven Rezic (BBBike) ############################################################## sub lang { my $msg = shift; $msg = $messages{$msg} if (exists $messages{$msg}); return $msg; } ############################################################## # Return a language dependent version of $msg. # based on Mfmt() from Msg.pm from Slaven Rezic (BBBike) ############################################################## sub langf { return sprintf lang(shift), @_; } ############################################################## ############################################################## sub log_it { my $text = shift; # do not store picture coordinates if ($text !~ m/^coordinates: .*/) { my $time_stamp = getDateTimeISOString(time()); # save to global log $global_log .= "\n$time_stamp: $text"; # show in log window if it is opened if (Exists($top->{log_box})) { $top->{log_box}->insert('end', "\n$time_stamp: $text"); # insert new text at end after timestamp $top->{log_box}->see('end'); } } if (Exists($userInfoL)) { $userinfo = $text; } else { # fallback solution during startup, the userInfoL label may not be available print "log_it: $text\n"; } return; } ############################################################## # find the users home directory ############################################################## sub get_home_path { my $home = glob("~"); if ($EvilOS) { $home = $ENV{"USERPROFILE"}; $home = $ENV{HOME} if ((not -d $home) and (defined $ENV{HOME})); $home = $ENV{HOMEDRIVE}.$ENV{HOMEPATH} if ((not -d $home) and (defined $ENV{HOMEDRIVE} and defined $ENV{HOMEPATH})); $home = "C:/" if (!-d $home); $home =~ s!\\!\/!g; # replace Windows path delimiter with UNIX style \ -> / } return $home; } ############################################################## # determine the path to store the user data ############################################################## sub get_user_data_path { my $home = shift; my $user_data_path = "$home/.mapivi"; if ($EvilOS and defined $ENV{APPDATA}) { # for windows we use this path $user_data_path = $ENV{APPDATA}."/Mapivi"; } # if the environment variable MAPIVIUSERDATAPATH is set to an existing folder # Mapivi will use this folder to store all configuration files # This feature may e.g. be used to separate private and business pictures or to keep different keyword trees # usage in Linux with bash shell: # set the env variable: export MAPIVIUSERDATAPATH=/home/username/.mapivi-private # to check the variable: echo $MAPIVIUSERDATAPATH # usage in Windows with DOS box: # set the env variable: set MAPIVIUSERDATAPATH=C:\temp\mapivi-office # to check the variable: echo %MAPIVIUSERDATAPATH% if (defined $ENV{MAPIVIUSERDATAPATH} and $ENV{MAPIVIUSERDATAPATH} ne '') { if (-d $ENV{MAPIVIUSERDATAPATH}) { $user_data_path = $ENV{MAPIVIUSERDATAPATH}; print langf(" Mapivi info: Using folder:\n \"$user_data_path\"\n as set in environment variable MAPIVIUSERDATAPATH.\n"); } else { print langf(" Mapivi info: Environment variable MAPIVIUSERDATAPATH is set to\n \"$ENV{MAPIVIUSERDATAPATH}\".\n This folder does not exist, please create it before starting Mapivi.\n Mapivi will now use the default folder:\n \"$user_data_path\".\n"); } } $user_data_path =~ s!\\!\/!g; # replace Windows path delimiter with UNIX style \ -> / return $user_data_path; } ############################################################## # stillBusy - block some keys, untill loading of pictures is finished ############################################################## sub stillBusy { if ($showPicInAction) { beep(); log_it(lang('Busy (loading pic), please retry later')); return 1; } return 0; } ############################################################## # show short information in borderless window # window will close itself after given time ############################################################## sub info_window { my $w = shift; # parent widget ref my $text = shift; # text to display in window my $timeout = shift; # optional, time in ms return if not $w; eval { $w->ismapped }; # check if widget is available w/o causing error message return if ($@); return if ((not defined $text) or ($text eq '')); $timeout = 1000 if (not $timeout); # default timeout is 1 second # open info window my $win = $w->Toplevel(-bg => 'gray'); $win->Label(-text => $text, -fg => 'black', -bg => 'white', -bd => 2)->pack(-side => 'top', -padx => 2, -pady => 2); # no window decoration/border $win->overrideredirect(1); # center window above widget $win->Popup(-popover => $w, -overanchor => 'c', -popanchor => 'c'); # close window after $timeout msec my $timer; $timer = $w->after($timeout, sub { $win->destroy() if Exists($win); $timer->cancel if ($timer); }); return; } ############################################################## ############################################################## sub folder_preview_start { return if (not $conf{folder_preview}{value}); my $top = shift; # parent widget ref my $folder = shift; # folder with pictures to preview return unless -d $folder; my @pics = getPics($folder, WITH_PATH, NO_CHECK_JPEG); filter_pics(\@pics, $conf{filter_pics_keywords}{value}); return unless (@pics); # show best rated pictures first sortPics('urgency', 0, \@pics); # open preview window my $win = $top->Toplevel(); $win->Label(-text => scalar @pics." ".lang('Pictures'))->pack(-side => 'top', -padx => 0, -pady => 0); $win->{CANVAS} = $win->Canvas(-width => $config{ThumbSize}, -height => $config{ThumbSize})->pack(-side => 'top', -padx => 0, -pady => 0); # no window decoration/border $win->overrideredirect(1); # store window reference in top for cancel function $top->{FOLDER_PREVIEW_WIN} = $win; $win->{FOLDER_PREVIEW_PICS} = \@pics; $win->{PIC_INDEX} = 0; # start with index 0 in list $win->Popup(-popover => 'cursor'); # move window 100 pixels to the right my $geo = $win->geometry; my ($w, $h, $x, $y) = splitGeometry($geo); $x += 100; $win->geometry($w . 'x' . $h . "+" . $x . "+" . $y); # preview first picture/thumbnail folder_preview_next($win); my $update_period = 500; #[msec] # after $update_period msec show next thumbnail .. $top->{FOLDER_PREVIEW_TIMER} = $top->repeat($update_period, sub { folder_preview_next($win); }); return; } ############################################################## ############################################################## sub folder_preview_next { my $win = shift; my @pics = @{$win->{FOLDER_PREVIEW_PICS}}; my $index = $win->{PIC_INDEX}; my $dpic = $pics[$index]; if (-f $dpic) { my $thumb = getThumbFileName($dpic); if (-f $thumb) { my $photo = $top->Photo(-file => $thumb, -gamma => $config{Gamma}); $win->{CANVAS}->createImage(0,0, -image => $photo, -anchor => 'nw'); } } $index++; # repeat when finished with all pictures $index = 0 if ($index >= @pics); $win->{PIC_INDEX} = $index; return; } ############################################################## ############################################################## sub folder_preview_cancel { # todo: delete photo object after usage!!! # but this doesn't work - gives an "error: image image457 doesn't exists ,..." #if (defined $folder_preview_photo) { $folder_preview_photo->delete; print "cancel:: folder_preview_photo defined \n";} $top->{FOLDER_PREVIEW_WIN}->destroy() if Exists($top->{FOLDER_PREVIEW_WIN}); $top->{FOLDER_PREVIEW_TIMER}->cancel if ($top->{FOLDER_PREVIEW_TIMER}); return; } ############################################################## ############################################################## sub add_nav_frame { my $w = shift; # rating constraint frame # Warning: must be defined in front of the NoteBook, else it won't be visible!!!! my $r_frame = $w->Frame(-bd => 0)->pack(-expand => 1, -side => 'bottom', -fill =>'x', -padx => 0, -pady => 0); $w->{rating_frame} = add_rating_constraint($r_frame); # navigation frame my $nav_tab = $w->NoteBook(-width => 40, -background => $conf{color_bg}{value}, # background of active page (including its tab) -inactivebackground => $conf{color_entry}{value}, # tabs of inactive pages -backpagecolor => $conf{color_bg}{value}, # background behind notebook -tabpadx => 0, -tabpady => 0 )->pack(-expand => 1, -fill => 'both', -padx => 3, -pady => 3); $w->{nav_tab} = $nav_tab; #################### # folder navigation $w->{dir_frame} = $nav_tab->add('dir', -image => $mapivi_icons{'Folder'}, -raisecmd => sub { $w->{rating_frame}->packForget if ($w->{rating_frame}->ismapped); $r_frame->configure(-height => 0); log_it(lang("Navigation by folders"));}); $dirtree = add_dir_tree($w->{dir_frame}); focus_on_enter($dirtree); # folder preview with thumbnail animation - attached to the dirtree $balloon->attach($dirtree, -balloonposition => 'mouse', -motioncommand => sub {folder_preview_cancel(); return 0;}, -cancelcommand => sub {folder_preview_cancel(); return 0;}, -postcommand => sub {my $folder = getNearestItem($dirtree); folder_preview_start($top, $folder); return 0;}, -msg => "Nav dirtree balloon"); #################### # keyword tree navigation + add + edit $w->{key_frame} = $nav_tab->add('key', -image => $mapivi_icons{'Keyword'}, -raisecmd => sub { $w->{rating_frame}->pack(-in => $r_frame, -fill =>'x', -padx => 3, -pady => 1) if (!$w->{rating_frame}->ismapped); log_it(lang('Navigation by keyword'));}); add_key_tree($w->{key_frame}, $picLB); #################### # keyword cloud navigation $w->{cloud_frame} = $nav_tab->add('cloud', -image => $mapivi_icons{'Keyword-Cloud'}, -raisecmd => sub { $w->{rating_frame}->pack(-in => $r_frame, -fill =>'x', -padx => 3, -pady => 1) if (!$w->{rating_frame}->ismapped); log_it(lang('Navigation by keyword cloud')); add_key_cloud($w->{cloud_frame}, $picLB) unless (Exists($w->{cloud_frame}->{canvas}));}); #################### # picture searching $w->{search_frame} = $nav_tab->add('search', -image => $mapivi_icons{'Search'}, -raisecmd => sub { $w->{rating_frame}->pack(-in => $r_frame, -fill =>'x', -padx => 3, -pady => 1) if (!$w->{rating_frame}->ismapped); log_it(lang('Picture search')); my $keys = keys %searchDB; $w->{search_frame}->{labelw}->configure(-text => langf("%d pictures in database", $keys));}); add_search_frame($w->{search_frame}); #################### # location navigation + add $w->{loc_frame} = $nav_tab->add('loc', -image => $mapivi_icons{'Location'}, -raisecmd => sub { $w->{rating_frame}->pack(-in => $r_frame, -fill =>'x', -padx => 3, -pady => 1) if (!$w->{rating_frame}->ismapped); log_it(lang('Navigation by location')); if (!$w->{loc_frame}->{tree}->info('children')){ # get all location info from the database (IPTC tags: country, state, city and sublocation) including pictures my %loc_hash = get_locations(UPDATE); insert_in_tree(LOCATION, $w->{loc_frame}->{tree}, \%loc_hash); } }); add_location_tree($w->{loc_frame}, $picLB); #################### # date navigation $w->{date_frame} = $nav_tab->add('date', -image => $mapivi_icons{'Date'}, -raisecmd => sub { $w->{rating_frame}->pack(-in => $r_frame, -fill =>'x', -padx => 3, -pady => 1) if (!$w->{rating_frame}->ismapped); log_it(lang('Navigation by date')); if (!$w->{date_frame}->{tree}->info('children')){ # get all dates from the database, if not already done my %date_hash = get_dates(UPDATE); insert_in_tree(DATE, $w->{date_frame}->{tree}, \%date_hash); } }); add_date_tree($w->{date_frame}, $picLB); #################### # picture collections / slideshow navigation $w->{collection_frame} = $nav_tab->add('collection', -image => $mapivi_icons{'Image'}, -raisecmd => sub { $w->{rating_frame}->packForget if ($w->{rating_frame}->ismapped); log_it(lang('Navigation by picture collection')); if (!$w->{collection_frame}->{tree}->info('children')){ insert_collections_in_tree($w->{collection_frame}->{tree}, \%slideshows); } }); add_collection_tree($w->{collection_frame}, $picLB); # Set the initial folder exists &Tk::DirTree::chdir ? $dirtree->chdir($actdir) : $dirtree->set_dir($actdir); $dirtree->bind('', sub { $dirMenu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); my $dtr = $dirtree->Subwidget('scrolled'); # change the binding order of the dirtree $dtr->bindtags([$dtr,ref $dtr,$dtr->toplevel,'all']); # stop the execution of the space key $dtr->bind('', sub { Tk->break; }); } ############################################################## # define some icons ############################################################## sub define_icons { # mapping of icon names to file names my %files = ( 'Folder' => 'folder-big.png', 'Keyword' => 'dialog-password-big.png', #'preferences-desktop-font-big.png', 'Location' => 'internet-web-browser-big.png', 'Keyword-Cloud' => 'weather-overcast-big.png', 'Date' => 'office-calendar-big.png', 'Search' => 'system-search-big.png', 'Update' => 'view-refresh-big.png', 'UpdateS' => 'view-refresh.png', 'Back' => 'go-previous.png', 'GoPrevious' => 'go-previous-big.png', 'GoFirst' => 'go-first-big.png', 'GoLast' => 'go-last-big.png', 'GoUp' => 'go-up-big.png', 'GoTop' => 'go-top-big.png', 'GoBottom' => 'go-bottom-big.png', 'MediaStop' => 'media-playback-stop-big.png', 'Clear' => 'edit-clear.png', 'Show' => 'edit-redo.png', 'Rating1' => 'rating-1.png', 'Rating2' => 'rating-2.png', 'Rating3' => 'rating-3.png', 'Rating4' => 'rating-4.png', 'Rating5' => 'rating-5.png', 'Rating6' => 'rating-6.png', 'Rating7' => 'rating-7.png', 'Rating8' => 'rating-8.png', 'Rating0' => 'rating-0.png', 'Stop' => 'process-stop.png', 'Help' => 'help-browser22.png', 'Plus' => 'list-add.png', 'PlusBig' => 'list-add-big.png', 'Minus' => 'list-remove.png', 'Frame-bw' => 'frame-bw.png', 'Frame-wb' => 'frame-wb.png', 'Frame-bwp' => 'frame-bwp.png', 'Frame-wbp' => 'frame-wbp.png', 'EmptyThumb' => 'EmptyThumb.jpg', 'Preferences' => 'preferences-system.png', 'FlagRed' => 'media-record16-red.png', 'FlagGreen' => 'media-record16-green.png', 'FlagBlue' => 'media-record16-blue.png', 'Image' => 'image-x-generic22.png', 'Editor' => 'accessories-text-editor22.png', 'Trash' => 'user-trash22.png', 'Save' => 'media-floppy-big.png', #'Fullscreen' => 'view-fullscreen-big.png', #'Slideshow' => 'x-office-presentation-big.png', ); my %icons; my $error = ''; foreach my $name (keys %files) { my $icon_file = "$icon_path/$files{$name}"; if (-f $icon_file) { $icons{$name} = $top->Photo(-file => $icon_file); } else { $error .= langf("File %s used as %s icon is missing.\n", $icon_file, $name); } } if ($error ne '') { print langf("Errors during Mapivi startup:\n%s\n\nPlease copy the needed icons to %s and restart Mapivi.\n", $error, $icon_path); die; } return %icons; } ############################################################## # makeThumbListbox - create a scrolled HList for thumbnail display ############################################################## sub makeThumbListbox { my $widget = shift; my $lb = $widget->Scrolled('HList', -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 0, -columns => 6, -scrollbars => 'osoe', -selectmode => 'extended', -background => $conf{color_bg}{value}, -width => 30, -height => 200, )->pack(-expand => 1, -fill => 'both'); my $colNr = 0; if ($resizeAvail) { my $thumbH = $lb->ResizeButton(-text => 'Thumbnail', -relief => 'flat', -pady => 0,-anchor => 'w', -widget => \$lb, -column => $colNr); $lb->{thumbcol} = $colNr; $lb->header('create', $colNr++, -itemtype => 'window', -widget => $thumbH, -headerbackground => $conf{color_entry}{value}); my $sizeH = $lb->ResizeButton(-text => lang('File'), -relief => 'flat', -pady => 0,-anchor => 'w', -command => sub { return unless ($lb == $picLB); if ($config{SortBy} eq 'name') { toggle(\$config{SortReverse}); } else { $config{SortReverse} = 0; } $config{SortBy} = 'name'; updateThumbsPlus(); }, -widget => \$lb, -column => $colNr); $lb->{filecol} = $colNr; $lb->header('create', $colNr++, -itemtype => 'window', -widget => $sizeH, -headerbackground => $conf{color_entry}{value}); my $iptcH = $lb->ResizeButton(-text => 'IPTC', -relief => 'flat', -pady => 0,-anchor => 'w', -command => sub { return unless ($lb == $picLB); if ($config{SortBy} eq 'urgency') { toggle(\$config{SortReverse}); } else { $config{SortReverse} = 0; } $config{SortBy} = 'urgency'; updateThumbsPlus(); }, -widget => \$lb, -column => $colNr); $lb->{iptccol} = $colNr; $lb->header('create', $colNr++, -itemtype => 'window', -widget => $iptcH, -headerbackground => $conf{color_entry}{value}); my $comH = $lb->ResizeButton(-text => lang('Comments'), -relief => 'flat', -pady => 0,-anchor => 'w', -widget => \$lb, -column => $colNr); $lb->{comcol} = $colNr; $lb->header('create', $colNr++, -itemtype => 'window', -widget => $comH, -headerbackground => $conf{color_entry}{value}); my $exifH = $lb->ResizeButton(-text => 'EXIF', -relief => 'flat', -pady => 0,-anchor => 'w', -command => sub { return unless ($lb == $picLB); $config{SortBy} = 'exifdate'; toggle(\$config{SortReverse}); updateThumbsPlus(); }, -widget => \$lb, -column => $colNr); $lb->{exifcol} = $colNr; $lb->header('create', $colNr++, -itemtype => 'window', -widget => $exifH, -headerbackground => $conf{color_entry}{value}); my $dirH = $lb->ResizeButton(-text => lang('Folder'), -relief => 'flat', -pady => 0,-anchor => 'w', -command => sub { return unless ($lb == $picLB); if ($config{SortBy} eq 'name') { toggle(\$config{SortReverse}); } else { $config{SortReverse} = 0; } $config{SortBy} = 'name'; updateThumbsPlus(); }, -widget => \$lb, -column => $colNr); $lb->{dircol} = $colNr; $lb->header('create', $colNr, -itemtype => 'window', -widget => $dirH, -headerbackground => $conf{color_entry}{value}); } else { # no resizeAvail $lb->{thumbcol} = $colNr; $lb->header('create', $colNr++, -text => 'Thumbnail', -headerbackground => $conf{color_entry}{value}); #$lb->{namecol} = $colNr; #$lb->header('create', $colNr++, -text => 'Name', -headerbackground => $conf{color_entry}{value}); $lb->{filecol} = $colNr; $lb->header('create', $colNr++, -text => lang('File'), -headerbackground => $conf{color_entry}{value}); $lb->{iptccol} = $colNr; $lb->header('create', $colNr++, -text => 'IPTC', -headerbackground => $conf{color_entry}{value}); $lb->{comcol} = $colNr; $lb->header('create', $colNr++, -text => lang('Comments'), -headerbackground => $conf{color_entry}{value}); $lb->{exifcol} = $colNr; $lb->header('create', $colNr++, -text => 'EXIF', -headerbackground => $conf{color_entry}{value}); $lb->{dircol} = $colNr; $lb->header('create', $colNr, -text => lang('Folder'), -headerbackground => $conf{color_entry}{value}); } return $lb; } ############################################################## # addWindowKeyBindings - add key shortcuts to a widget ############################################################## sub addWindowKeyBindings { my $bind_w = shift; # widget to bind keys to my $lb_w = shift; # thumbnail listbox to use # key-desc,b,show backup or original picture (if available) $bind_w->bind('', sub { showBackup(); }); # key-desc,w,show window list $bind_w->bind('', sub { showWindowList(); }); #$bind_w->bind('', sub { rebuildThumbs(); } ); # key-desc,Ctrl-s,advanced search in database $bind_w->bind('', sub { searchMetaInfo(); } ); # key-desc,Ctrl-f,find pictures (advanced search in database) $bind_w->bind('', sub { searchMetaInfo(); } ); # key-desc,k,search by keyword (tag cloud) $bind_w->bind('', sub { keyword_browse(); } ); # key-desc,o,open a new folder $bind_w->bind('', sub { openDir(); } ); # key-desc,h,show hot folders $bind_w->bind('', sub { $dirMenu->Popup(-popover => "cursor", -popanchor => "nw"); } ); # key-desc,u,update (# and Image) $bind_w->bind('', sub { updateThumbsPlus(); } ); # key-desc,m,open folder of current picture $bind_w->bind('', sub { open_pic_folder($picLB); } ); # key-desc,F05,smart update (add new and remove deleted images) $bind_w->bind('', sub { smart_update(); } ); # key-desc,U,update image $bind_w->bind('', sub { deleteCachedPics($actpic); showPic($actpic); } ); # layouts # key-desc,Ctrl-l,toggle layout of folder thumbnail and picture frame $bind_w->bind('', sub { $config{Layout}++; layout(1); } ); # key-desc,F01,toggle show menu bar $bind_w->bind('', sub { toggle(\$config{ShowMenu}); showHideFrames(); } ); # key-desc,F02,toggle show status bar $bind_w->bind('', sub { toggle(\$config{ShowInfoFrame}); showHideFrames(); } ); # key-desc,F03,toggle overlay information (EXIF, IPTC, ...) $bind_w->bind('', sub { toggle(\$config{ShowInfoInCanvas}); showImageInfoCanvas($actpic); } ); # key-desc,F04,toggle show comment box $bind_w->bind('', sub { toggle(\$config{ShowIPTCFrame}); showHideFrames(); } ); # key-desc,F06,layout 0: folders-thumbnails-picture (25-30-45) $bind_w->bind('', sub { $config{Layout} = 0 ; layout(1);} ); # key-desc,F07,layout 1: folders-thumbnails (20-80-0) $bind_w->bind('', sub { $config{Layout} = 1 ; layout(1);} ); # key-desc,F08,layout 2: thumbnails (0-100-0) $bind_w->bind('', sub { $config{Layout} = 2 ; layout(1);} ); # key-desc,F09,layout 3: thumbnails-picture (0-50-50) $bind_w->bind('', sub { $config{Layout} = 3 ; layout(1);} ); # key-desc,F10,layout 4: picture (0-0-100) $bind_w->bind('', sub { $config{Layout} = 4 ; layout(1); Tk->break; # stop default binding of this key } ); # key-desc,F11,fullscreen mode $bind_w->bind('', sub { fullscreen($top); }); # key-desc,Delete,delete selected pictures to trash $bind_w->bind('', sub { deletePics($lb_w, TRASH); } ); # key-desc,Shift-Delete,remove selected pictures $bind_w->bind('', sub { deletePics($lb_w, REMOVE); } ); # key-desc,Ctrl-q,quit mapivi $bind_w->bind('', sub { quitMain(); } ); # key-desc,Ctrl-r,smart rename selected pictures (e.g to EXIF date) $bind_w->bind('', sub { renameSmart($lb_w); } ); # key-desc,F12,quit mapivi $bind_w->bind('', sub { quitMain(); } ); # show picture, EXIF, Comment and IPTC info # key-desc,c,display JPEG comment $bind_w->bind('', sub { showComment(); } ); # key-desc,Ctrl-t,display embedded EXIF thumbnail $bind_w->bind('', sub { showEXIFThumb(); } ); # key-desc,Ctrl-v,toggle verbose output $bind_w->bind('', sub { toggle(\$verbose); log_it("verbose switched to $verbose");} ); # key-desc,Ctrl-c,crop (lossless) $bind_w->bind('', sub { crop($lb_w); } ); # key-desc,Ctrl-b,add border and/or copyright $bind_w->bind('', sub { losslessBorder(PIXEL); } ); # key-desc,Ctrl-d,change EXIF date/time $bind_w->bind('', sub { setEXIFDate(); } ); # key-desc,Q,change size/quality $bind_w->bind('', sub { changeSizeQuality(); } ); # key-desc,Ctrl-o,open options dialog $bind_w->bind('', sub { options_edit(); } ); # key-desc,Ctrl-m,move pictures to originals sub folder $bind_w->bind('', sub { copy_move_to_origs($lb_w, MOVE); } ); # key-desc,Ctrl-e,edit picture in external edior (e.g. GIMP) $bind_w->bind('', sub { edit_pic($lb_w); } ); # key-desc,H,display picture histogram $bind_w->bind('', sub { showHistogram($lb_w); }); # key-desc,9,rotate picture(s) 90 degrees clockwise $bind_w->bind('', sub { rotate(90); }); # key-desc,8,rotate picture(s) 180 degrees clockwise $bind_w->bind('', sub { rotate(180); }); # key-desc,7,rotate picture(s) 270 degrees clockwise $bind_w->bind('', sub { rotate(270); }); # key-desc,0,auto rotate picture(s) (EXIF orientation) $bind_w->bind('', sub { rotate("auto"); }); # key-desc,Escape,iconify the main window/close any other window $bind_w->bind('', sub { $top->iconify; } ); # thumbnail navigation # key-desc,Space,display the next picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off showPic(nextPic($actpic)); } ); # key-desc,S,display the next selected picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off my @sellist = $lb_w->info('selection'); showPic(nextSelectedPic($actpic)); reselect($lb_w, @sellist); } ); # key-desc,Page-Down,display the next picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off showPic(nextPic($actpic));} ); # key-desc,Backspace,display the previous picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off showPic(prevPic($actpic));} ); # key-desc,Page-Up,display the previous picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off showPic(prevPic($actpic));} ); # key-desc,Home,display the first picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off my @childs = $lb_w->info('children'); return unless (@childs); showPic($childs[0]); } ); # key-desc,End,display the last picture $bind_w->bind('', sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off my @childs = $lb_w->info('children'); return unless (@childs); showPic($childs[-1]); }); # key-desc,Ctrl-g,set GPS coordinates $bind_w->bind('', sub { gps_set($lb_w); } ); # key-desc,s,start/stop slideshow $bind_w->bind('', sub { if ($slideshow == 0) { $slideshow = 1; } else { $slideshow = 0; } slideshow(); } ); # key-desc,-,zoom out or faster slideshow $bind_w->bind('', sub { if ($slideshow) { $config{SlideShowTime}-- if ($config{SlideShowTime} >= 1); log_it("slideshow time: ".$config{SlideShowTime}." sec"); } else { zoomStep(-1); } } ); # key-desc,+,zoom in or slideshow slower $bind_w->bind('', sub { if ($slideshow) { $config{SlideShowTime}++ if ($config{SlideShowTime} < 30); log_it("slideshow time: ".$config{SlideShowTime}." sec"); } else { zoomStep(1); } }); # key-desc,Ctrl-h,display picture in original size (100% zoom) $bind_w->bind('', sub { zoom100(); }); # key-desc,z,display picture in original size (100% zoom) $bind_w->bind('', sub { zoom100(); }); # key-desc,Alt-1,display picture in original size (100% zoom) $bind_w->bind('', sub { zoom100(); }); # key-desc,f,fit picture in canvas (auto zoom) $bind_w->bind('', sub { $conf{zoom_fit_fill}{value} = FIT; fitPicture(); }); # key-desc,Alt-3,fit picture in canvas (auto zoom) $bind_w->bind('', sub { $conf{zoom_fit_fill}{value} = FIT; fitPicture(); }); # key-desc,Alt-2,fill picture in canvas (auto zoom) $bind_w->bind('', sub { $conf{zoom_fit_fill}{value} = FILL; fitPicture(); }); } ############################################################## # addCommonKeyBindings - add key shortcuts to a widget ############################################################## sub addCommonKeyBindings { my $bind_w = shift; # widget to bind keys to my $lb_w = shift; # thumbnail listbox to use # key-desc,a,add JPEG comment $bind_w->bind('', sub { addComment($lb_w); } ); # key-desc,j,edit JPEG comment $bind_w->bind('', sub { editComment($lb_w); } ); # key-desc,v,open picture in external viewer $bind_w->bind('', sub { openPicInViewer($lb_w); } ); # key-desc,r,rename selected pictures $bind_w->bind('', sub { renamePic($lb_w); } ); # key-desc,e,display embedded EXIF data $bind_w->bind('', sub { displayEXIFData($lb_w); } ); # key-desc,x,display embedded XMP data $bind_w->bind('', sub { xmp_show($lb_w); } ); # key-desc,Ctrl-a,select all pictures $bind_w->bind('', sub { selectAll($lb_w); } ); # key-desc,i,display IPTC data $bind_w->bind('', sub { displayIPTCData($lb_w); } ); # key-desc,Alt-c,copy EXIF and IPTC data $bind_w->bind('', sub { copyIPTC(); copyEXIFData(); } ); # key-desc,Alt-v,paste IPTC data $bind_w->bind('', sub { pasteIPTC(); } ); # key-desc,Ctrl-i,edit IPTC data $bind_w->bind('', sub { editIPTC($lb_w); } ); # key-desc,Ctrl-p,copy to print $bind_w->bind('', sub { copyToPrint($lb_w); } ); # key-desc,l,add selected thumbnails to collection (light table) $bind_w->bind('', sub { light_table_add_from_lb($lb_w); } ); # key-desc,Ctrl-t,Show thumbnails in picture frame $bind_w->bind('', sub { my @pics = $picLB->info('children'); show_canvas_thumbs($c, \@pics);} ); # these buttons fit to the rating with IPTC urgency # key-desc,Ctrl-F01,set IPTC urgency to 1 - high $bind_w->bind('', sub { setIPTCurgency($lb_w, 1); } ); # key-desc,Ctrl-F02,set IPTC urgency to 2 $bind_w->bind('', sub { setIPTCurgency($lb_w, 2); } ); # key-desc,Ctrl-F03,set IPTC urgency to 3 $bind_w->bind('', sub { setIPTCurgency($lb_w, 3); } ); # key-desc,Ctrl-F04,set IPTC urgency to 4 $bind_w->bind('', sub { setIPTCurgency($lb_w, 4); } ); # key-desc,Ctrl-F05,set IPTC urgency to 5 - normal $bind_w->bind('', sub { setIPTCurgency($lb_w, 5); } ); # key-desc,Ctrl-F06,set IPTC urgency to 6 $bind_w->bind('', sub { setIPTCurgency($lb_w, 6); } ); # key-desc,Ctrl-F07,set IPTC urgency to 7 $bind_w->bind('', sub { setIPTCurgency($lb_w, 7); } ); # key-desc,Ctrl-F08,set IPTC urgency to 8 - low $bind_w->bind('', sub { setIPTCurgency($lb_w, 8); } ); # key-desc,Ctrl-F09,set IPTC urgency to 0 - none $bind_w->bind('', sub { setIPTCurgency($lb_w, 0); } ); # key-desc,Ctrl-F10,remove IPTC urgency flag $bind_w->bind('', sub { setIPTCurgency($lb_w, 9); } ); # additional rating buttons 1 = 1 star to 5 = 5 stars # these buttons fit to the rating with stars # key-desc,5, set 5 star rating (IPTC urgency to 1 - high) $bind_w->bind('', sub { setIPTCurgency($lb_w, 1); } ); # key-desc,4, set 4 star rating (IPTC urgency to 2) $bind_w->bind('', sub { setIPTCurgency($lb_w, 2); } ); # key-desc,3, set 3 star rating (IPTC urgency to 3) $bind_w->bind('', sub { setIPTCurgency($lb_w, 3); } ); # key-desc,2, set 2 star rating (IPTC urgency to 4) $bind_w->bind('', sub { setIPTCurgency($lb_w, 4); } ); # key-desc,1, set 1 star rating (IPTC urgency to 5 - normal) $bind_w->bind('', sub { setIPTCurgency($lb_w, 5); } ); # key-desc,R, toggle red flag $bind_w->bind('', sub { flag_toggle($lb_w, FLAG_RED); } ); # key-desc,G, toggle green flag $bind_w->bind('', sub { flag_toggle($lb_w, FLAG_GREEN); } ); # key-desc,B, toggle blue flag $bind_w->bind('', sub { flag_toggle($lb_w, FLAG_BLUE); } ); } ############################################################## ############################################################## sub session_info { my $pics_nr = keys(%searchDB); my $start_nr = $top->{sessioninfo}{startpics}; my $diff = $pics_nr - $start_nr; my $start_time = localtime($top->{sessioninfo}{starttime}); my $duration = time() - $top->{sessioninfo}{starttime}; #print "start of session $start_nr\nNow: $pics_nr\n"; log_it("Session: Start: $start_time; Pics in database: $pics_nr, at start: $start_nr, diff: $diff"); } ############################################################## # startup - process all stuff needed to set up mapivi ############################################################## sub startup { print lang("Sub startup ...\n") if $verbose; $picLB->focus; if ($config{NrOfRuns} == 0) { print lang("First run ...\n") if $verbose; make_mapivi_folders(); } $config{NrOfRuns}++; gratulation() if (($config{NrOfRuns} % 1000 == 0) and ($config{NrOfRuns} > 0)); # modulo # create menus createMenubar(); createDirMenu(); createThumbMenu(); createPicMenu(); checkSystem(); startStopClock(); # try to get the saved database (meta info hash) if ($config{SaveDatabase} and -f $searchDBfile) { my $hashRef = retrieve($searchDBfile); if (defined $hashRef) { %searchDB = %{$hashRef}; } else { warn langf("Could not retrieve %s","searchDB ($searchDBfile)"); } } # store number of pictures in DB at session start $top->{sessioninfo}{startpics} = keys(%searchDB); # try to get the saved hotlist folders if (-f "$user_data_path/hotlist") { my $hashRef = retrieve("$user_data_path/hotlist"); if (defined $hashRef) { %dirHotlist = %{$hashRef}; } else { warn langf("Could not retrieve %s","hotlist"); } } # try to get the saved folder properties if (-f "$user_data_path/dirProperties") { my $hashRef = retrieve("$user_data_path/dirProperties"); if (defined $hashRef) { %dirProperties = %{$hashRef}; } else { warn langf("Could not retrieve %s","dirProperties"); } } # add additional folder properties (may be usefull when merging two Mapivi installations) if (-f "$user_data_path/dirProperties.add") { my $hashRef = retrieve("$user_data_path/dirProperties.add"); if (defined $hashRef) { print lang("Found additional dirProperties. Merging information ...\n"); foreach my $dir (keys %{$hashRef}) { foreach my $key (keys %{$hashRef->{$dir}}) { $dirProperties{$dir}{$key} = $hashRef->{$dir}->{$key}; } #$dirProperties{$dir} = dclone($hashRef->$dir); print " adding $dir\n"; } print lang("Merging finished. Added ".keys(%{$hashRef})." folders.\nYou may now delete or rename $user_data_path/dirProperties.add\nto prevent merging during the next start of Mapivi\n"); } } # try to get the saved ignore keywords if (-f "$user_data_path/keywords_ignore") { my $hashRef = retrieve("$user_data_path/keywords_ignore"); if (defined $hashRef) { %ignore_keywords = %{$hashRef}; } else { warn langf("Could not retrieve %s","keywords_ignore"); } } # try to get the saved slideshows if (-f "$user_data_path/slideshows") { my $hashRef = retrieve("$user_data_path/slideshows"); if (defined $hashRef) { %slideshows = %{$hashRef}; } else { warn langf("Could not retrieve %s","slideshows"); } } if (MatchEntryAvail) { # try to get the saved entry values if (-f $file_Entry_values) { my $hashRef = retrieve($file_Entry_values); if (defined $hashRef) { %entryHistory = %{$hashRef}; } else { warn langf("Could not retrieve %s",$file_Entry_values); } } } updateDirMenu(); layout(0); # remove splash screen $splash->Destroy if $splash; # show main window $top->deiconify; $top->raise; setDirProperties(); updateThumbs(); setAdjusterPos(); my $tmp = $config{ShowPic}; $config{ShowPic} = 0; showPic($actpic) if ($config{SelectLastPic} and (defined $actpic) and ($actpic ne '') and (dirname($actpic) eq $actdir)); $config{ShowPic} = $tmp; selectDirInTree($actdir); checkTrash(); # if command line option -i is set or a memory card is inserted we start the import wizard importWizard() if (($opt_i) or ($config{AutoImport} and (-d $config{ImportSource}))); if ($EvilOS) { warn "Win32::Process module not available\n" unless (Win32ProcAvail); } $top->update(); # display the number of pics in the database in the canvas log_it($top->{sessioninfo}{startpics}.' '.lang("pictures")); #show_text_in_canvas($c, $top->{sessioninfo}{startpics}."\n".lang("pictures")); } ############################################################## ############################################################## sub show_text_in_canvas { my $c = shift; # canvas widget ref my $text = shift; my $font = $c->Font(-family => $config{FontFamily}, -size => 50, -weight => 'bold'); my $id = $c->createText( int($c->width/2), int($c->height/2), -font => $font, -text => $text, -anchor => 'c', -justify => 'center', -fill => $conf{color_fg}{value}, -tags => ['TEXT']); my ($x1, $y1, undef, undef) = $c->bbox($id); if (($x1 < 0) or ($y1 < 0)) { # delete text if it doesn't fit into the canas $c->delete('withtag', 'TEXT'); # and log it instead, after replacing all newlines with spaces $text =~ s/\n/ /g; log_it($text); } } ############################################################## # testSuite - some tests for mapivi # idea: start with the pictures which are currently shown - # independend of navigation kind. Copy all of them to a # temp folder in the trash dir. # then call separated test functions which do some tests on this # set of pictures. Before the next test function is called the # pictures are restored so that all functions get the same setup # independend of the calling order by test_prepare(). ############################################################## sub testSuite { # set original file list - should not be changed during test suite! my @childs = $picLB->info('children'); if (@childs < 2) { $top->messageBox(-icon => 'error', -message => lang('Test suite must be started in a folder with at least two picture!'), -title => lang('Test suite'), -type => 'OK'); return; } if (@childs > 30) { my $rc = $top->messageBox(-icon => 'question', -message => "Folder contains ".scalar(@childs)." files.\nIt is recommended to use between 2 and 30 files.\nMore files is Ok, but takes more time. Continue?", -title => lang('Many files?'), -type => 'OKCancel'); return unless ($rc =~ m/Ok/i); } # store first folder to open after tests are finished my $startdir = dirname($childs[0]); my $rc = $top->messageBox(-icon => 'question', -message => langf("Start some internal tests with ".scalar @childs." pictures in $actdir.\nTest results will go to STDOUT (shell/DOS-box where you've started Mapivi).\nOk to go on?"), -title => lang('Start test suite?'), -type => 'OKCancel'); return unless ($rc =~ m/Ok/i); # store and reset some configurations which may require user interaction # reset at end ... my $tmp_AskDeleteHighRatingLevel = $config{AskDeleteHighRatingLevel}; $config{AskDeleteHighRatingLevel} = 0; # preparation # set up temp folders my $dir0 = "$trashdir/testdir0"; my $dir1 = "$trashdir/testdir1"; my $dir2 = "$trashdir/testdir2"; # cleanup if folder is already there print "testSuite: removing temp $dir0\n"; rmtree($dir0, 0, 1) if (-d $dir0); # dir, 0 = no message for each file, 1 = skip write protected files unless (makeDir($dir0, NO_ASK)) { print "testSuite: could not create $dir0\n"; } # check if everything worked unless (-d $dir0) { warn "testSuite: *** $dir0 not found! Stopping test suite!\n"; return; } print "testSuite: temp dir created\n"; # test initial copy actdir -> dir0 print "testSuite: testing copy all\n"; selectAll($picLB); copyPics($dir0, COPY, $picLB, @childs); openDirPost($dir0); my $test_nr = 0; return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_selection($dir1); return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_copy($dir1, $dir2); return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_move($dir1, $dir2); return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_backup($dir1); if (not $EvilOS) { return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_link($dir1, $dir2); } return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_comment($dir1); return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_rotate($dir1); return if (not test_prepare(\@childs,$dir0,$dir1,$dir2,\$test_nr)); test_exif($dir1); ################################################## print "testSuite: going back to start dir\n"; openDirPost($startdir); changeDir($startdir); # linking files changes the cwd so we must move back before we try to remove the dirs # end $top->messageBox(-icon => 'info', -message => "test suite finished", -title => "test suite", -type => 'OK'); # cleanup foreach ($dir0, $dir1, $dir2) { print "testSuite: removing temp dir $_\n"; rmtree($_, 0, 1) if (-d $_); # dir, 0 = no message for each file, 1 = skip write protected files } # restore configurations $config{AskDeleteHighRatingLevel} = $tmp_AskDeleteHighRatingLevel; } ############################################################## ############################################################## sub test_prepare { my $childs = shift; # array ref my ($dir0, $dir1, $dir2, $test_nr) = @_; $$test_nr++; openDirPost($dir0); # cleanup if folders are already there foreach ($dir1, $dir2) { #print "testSuite: removing temp dir $_\n"; rmtree($_, 0, 1) if (-d $_); # dir, 0 = no message for each file, 1 = skip write protected files } foreach ($dir1, $dir2) { unless (makeDir($_, NO_ASK)) { print "testSuite: could not create $_\n"; } } # check if everything worked foreach ($dir1, $dir2) { unless (-d $_) { warn "testSuite: *** $_ not found! Stopping test suite!\n"; return 0; } } print "testSuite: temp dirs created\n"; # test initial copy actdir -> dir0 print "testSuite: prepare pics testing. Test # $$test_nr\n"; selectAll($picLB); copyPics($dir1, COPY, $picLB, @$childs); openDirPost($dir1); return 1; } ############################################################## ############################################################## sub test_selection { my @childs = $picLB->info('children'); # test single selection print "testSuite: testing single selection\n"; foreach (@childs) { selectThumb($picLB, $_); my @sel = $picLB->info('selection'); print "testSuite: *** wrong selection\n" if (@sel != 1); print "testSuite: *** wrong selection\n" if ($sel[0] ne $_); } # test all selection print "testSuite: testing all selection\n"; selectAll($picLB); my @sel = $picLB->info('selection'); print "testSuite: *** wrong selection\n" if (@sel != @childs); } ############################################################## ############################################################## sub test_copy { my ($dir1, $dir2) = @_; my @childs1 = $picLB->info('children'); # copy first pic dir1 -> dir2 print "testSuite: testing copy first\n"; selectThumb($picLB, $childs1[0]); my @sel = $picLB->info('selection'); if (@sel ne 1) { warn "testSuite: *** sel error ".scalar @sel." ne 1\n"; } copyPics($dir2, COPY, $picLB, @sel); openDirPost($dir2); my @childs2 = $picLB->info('children'); if (@childs2 ne 1) { warn "testSuite: *** copy error ".scalar @childs2." ne 0\n"; } if (basename($childs1[0]) ne basename($childs2[0])) { warn "testSuite: *** copy error $childs1[0] ne $childs2[0]\n"; } # copy last pic dir1 -> dir2 print "testSuite: testing copy last\n"; openDirPost($dir1); @childs1 = $picLB->info('children'); selectThumb($picLB, $childs1[-1]); @sel = $picLB->info('selection'); if (@sel ne 1) { warn "testSuite: *** sel error ".scalar @sel." ne 1\n"; } copyPics($dir2, COPY, $picLB, @sel); openDirPost($dir2); @childs2 = $picLB->info('children'); if (@childs2 ne 2) { warn "testSuite: *** copy error ".scalar @childs2." ne 2\n"; } if (basename($childs1[-1]) ne basename($childs2[-1])) { warn "testSuite: *** copy error $childs1[-1] ne $childs2[-1]\n"; } # test copy all dir1 -> dir2 openDirPost($dir1); #my @childs1rest = $picLB->info('children'); my @childs1rest = @childs1; # remove first and last from list, else we get an overwrite requester to the user shift @childs1rest; pop @childs1rest; print "testSuite: testing copy all\n"; selectAll($picLB); copyPics($dir2, COPY, $picLB, @childs1rest); openDirPost($dir2); @childs2 = $picLB->info('children'); if (@childs1 != @childs2) { warn "testSuite: *** copy error ".scalar @childs1." ne ".scalar @childs2."\n"; } foreach my $i (0 .. $#childs1) { # todo this will fail, if files are sorted by file date (copy date) if (basename($childs2[$i]) ne basename($childs1[$i])) { warn "testSuite: *** copy error $childs2[$i] ne $childs1[$i]\n"; } } } ############################################################## ############################################################## sub test_move { my ($dir1, $dir2) = @_; my @childs1 = $picLB->info('children'); # move first and last pics dir2 -> dir1 print "testSuite: testing move first and last\n"; selectThumb($picLB, $childs1[0]); my @sel = $picLB->info('selection'); movePics($dir2, $picLB, @sel); selectThumb($picLB, $childs1[-1]); @sel = $picLB->info('selection'); movePics($dir2, $picLB, @sel); openDirPost($dir2); my @childs2 = $picLB->info('children'); if (@childs2 != 2) { warn "testSuite: *** move error ".scalar @childs2." ne 2\n"; } # move all pics dir1 -> dir2 print "testSuite: testing move all\n"; openDirPost($dir1); selectAll($picLB); @sel = $picLB->info('selection'); movePics($dir2, $picLB, @sel); openDirPost($dir2); @childs2 = $picLB->info('children'); if (@childs2 != @childs1) { warn "testSuite: *** move error ".scalar @childs2." ne ".scalar @childs1."\n"; } } ############################################################## ############################################################## sub test_backup { my $dir1 = shift; # prepare: we should have no backup files (*-bak.jpg) before we start # as this gives a warning when there is no backup we first have to create one my @childs1 = $picLB->info('children'); copyPics($dir1, BACKUP, $picLB, $childs1[0]); selectBak(); deletePics($picLB, TRASH); @childs1 = $picLB->info('children'); my $nr = scalar(@childs1); if ($nr <= 0) { warn "testSuite: *** backup error after deleting backup files nothing left!\n"; return; } # test backup dir1 print "testSuite: testing backup all\n"; selectAll($picLB); my @sel = $picLB->info('selection'); copyPics($dir1, BACKUP, $picLB, @sel); @childs1 = $picLB->info('children'); if (@childs1 != $nr*2) { warn "testSuite: *** backup error ".scalar @childs1." ne 2*$nr\n"; } # test delete backups dir1 selectBak(); @sel = $picLB->info('selection'); warn "testSuite: *** sel error ".scalar @sel." ne 2\n" if (@sel != $nr); deletePics($picLB, TRASH); @childs1 = $picLB->info('children'); warn "testSuite: *** delete backup error ".scalar @childs1." ne 2\n" if (@childs1 != $nr); } ############################################################## ############################################################## sub test_link { my ($dir1, $dir2) = @_; # link all pics dir1 -> dir2 print "testSuite: testing link all\n"; openDirPost($dir1); my @childs1 = $picLB->info('children'); selectAll($picLB); my @sel = $picLB->info('selection'); linkPics($dir2, @sel); @childs1 = $picLB->info('children'); openDirPost($dir2); my @childs2 = $picLB->info('children'); warn "testSuite: link ".scalar @childs1." = ".scalar @childs2."?\n"; if (@childs1 != @childs2) { warn "testSuite: *** link error ".scalar @childs1." ne ".scalar @childs2."\n"; } } ############################################################## ############################################################## sub test_comment { my $dir1 = shift; # test comments first pic print "testSuite: testing comment single\n"; my $testcom = "xxxcccxxx1234ABC"; my @childs1 = $picLB->info('children'); selectThumb($picLB, $childs1[0]); my @sel = $picLB->info('selection'); addCommentToPic($testcom, $sel[0], TOUCH); my $com = getComment($sel[0], LONG); if ($com !~ m/.*$testcom.*/) { warn "testSuite: *** comment $com does not contain $testcom\n"; } # test comments join print "testSuite: testing comments remove and join\n"; # add a comment to all pics selectAll($picLB); @sel = $picLB->info('selection'); addCommentToPic($testcom, $_, TOUCH) foreach (@sel); # remove the comments from the last pic, so we have at least one example for no comment selectThumb($picLB, $childs1[-1]); removeAllComments(NO_ASK); warn "testSuite: *** remove comment error\n" if (scalar getComments($childs1[-1]) != 0); selectAll($picLB); my %comNr; # hash: key:dpic value:nr of comments foreach (@childs1) { my @com = getComments($_); $comNr{$_} = scalar @com; } joinComments(NO_ASK); foreach (@childs1) { my @com = getComments($_); my $nr = $comNr{$_}; $nr = 1 if ($nr >= 2); print $comNr{$_}." -> $nr act: ".scalar @com."($#com)\n" if $verbose; warn "testSuite: *** comment join error\n" if ($nr != @com); } } ############################################################## ############################################################## sub test_rotate { my $dir1 = shift; my @childs1 = $picLB->info('children'); print "testSuite: testing rotate single\n"; selectThumb($picLB, $childs1[0]); rotate(90); rotate(270); my $size = getFileSize($childs1[0]); rotate(90); rotate(270); warn "testSuite: *** rotate single file mismatch!\n" if ($size != getFileSize($childs1[0])); } ############################################################## ############################################################## sub test_exif { my $dir1 = shift; my @childs1 = $picLB->info('children'); print "testSuite: testing EXIF date set/get\n"; selectThumb($picLB, $childs1[0]); my $testdate = '2099:11:30 23:59:59'; my $errorstr = ''; setEXIFDatePic($childs1[0],$testdate,\$errorstr); my $datetime = getEXIFDate($childs1[0]); #print "EXIF date of $childs1[0]: $testdate -> $datetime\n"; warn "testSuite: *** set EXIF date $testdate != $datetime!\n" if ($testdate ne $datetime); } ############################################################## # addToCachedPics - add a image (path and file name) to # the cachedPics list # if it is already in the list, move it to # the end ############################################################## sub addToCachedPics { my $dpic = shift; for my $t ( 0 .. $#cachedPics ) { if ($cachedPics[$t] eq $dpic) { splice @cachedPics, $t, 1; # remove it from list last; } } push @cachedPics, $dpic; # add item to the list print "addToCachedPics: $dpic list:$#cachedPics\n" if $verbose; checkCachedPics(); } ############################################################## # checkCachedPics - check if the cachedPics list contains more # images than allowed, remove the oldest # if necessary ############################################################## sub checkCachedPics { # first check if all entries are valid pictures my @rm_list; for my $t ( 0 .. $#cachedPics ) { push @rm_list, $t unless (-f $cachedPics[$t]); } # remove the invalid pictures for my $t (reverse @rm_list) { my $dpic = $cachedPics[$t]; next unless ($dpic); print "checkCachedPics: removing not existing $dpic\n" if $verbose; $c->delete('withtag', $dpic); # remove it from the canvas $photos{$dpic}->delete if $photos{$dpic}; # delete the photo object delete $photos{$dpic}; # delete the hash item splice @cachedPics, $t, 1; # remove not existing pictures it from list } # short the list, if it is to long while (@cachedPics > $config{MaxCachedPics}) { if ($actpic eq $cachedPics[0]) { print "this is the actual pic - skipping!\n" if $verbose; next; } my $dpic = shift @cachedPics; # get the oldest print "checkCachedPics: removing old $dpic list:$#cachedPics\n" if $verbose; $c->delete('withtag', $dpic); # remove it from the canvas $photos{$dpic}->delete if $photos{$dpic}; # delete the photo object delete $photos{$dpic}; # delete the hash item } #printlist(@cachedPics); # just for safety warn "*** checkCachedPics: photos hash contains more than MaxCachedPics pics (".scalar @cachedPics."(".scalar(keys(%photos)).") > ".$config{MaxCachedPics}.")" if (keys %photos > $config{MaxCachedPics}); } ############################################################## # renameCachedPic - rename a list item ############################################################## sub renameCachedPic { my $old = shift; my $new = shift; return unless (defined $photos{$old}); # open new photo object $photos{$new} = $top->Photo; $photos{$new}->blank; $photos{$new}->copy($photos{$old}); $c->delete('withtag', $old); # remove it from the canvas $photos{$old}->delete if $photos{$old}; # delete the photo object delete $photos{$old}; # delete the hash item my $xoffset = 0; my $yoffset = 0; $xoffset = int(($c->width - $photos{$new}->width) /2) if ($c->width > $photos{$new}->width); $yoffset = int(($c->height - $photos{$new}->height)/2) if ($c->height > $photos{$new}->height); # hide all items on the canvas canvasHide(); # insert pic my $id = $c->createImage($xoffset, $yoffset, -image => $photos{$new}, -tag => ["pic", $new], -anchor => "nw"); bindItem($id); for my $t ( 0 .. $#cachedPics ) { if ($cachedPics[$t] eq $old) { $cachedPics[$t] = $new; # rename list item } } print "renameCachedPic: $old -> $new\n" if $verbose; checkCachedPics(); } ############################################################## # deleteCachedPics - delete all or just one element(s) # and photo objects of the cachedPics list ############################################################## sub deleteCachedPics { my $dpic = shift; # optional, if available this picture will be removed from the cachedPics list, # if not available all elements will be deleted if (defined($dpic) and isInList($dpic, \@cachedPics)) { print "deleteCachedPics: delete single pic $dpic (".scalar @cachedPics.")\n" if $verbose; $c->delete('withtag', $dpic); # remove it from the canvas $photos{$dpic}->delete if $photos{$dpic}; # delete the photo object delete $photos{$dpic}; # delete the hash item #printlist(@cachedPics); my @list = @cachedPics; # copy list @cachedPics = (); # empty list foreach my $i (reverse 0 .. $#list) { unless ($list[$i] eq $dpic) { print "deleteCachedPics: adding $list[$i]\n" if $verbose; push @cachedPics, $list[$i]; } } } else { print "deleteCachedPics: delete all (".scalar @cachedPics.")\n" if $verbose; foreach (@cachedPics) { $c->delete('withtag', $_); # remove it from the canvas $photos{$_}->delete if $photos{$_}; # delete the photo object delete $photos{$_}; # delete the hash item print "deleteCachedPics: deleting pic $_\n" if $verbose; } @cachedPics = (); # empty list } } ############################################################## # showSelectedPic - displays the original picture of the # selected thumbnail ############################################################## sub showSelectedPic { return if (stillBusy()); # block, until last picture is loaded my @sellist = $picLB->info('selection'); # show index number in window showNrOf(); return unless ($picLB->info('children')); return if (@sellist > 1); showPic($sellist[0]); } ############################################################## # showNrOf ############################################################## sub showNrOf { my @pics = $picLB->info('children'); my @sellist = $picLB->info('selection'); my $index = 0; my $size = 0; my $sizeStr = ''; if (@sellist >= 1) { # selection available foreach (@pics) { $index++; last if ($_ eq $sellist[0]); } } if (@sellist >= 2) { # more than one selected foreach (@sellist) { $size += getFileSize($_, NO_FORMAT); } $sizeStr = computeUnit($size) if $size; $sizeStr = ", $sizeStr" if ($sizeStr ne ''); } # show index number in window $nrof = "$index/".@pics." (".@sellist."$sizeStr)"; } ############################################################## # computeUnit - do a byte to kB or MB conversion ############################################################## sub computeUnit { my $size = shift; my $sizeStr; $size = int($size/1024); # KiloByte if ($size > 1024) { # MegaByte if ($size > 1024*1024) { # GigaByte if ($size < (1024*1024*100)) { # less than 100GB $size = int($size*10/(1024*1024))/10; # e.g. 6.9GB or 23.4GB } else { $size = int($size/(1024*1024)); # e.g. 104GB } $sizeStr = "${size}GB"; } else { if ($size < (1024*100)) { # less than 100MB $size = int($size*10/1024)/10; # e.g. 6.9MB or 23.4MB } else { $size = int($size/1024); # e.g. 104MB } $sizeStr = "${size}MB"; } } else { $sizeStr = "${size}kB"; } return $sizeStr; } ############################################################## ############################################################## sub string_changed { my ($old, $new) = @_; my $change = 0; # changed is true if ... # a) old and new are defined and new is not empty and not equal # b) old is undefined and new is defined and not empty if ((defined $new) and ($new ne '')) { if (defined $old) { if ($old ne $new) { $change = 1; } } else { # old not defined #if ($new ne '') { $change = 1; #} } } return $change; } ############################################################## # check_IPTC_edit - check if the user added or edited the IPTC # headline or caption of the actual pic and ask to save it ############################################################## sub check_IPTC_edit { my $dpic = shift; # prevent question at startup return if (not $config{ShowPic}); # we need a picture to compare return unless defined $dpic; return unless -f $dpic; # function is only relevant when caption frame is visible return unless $iptcF->ismapped; my $change = 0; my $headline_new = $titleText->get(0.1, 'end'); my $headline = getIPTCHeadline($dpic); $headline_new =~ s/\n+$// if (defined $headline_new); # cut off trailing newline(s) $change = string_changed($headline, $headline_new); # only if no change has been detected so far we also check the caption if (not $change) { my $caption_new = $captionText->get(0.1, 'end'); my $caption = getIPTCCaption($dpic); $caption_new =~ s/\n+$// if (defined $caption_new); # cut off trailing newline(s) $caption =~ s/\n+$// if (defined $caption); # cut off trailing newline(s) $change = string_changed($caption, $caption_new); } if ($change) { my $rc = $top->messageBox(-icon => 'question', -message => langf("Headline/caption of %s have been changed.\nShould Mapivi save the changes?", $dpic), -title => lang("Save changes?"), -type => 'YesNo'); $saveB->Invoke if ($rc =~ m/Yes/i); } return; } ############################################################## # showPic - displays a picture in the main window canvas ############################################################## sub showPic { # check if the user added or edited the IPTC caption of the actual pic and ask to save it # Warning: must be called before $actpic is set to new picture ($dpic) check_IPTC_edit($actpic); my $dpic = shift; my @pics = $picLB->info('children'); return if ((!defined $dpic) or (!@pics)); if (@pics < 1) { warn lang("Error: No pictures in listbox") if $verbose; log_it(lang("Error: No pictures in listbox")); return; } $actpic = $dpic; return if ((!defined $actpic) or ($actpic eq '')); setTitle(); my $pic = basename($dpic); # select thumb in list even if picture is not shown (see "ShowPic" below) selectThumb($picLB, $dpic); return if (!$config{ShowPic}); # show EXIF info and comment showImageInfo($dpic); # we are still not able to display RAW pictures (nefextract may be a solution for NEFs, see also extract_jpeg()) return if ($dpic =~ m/.*\.(nef)|(raw)$/i); # do not show a picture if the frame is very small if ($dpic =~ m/.*\.(tif)|(tiff)|(xbm)$/i) { log_it("$pic ".lang("Not displayed (unsupported picture format)")); return; } # do not show a picture if there is no picture frame if (!$config{ShowPicFrame}) { log_it("$pic ".lang("Not displayed - no picture frame (hint: try F9 or F11)")); return; } # do not show a picture if the frame is very small if ($mainF->width < 200) { log_it("$pic ".lang("Not displayed (picture frame too small)")); return; } $showPicInAction = 1; # remove thumbnails from canvas, if any clear_canvas_thumbs($c); my $do_center = 0; # flag to center picture (do only when loading or resizing) $balloon->detach($c); # clear the balloon info for the actual pic (right frame of main window) log_it(langf("Loading %s ...", $pic)); my @ids = $c->find('withtag', $dpic); my $id; if (@ids > 0) { # pic is already loaded print "showPic: using cached pic $dpic\n" if $verbose; $id = $ids[0]; # hide all items on the canvas canvasHide(); # make hidden picture visible again $c->itemconfigure($id, -state => 'normal'); $top->update(); } else { print langf("showPic: loading %s\n",$dpic) if $verbose; if (-f $dpic) { # load pic $top->Busy(); #if ($dpic =~ m/.*\.avi$/i) { # hide all items on the canvas #canvasHide(); # load thumbnail #my $thumb = getThumbFileName($dpic); #$photos{$dpic} = $top->Photo(-file => $thumb, -gamma => $config{Gamma}) if -f $thumb; #my $command = "vlc \"$dpic\" "; #execute($command); #} #my $dpic_jpg = ''; #if ($dpic =~ m/(.*)\.nef$/i) { # $dpic_jpg = $1.".jpg"; # print "$dpic is a NEF -> $dpic_jpg\n"; # my $command = "nefextract \"$dpic\" > \"$dpic_jpg\" "; # execute($command); #} #if (-f $dpic_jpg) { # load pic # $photos{$dpic} = $top->Photo(-file => $dpic_jpg, -gamma => $config{Gamma}); # zoom pic # autoZoom(\$photos{$dpic}, $dpic_jpg, $c->width, $c->height); #} #else { # load pic eval { $photos{$dpic} = $top->Photo(-file => $dpic, -gamma => $config{Gamma}); }; # error handling if ($@) { log_it("Error loading $pic"); print "Error loading $pic: \"$@\"\n"; $showPicInAction = 0; $top->Unbusy(); return; } # zoom pic autoZoom(\$photos{$dpic}, $dpic, $c->width, $c->height) if (exists $photos{$dpic} and $config{AutoZoom}); #} if (exists $photos{$dpic}) { # center pic in canvas, only when it's smaller my $xoffset = 0; my $yoffset = 0; $xoffset = int(($c->width - $photos{$dpic}->width) /2) if ($c->width > $photos{$dpic}->width); $yoffset = int(($c->height - $photos{$dpic}->height)/2) if ($c->height > $photos{$dpic}->height); # hide all items on the canvas canvasHide(); # insert pic in canvas $id = $c->createImage($xoffset, $yoffset, -image => $photos{$dpic}, -tag => ['pic',$dpic], -anchor => 'nw'); $do_center = 1; # set flag to center picture bindItem($id); addToCachedPics($dpic); } else { log_it(langf("showPic: error loading %s!",$actpic)); warn langf("showPic: error loading %s!",$actpic) if $verbose; } $top->Unbusy(); addToCachedPics($dpic); } else { canvasHide(); warn langf("showPic: error %s not available!", $actpic) if $verbose; } } # show zoom info showZoomInfo($dpic, $id); showImageInfoCanvas($dpic); increasePicPopularity($dpic); updateOneRow($dpic, $picLB) if ($config{trackPopularity}); if ($config{ShowPicInfo}) { # balloon info for displayed picture (right frame of the main window) my $balloonmsg = makeBalloonMsg($dpic); # bind the balloon to the canvas $balloon->attach($c->Subwidget('canvas'), -balloonposition => 'mouse', -msg => {"pic" => $balloonmsg} ); } else { $balloon->detach($c->Subwidget('canvas')); } log_it($pic); # adjust the canvas scrollbars, only if the canvas contains a picture if ($c->bbox("all") and $photos{$dpic}) { $c->configure(-scrollregion => [ $c->bbox("all") ]); # move canvas view to upper left corner for small pictures #$c->xviewMoveto(0) if ($c->Width > $photos{$dpic}->width); #$c->yviewMoveto(0) if ($c->Height > $photos{$dpic}->height); # center canvas over big pictures only the first time (new picture) canvas_center($c, $photos{$dpic}->width, $photos{$dpic}->height) if $do_center; } $top->Unbusy(); $showPicInAction = 0; } ############################################################## # center canvas scrollbars over big pictures ############################################################## sub canvas_center { my ($c, $pic_w, $pic_h) = @_; # canvas widet, picture width (pixels), picture height (pixels) $c->xviewMoveto(($pic_w - $c->width) /($pic_w*2)) if ($c->width < $pic_w); $c->yviewMoveto(($pic_h - $c->height)/($pic_h*2)) if ($c->height < $pic_h); } ############################################################## # canvasHide ############################################################## sub canvasHide { # hide all items on the canvas $c->update(); #$c->itemconfigure('all', -state => 'hidden'); #$c->itemconfigure('withtag', 'pic', -state => 'hidden'); foreach ($c->find('withtag', 'pic')) { $c->itemconfigure($_, -state => 'hidden'); } } ############################################################## # setTitle - set the window title and the userinfo to the # actual pic ############################################################## sub setTitle { my $title = ''; $title = basename($actpic)." - " if ((defined $actpic) and ($actpic ne '') and (-f $actpic)); $title .= "Mapivi $version $svnrevision"; # just a little gag my (undef,undef,undef,$d,$m,$y) = getDateTime(time()); $title .= langf(" - Happy new year $y!") if ($d == 1 and $m == 1); $top->title($title); log_it(basename($actpic)); } ############################################################## # increasePicPopularity ############################################################## sub increasePicPopularity { return unless ($config{trackPopularity}); my $dpic = shift; if (defined $searchDB{$dpic}{POP}) { $searchDB{$dpic}{POP}++; } else { $searchDB{$dpic}{POP} = 1; } print langf("$dpic has been shown $searchDB{$dpic}{POP} times.\n") if $verbose; } ############################################################## # showMostPopularPics - display the Top 100 of the best rated pics ############################################################## sub showMostPopularPics { # open window my $win = $top->Toplevel(); window_size($win, 80); $win->title(lang('Best rated pictures - TOP 100')); $win->iconimage($mapiviicon) if $mapiviicon; my $text = lang('Searching ...'); my $butF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $Xbut = $butF->Button(-text => lang('Close'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); $butF->Label(-textvariable => \$text)->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $tlb = makeThumbListbox($win); # key bindings bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { selectAll($tlb); } ); $win->bind('', sub { return if (!$tlb->info('children')); my $dpic = getNearestItem($tlb); showPicInOwnWin($dpic); }); $win->bind('', sub { my @sellist = getSelection($tlb); return unless checkSelection($win, 1, 0, \@sellist, lang("picture(s)")); show_multiple_pics(\@sellist, 0); } ); # show picture in main window and in lighttable $win->bind('', sub { open_pic_in_main($tlb); }); $win->bind('', sub { light_table_add_from_lb($tlb); } ); $win->Popup(-popover => 'cursor'); repositionWindow($win); my @populatity_list = sort { my $urga = 0; $urga = $searchDB{$a}{URG} if (defined $searchDB{$a}{URG}); $urga = 9 if ($urga == 0); my $urgb = 0; $urgb = $searchDB{$b}{URG} if (defined $searchDB{$b}{URG}); $urgb = 9 if ($urgb == 0); my $popa = 0; $popa = $searchDB{$a}{POP} if (defined $searchDB{$a}{POP}); my $popb = 0; $popb = $searchDB{$b}{POP} if (defined $searchDB{$b}{POP}); $urga <=> $urgb || $popb <=> $popa; } keys %searchDB; $text = lang('Loading ...'); $win->update(); my %thumbs; foreach my $nr (0 .. 99) { my $dpic = $populatity_list[$nr]; insertPic($tlb, $dpic, \%thumbs); } $text = lang('Ready'); $win->waitWindow; # clean up memory - delete all found thumbnail photo objects delete_thumb_objects(\%thumbs); } ############################################################## # resize window to x% of screensize and center it on screen # (center does not work, at least on windows) ############################################################## sub window_size { my $win = shift; my $size = shift; # in percent of screensize; $size = $size/100; my $w = int($size * $win->screenwidth); my $h = int($size * $win->screenheight); my $x = int(($win->screenwidth - $w)/2); my $y = int(($win->screenheight - $h)/2); $win->geometry("${w}x${h}+${x}+${y}"); } ############################################################## # slideshow_all_pics - show all pictures of the search database # in a slideshow # pictures may be filtered based on rating and keywords ############################################################## sub slideshow_all_pics { my $set_or_start = shift; if ($set_or_start == SETTINGS) { # GUI to edit several filters stored in %conf hash my $ok = picture_filter(lang("Filter all accessible pictures of your database to create a slideshow.")); return if (not $ok); } my @include_keys = split / /, $conf{slideshow_keywords_include}{value}; my @exclude_keys = split / /, $conf{slideshow_keywords_exclude}{value}; my @exclude_folders = split / /, $conf{slideshow_folders_exclude}{value}; my @pic_list; log_it("Scanning database ..."); my $i = 0; my $pw = progressWinInit($top, 'Filtering pictures ...'); my $pic_total = scalar(keys(%searchDB)); my %removed_by; my $total = 0; # build keyword/tag hash #foreach my $dpic (keys %searchDB) { while (my ($dpic, undef) = each %searchDB) { last if progressWinCheck($pw); $total = scalar @pic_list; $i++; progressWinUpdate($pw, "scanning ($i/$pic_total), found $total ...", $i, $pic_total); # exclude non-JPEGS by file suffix (this is much faster than using is_a_JPEG()) if ($dpic !~ m/.*\.jp(g|eg)$/i) { $removed_by{filesuffix}++; next; } # exclude pics from certain folders if (string_contains(dirname($dpic), \@exclude_folders)) { $removed_by{folder}++; next; } # include only pictures with certain keywords if (@include_keys) { if ((not defined $searchDB{$dpic}{KEYS}) or ($searchDB{$dpic}{KEYS} eq '') or (string_contains_nor($searchDB{$dpic}{KEYS}, \@include_keys))) { $removed_by{keyword}++; next; } } # don't show pictures with a low rating (but show pictures without rating) if ($conf{slideshow_rating_exclude}{value} and defined $searchDB{$dpic}{URG} and $searchDB{$dpic}{URG} >= $conf{slideshow_rating_level}{value}) { $removed_by{rating}++; next; } # don't show pictures without rating if ($conf{slideshow_norating_exclude}{value} and not defined $searchDB{$dpic}{URG}) { $removed_by{norating}++; next; } # don't show pictures which have been shown more than n times if ($conf{slideshow_pop_exclude}{value} and defined $searchDB{$dpic}{POP} and $searchDB{$dpic}{POP} >= $conf{slideshow_pop_level}{value}) { $removed_by{popularity}++; next; } # exclude pictures with certain keywords if (@exclude_keys) { if (string_contains($searchDB{$dpic}{KEYS}, \@exclude_keys)) { $removed_by{keyword}++; next; } } # exclude non-existing pictures (e.g. pictures stored on unconnected external media) if (not -f $dpic) { $removed_by{availability}++; next; } # collect matching pics in a list push @pic_list, $dpic; last if ($conf{slideshow_number_limit}{value} and (scalar(@pic_list) >= $conf{slideshow_number}{value})); } progressWinEnd($pw); log_it("Found $total pictures, $removed_by{keyword} removed due to keyword match."); foreach my $reason (keys(%removed_by)) { printf "%6d pictures removed by %s\n", $removed_by{$reason}, $reason; } if ($conf{slideshow_random}{value}) { # shuffle randomly fisher_yates_shuffle(\@pic_list); } else { # sort by name @pic_list = sort { uc(basename($a)) cmp uc(basename($b)) } @pic_list; } show_multiple_pics(\@pic_list, 0, NORMAL, SHOW); } ############################################################## # set some picture filter options (e.g. for the slideshow) ############################################################## sub picture_filter { my $info_text = shift; my $ok = 0; my $w = 30; # open window my $win = $top->Toplevel(); $win->title(lang('Picture filter settings')); $win->iconimage($mapiviicon) if $mapiviicon; $win->Label(-text => $info_text, -anchor => 'w')->pack(-anchor => 'w'); # window frames my $excf = $win->Frame(-bd => 1, -relief => 'raised')->pack(-fill => 'both', -padx => 6, -pady => 6); $excf->Label(-text => lang('Exclude pictures ...'))->pack(-anchor=>'w', -padx => 3, -pady => 3); my $incf = $win->Frame(-bd => 1, -relief => 'raised')->pack(-fill => 'both', -padx => 6, -pady => 6); $incf->Label(-text => lang('Include pictures ...'))->pack(-anchor=>'w', -padx => 3, -pady => 3); my $sortf = $win->Frame(-bd => 1, -relief => 'raised')->pack(-fill => 'both', -padx => 6, -pady => 6); $sortf->Label(-text => lang('Sort pictures ...'))->pack(-anchor=>'w', -padx => 3, -pady => 3); my $limitf = $win->Frame(-bd => 1, -relief => 'raised')->pack(-fill => 'both', -padx => 6, -pady => 6); $limitf->Label(-text => lang('Limit number of pictures ...'))->pack(-anchor=>'w', -padx => 3, -pady => 3); # exclude by rating $excf->Checkbutton(-variable => \$conf{slideshow_rating_exclude}{value}, -text => lang('with rating lower than'))->pack(-anchor=>'w', -padx => 3, -pady => 3); $excf->Scale(-variable => \$conf{slideshow_rating_level}{value}, -from => 1, -to => 8, -resolution => 1, -orient => 'horizontal', -showvalue => 1, )->pack(-expand => 1, -fill => 'x', -padx => 6, -pady => 6); $excf->Checkbutton(-variable => \$conf{slideshow_norating_exclude}{value}, -text => lang('without rating'))->pack(-anchor=>'w', -padx => 3, -pady => 3); # exclude by popularity $excf->Checkbutton(-variable => \$conf{slideshow_pop_exclude}{value}, -text => lang('shown more often than'))->pack(-anchor=>'w', -padx => 3, -pady => 3); $excf->Scale(-variable => \$conf{slideshow_pop_level}{value}, -from => 1, -to => 200, -resolution => 1, -orient => 'horizontal', -showvalue => 1, )->pack(-expand => 1, -fill => 'x', -padx => 6, -pady => 6); # exclude by keywords $excf->Label(-text => lang('matching one of these keywords (space separated, case insensitive)'), -anchor => 'w')->pack(-anchor => 'w', -padx => 3, -pady => 3); $excf->Entry(-textvariable => \$conf{slideshow_keywords_exclude}{value}, -width => $w)->pack(-fill => 'x', -padx => 3, -pady => 3); # exclude by folders $excf->Label(-text => lang('in folders matching (space separated, case insensitive)'), -anchor => 'w')->pack(-anchor => 'w', -padx => 3, -pady => 3); $excf->Entry(-textvariable => \$conf{slideshow_folders_exclude}{value}, -width => $w)->pack(-fill => 'x', -padx => 3, -pady => 3); # include by keywords $incf->Label(-text => lang('matching one of these keywords (space separated, case insensitive)'), -anchor => 'w')->pack(-anchor => 'w', -padx => 3, -pady => 3); $incf->Entry(-textvariable => \$conf{slideshow_keywords_include}{value}, -width => $w)->pack(-fill => 'x', -padx => 3, -pady => 3); # sort random or by filename $sortf->Checkbutton(-variable => \$conf{slideshow_random}{value}, -text => lang('by random order'))->pack(-anchor=>'w', -padx => 3, -pady => 3); # limit amount of pictures (low number will speed up time to scan database, but not all pictures are considered) $limitf->Checkbutton(-variable => \$conf{slideshow_number_limit}{value}, -text => lang('use limit'))->pack(-anchor=>'w', -padx => 3, -pady => 3); $limitf->Scale(-variable => \$conf{slideshow_number}{value}, -from => 2, -to => scalar(keys(%searchDB)), -resolution => 1, -orient => 'horizontal', -showvalue => 1, )->pack(-expand => 1, -fill => 'x', -padx => 6, -pady => 6); my $but_frame = $win->Frame()->pack(-fill =>'x'); my $ok_but = $but_frame->Button(-text => lang('OK'), -command => sub { $ok = 1; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); my $x_but = $but_frame->Button(-text => lang('Cancel'), -command => sub { $ok = 0; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); $win->Popup(-popover => 'cursor'); repositionWindow($win); $win->waitWindow; return $ok; } ############################################################## # check if any items of the list are contained in the string # case insensitive! ############################################################## sub string_contains { my $string = shift; my $list_ref = shift; my $match = 0; if (defined $string) { foreach (@$list_ref) { if ($string =~ m/$_/i) { $match = 1; last; } } } return $match; } ############################################################## # check if any items of the list are not contained in the string # case insensitive! (this is equivalent to an AND-Search!) ############################################################## sub string_contains_not { my $string = shift; my $list_ref = shift; my $match = 0; if (defined $string) { foreach (@$list_ref) { if (not ($string =~ m/$_/i)) { $match = 1; last; } } } return $match; } ############################################################## # check if any items of the list are not contained in the string # case insensitive! (this is equivalent to a NOR-Search!) ############################################################## sub string_contains_nor { my $string = shift; my $list_ref = shift; # include keyword list my $match = 0; if (defined $string) { # or-function "Tim, Tom" -> "Tim|Tom" my $pattern = ''; foreach (@$list_ref) { $pattern .= $_.'|'; } # remove last "|" again $pattern = substr($pattern, 0, length($pattern)-1); if (not ($string =~ m/$pattern/i)) { $match = 1; } } return $match; } ############################################################## # exif_histogram - display a histogram of some EXIF data ############################################################## sub exif_histogram { my $lb = shift; my @pics; # if a listbox reference is given we try to get the selected pictures from there if (defined $lb and Exists($lb)) { @pics = getSelection($lb); } # else we use all pictures of the search database if (!@pics or (scalar @pics < 1)) { @pics = keys %searchDB; } # open window my $win = $top->Toplevel(); $win->title(lang('EXIF histograms')); $win->iconimage($mapiviicon) if $mapiviicon; my $text = langf("Collecting EXIF data in ".scalar @pics." pictures ..."); my $c_w = 800; my $c_h = 600; my $border = 20; $win->Label(-textvariable => \$text)->pack(-expand => 0, -fill => 'x'); my $canvas = $win->Canvas(-width => $c_w, -height => $c_h, -background => $conf{color_bg}{value}, -relief => 'sunken',)->pack(-side => 'top', -padx => 3, -pady => 3); my %balloon_hash; $balloon->attach($canvas, -balloonposition => "mouse", -msg => \%balloon_hash ); my $butF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $Xbut = $butF->Button(-text => lang('Close'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->Popup(-popover => 'cursor'); repositionWindow($win); my %histogram; my %histogram_long = ( 'focal_distance35' => lang('Focal distance in 35mm (mm)'), 'focal_distance' => lang('Focal distance (mm)'), 'aperture' => lang('Aperture (F)'), 'iso' => lang('Film speed (ISO)'), 'exposure_time' => lang('Exposure time (1/s)'), ); $win->Busy; #my %aperture; foreach my $dpic (@pics) { next unless exists $searchDB{$dpic}; next unless exists $searchDB{$dpic}{EXIF}; my $exif = $searchDB{$dpic}{EXIF}; # Focal distance - the EXIF info should look like this: ... 105mm ... with no brackets if ($exif =~ m|.*[^\(](\d+)mm[^\)].*|) { $histogram{focal_distance}{$1}++; } if ($exif =~ m|.*\((\d+)mm\).*|) { $histogram{focal_distance35}{$1}++; } # Aperture - the EXIF info should look like this: ... F2.8 ... if ($exif =~ m|.* F(\d+\.\d) .*|) { $histogram{aperture}{$1}++; } # ISO - the EXIF info should look like this: ... ISO400 ... if ($exif =~ m|.* ISO(\d+) .*|) { $histogram{iso}{$1}++; } # exposure time - the EXIF info should look like this: ... 1/125s ... if ($exif =~ m|.* (\d+)\/(\d+)s .*|) { $histogram{exposure_time}{$2}++ if ($1 == 1); } } # my @populatity_list = sort { # my $popa = 0; # $popa = $searchDB{$a}{POP} if (defined $searchDB{$a}{POP}); # my $popb = 0; # $popb = $searchDB{$b}{POP} if (defined $searchDB{$b}{POP}); # $popb <=> $popa; # } keys %searchDB; $text = lang('Printing histogram ...'); $win->update(); #my $font = $win->Font(-family => $config{FontFamily}, -size => $config{FontSize}); $c_w -= 2*$border; $c_h -= 2*$border; my $size_min = 3; my $nr_of_diagrams = keys %histogram; # diagram height my $d_h = ($c_h - ($nr_of_diagrams - 1)*$border)/$nr_of_diagrams ; # bottom line of next diagram my $y = 0; foreach my $kind (sort keys %histogram) { $y = $y + $border + $d_h; # determine maximum values of this hash my $max_value = 0; my $max_key = 0; my $sum = 0; foreach (keys %{$histogram{$kind}}) { $max_value = $histogram{$kind}{$_} if ($histogram{$kind}{$_} > $max_value); $max_key = $_ if ($_ > $max_key); $sum += $histogram{$kind}{$_}; } draw_coordinate_system($canvas, $border, $y-$d_h, $c_w, $d_h); foreach (sort {$a <=> $b} keys %{$histogram{$kind}}) { my $x = $_/$max_key*$c_w + $border; $x = sqrt($_/$max_key)*$c_w + $border if ($kind eq 'exposure_time');; my $h = $histogram{$kind}{$_}/$max_value*$d_h; $h = $size_min if ($h < $size_min); # minimum height $canvas->createLine( $x, $y, $x, $y-$h, -fill => 'red', -width => $size_min, -tags => "$_/$histogram{$kind}{$_}"); my $procent = sprintf "%2.2f%%", $histogram{$kind}{$_}/$sum*100; $balloon_hash{"$_/$histogram{$kind}{$_}"} = langf("$histogram_long{$kind}: ${_} $histogram{$kind}{$_} pictures ($procent)\nFound this information type in $sum pictures."); } # diagram title $canvas->createText($border + $c_w/2, $y-$d_h, -font => $font, -fill => $conf{color_fg}{value}, -text => $histogram_long{$kind}, -anchor => 'n',); } $win->Unbusy; $text = langf("EXIF histogram of ".scalar @pics." pictures. (Hint: Stop mouse pointer above a line to see details.)"); $win->waitWindow; } ############################################################## ############################################################## sub draw_coordinate_system { my ($c, $x, $y, $w, $h) = @_; # y-axis $c->createLine($x, $y+$h, $x, $y-8, -fill => $conf{color_fg}{value}, -tags => 'coordinate system', -arrow => 'last', -arrowshape => [6,6,3], -width => 1, ); # x-axis $c->createLine($x, $y+$h, $x+$w+8, $y+$h, -fill => $conf{color_fg}{value}, -tags => 'coordinate system', -arrow => 'last', -arrowshape => [6,6,3], -width => 1, ); } ############################################################## # stopWatchStart ############################################################## my $stopWatchTime; sub stopWatchStart { $stopWatchTime = Tk::timeofday(); } ############################################################## # stopWatchStop ############################################################## sub stopWatchStop { my $text = ''; $text = shift; printf langf("stopWatch: %.5f secs ($text)\n"), (Tk::timeofday() - $stopWatchTime); } ############################################################## # selectThumb ############################################################## sub selectThumb { my $lb = shift; my $index = shift; $lb->selectionClear(); return unless (defined $index); unless ($lb->info("exists", $index)) { warn "selectThumb: $index is not available!" if $verbose; return; } $lb->selectionSet($index); $lb->anchorSet($index); $lb->see($index); if ($config{CenterThumb}) { my $next = $lb->info('next', $index); my $prev = $lb->info('prev', $index); $lb->see($prev) if ($prev); $lb->update; $lb->see($next) if ($next); } $lb->update; showNrOf(); } ############################################################## # selectAll ############################################################## sub selectAll { my $lb = shift; my @item = $lb->info('children'); return unless (@item); $lb->selectionSet($item[0], $item[-1]); # Hlist doesn't work with 'end' or 'all' showNrOf() if ($lb == $picLB); } ############################################################## # selectBak ############################################################## sub selectBak { $picLB->selectionClear(); my @pics = $picLB->info('children'); foreach (@pics) { if ($_ =~ m/.*-bak\.jp(g|eg)$/i) { $picLB->selectionSet($_); } } showNrOf(); if (!defined $picLB->info('selection')) { $top->messageBox(-icon => 'info', -message => "Nothing selected!\nThere are no file names matching the pattern: \"*-bak.jp(e)g\".", -title => "No backups", -type => 'OK'); } } ############################################################## # selectInv ############################################################## sub selectInv { my @sellist = $picLB->info('selection'); $picLB->selectionClear(); my @pics = $picLB->info('children'); foreach (@pics) { if (!isInList($_, \@sellist)) { $picLB->selectionSet($_); } } showNrOf(); } ############################################################## # getThumbFileName - return the location of the corresponding # thumbnail file (full path) ############################################################## sub getThumbFileName { my $dpic = shift; my $dir = dirname( $dpic); my $pic = basename($dpic); # normalize the path $dir =~ s!\\!\/!g; # replace Windows path delimiter with UNIX style \ -> / if (defined $thumbDBhash{$dir}) { return $thumbDBhash{$dir}."/$pic"; } # the default place for thumbnails is always a sub folder called ".thumbs" in the actual folder my $thumbdir = "$dir/$thumbdirname"; # central thumbDB if (($config{CentralThumbDB}) or # config option set to central thumbdir (!-d $dir) or # if the folder is not mounted/available ((-d $thumbdir) and (!-w $thumbdir)) or # or .thumbdir exists but is write protected (-f "$dir/.nothumbs") or # or file .nothumbs is found ((!-w $dir) and (!-d $thumbdir))) { # or dir is write protected but there is no .thumbdir $dir = cut_device_letter($dir); $thumbdir = "$thumbDB/$dir"; $thumbdir =~ s/\/+/\//g; # replace multiple slashes with one // -> / } $thumbDBhash{$dir} = $thumbdir; # store for quicker response return "$thumbdir/$pic"; } ############################################################## # get rid of the device names (C:\ d:/ etc.) in Windows paths ############################################################## sub cut_device_letter { my $dir = shift; if ($EvilOS) { # in windows we have to get rid of the device names (C:\ d:/ etc.) because they may change between sessions and we can't use them inside a path print "cut_device_letter: $dir " if $verbose; $dir =~ s!^[a-z]:/!!i; # for slash $dir =~ s!^[a-z]:\\!!i; # for backslash print "-> $dir\n" if $verbose; } return $dir; } ############################################################## # generateThumbs - generate thumbnails for each picture # remove outdated thumbs in folder $actdir (global variable!) ############################################################## sub generateThumbs { print "generateThumbs\n" if $verbose; my $ask = shift; # ASK = ask the user befor making a thumbnail dir, NO_ASK my $show = shift; # SHOW = show the generated thumbs in $picLB, NO_SHOW my $getpics = shift; # optional bool, get the pics with getpics not from the listbox my $nrofprocs = 0; my @pics; if ((defined $getpics) and ($getpics == 1)) { @pics = getPics($actdir, WITH_PATH, NO_CHECK_JPEG); # if the thumbs won't be shown, no need to sort sortPics($config{SortBy}, $config{SortReverse}, \@pics) if ($show == SHOW); } else { @pics = $picLB->info('children'); # this should be much faster than getPics($actdir); } # remove outdated thumbs and exif data cleanSubDirs($actdir); return 0 if (@pics <= 0); $nrToConvert = 0; # count first foreach my $lpic (@pics) { my $dpic = $lpic; next if (!getRealFile(\$dpic)); my $thumb = getThumbFileName($lpic); if (aNewerThanb($dpic, $thumb)) { $nrToConvert++; # count the nr of thumbs to generate/refresh } } return 0 if ($nrToConvert == 0); # nothing to do # ask the user, if he wants to update the thumbs now if ($config{AskGenerateThumb}) { my $rc = checkDialog("Generate thumbnails?", "There are $nrToConvert thumbnails to generate.\nShall I do this now?", \$config{AskGenerateThumb}, "ask every time", '', 'OK', 'Cancel'); return 0 if ($rc ne 'OK'); } my $prefix = thumbnail_prefix(\%config); # generate thumbs my $generated_thumbs = 0; foreach my $lpic (@pics) { my $dpic = $lpic; next if (!getRealFile(\$dpic)); my $pic = basename($dpic); my $thumb = getThumbFileName($lpic); next if (!aNewerThanb($dpic, $thumb)); if (-z $dpic) { # file is empty (size zero) log_it("Generating thumbnails: $pic is an empty file. Skipping."); next; } removeFile($thumb); my $thumbdir = dirname($thumb); next if (!makeDir($thumbdir, $ask)); if (!-w $thumbdir) { log_it("Thumb folder $thumbdir is not writeable, so mapivi is not able to generate thumbnails"); next; } # try to get the embedded thumbnail when option is selected and for all RAW pictures if ($config{UseEXIFThumb} or is_raw_file($dpic)) { my $errors = ''; extractThumb($dpic, $thumb, \$errors); # resize/process according thumbnail options if (-f $thumb) { # especially for RAW files we need both "JPEG:" statements so we can't use thumbnail_postfix here my $com = "$prefix JPEG:\"$thumb\" JPEG:\"$thumb\" "; execute($com); } } # found a EXIF thumbnail -> show it if (-f $thumb) { # here we increase the process counter, just because ... proccount(1); # ... in updateOneThumb it will be decreased updateOneThumb($thumb, $lpic, $show); next; # thumbnail finished, do next pic in loop } my $string = thumbnail_postfix($prefix, $dpic, $thumb); print "command: $string\n" if $verbose; if (!$EvilOS) { # running on a "good" OS like Linux we can use background processes :) # start a background process for each pic my $fh = Tk::IO->new(-linecommand => \&nop, -childcommand => [\&updateOneThumb, $thumb, $lpic, $show]); #$hiresstart = [gettimeofday]; # hires - measure the loading time $fh->exec($string); proccount(1); # count processes $nrofprocs = proccount(); if ($nrofprocs >= $config{MaxProcs}) { # waiting for current process to finish $fh->wait(); } } else { # we run on a evil OS like windows - no threading :( proccount(1); # count processes (system "$string") == 0 or warn "$string failed: $!"; updateOneThumb($thumb, $lpic, $show); } $generated_thumbs++; } return $generated_thumbs; } ############################################################## # generateOneThumb ############################################################## sub generateOneThumb { my $dpic = shift; my $thumb = getThumbFileName($dpic); my $prefix = thumbnail_prefix(\%config); my $string = thumbnail_postfix($prefix, $dpic, $thumb); execute($string); } ############################################################## # cleanSubDirs - remove thumbs and exif infos without a # corresponding picture ############################################################## sub cleanSubDirs { my $dir = shift; my $thumbdir = dirname(getThumbFileName("$dir/dummy.jpg")); my $exifdir = "$dir/$exifdirname"; return if (!-d $dir); # clean thumb and exif dir foreach my $subdir ($thumbdir, $exifdir) { if (-d $subdir) { my @subpics = getPics($subdir, JUST_FILE, NO_CHECK_JPEG); # no sort needed foreach my $pic (@subpics) { if (!-f "$dir/$pic") { removeFile("$subdir/$pic"); } } } } } ############################################################## # thumbnail_prefix - build up the command string for the # generation of thumbnails depending on # the settings in the given config hash ############################################################## sub thumbnail_prefix { my $conf = shift; my $pre = ''; $pre = " montage -thumbnail \"$conf->{'ThumbSize'}x$conf->{'ThumbSize'}>+$conf->{'ThumbBorder'}+$conf->{'ThumbBorder'}\" -quality $conf->{'ThumbQuality'} -background \"$conf->{'ColorThumbBG'}\" "; #$pre = " montage -size \"$conf->{'ThumbSize'}x$conf->{'ThumbSize'}>\" -geometry \"$conf->{'ThumbSize'}x$conf->{'ThumbSize'}>+$conf->{'ThumbBorder'}+$conf->{'ThumbBorder'}\" -quality $conf->{'ThumbQuality'} -background \"$conf->{'ColorThumbBG'}\" "; #$pre .= "-frame $conf->{'ThumbBorder'}x$conf->{'ThumbBorder'} " if $conf->{UseThumbFrame}; $pre .= "-shadow " if $conf->{UseThumbShadow}; # ! Sharpen is the most time consuming option, when building thumbnails! if ($conf->{ThumbSharpen} > 0) { $pre .= "-sharpen $conf->{'ThumbSharpen'} " # the higher the value the slower } return $pre; } ############################################################## # build the second part of the image magick command line # this is only separated from the first part (thumbnail_prefix) # due to performance reasons (prefix is generic and has to be # called just once, while postfix is specific for each file) ############################################################## sub thumbnail_postfix { my ($prefix, $dpic, $thumb) = @_; # thumbnail is always in JPEG format, but the suffix of the picture is not changed my $string = "$prefix \"$dpic\""; # for avi videos or animated GIFs we generate just one thumbnail from the first frame $string .= "[0]" if (($dpic =~ /.*\.avi$/i) or ($dpic =~ /.*\.gif$/i)); $string .= " JPEG:\"$thumb\" "; return $string; } ############################################################## # light_table_open_window ############################################################## sub light_table_open_window { if (Exists($ltw)) { $ltw->deiconify; $ltw->raise; $ltw->focus; return; } # open window $ltw = $top->Toplevel(); $ltw->title(lang('Picture collection')); $ltw->iconimage($mapiviicon) if $mapiviicon; $ltw->bind('', sub {light_table_close();}); $ltw->bind('', sub {light_table_close();}); $ltw->bind('', sub {light_table_select_all();}); $ltw->bind('', sub {fullscreen($ltw);}); # call quitMain when the window is closed by the window manager $ltw->protocol("WM_DELETE_WINDOW" => sub { light_table_close(); }); if ($EvilOS) { $ltw->DropSite (-dropcommand => [\&light_table_dragAndDropExtern, $ltw], -droptypes => 'Win32' ); } # bool flag: if true we ask the user for confirmation before closing $ltw->{unsafed_changes} = 0; # status bars my $collection_frame = $ltw->Frame()->pack(-anchor => 'w', -fill => 'x', -expand => 0, -pady => 1); $collection_frame->Label(-text => lang('Collection').':')->pack(-side => 'left', -padx => 2, -pady => 2); $collection_frame->Label(-textvariable => \$ltw->{folder})->pack(-side => 'left', -padx => 2, -pady => 2); $collection_frame->Label(-textvariable => \$ltw->{collection})->pack(-side => 'left', -padx => 2, -pady => 2); $collection_frame->Label(-text => ', '.lang('File').':')->pack(-side => 'left', -padx => 2, -pady => 2); $collection_frame->Label(-textvariable => \$ltw->{file})->pack(-side => 'left', -padx => 2, -pady => 2); $collection_frame->Label(-text => ', '.lang('Unsaved').':')->pack(-side => 'left', -padx => 2, -pady => 2); $collection_frame->Label(-textvariable => \$ltw->{unsafed_changes})->pack(-side => 'left', -padx => 2, -pady => 2); my $status_frame = $ltw->Frame()->pack(-anchor => 'w', -fill => 'x', -expand => 0, -pady => 1); $status_frame->Label(-textvariable => \$ltw->{label})->pack(-side => 'left', -padx => 2, -pady => 2); # main canvas $ltw->{frame} = $ltw->Scrolled('Canvas', -scrollbars => 'oe', -confine => 1, -xscrollincrement => 117, -yscrollincrement => 117, -height => 570, -width => 370, -relief => 'flat', -borderwidth => 0, -highlightthickness => 0, )->pack(-fill =>'both', -expand => 1, -padx => 2, -pady => 2); $ltw->{canvas} = $ltw->{frame}->Subwidget('canvas'); # add menu $ltw->{menu} = $ltw->Menu; $ltw->configure(-menu => $ltw->{menu}); my $file_menu = $ltw->{menu}->cascade(-label => lang('Slideshow')); $file_menu->cget(-menu)->configure(-title => lang('Slideshow menu')); $file_menu->command(-label => lang('Open ...'), -command => sub { collection_open(RESET); }); $file_menu->separator; $file_menu->command(-label => lang('Show slideshow'), -command => sub { show_multiple_pics($ltw->{canvas}->{thumb_list}, 0); }); $file_menu->command(-label => lang('Show slideshow, start from selected picture'), -command => sub { my @sel = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); return unless checkSelection($ltw, 1, 0, \@sel, lang("picture(s)")); my $dpic = get_path_from_id($ltw->{canvas},$sel[0]); my $index = index_in_list($dpic, $ltw->{canvas}->{thumb_list}); show_multiple_pics($ltw->{canvas}->{thumb_list}, $index); }); $file_menu->command(-label => lang('Show selected pictures'), -command => sub { light_table_show_sel_pics($ltw, $ltw->{canvas}->{thumb_list}); }); $file_menu->command(-label => lang('Open selected pictures in external viewer'), -command => sub { openPicInViewer($ltw->{canvas}); }, -accelerator => ''); $file_menu->separator; $file_menu->command(-label => lang('Save'), -command => sub { if ((defined $ltw->{folder} and $ltw->{folder} ne '') and (defined $ltw->{collection} and $ltw->{collection} ne '')) { light_table_save(); } else { light_table_save_as(); } }); $file_menu->command(-label => lang('Save as ...'), -command => sub { light_table_save_as(); }); $file_menu->separator; $file_menu->command(-label => lang('Import from file ...'), -command => sub { light_table_open(RESET); }); $file_menu->command(-label => lang('Add from file ...'), -command => sub { light_table_open(ADD); }); $file_menu->command(-label => lang('Export to file'), -command => sub { if ((defined $ltw->{file}) and (-f $ltw->{file})) { light_table_save_to_file($ltw->{file}); } else { light_table_save_to_file_as(); } }); $file_menu->command(-label => lang('Export to file as ...'), -command => sub { light_table_save_to_file_as(); }); $file_menu->separator; $file_menu->command(-label => lang('Clear'), -command => sub { undef @{$ltw->{canvas}->{thumb_list}}; light_table_clear(); light_table_reset_collection();}); $file_menu->command(-label => lang('Update'), -command => sub { light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection();}); $file_menu->command(-label => lang('Reload thumbnails'), -command => sub { light_table_reload(); }); $file_menu->separator; $file_menu->command(-label => lang('Close'), -command => sub { light_table_close(); }); my $sort_menu = $ltw->{menu}->cascade(-label => lang('Sort')); $sort_menu->command(-label => lang('File name (A - Z)'), -command => sub { $ltw->Busy; sortPics('name', 0, $ltw->{canvas}->{thumb_list}); $ltw->{unsafed_changes} = 1; # set dirty bit $ltw->Unbusy; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); $sort_menu->command(-label => lang('File name (Z - A)'), -command => sub { $ltw->Busy; sortPics('name', 1, $ltw->{canvas}->{thumb_list}); $ltw->{unsafed_changes} = 1; # set dirty bit $ltw->Unbusy; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); $sort_menu->separator; $sort_menu->command(-label => lang('Date (new first)'), -command => sub { $ltw->Busy; sortPics('exifdate', 0, $ltw->{canvas}->{thumb_list}); $ltw->{unsafed_changes} = 1; # set dirty bit $ltw->Unbusy; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); $sort_menu->command(-label => lang('Date (old first)'), -command => sub { $ltw->Busy; sortPics('exifdate', 1, $ltw->{canvas}->{thumb_list}); $ltw->{unsafed_changes} = 1; # set dirty bit $ltw->Unbusy; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); $sort_menu->separator; $sort_menu->command(-label => lang('Rating (high first)'), -command => sub { $ltw->Busy; sortPics('urgency', 0, $ltw->{canvas}->{thumb_list}); $ltw->{unsafed_changes} = 1; # set dirty bit $ltw->Unbusy; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); $sort_menu->command(-label => lang('Rating (low first)'), -command => sub { $ltw->Busy; sortPics('urgency', 1, $ltw->{canvas}->{thumb_list}); $ltw->{unsafed_changes} = 1; # set dirty bit $ltw->Unbusy; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); my $opt_menu = $ltw->{menu}->cascade(-label => lang("Options")); $ltw->{show_balloon} = 1; # todo: move to config hash $ltw->{show_status} = 1; # todo: move to config hash $opt_menu->checkbutton(-label => lang("Show picture info"), -variable => \$ltw->{show_balloon}, -command => sub { light_table_balloon();}); # window resize event $ltw->bind("" => sub { # get canvas size my $cw = $ltw->{canvas}->width; my $ch = $ltw->{canvas}->height; # compare with last size if (defined $ltw->{LAST_CANVAS_WIDTH} and defined $ltw->{LAST_CANVAS_HEIGHT}) { # if the canvas size didn't change we need no reorder return if (($cw == $ltw->{LAST_CANVAS_WIDTH}) and ($ch == $ltw->{LAST_CANVAS_HEIGHT})); } # store new size $ltw->{LAST_CANVAS_WIDTH} = $cw; $ltw->{LAST_CANVAS_HEIGHT} = $ch; # if there is a timer running cancel it $ltw->{LAST_RESIZE_TIMER_MH}->cancel if ($ltw->{LAST_RESIZE_TIMER_MH}); $ltw->{LAST_RESIZE_MH} = Tk::timeofday; # after 200 msec we reorder the thumbnails according to the new geometry to give a preview $ltw->{LAST_RESIZE_TIMER_MH} = $ltw->after(200, sub { light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); }); }); my $context_menu = $ltw->Menu(-title => lang("Context Menu")); $ltw->bind('', sub { $context_menu->Popup(-popover => "cursor", -popanchor => "nw"); } ); $ltw->bind('', sub { light_table_delete(); }); $ltw->bind('', sub { light_table_show_sel_pics($ltw, $ltw->{canvas}->{thumb_list}); }); $ltw->bind('', sub { openPicInViewer($ltw->{canvas}); }); $context_menu->command(-image => compound_menu($top, lang('move selected to top'), 'go-first.png'), -command => sub { light_table_shift('top'); }); $context_menu->command(-image => compound_menu($top, lang('move selected to bottom'), 'go-last.png'), -command => sub { light_table_shift('bottom'); }); $context_menu->command(-label => lang('remove selected from collection'), -accelerator => "", -command => sub { light_table_delete(); }); $context_menu->separator; # ----------------------- # $context_menu->command(-label => lang('Select all'), -command => sub { light_table_select_all(); }, -accelerator => ''); $context_menu->command(-label => lang('Select to end'), -command => sub { light_table_select_to_end(); }); $context_menu->separator; # ----------------------- # addFileActionsMenu($context_menu, $ltw->{canvas}); $context_menu->command(-label => lang('copy and rename selected'), -command => sub { light_table_copy_rename(); }); $context_menu->command(-image => compound_menu($top, lang('Copy to print ...'), 'printer.png'), -command => sub { copyToPrint($ltw->{canvas}); }); $context_menu->separator; # ----------------------- # $context_menu->command(-image => compound_menu($top, lang('Crop (lossless) ...'), 'edit-cut.png'), -command => sub { crop($ltw->{canvas}); }, -accelerator => ""); $context_menu->command(-label => lang('Collage/index print ...'), -command => sub { my @pics = selection_get_sort($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); indexPrint(\@pics); }); $context_menu->separator; # ----------------------- # $context_menu->command(-image => compound_menu($top, lang('Open pictures in new window'), 'image-x-generic.png'), -command => sub { light_table_show_sel_pics($ltw, $ltw->{canvas}->{thumb_list});}, -accelerator => ''); $context_menu->command(-image => compound_menu($top, lang('Open pictures in external viewer'), 'image-x-generic.png'), -command => sub { openPicInViewer($ltw->{canvas}); }); $ltw->{canvas}->{thumb_distance} = 5; # store values also in canvas $ltw->{canvas}->{thumb_size} = 108; # for light_table_reorder $ltw->Popup; checkGeometry(\$config{LtwGeometry}); $ltw->geometry($config{LtwGeometry}); } ############################################################## ############################################################## sub collection_open { my @collections; foreach my $folder (keys %slideshows) { # safety check: neither $folder nor $collection must contain string " - " # as this is used as delimiter warn "folder \"$folder\" contains delimiter string!" if ($folder =~ m/.* - .*/); foreach my $collection (keys %{$slideshows{$folder}}) { warn "collection \"$collection\" contains delimiter string!" if ($folder =~ m/.* - .*/); push @collections, "$folder - $collection"; } } @collections = sort @collections; my $title = 'Open collection'; my $text = 'Please select collection to open'; my @sellist; return 0 unless (mySelListBoxDialog($title, $text, SINGLE, 'OK', \@sellist, @collections)); if ($sellist[0]) { my $fold_col = $collections[$sellist[0]] ; my @tmp = split / - /, $fold_col; my $folder = $tmp[0]; my $collection = $tmp[1]; if (exists $slideshows{$folder}{$collection}) { my $pics = $slideshows{$folder}{$collection}{pics}; #my $file = $slideshows{$folder}{$collection}{file}; light_table_edit($pics, $folder, $collection); log_it("Open collection: $folder $collection"); } else { log_it("Error in open collection: $folder $collection does not exists!"); } } } ############################################################## ############################################################## sub light_table_show_sel_pics { my $widget = shift; my $picture_list_ref = shift; my @pics = selection_get_sort($widget->{canvas}, $picture_list_ref); return unless checkSelection($widget, 1, 0, \@pics, lang("picture(s)")); show_multiple_pics(\@pics, 0); } ############################################################## # light_table_open ############################################################## sub light_table_open { my $mode = shift; # must be ADD or RESET my $filei = shift; # optional slideshow file my ($ok, $errors, $info, $doubles_count, $doubles, $file) = light_table_open_int($mode, $filei); my $text = ''; $text .= "These $doubles_count pictures are already in the slideshow and have been skipped:\n$doubles\n\n" if ($doubles_count > 0); $text .= "Errors while reading $file:\n$errors" if ($errors ne ''); $text .= "\nInformation while reading $file:\n$info" if ($info ne ''); showText("Information and Errors", $text, NO_WAIT); if ($ok and $mode == RESET) { $ltw->title(lang('Collection').': '.basename($file)); $ltw->{file} = $file; } } ############################################################## # light_table_reopen - same as light_table_open, but shows only # errors and does not change the slideshow file name ############################################################## sub light_table_reopen { my $mode = shift; # must be ADD or RESET my $filei = shift; # optional slideshow file my ($ok, $errors, $info, $doubles_count, $doubles, $file) = light_table_open_int($mode, $filei); if ($$errors ne '') { my $text = ''; $text .= "These $doubles_count pictures are already in the slideshow and have been skipped:\n$doubles\n\n" if ($doubles_count > 0); $text .= "Errors while reading $file:\n$$errors" if ($$errors ne ''); $text .= "\nInformation while reading $file:\n$$info" if ($$info ne ''); showText("Information and Errors", $text, NO_WAIT); } } ############################################################## # light_table_open_int ############################################################## sub light_table_open_int { my $mode = shift; # must be ADD or RESET my $file = shift; # optional slideshow file my $doubles = ''; my $doubles_count = 0; my $text = 'Open'; $text = 'Add to' if ($mode == ADD); # open file requester only when needed if (!defined $file or !-f $file) { my $types = [ ['gqview slideshow', '.gqv',], ['XnView slideshow', '.sld',], ['All Files', '*',], ]; $file = $ltw->getOpenFile(-title => "$text slideshow", -defaultextension => 'gqv', -initialdir => $config{SlideShowDir}, -filetypes => $types); } return (0, 'No valid file', '', $doubles_count, $doubles, $file) if ((!defined $file) or ($file eq '') or (!-f $file)); unless (-T $file) { $ltw->messageBox(-icon => 'warning', -message => 'Please select a valid slideshow (ASCII) file.', -title => 'Wrong file type', -type => 'OK'); return (0, 'Wrong file type', '', $doubles_count, $doubles, $file); } $config{SlideShowDir} = dirname($file) if (-d dirname($file)); my ($ok, $errors, $info, $pics) = read_slideshow_from_file($file); if ($mode == RESET) { # reset list and clean up canvas undef @{$ltw->{canvas}->{thumb_list}}; light_table_clear(); } my @pics_valid; foreach my $dpic (@$pics) { if (isInList($dpic, $ltw->{canvas}->{thumb_list})) { $doubles .= "$dpic\n"; $doubles_count++; } else { push @pics_valid, $dpic; } } # add pics to end of thumb list push @{$ltw->{canvas}->{thumb_list}}, @pics_valid; # add new pictures to collection (light table) light_table_add_list(\@pics_valid); $ltw->{label} = scalar(@{$ltw->{canvas}->{thumb_list}}).' '.lang('pictures'); return (1, $errors, $info, $doubles_count, $doubles, $file); } ############################################################## ############################################################## sub read_slideshow_from_file { my $file = shift; my $fh; my @pics; my $info = ''; my $error = ''; if (!open($fh, '<', $file)) { warn "read_slideshow_from_file: Couldn't open $file: $!"; return (0, "Couldn't open $file: $!", $info, \@pics); } my $pic_number = 0; my $not_found = 0; my $double = 0; while (<$fh>) { chomp; # no newline if ($_ =~ m|\"(.*)\"|) { # match just quoted lines $pic_number++; my $dpic; # $dpic may also have a relative path! $dpic = File::Spec->rel2abs($1, dirname($file)); # replace Windows path delimiter with UNIX style \ -> / $dpic =~ s!\\!\/!g; if (-f $dpic) { if (isInList($dpic, \@pics)) { $double++; $info .= "info: $dpic is already in list\n"; } else { push @pics, $dpic; } } else { $not_found++; $error .= "error: $dpic not found! (number: $pic_number)\n"; } } else { $info .= "info: ignoring line \"$_\"\n"; } } $info .= basename($file)." references $pic_number pictures, "; $info .= "$not_found could not be found, " if ($not_found); $info .= "$double were already in list, " if ($double); $info .= "using ".scalar(@pics)." pictures.\n"; close $fh; return (1, $error, $info, \@pics); } ############################################################## # light_table_reload - save slideshow to temp file and open it again # can be used to reload updated thumbnails ############################################################## sub light_table_reload { my $datetime = getDateTimeShortString(time()); my $file = "$trashdir/slideshow-$datetime.gqv"; if (light_table_save_to_file_int($file)) { # todo: $ltw->{file} is set to $file, so the original slideshow file name is lost!!! light_table_reopen(RESET, $file); } } ############################################################## # light_table_save_to_file_as ############################################################## sub light_table_save_to_file_as { my $types = [ ['gqview slideshow', '.gqv',], ['Slideshow file', '.sld',], ['All Files', '*',], ]; my $file = $ltw->getSaveFile(-title => "Save slideshow as, use .gqv (gqview) or .sld (XnView) suffix", -defaultextension => 'gqv', -initialfile => "slideshow.gqv", -initialdir => $config{SlideShowDir}, -filetypes => $types); return 0 if ((!defined $file) or ($file eq '')); $config{SlideShowDir} = dirname($file) if (-d dirname($file)); my ($basename,$dir,$suffix) = fileparse($file, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $ok = 0; if ($suffix eq '.sld') { $ok = set_sld_options(); } elsif ($suffix eq '.gqv') { $ok = set_gqv_options(); } else { $ltw->messageBox(-icon => 'error', -message => "Sorry, but the slideshow suffix $suffix is not supported.", -title => "Wrong file suffix", -type => 'OK'); } return 0 if (not $ok); light_table_save_to_file($file); return 1; } ############################################################## # set some options for XnView slideshow (suffix: .sld) ############################################################## sub set_sld_options { my $ok = 0; # open window my $win = $ltw->Toplevel(); $win->title('Save slideshow options'); $win->iconimage($mapiviicon) if $mapiviicon; $win->Checkbutton(-variable => \$config{relative_path}, -text => "Use relative file paths")->pack(-anchor=>'w'); $win->Checkbutton(-variable => \$config{xnview_loop}, -text => "Loop slide show")->pack(-anchor=>'w'); $win->Checkbutton(-variable => \$config{xnview_fullscreen}, -text => "Full screen display")->pack(-anchor=>'w'); $win->Checkbutton(-variable => \$config{xnview_title}, -text => "Show title bar")->pack(-anchor=>'w'); $win->Checkbutton(-variable => \$config{xnview_filename}, -text => "Show file name")->pack(-anchor=>'w'); $win->Checkbutton(-variable => \$config{xnview_mouse}, -text => "Hide mouse")->pack(-anchor=>'w'); $win->Checkbutton(-variable => \$config{xnview_random}, -text => "Random order")->pack(-anchor=>'w'); my $but_frame = $win->Frame()->pack(-fill =>'x'); my $ok_but = $but_frame->Button(-text => lang('OK'), -command => sub { $ok = 1; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); my $xBut = $but_frame->Button(-text => lang('Cancel'), -command => sub { $ok = 0; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); bind_exit_keys_to_button($win, $xBut); $win->Popup(-popover => 'cursor'); repositionWindow($win); $win->waitWindow; return $ok; } ############################################################## # set some options for gqview slideshow (suffix: .gqv) ############################################################## sub set_gqv_options { $config{relative_path} = 0; # gqview only supports absolut paths return 1; } ############################################################## # light_table_save - save slideshow to slideshows hash ############################################################## sub light_table_save { # save slideshow to %slideshows hash #use Data::Dumper; # and then in the code e.g. print Dumper(\%conf); #print "--------- before ----------\n"; #print Dumper(%slideshows); # we need a local copy my @pics = @{$ltw->{canvas}->{thumb_list}}; $slideshows{$ltw->{folder}}{$ltw->{collection}}{pics} = \@pics; #print "--------- after ----------\n"; #print Dumper(%slideshows); # and save slideshow hash to file save_slideshows(); log_it("wrote collection: $ltw->{folder}: $ltw->{collection}"); $ltw->{label} = lang("wrote collection: ").$ltw->{folder}.' '.$ltw->{collection}; $ltw->title(lang('Collection').': '.$ltw->{folder}.' '.$ltw->{collection}); $ltw->{unsafed_changes} = 0; # reset dirty bit } ############################################################## # light_table_save_as - get folder and collection name ############################################################## sub light_table_save_as { # todo:improve folder/collection selection by providing one dialog and # displaying all available collections # check if user selected folder and or collection in tree of main window my ($ok, $folder, $collection) = get_selected_collection($nav_F->{collection_frame}->{tree}, 0); if ($ok == 0) { # no selection $folder = 'folder'; $collection = 'collection'; } if ($ok == 1) { # only folder selection $collection = 'collection'; } # hint: $ok == 2 means folder and collection are selected my $rc = myEntryDialog(lang('Save collection'), lang('Please enter folder name'), \$folder); return if (($rc ne 'OK') or ($folder eq '')); $rc = myEntryDialog(lang('Save collection'), langf("Please enter collection name in folder %s",$folder), \$collection); return if (($rc ne 'OK') or ($collection eq '')); if (exists $slideshows{$folder}{$collection}) { my $rc = $ltw->messageBox(-icon => 'question', -message => langf("Collection %s %s exists. Overwrite this collection?", $folder, $collection), -title => lang('Overwrite collection?'), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } $ltw->{folder} = $folder; $ltw->{collection} = $collection; light_table_save(); } ############################################################## # light_table_save_to_file ############################################################## sub light_table_save_to_file { my $file = shift; if (light_table_save_to_file_int($file)) { log_it("wrote collection: $file"); $ltw->{label} = lang("wrote collection: ").basename($file); $ltw->title(lang('Collection').': '.basename($file)); $ltw->{file} = $file; print "collection saved to file, but not to hash!\n"; } else { log_it("Error writing collection: $file"); $ltw->{label} = "Error writing collection: ".basename($file); } } ############################################################## # light_table_save_to_file_int ############################################################## sub light_table_save_to_file_int { my $file = shift; print "writing slideshow to $file\n" if $verbose; my $fh; my ($basename,$dir,$suffix) = fileparse($file, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $header; if ($suffix eq '.sld') { $header = '# Slide Show Sequence View = 1 CenterWindow = 0 ReadErrors = 1 BackgroundColor = 0'; } elsif ($suffix eq '.gqv') { $header = "#GQView collection\n#Created with Mapivi version $version\n"; } if (!open($fh, '>', $file)) { $ltw->messageBox(-icon => 'error', -message => "Error writing slideshow file:\ncould not open $file for write access!: $!", -title => "Error writing file", -type => 'OK'); print "could not open $file for write access!: $!\n" if $verbose; return 0; } print $fh "$header\n"; if ($suffix eq '.sld') { print $fh "Loop = $config{xnview_loop}\n"; print $fh "FullScreen = $config{xnview_fullscreen}\n"; print $fh "TitleBar = $config{xnview_title}\n"; print $fh "HideMouse = $config{xnview_mouse}\n"; print $fh "RandomOrder = $config{xnview_random}\n"; print $fh "ShowFilename = $config{xnview_filename}\n"; } foreach my $dpic (@{$ltw->{canvas}->{thumb_list}}) { my $rel = $dpic; if ($config{relative_path}) { $rel = File::Spec->abs2rel($dpic, dirname($file)); } print $fh "\"$rel\"\n"; print "\"$rel\"\n" if $verbose; } if ($suffix eq '.gqv') { print $fh "#end\n"; } close $fh; $ltw->{unsafed_changes} = 0; # reset dirty bit return 1; } ############################################################## # light_table_close ############################################################## sub light_table_close { my $ask = shift; if ($ltw->{unsafed_changes}) { my $rc = $ltw->messageBox(-icon => 'question', -message => lang("Collection will not be saved automatically.\nAll changes will be lost.\nReally close collection?"), -title => lang("Close collection?"), -type => 'YesNo'); return 0 unless ($rc =~ m/Yes/i); } undef @{$ltw->{canvas}->{thumb_list}}; light_table_clear(); $config{LtwGeometry} = $ltw->geometry; $ltw->destroy(); return 1; } ############################################################## # light_table_clear ############################################################## sub light_table_clear { $ltw->{canvas}->delete('all'); # delete all photo objects (thumbnnails) foreach my $dpic (keys %{$ltw->{canvas}->{thumbs}}) { # delete defined photo object delete_photo_object($ltw->{canvas}->{thumbs}->{$dpic}); # delete hash entry delete $ltw->{canvas}->{thumbs}->{$dpic}; } $ltw->{label} = scalar @{$ltw->{canvas}->{thumb_list}}.' '.lang('pictures'); $ltw->title(lang('Picture collection')); $ltw->{unsafed_changes} = 0; # reset dirty bit } ############################################################## # light_table_reset_collection ############################################################## sub light_table_reset_collection { $ltw->{folder} = ''; $ltw->{collection} = ''; } ############################################################## # light_table_add_from_lb ############################################################## sub light_table_add_from_lb { my $lb = shift; my @sellist = getSelection($lb); light_table_add(\@sellist); } ############################################################## # light_table_add ############################################################## sub light_table_add { my $list_ref = shift; return unless checkSelection($top, 1, 0, $list_ref, lang("picture(s)")); # open light table window if needed light_table_open_window() unless (Exists($ltw)); my $error = ''; my $error_count = 0; my @list; # check for double pictures (not yet supported) foreach my $dpic (@$list_ref) { if (isInList($dpic, $ltw->{canvas}->{thumb_list})) { $error .= "$dpic\n"; $error_count++; } else { push @list, $dpic; log_it(lang("added to collection: ").$dpic); $ltw->{unsafed_changes} = 1; # set dirty bit } } if ($error ne '') { $error = "These $error_count pictures are already in the slideshow and have been skipped:\n\n".$error; showText('Ignored pictures', $error, NO_WAIT); } return unless (@list); # add selected pictures at end of slideshow list push @{$ltw->{canvas}->{thumb_list}}, @list; # add selected pictures to light table light_table_add_list(\@list); } ############################################################## # light_table_edit ############################################################## sub light_table_edit { my $list_ref = shift; my $folder = shift; # optional slideshow folder (not file folder) my $collection = shift; # optional slideshow name (not file name) if (Exists($ltw)) { my $rc = $top->messageBox(-icon => 'question', -message => langf("Close current collection and open %s %s?",$folder, $collection), -title => lang('Close collection?'), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } # open light table window if needed light_table_open_window() unless (Exists($ltw)); undef @{$ltw->{canvas}->{thumb_list}}; light_table_clear(); # store folder and collection name $ltw->{folder} = $folder if (defined $folder); $ltw->{collection} = $collection if (defined $collection); $ltw->{unsafed_changes} = 0; # clear dirty bit $ltw->{file} = $slideshows{$folder}{$collection}{file} if (defined $slideshows{$folder}{$collection}{file}); # copy collection pics to slideshow list @{$ltw->{canvas}->{thumb_list}} = @$list_ref; # add selected pictures to light table light_table_add_list($list_ref); } ############################################################## # light_table_add_list ############################################################## sub light_table_add_list { my $list_ref = shift; # list of JPEG pics with full path return if (@$list_ref < 1); # no pics to add # get thumb size info from first thumbnail in list (this may be wrong, as others may be bigger) my ($tw, $th) = getSize(getThumbFileName($$list_ref[0])); if ($tw > 1) { $ltw->{canvas}->{thumb_size} = $tw; } my $i = 0; my $pw = 0; $pw = progressWinInit($ltw, "Add pictures to collection")if (@$list_ref > 1);; foreach my $dpic (@$list_ref) { my $thumb = getThumbFileName($dpic); last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "adding picture ($i/".scalar @$list_ref.") ...", $i, scalar @$list_ref) if $pw; if (-f $thumb) { # save all thumb photo objects in canvas hash to delete them later $ltw->{canvas}->{thumbs}->{$dpic} = $ltw->Photo(-file => $thumb); } else { # reminder: when we delete this photo object later, we should not delete the empty thumb object! $ltw->{canvas}->{thumbs}->{$dpic} = $mapivi_icons{'EmptyThumb'}; } if ($ltw->{canvas}->{thumbs}->{$dpic}) { # create image on canvas # all mapivi thumb tags contain _MH!!! # this is needed to extract the dpic from the id in get_path_from_id() my $id = $ltw->{canvas}->createImage(0, 0, -image => $ltw->{canvas}->{thumbs}->{$dpic}, -tag => ['THUMB_MH', 'THUMB_MH'.$dpic], -anchor => 'nw'); # add bindings $ltw->{canvas}->bind($id,'', sub { light_table_select($id); }); $ltw->{canvas}->bind($id,'', sub {$ltw->{LOCK_MH} = 1; light_table_select_range();}); $ltw->{canvas}->bind($id,'', sub {$ltw->{LOCK_MH} = 1; light_table_select_add($id); }); $ltw->{canvas}->bind($id,'', sub { light_table_move($id); }); $ltw->{canvas}->bind($id,'', sub { return if ($ltw->{LOCK_MH}); light_table_drop($id); }); $ltw->{canvas}->bind($id,'', sub { $ltw->{LOCK_MH} = 0; }); $ltw->{canvas}->bind($id,'', sub { $ltw->{LOCK_MH} = 0; }); $ltw->{canvas}->bind($id,'', sub { my @sel = selection_get_sort($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); show_multiple_pics(\@sel, 0); }); } } progressWinEnd($pw) if $pw; light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); $ltw->{canvas}->yviewMoveto(1); $ltw->{label} = scalar @{$ltw->{canvas}->{thumb_list}}.' pictures'; } ############################################################## # show_canvas_thumbs # shows a list of thumbnails in the canvas (tag: THUMB_MH) # stores photo objects in $c->{thumbs} ############################################################## sub show_canvas_thumbs { return if (not $conf{show_canvas_thumbs}{value}); my $c = shift; # canvas widget my $list_ref = shift; # list of JPEG pics with full path return if (@$list_ref < 1); # no pics to add # remove all thumbnails from canvas clear_canvas_thumbs($c); # hide all picture items on the canvas canvasHide(); # clear image info showImageInfo(); # clear canvas info #showImageInfoCanvas(); # get thumb size info from first thumbnail in list (todo: this may be wrong, as others may be bigger) my ($tw, $th) = getSize(getThumbFileName($$list_ref[0])); if ($tw > 1) { $c->{thumb_size} = $tw; } my $i = 0; my $pw = progressWinInit($c, "Add pictures to canvas"); foreach my $dpic (@$list_ref) { my $thumb = getThumbFileName($dpic); last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "adding picture ($i/".scalar @$list_ref.") ...", $i, scalar @$list_ref); if (-f $thumb) { # save all thumb photo objects in canvas hash to delete them later $c->{thumbs}->{$dpic} = $c->Photo(-file => $thumb); } else { $c->{thumbs}->{$dpic} = $mapivi_icons{'EmptyThumb'}; } add_canvas_thumb($c, $dpic, 0, 0); } light_table_reorder($c, $list_ref); add_canvas_ratings($c); # has to be called after light_table_reorder()! canvas_update_selection($c); progressWinEnd($pw); $c->yviewMoveto(0); } ############################################################## # Hint: has to be called after light_table_reorder()! ############################################################## sub add_canvas_ratings { my $c = shift; # canvas my @ids = $c->find('withtag', 'THUMB_MH'); foreach my $id (@ids) { my $dpic = get_path_from_id($c, $id); my $stars = iptc_rating_stars_nr($searchDB{$dpic}{URG}); if ($stars) { my $size = 7; my $offset = $size+1; my ($x, $y, $x1, $y1) = $c->coords($id); # add rating as circles for (1 .. $stars) { $c->createOval( $x+1, $y+1, $x+$size, $y+$size, -tags => ['STAR', 'STAR'.$dpic], -outline => 'sienna1', -fill => 'sienna2', -width => 2, ); $x += $offset; } } } } ############################################################## # add one thumbnail to the canvas ############################################################## sub add_canvas_thumb { my ($c, $dpic, $x, $y) = @_; if ($c->{thumbs}->{$dpic}) { my $id = $c->createImage($x, $y, -image => $c->{thumbs}->{$dpic}, # we add THUMB_MH to the file name to avoid problems with other # picture items on the canvas, see also checkCachedPics() -tag => ['THUMB_MH', 'THUMB_MH'.$dpic], -anchor => 'nw'); # add bindings $c->bind($id,'', sub { #clear_canvas_thumbs($c); # is already called within showPic()! showPic($dpic);}); $c->bind($id,'', sub { $picLB->selectionClear(); $picLB->selectionSet($dpic); $picLB->anchorSet($dpic); $picLB->see($dpic); canvas_update_selection($c); showNrOf(); log_it(lang('Double click to display').' '.basename($dpic));}); $c->bind($id,'', sub { if ($picLB->selectionIncludes($dpic)) { $picLB->selectionClear($dpic); } else { $picLB->selectionSet($dpic); } $picLB->anchorSet($dpic); $picLB->see($dpic); canvas_update_selection($c); showNrOf(); }); $c->bind($id,'', sub { my $anchor = $picLB->info('anchor'); if ((defined $anchor) and ($anchor ne '')) { $picLB->selectionSet($anchor, $dpic); $picLB->see($dpic); canvas_update_selection($c); showNrOf(); }}); } else { warn "add_canvas_thumb: Found no thumbnail for $dpic!"; } } ############################################################## # clear_canvas_thumbs # remove all canvas items with tag 'THUMB_MH' and free # the canvas local photo objects stored in $c->{thumbs} ############################################################## sub clear_canvas_thumbs { return if (not $conf{show_canvas_thumbs}{value}); my $c = shift; # canvas widget my @ids = $c->find('withtag', 'THUMB_MH'); foreach my $id (@ids) { $c->delete('withtag', $id); } $c->delete('withtag', 'MARK'); $c->delete('withtag', 'STAR'); my @thumb_objects = keys %{$c->{thumbs}}; foreach my $dpic (@thumb_objects) { # delete photo object delete_photo_object($c->{thumbs}->{$dpic}); # delete hash entry delete $c->{thumbs}->{$dpic}; } if (scalar(@ids) != scalar(@thumb_objects)) { warn "clear_canvas_thumbs: Warning: ".scalar(@ids)." canvas thumbs and ".scalar(@thumb_objects)." photo objects are not equal"; } canvas_update_selection($c); } ############################################################## # canvas_update_selection - draw a mark on each selected # thumbnail ############################################################## sub canvas_update_selection { my $c = shift; # canvas widget # first we remove all selection markers $c->delete('withtag', 'MARK'); # find all selected thumbs from picLB my @sel = getSelection($picLB); return if (not @sel); # draw a mark on all selected pictures foreach my $dpic (@sel) { my ($x, $y) = $c->coords('THUMB_MH'.$dpic); if (defined $x) { $c->createRectangle( $x, $y, $x+$c->{thumb_size}, $y+$c->{thumb_size}, -tags => ['MARK'], #-fill => 'yellow2' -outline => 'yellow2', -width => 2 ); } } } ############################################################## # light_table_balloon ############################################################## sub light_table_balloon { if ($ltw->{show_balloon}) { my $msg; # the balloon message is generated on demand later, to speed up the loading of the thumbs $balloon->attach($ltw->{canvas}, -postcommand => sub { my ($current) = $ltw->{canvas}->find('withtag', 'current'); my $dpic = get_path_from_id($ltw->{canvas},$current); $msg = makeBalloonMsg($dpic); }, -balloonposition => 'mouse', -msg => \$msg); } else { $balloon->detach($ltw->{canvas}); } } ############################################################## # light_table_reorder ############################################################## sub light_table_reorder { my $c = shift; # canvas widget my $pic_list = shift; # list reference, thumbnails will be sorted using this list return if (not defined $pic_list); return if (scalar(@$pic_list) <= 1); $c->update; #$c->Busy; # resizing the window does not work under windows if Busy is used my $dis = $c->{thumb_size} + $c->{thumb_distance}; # get canvas size my $cx = $c->width; my $cy = $c->height; # calc visible columns and rows my $c_cols = int($cx/$dis); $c_cols = 1 if ($c_cols < 1); # avoid division by zero my $c_rows = int($cy/$dis); # how many rows are needed for all pics? my $all_rows = int(@$pic_list / $c_cols); $all_rows++ if ((@$pic_list % $c_cols) != 0); # adjust scrollbar $c->configure(-scrollregion => [0, 0, $c_cols*$dis + $c->{thumb_distance}, $all_rows*$dis + $c->{thumb_distance}]); my $index = 0; foreach my $dpic (@$pic_list) { my ($id) = $c->find('withtag', 'THUMB_MH'.$dpic); if (not defined $id) { warn "light_table_reorder: Found no item for $dpic (index:$index)!"; } my $row = int ($index / $c_cols); my $col = $index % $c_cols; # modulo # we move the thumbs by tag which is the THUMB_MH+path+file name # this excludes the possibility to have a pic twice in the list :-( $c->coords('THUMB_MH'.$dpic, $col*$dis+$c->{thumb_distance}, $row*$dis+$c->{thumb_distance}); $index++; } } ############################################################## # get_path_from_id # assumes that the item has a tag "THUMB_MH/path/to/file" # and returns /path/to/file or empty string ############################################################## sub get_path_from_id { my $c = shift; # canvas my $id = shift; # id of canvas item my @tags = $c->gettags($id); my $dpic = ''; foreach (@tags) { next if ($_ eq 'current'); next if ($_ =~ m/.*_MH$/); # all mapivi thumb tags are ending with _MH $dpic = $_; # so this must be the THUMB_MH+path+file name } if ($dpic eq '') { print "get_path_from_id: Error could not find path from item: "; print "$_ " foreach (@tags); print "\n"; } if ($dpic =~ m/^THUMB_MH(.*)/) { $dpic = $1; # cut off THUMB_MH prefix } else { print "get_path_from_id: Error contains no THUMB_MH prefix: $dpic\n"; $dpic = ''; } return $dpic; } ############################################################## # light_table_copy_rename ############################################################## sub light_table_copy_rename { # find all selected thumbs my @sel = selection_get_sort($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); #my @sel = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); return unless checkSelection($top, 1, 0, \@sel, lang("picture(s)")); my $rc = $ltw->messageBox(-icon => 'warning', -message => "Copy and rename the ".scalar @sel." selected pictures.\nThe pictures will be renamed by adding a leading number according to the current order.\npic.jpg will for example be renamed to: 000-pic.jpg.\n\nOk to proceed?", -title => "Copy and rename", -type => "OKCancel"); return if ($rc !~ m/Ok/i); my $targetdir = getDirDialog("Copy pictures to"); return if ($targetdir eq ''); return unless (-d $targetdir); makeDir(dirname(getThumbFileName("$targetdir/dummy.jpg")), ASK); my $i = 0; my $overwrite = OVERWRITE; #my $digits = 3; # idea from Yann Michel my $digits = int(log(@sel)/log(10))+1; # calculate the needed digits dynamically my $pw = progressWinInit($ltw, "Copy and rename pictures"); foreach my $dpic (@sel) { last if progressWinCheck($pw); my $pic = basename($dpic); my $tpic = $targetdir.'/'.sprintf "%0*d-$pic", $digits, $i; my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); $i++; progressWinUpdate($pw, "copy and rename picture ($i/".scalar @sel.") ...", $i, scalar @sel); $overwrite = overwritePic($tpic, $dpic, (scalar(@sel) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); if (mycopy($dpic, $tpic, OVERWRITE)) { if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { mycopy($thumbpic, $thumbtpic, OVERWRITE) } $searchDB{$tpic} = $searchDB{$dpic}; # copy meta info in search database } } # foreach - end progressWinEnd($pw); } ############################################################## # light_table_drop ############################################################## sub light_table_drop { # where the drop happened my $x = $ltw->{canvas}->canvasx($Tk::event->x()); my $y = $ltw->{canvas}->canvasy($Tk::event->y()); # distance between upper left corner of thumbs my $dis = $ltw->{canvas}->{thumb_size} + $ltw->{canvas}->{thumb_distance}; $dis = 1 if ($dis == 0); # avoid division by zero # drop position in cols/rows my $col = sprintf "%0d", ($x / $dis); # round my $row = sprintf "%0d", ($y / $dis); print "drop at x=$x y=$y col=$col row=$row\n" if $verbose; # get size of canvas in cols/rows my $cx = $ltw->{canvas}->width; my $cy = $ltw->{canvas}->height; my $c_cols = int($cx/$dis); my $c_rows = int($cy/$dis); # new position in list my $to_index = $row * $c_cols + $col; my $max_index = scalar(@{$ltw->{canvas}->{thumb_list}}) - 1; if ($to_index > $max_index) { #print "to_index $to_index is bigger than max_index $max_index - reducing\n"; $to_index = $max_index; } #my $to_dpic = ${$ltw->{canvas}->{thumb_list}}[$to_index]; # find all selected thumbs my @sel = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); my @sel_dpics; my @old_order = @{$ltw->{canvas}->{thumb_list}}; # remove selected pics from the pic list foreach my $id (@sel) { my $dpic = get_path_from_id($ltw->{canvas}, $id); my $index = index_in_list($dpic, $ltw->{canvas}->{thumb_list}); #print "drop: removing index $index ($dpic)\n"; # remove this pic from the list push @sel_dpics, splice @{$ltw->{canvas}->{thumb_list}}, $index, 1; } # add the removed pics at the right place again foreach my $dpic (@sel_dpics) { #print "drop: adding at $to_index $dpic\n"; # add it at the new position splice @{$ltw->{canvas}->{thumb_list}}, $to_index, 0, $dpic; } # set dirty bit only if order of pics really changed (compare lists by joining them to strings) if ("@old_order" ne "@{$ltw->{canvas}->{thumb_list}}") { $ltw->{unsafed_changes} = 1; } light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); } ############################################################## # index_in_list - returns the index of an element in a list # return -1 if not found ############################################################## sub index_in_list { my $e = shift; my $listRef = shift; my $index = 0; foreach (@$listRef) { last if ($e eq $_); $index++; } if ($index > @$listRef) { print "$index is bigger than @$listRef\n" if $verbose; $index = -1; } return $index; } ############################################################## # light_table_select - select a thumbnail, remove all other selections ############################################################## sub light_table_select { my $id = shift; # remember the current selection my @sel_IDs = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); $ltw->{sel_IDs} = \@sel_IDs; $ltw->{sel_time} = Tk::timeofday(); # delete all selection frames remove_tag_from_all('THUMBSELECT_MH'); remove_tag_from_all('ANCHOR_MH'); # select just the current thumb $ltw->{canvas}->addtag('THUMBSELECT_MH', 'withtag', 'current'); # this is the new anchor $ltw->{canvas}->addtag('ANCHOR_MH', 'withtag', 'current'); # update the selection frames light_table_update_selection(); } ############################################################## # remove_tag_from_all - delete a certain tag from all elements # in the canvas ############################################################## sub remove_tag_from_all { my $tag = shift; #print "remove_tag_from_all: $tag\n"; # build a list of all thumbs with this tag #my @sel = $ltw->{canvas}->find( qw|withtag $tag| ); my @sel = $ltw->{canvas}->find('withtag', $tag); # remove the tag from these thumbs foreach my $id (@sel) { #print "remove_tag_from_all: removing $tag\n"; $ltw->{canvas}->dtag($id, $tag); } } ############################################################## # light_table_select_add - toggle selection of single thumbnail ############################################################## sub light_table_select_add { my @tags = $ltw->{canvas}->gettags('current'); if (isInList('THUMBSELECT_MH', \@tags)) { # delete existing tag $ltw->{canvas}->dtag('current', 'THUMBSELECT_MH'); } else { # add tag $ltw->{canvas}->addtag('THUMBSELECT_MH', 'withtag', 'current'); } light_table_update_selection(); } ############################################################## # light_table_select_all - select all thumbnail ############################################################## sub light_table_select_all { remove_tag_from_all('THUMBSELECT_MH'); my @all = $ltw->{canvas}->find('all'); foreach my $id (@all) { $ltw->{canvas}->addtag('THUMBSELECT_MH', 'withtag', $id); } light_table_update_selection(); } ############################################################## # light_table_select_range - select range of thumbnail ############################################################## sub light_table_select_range { # build a list of all thumbs with tag ANCHOR_MH my @sel = $ltw->{canvas}->find('withtag', 'ANCHOR_MH'); if (@sel < 1) { print "no anchor found!\n" if $verbose; return; } if (@sel > 1) { print "error ".scalar @sel." anchors found! - removing anchors\n" if $verbose; remove_tag_from_all('ANCHOR_MH'); return; } my $start_id = $sel[0]; my $start_dpic = get_path_from_id($ltw->{canvas},$start_id); my $start_index = index_in_list($start_dpic, $ltw->{canvas}->{thumb_list}); @sel = $ltw->{canvas}->find('withtag', 'current'); my $end_id = $sel[0]; my $end_dpic = get_path_from_id($ltw->{canvas},$end_id); my $end_index = index_in_list($end_dpic, $ltw->{canvas}->{thumb_list}); print "light_table_select_range: select from $start_dpic ($start_index) to $end_dpic ($end_index)\n" if $verbose; # do we need to swap? if ($end_index < $start_index) { my $tmp = $start_index; $start_index = $end_index; $end_index = $tmp; } foreach ($start_index .. $end_index) { $ltw->{canvas}->addtag('THUMBSELECT_MH', 'withtag', 'THUMB_MH'.${$ltw->{canvas}->{thumb_list}}[$_]); } light_table_update_selection(); } ############################################################## # light_table_select_to end - select range of thumbnail from # selected (must be just one!) to the last ############################################################## sub light_table_select_to_end { my @pics_sel = getSelection($ltw->{canvas}); # or $ltw->{canvas} ??? return unless checkSelection($ltw, 1, 1, \@pics_sel, lang("picture(s)")); my $start_dpic = $pics_sel[0]; my $start = 0; foreach my $dpic (@{$ltw->{canvas}->{thumb_list}}) { # skip through list until we see the selected start picture # then start tagging with select tag to end of list $start = 1 if ($dpic eq $start_dpic); if ($start) { $ltw->{canvas}->addtag('THUMBSELECT_MH', 'withtag', 'THUMB_MH'.$dpic); } } light_table_update_selection(); } ############################################################## # light_table_update_selection - draw a frame around each selected # thumbnail (with tag THUMBSELECT_MH) ############################################################## sub light_table_update_selection { # first we remove all frames $ltw->{canvas}->delete('withtag', 'FRAME'); # find all selected thumbs my @sel = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); return if (not @sel); # draw a frame around all selected pictures foreach my $thumb (@sel) { my ($x, $y) = $ltw->{canvas}->coords($thumb); $ltw->{canvas}->createRectangle( $x, $y, $x+$ltw->{canvas}->{thumb_size}+1, $y+$ltw->{canvas}->{thumb_size}+1, -tags => ['FRAME'], -outline => 'yellow2', #$config{ColorSelBut}, -width => 2, ); } # add the filename if just one thumbnail is selected my $picture = ''; if (@sel == 1) { my $dpic = get_path_from_id($ltw->{canvas},$sel[0]); $picture = ' ('.basename($dpic).')'; } $ltw->{label} = scalar @{$ltw->{canvas}->{thumb_list}}.' pictures, '.scalar @sel.' selected'.$picture; } ############################################################## # light_table_delete - remove the selected thumbs from the list # will - of course - not remove the files!!! ############################################################## sub light_table_delete { # find all selected thumbs my @sel = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); # remove them from the list and the canvas foreach my $id (@sel) { my $dpic = get_path_from_id($ltw->{canvas},$id); my $index = index_in_list($dpic, $ltw->{canvas}->{thumb_list}); # remove this pic from the list splice @{$ltw->{canvas}->{thumb_list}}, $index, 1; # delete item from canvas $ltw->{canvas}->delete($id); # delete defined photo object delete_photo_object($ltw->{canvas}->{thumbs}->{$dpic}); # delete hash entry delete $ltw->{canvas}->{thumbs}->{$dpic}; } $ltw->{unsafed_changes} = 1; # set dirty bit light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); } ############################################################## # delete photo object, exception: EmptyThumb object ############################################################## sub delete_photo_object { my $object = shift; if (defined $object) { # delete photo object # exception: EmptyThumb photo object, because we still need this if ($object != $mapivi_icons{'EmptyThumb'}) { $object->delete; } } } ############################################################## # light_table_shift - move the selected thumbs to the top or # bottom of the list ############################################################## sub light_table_shift { my $where = shift; # must be 'top' or 'bottom' return unless (defined $where); return if (($where ne 'top') and ($where ne 'bottom')); # find all selected thumbs my @sel = $ltw->{canvas}->find('withtag', 'THUMBSELECT_MH'); my @shift_pics; # pics to move # remove them from the list foreach my $id (@sel) { my $dpic = get_path_from_id($ltw->{canvas},$id); my $index = index_in_list($dpic, $ltw->{canvas}->{thumb_list}); # remove this pic from the list and add it to @shift_pics push @shift_pics, splice @{$ltw->{canvas}->{thumb_list}}, $index, 1; } if ($where eq 'top') { unshift @{$ltw->{canvas}->{thumb_list}}, @shift_pics; # add them at the start of the list } elsif ($where eq 'bottom') { push @{$ltw->{canvas}->{thumb_list}}, @shift_pics; # add them to the end of the list } else { warn "light_table_shift: should not be reached where = $where"; } $ltw->{unsafed_changes} = 1; # set dirty bit light_table_reorder($ltw->{canvas}, $ltw->{canvas}->{thumb_list}); light_table_update_selection(); } ############################################################## # light_table_move - called if a thumbnail is dragged inside the light table ############################################################## sub light_table_move { # stop repeat timer $ltw->{SCROLL_MH}->cancel if $ltw->{SCROLL_MH}; my $id = shift; # if the last selection happened just 400ms ago and the clicked # thumb was inside the last selection, we reselect the last selection if (((Tk::timeofday() - $ltw->{sel_time}) < 0.4) and isInList($id, $ltw->{sel_IDs})) { # reset time $ltw->{sel_time} = 0; # first remove the tags remove_tag_from_all('THUMBSELECT_MH'); # then add the selection from the saved list foreach my $id (@{$ltw->{sel_IDs}}) { my $dpic = get_path_from_id($ltw->{canvas},$id); $ltw->{canvas}->addtag('THUMBSELECT_MH', 'withtag', 'THUMB_MH'.$dpic); } light_table_update_selection(); } $ltw->{canvas}->raise($id); # get mouse coordinates my $ex = $Tk::event->x(); my $ey = $Tk::event->y(); my $x = $ltw->{canvas}->canvasx($ex); my $y = $ltw->{canvas}->canvasy($ey); my $offset = int($ltw->{canvas}->{thumb_size}/2); # move thumb to mouse position $ltw->{canvas}->coords($id, $x-$offset, $y-$offset); # autoscroll: scroll up or down if needed # get actual scroll state my ($y1,$y2) = $ltw->{canvas}->yview; my $cy = $Tk::event->y; print "light_table_move cy:$cy\n" if $verbose; # everything is visible no scrolling needed return if ($y1 == 0 and $y2 == 1); my $c_h = $ltw->{canvas}->height; # the visible height #my @sr = $ltw->{canvas}->cget(-scrollregion); #my @sr = $ltw->{frame}->cget(-scrollregion); #my $c_h_all = $sr[3] - $sr[1]; # the height of the scrollregion # scroll up if mouse is less then a half thumbnailsize away from the upper border # and there is still room to scroll ($y1 > 0) and no button release has happened if (($cy < $offset) and ($y1 > 0)) { $ltw->{SCROLL_MH} = $ltw->repeat(100, sub { print "scroll up\n" if $verbose; $ltw->{canvas}->yview('scroll',-1,'units'); # move thumb to mouse position my $x = $ltw->{canvas}->canvasx($ex); my $y = $ltw->{canvas}->canvasy($ey); $ltw->{canvas}->coords($id, $x-$offset, $y-$offset); $ltw->idletasks; }); } # scroll down if mouse is less then a half thumbnailsize away from the lower border # and there is still room to scroll ($y2 < 1) and no button release has happened if (($cy > $c_h - $offset) and ($y2 < 1)) { $ltw->{SCROLL_MH} = $ltw->repeat(100, sub { print "scroll down\n" if $verbose; $ltw->{canvas}->yview('scroll',1,'units'); # move thumb to mouse position my $x = $ltw->{canvas}->canvasx($ex); my $y = $ltw->{canvas}->canvasy($ey); $ltw->{canvas}->coords($id, $x-$offset, $y-$offset); $ltw->idletasks; }); } } ############################################################## # nop - a do nothing function, needed from Tk::IO ############################################################## sub nop { return; } ############################################################## # getThumbCaption - return the appropriate caption for the # thumbnail of a picture, possibly empty ############################################################## sub getThumbCaption { my $dpic = shift; if (($config{ThumbCapt} eq '') or ($config{ThumbCapt} eq 'none')) { return ''; } elsif ($config{ThumbCapt} eq 'filename') { my $capt = basename($dpic); $capt =~ s/(.*)\.jp(g|eg)$/$1/i; # remove suffix return $capt; } elsif ($config{ThumbCapt} eq 'filenameSuffix') { my $capt = basename($dpic); return $capt; } elsif ($config{ThumbCapt} eq 'objectname') { return getIPTCObjectName($dpic); } else { warn 'getThumbCaption: ThumbCapt has unexpected value: "'.$config{ThumbCapt}.'"'; return ''; } } ############################################################## # updateOneThumb - this function is called when a convert # process is finished; replaces the default # thumbnail with the actual thumbnail ############################################################## sub updateOneThumb { my $thumb = shift; my $dpic = shift; # the index (entrypath) of the hlist element my $show = shift; # SHOW, NO_SHOW proccount(-1); $nrToConvert--; $nrToConvert = 0 if ($nrToConvert < 0); # check if we are still in the same dir if (dirname($thumb) ne dirname(getThumbFileName("$actdir/dummy.jpg"))) { return; # no, we are not so do not display the generated thumbs } if (($show == SHOW) and (-f $thumb)) { $thumbs{$thumb} = $picLB->Photo(-file => $thumb, -gamma => $config{Gamma}); # if there is already an image ... if ($picLB->itemCget($dpic, $picLB->{thumbcol}, -itemtype) eq "imagetext") { # ... configure it $picLB->itemConfigure($dpic, $picLB->{thumbcol}, -image => $thumbs{$thumb}, -itemtype => "imagetext"); } else { $picLB->itemCreate($dpic, $picLB->{thumbcol}, -style => $thumbS, -itemtype => 'imagetext', -image => $thumbs{$thumb}, -text => getThumbCaption($dpic)); } # update thumbnails shown in picture frame / canvas (if enabled) if ($conf{show_canvas_thumbs}{value}) { my @ids = $c->find('withtag', 'THUMB_MH'.$dpic); if (@ids) { # store coordinates my ($x,$y,undef,undef) = $c->coords($ids[0]); # delete outdated thumbnail $c->delete('withtag', $ids[0]); # create and add new thumb $c->{thumbs}->{$dpic} = $c->Photo(-file => $thumb); add_canvas_thumb($c, $dpic, $x, $y); } } } } ############################################################## # proccount - count the spawned processes # returns the number of running processes if no # parameter is given ############################################################## sub proccount { my $diff = shift; # optional parameter return $proccount unless (defined $diff); $proccount = 0 unless (defined $proccount); # todo why? $proccount += $diff; $proccount = 0 if ($proccount < 0); # should never happen! $top->update; print "proccount = $proccount\n" if $verbose; } ############################################################## # smart_update - reread actual directory, add new and remove # deleted pics, without reloading the existing # thumbnails; the goal is to have a faster # update for large folders ############################################################## sub smart_update { log_it(lang("Smart update")); $top->update; my @act_pics; my $rc = get_pics_by_modus(\@act_pics); return $rc if ($rc != 1); # get the new list of pics in the actual folder #my @act_pics = getPics($actdir, WITH_PATH); sortPics($config{SortBy}, $config{SortReverse}, \@act_pics); # get the displayed pics from the listbox my @disp_pics = $picLB->info('children'); my $removed_pics = 0; my $new_pics = 0; # remove deleted pictures first foreach my $dpic (@disp_pics) { if ((!isInList($dpic, \@act_pics)) and ($picLB->info('exists', $dpic))) { print "deleting $dpic from picLB\n" if $verbose; $removed_pics++; $picLB->delete('entry', $dpic); } } # get the displayed pics from the listbox again after the deletion @disp_pics = $picLB->info('children'); # count new pictures first foreach my $dpic (@act_pics) { $new_pics++ if (!$picLB->info('exists', $dpic)); } if ($new_pics > 0) { # todo this init is not the perfect solution as a rename of the # first pic will be shown as second pic my $after = $disp_pics[0]; my $pw = progressWinInit($picLB, lang("Smart update")); my $n = 0; # add the new pics to the listbox foreach my $dpic (@act_pics) { last if progressWinCheck($pw); if (!$picLB->info('exists', $dpic)) { $n++; progressWinUpdate($pw, "adding new picture ($n/$new_pics) ...", $n, $new_pics); print "adding $dpic to picLB\n" if $verbose; addOneRow($picLB, $dpic, 1, $after); } $after = $dpic; } progressWinEnd($pw); } showNrOf(); log_it("ready! removed $removed_pics and added $new_pics picture(s)"); generateThumbs(ASK, SHOW); } ############################################################## # updateThumbsPlus - update and show the actual pic again ############################################################## sub updateThumbsPlus { updateThumbs(); showPic($actpic); } ############################################################## # updateThumbs - reads the pictures of the actual dir, shows the # thumbnails, the given picture and generates # the thumbnails ############################################################## sub updateThumbs { log_it(lang('Loading thumbnails ...')); $top->update; checkCachedPics(); canvasHide(); # delete all photo objects (thumbnnails) foreach (keys %thumbs) { print "updateThumbs: deleting thumbnail object of $_\n" if $verbose; # delete defined photo object delete_photo_object($thumbs{$_}); # delete photo object delete $thumbs{$_}; # delete hash entry } if ($verbose) { my @check = $top->imageNames; print " there are ".scalar @check." pics left\n"; } if (showThumbs()) { log_it(lang('Loading thumbnails ...').' '.lang('Ready!')); generateThumbs(ASK, SHOW); } else { log_it(lang("user abord (not all pictures are loaded!)")); } showNrOf(); check_new_keywords(); } ############################################################## # showThumbs - display all thumbnail pictures of the actual # directory in the listbox ############################################################## sub showThumbs { # clean the thumbnail table # with this step all references to the already deleted photo objects are cleared # -> the memory is available $picLB->delete('all'); if ($verbose) { my @check = $top->imageNames; print " there are ".scalar @check." pics left\n"; } my @pics; my $rc = get_pics_by_modus(\@pics); return $rc if ($rc != 1); # if we have many pictures we ask first if the user wants to see them all my $cut_after = 0; if (@pics > $config{ThumbMaxLimit}) { my $cancel = lang('Cancel'); my $all = lang('All'); my $some = langf("Only %d", $config{ThumbMaxLimit}); my $rc = $top->Dialog(-text => langf("Show %d pictures?", scalar(@pics)), -title => lang("Show all pictures?"), -width => 40, -buttons => [$all, $some, $cancel])->Show(); return 0 if ($rc eq $cancel); $cut_after = 1 if ($rc eq $some); } sortPics($config{SortBy}, $config{SortReverse}, \@pics); if ($cut_after) { cut_list(\@pics, $config{ThumbMaxLimit}); } # remove .thumbs subdir etc. cleanOneDir($actdir) if (($act_modus == FOLDER) and (@pics == 0)); $rc = showThumbsInList($picLB, \@pics); show_canvas_thumbs($c, \@pics); set_act_nav_label(); return $rc; } ############################################################## ############################################################## sub set_act_nav_label { $actdirF->{folder_check_buttons}->packForget if ($actdirF->{folder_check_buttons}->ismapped); if ($act_modus == FOLDER) { $act_nav_label = lang("Folder").": $actdir"; $actdirF->{folder_check_buttons}->pack(-in => $actdirF, -side => 'left', -expand => 0, -fill => 'x', -padx => 2, -pady => 1) unless ($actdirF->{folder_check_buttons}->ismapped); } elsif ($act_modus == LOCATION) { $act_nav_label = lang("Location").": "; $act_nav_label .= "$_ " foreach (@act_location); } elsif ($act_modus == DATE) { $act_nav_label = lang("Date").": "; $act_nav_label .= "$_ " foreach (@act_date); } elsif ($act_modus == KEYWORDCLOUD) { $act_nav_label = lang("Keywordcloud").": "; $act_nav_label .= "$_ " foreach (@act_keywords); } elsif ($act_modus == KEYWORD) { $act_nav_label = lang("Keyword").": "; $act_nav_label .= "$_ " foreach (@act_keywords); } elsif ($act_modus == SEARCH) { $act_nav_label = lang("Search").": $config{SearchPattern}"; } elsif ($act_modus == COLLECTION) { $act_nav_label = lang("Collection").": "; $act_nav_label .= "$_ " foreach (@act_collection); } else { $act_nav_label = lang("Unknown navigation modus!"); } $act_nav_label =~ s/\s+$//; # remove trailing whitespace if (($act_modus != FOLDER) and $conf{nav_rating_on}{value}) { $act_nav_label .= ", ".lang("Rating").": ".iptc_rating_stars_urg($conf{search_rating_max}{value})." - ".iptc_rating_stars_urg($conf{search_rating_min}{value}); } } my @navigation_history; ############################################################## ############################################################## sub navigation_history_save { my $hash = {}; $hash->{modus} = $act_modus; $hash->{folder} = $actdir; $hash->{location} = join(" ", @act_location); $hash->{date} = join(" ", @act_date); $hash->{keywords} = join(" ", @act_keywords); $hash->{exkeywords} = join(" ", @act_keywords_ex); $hash->{search} = $config{SearchPattern}; push @navigation_history, $hash; print "navigation_history_save: ".scalar @navigation_history." history entries\n"; foreach my $href (@navigation_history) { print "modus: $href->{modus} folder: $href->{folder} keys: $href->{keywords} date: $href->{date} loc: $href->{location} search: $href->{search}\n"; } } ############################################################## ############################################################## sub get_pics_by_modus { my $pics = shift; # list reference #navigation_history_save(); if ($act_modus == FOLDER) { @$pics = getPics($actdir, WITH_PATH, $config{CheckForNonJPEGs}); } elsif ($act_modus == LOCATION) { @$pics = get_pics_by(LOCATION, \@act_location); } elsif ($act_modus == DATE) { @$pics = get_pics_by(DATE, \@act_date); } elsif ($act_modus == KEYWORDCLOUD) { @$pics = get_pics_with_keywords(\@act_keywords, \@act_keywords_ex); } elsif ($act_modus == KEYWORD) { @$pics = get_pics_with_keywords(\@act_keywords, \@act_keywords_ex); } elsif ($act_modus == SEARCH) { @$pics = get_pics_by_searching($config{SearchPattern}, ''); } elsif ($act_modus == COLLECTION) { # print "act collection: $_\n" foreach (@act_collection); # index 0 = folder, 1 = collection @$pics = @{$slideshows{$act_collection[0]}{$act_collection[1]}{pics}}; } else { warn "showThumbs called with unknown modus: $act_modus"; return 0; } $actdirF->{Filter}->{excluded_pics} = 0; # reset filter display if ($conf{filter_pics}{value}) { $actdirF->{Filter}->{excluded_pics} = filter_pics($pics, $conf{filter_pics_keywords}{value}); } return 1; } ############################################################## # showThumbsInList ############################################################## sub showThumbsInList { my $lb = shift; # the listbox widget my $listR = shift; # the list of pics to show # show some infos to the user while loading my $n = 0; # actual number my $nr = @$listR; # total number my $pw = progressWinInit($lb, lang('Load pictures')); foreach my $dpic (@$listR) { last if progressWinCheck($pw); $n++; # debug helper: print "xxx loading picture ($n/$nr)\n"; progressWinUpdate($pw, lang('loading picture')." ($n/$nr) ...", $n, $nr); addOneRow($lb, $dpic, 1); } progressWinEnd($pw); if (($lb == $picLB) and ($n != $nr)) { log_it("user abord at $n of $nr"); #$lb->after(1000); # just a little delay to show the message above return 0; } return 1; } ############################################################## # Source: http://newsgroups.derkeiler.com/Archive/Comp/comp.lang.perl.tk/2006-02/msg00050.html ############################################################## sub get_encode_file { my $filename = shift; # todo: should not die here!!!!!! open my $filehandle, '<', $filename or die "Can't open $filename. $!\n"; local $/; return encode_base64(<$filehandle>); } ############################################################## # addOneRow - adds a new row, or updates an existing row ############################################################## sub addOneRow { my $lb = shift; my $dpic = shift; my $with_thumb = shift; # bool 1 = thumb, 0 = defaultthumb my $after; $after = shift; # optional unless ($lb->info('exists', $dpic)) { # create new row, we use the path and file name (=$dpic) as unique index for the hlist entry if (($after) and ($lb->info('exists', $after))) { $lb->add($dpic, -after => $after); } else { $lb->add($dpic); } } my $thumb = getThumbFileName($dpic); my $thumbP = undef; if (-f $thumb) { # Source: http://newsgroups.derkeiler.com/Archive/Comp/comp.lang.perl.tk/2006-02/msg00050.html # Some of the Tk modules (like Photo) do their own file IO and just # cannot deal with non-ASCII characters in file names (on Windows, # at least, I don't have as much experience with Linux, OSX, et al.) # I have been bitten by this before and haven't really found a good # solution. A kind of clunky work-around I have employed in the past # is to use the standard perl IO functions to load the file into # memory, base64 encode it and then feed it to the module as data. # Extra work, but it side steps the problem without resorting to # measures which may be beyond your control. (Like requiring all # file names to use only ASCII characters.) if ($EvilOS) { $thumbP = $lb->Photo(-format => 'jpeg', -data => get_encode_file($thumb), -gamma => $config{Gamma}); } else { $thumbP = $lb->Photo(-format => 'jpeg', -file => $thumb, -gamma => $config{Gamma}); } $thumbs{$dpic} = $thumbP; # save all thumb photo objects in global hash %thumbs to delete them when changing the dir } # test feature to improve speed: read meta info only if there is no info in the DB or the modification date has changed # on windows this is 10 times faster to read in a folder with 200 pics (34 secs vs. 3 secs) # todo there should be a possibility to force a reread, if somebody added metainfo without changing the modification date - however this is still possible using add to database if ($searchDB{$dpic} and $searchDB{$dpic}{MOD}) { if ($searchDB{$dpic}{MOD} != getFileDate($dpic, NO_FORMAT)) { addToSearchDB($dpic); # save the infos into the search data base } } else { # branch for pics not yet stored in the database or with missing modification dates addToSearchDB($dpic); # save the infos into the search data base } my $pic = basename($dpic); my $dir = dirname($dpic); my $com = $searchDB{$dpic}{COM}; my $exif = date_iso_to_relative($searchDB{$dpic}{EXIF}); my $iptc = displayIPTC($dpic); $com = formatString($com, $config{LineLength}, , $config{LineLimit}); # format the comment for the list $iptc = formatString($iptc, $config{LineLength},, $config{LineLimit}); # format the IPTC info for the list my $rating_size = get_rating_and_size($dpic, $lb); my $image; if ((defined $thumbP) and $with_thumb) { $image = $thumbP; } else { $image = $mapivi_icons{'EmptyThumb'}; } if (defined $image) { $lb->itemCreate($dpic, $lb->{thumbcol}, -style => $thumbS, -itemtype => 'imagetext', -image => $image, -text => getThumbCaption($dpic)); } # insert items in the table row $lb->itemCreate($dpic, $lb->{filecol}, -itemtype => "image", -image => $rating_size, -style => $fileS); $lb->itemCreate($dpic, $lb->{iptccol}, -text => $iptc, -style => $iptcS); $lb->itemCreate($dpic, $lb->{comcol}, -text => $com, -style => $comS); $lb->itemCreate($dpic, $lb->{exifcol}, -text => $exif, -style => $exifS); $lb->itemCreate($dpic, $lb->{dircol}, -text => $dir, -style => $dirS); } ############################################################## ############################################################## sub get_rating_and_size { my $dpic = shift; my $lb = shift; my $star_icon = iptc_rating_star_icons($dpic); my $file_info = getAllFileInfo($dpic); # use Tk::Compound to display a picture above a text, both aligned to the left side my $compound = $lb->Compound; # next line display the rating stars as small icons $compound->Line(-anchor => 'w'); $compound->Image(-image => $mapivi_icons{$star_icon}); # next line: text $compound->Line(-anchor => 'w'); $compound->Text(-text => $file_info, -justify => 'left'); # flags (optional) if (defined $searchDB{$dpic}{FLAG}) { my $flag = $searchDB{$dpic}{FLAG}; if ($flag != 0) { $compound->Line(-anchor => 'w'); $compound->Image(-image => $mapivi_icons{'FlagRed'}) if ($flag & FLAG_RED); $compound->Image(-image => $mapivi_icons{'FlagGreen'}) if ($flag & FLAG_GREEN); $compound->Image(-image => $mapivi_icons{'FlagBlue'}) if ($flag & FLAG_BLUE); } } return $compound; } ############################################################## # displayIPTC - convert the searchdb info into a formated string ############################################################## sub displayIPTC { my $dpic = shift; my $iptc = ''; $iptc = displayUrgency($searchDB{$dpic}{URG}); $iptc .= "Keywords: ".$searchDB{$dpic}{KEYS}."\n" if (defined $searchDB{$dpic}{KEYS}); $iptc .= $searchDB{$dpic}{IPTC} if (defined $searchDB{$dpic}{IPTC}); return $iptc; } ############################################################## # displayUrgency - create string with rating/urgency number ############################################################## sub displayUrgency { my $urg = shift; return '' unless (defined $urg); return "Rating: $urg\n"; } ############################################################## # iptc_rating_stars_urg - convert the IPTC urgency number into a # rating string with zero to five stars (*) and the urgency in parenthesis ############################################################## sub iptc_rating_stars_urg { my $urgency = shift; my $stars = iptc_rating_stars($urgency); $urgency = '-' if ($urgency eq ''); return "$stars ($urgency)"; } ############################################################## # iptc_rating_stars - convert the IPTC urgency number into a # rating string with zero to five stars (*) ############################################################## sub iptc_rating_stars { my $urgency = shift; return '' unless (defined $urgency); return '' if ($urgency <= 0); my $stars = ''; for (my $x = 5; $x >= $urgency; $x -= 1) { $stars .= '*'; } return $stars; } ############################################################## # iptc_rating_stars_nr - convert the IPTC urgency number into # the number of rating stars ############################################################## sub iptc_rating_stars_nr { my $urgency = shift; return 0 unless (defined $urgency); return 0 if ($urgency <= 0); return 0 if ($urgency > 5); # todo: should we also handle 6,7,8 -> 2/3, 1/2, 1/3? return (6 - $urgency); } ############################################################## # iptc_rating_stars_icons - convert the rating into a icon file name ############################################################## sub iptc_rating_star_icons { my $dpic = shift; return 'Rating0' unless (defined $dpic); my $urg = $searchDB{$dpic}{URG}; return 'Rating0' unless (defined $urg); return 'Rating0' if (($urg <= 0) or ($urg > 8)); return "Rating$urg"; } ############################################################## ############################################################## sub rating_button { my ($widget, $callback, $info, $side, $fill, $rating) = @_; my $frame = $widget->Frame(-relief => 'sunken')->pack(-side => $side, -fill => $fill); $balloon->attach($frame, -msg => $info); my $label; # mapping of IPTC urgency to 5 star rating string my %urg2star = ( 1 => '5 stars', 2 => '4 stars', 3 => '3 stars', 4 => '2 stars', 5 => '1 star', 6 => '2/3 star', 7 => '1/2 star', 8 => '1/3 star', 0 => '0 stars', ); my @menuorder = (1,2,3,4,5,6,7,8,0); my $menu = $widget->Menu(-title => 'Rating'); foreach my $urg (@menuorder) { $menu->command(-image => compound_menu($widget, $urg2star{$urg}, "rating-$urg.png"), -command => sub {$$rating = $urg; $label->configure(-image => $mapivi_icons{"Rating$urg"}); &$callback();}); } $label = $frame->Label(-image => $mapivi_icons{"Rating$$rating"}, -bd => 0)->pack(-side => 'left', -fill => 'y'); $label->bind('', sub { $menu->Popup(-popover => 'cursor', -popanchor => 'n'); } ); return $label; } ############################################################## ############################################################## sub rating_button_min_max { my ($widget, $ratingA, $ratingB, $callback) = @_; my ($butA, $butB); # todo: fails is rating is 0!!! $butA = rating_button($widget, sub { $$ratingB = $$ratingA if ($$ratingB != 0 and (($$ratingB < $$ratingA) or ($$ratingA == 0))); $butB->configure(-image => $mapivi_icons{'Rating'.$$ratingB}); &$callback() if $callback }, "Rating Max", 'left', 'x', $ratingA); $widget->Label(-text => '-')->pack(-side => 'left'); $butB = rating_button($widget, sub { $$ratingA = $$ratingB if ($$ratingA == 0 or ($$ratingB > 0 and ($$ratingA > $$ratingB))); $butA->configure(-image => $mapivi_icons{'Rating'.$$ratingA}); &$callback() if $callback }, "Rating Min", 'left', 'x', $ratingB); return ($butA, $butB); } ############################################################## # addToSearchDB - add a picture to the search data base # this function can be called with one or four # parameters ############################################################## sub addToSearchDB { my $dpic = shift; # normalize the path $dpic =~ s/\\/\//g; # replace Windows path delimiter with UNIX style \ -> / $dpic =~ s/\/+/\//g; # replace multiple slashes with one // -> / $dpic =~ s/\/\.\//\//g; # replace dot dir /./ -> / if (!-f $dpic) { warn "addToSearchDB: $dpic not found!"; return; } print "addToSearchDB $dpic\n" if $verbose; # do not save pics to the database which are located in .thumbs/ .xvpics/ .exif/ my $dir = dirname($dpic); $dir =~ s!/$!!g; # remove trailing / if ($dir =~ m/$thumbdirname|$exifdirname|$xvpicsdirname$/) { print "addToSearchDB: ignoring $dpic\n" if $verbose; return; } my ($com, $exif, $ctime, $mtime, $iptc, $urgency, $size, $x, $y, $keys, @keys, $pop, $flag); # $meta is returned at the end of the sub, # the SOF segment is needed for the latter call of getAllFileInfo my $meta = getMetaData($dpic, "COM|APP1|APP13|SOF", 'FASTREADONLY'); $exif = getShortEXIF( $dpic, WRAP, $meta); $com = getComment( $dpic, LONG, $meta); $iptc = getIPTC( $dpic, SHORT, $meta); $size = getFileSize( $dpic, NO_FORMAT); ($x,$y) = getSize( $dpic, $meta); $mtime = getFileDate( $dpic, NO_FORMAT); @keys = getIPTCkeywords($dpic, $meta); $pop = 0; $pop = $searchDB{$dpic}{POP} if (defined $searchDB{$dpic}{POP}); $flag = $searchDB{$dpic}{FLAG} if (defined $searchDB{$dpic}{FLAG}); # handling of non-printables is already done in getIPTC and getIPTCkeywords # todo: It is needed here too, but why? $iptc =~ tr/\n -~//cd; # remove all non-printable chars, but not newline foreach (@keys) { $_ =~ tr/ -~//cd; # remove all non-printable chars (Picasa adds one to each keyword) } # build a space separated string from the keyword list # todo find a better separator, so that keywords with spaces can be supported better foreach (@keys) { $keys .= "$_ "; } # check if the pictures contain new keywords if ($config{CheckNewKeywords}) { foreach (@keys) { # store all keywords in a hash and count them if (defined $new_keywords{$_}) { $new_keywords{$_}++; } else { $new_keywords{$_} = 1; } } } # try to get the EXIF date from the short EXIF info format: "dd.mm.yyyy hh:mm:ss" # there may be [t] or [s] in front of the date! undef $ctime; if (defined($exif)) { my $year; my $mon; my $day; my $hour; my $min; my $sec; # support three different date formats # dd.mm.yyyy hh:mm:ss if ($exif =~ m/(\d\d)\.(\d\d)\.(\d\d\d\d)\s(\d\d):(\d\d):(\d\d)/) { $day = $1; $mon = $2; $year = $3; $hour = $4; $min = $5; $sec = $6; } # mm/dd/yyyy hh:mm:ss if ($exif =~ m/(\d\d)\/(\d\d)\/(\d\d\d\d)\s(\d\d):(\d\d):(\d\d)/) { $mon = $1; $day = $2; $year = $3; $hour = $4; $min = $5; $sec = $6; } # yyyy-mm-dd hh:mm:ss if ($exif =~ m/(\d\d\d\d)-(\d\d)-(\d\d)\s(\d\d):(\d\d):(\d\d)/) { $year = $1; $mon = $2; $day = $3; $hour = $4; $min = $5; $sec = $6; } $mon--; if (defined $year) { # todo: this may be dangerous or at least wrong! if ($year > $copyright_year) { # fix wrong dates print "Mapivi warning: $dpic: EXIF year: $year is in the future, correcting to $copyright_year\n"; $year = $copyright_year; } #$year -= 1900; if ($mon >= 0 and $mon <= 11) { # calculate the time as value in seconds since the Epoch (Midnight, January 1, 1970) $ctime = timelocal($sec,$min,$hour,$day,$mon,$year); #warn "using exifdate for $dpic: $ctime\n" if $verbose; # optional checks #my ($s,$m,$h,$d,$mo,$y) = localtime $ctime; #$y += 1900; $mo++; # do some adjustments # build up the date time string, sim#lar to the EXIF format #my $date1 = sprintf "%04d:%02d:%02d %02d:%02d:%02d", $y, $mo, $d, $h, $m, $s; #my $date2 = "$3:$2:$1 $4:$5:$6"; #print "$date2 $date $dpic\n" if ($date1 ne $date2); } } #else { print "mon = $mon $3:$2:$1 $4:$5:$6\n";} } #else { print "no exif date: $exif" if $verbose; } # if there is no exif time available use the file modification date unless (defined $ctime) { $ctime = (lstat $dpic)[9]; # 9 is the modification date time #warn "using filedate for $dpic: $ctime\n" if $verbose; } # replace all newlines with space before adding to the database #$com =~ s/\n/ /g if (defined $com); #$exif =~ s/\n/ /g if (defined $exif); #$iptc =~ s/\n/ /g if (defined $iptc); # maybe there was something defined before, so we better overwrite it with '' $com = '' unless (defined $com); $exif = '' unless (defined $exif); $iptc = '' unless (defined $iptc); $iptc =~ s/urgency\s*:\s*\d*\s*//i; # remove urgency from the IPTC field $iptc =~ s/keywords\s*:\s*.*\n*//i; # remove keywords from the IPTC field $urgency = getIPTCurgency($dpic, $meta); $urgency = undef if ($urgency == 9); delete $searchDB{$dpic}; # clear hash item first #print "adding: IPTC: $iptc\n"; #print "adding: Keys: $keys\n"; #print "adding: URG : $urgency\n"; $searchDB{$dpic}{COM} = $com; # save (complete!) comment $searchDB{$dpic}{EXIF} = $exif; # save short EXIF info $searchDB{$dpic}{SIZE} = $size; # save file size in Bytes $searchDB{$dpic}{PIXX} = $x; # save pixel size (x = width) $searchDB{$dpic}{PIXY} = $y; # save pixel size (y = height) $searchDB{$dpic}{TIME} = $ctime; # save EXIF/file creation time $searchDB{$dpic}{MOD} = $mtime; # save file modification time $searchDB{$dpic}{IPTC} = $iptc; # save complete IPTC info, but without urgency and keywords $searchDB{$dpic}{URG} = $urgency; # save IPTC urgency $searchDB{$dpic}{KEYS} = $keys; # save IPTC keywords $searchDB{$dpic}{POP} = $pop if ($config{trackPopularity}); # save popularity (how often the pic was shown) $searchDB{$dpic}{FLAG} = $flag; # save flag markers #print "---IPTC: $searchDB{$dpic}{IPTC}---\n"; return $meta; } ############################################################## # getMetaData - returns the Image::MetaData::JPEG # object of $dpic ############################################################## sub getMetaData { my $dpic = shift; my $what = shift; # regex to match the needed segments e.g. "COM" for comment, # or "APP13|COM" for IPTC info and comment segments my $option = shift; # optional option, if set to 'FASTREADONLY' will speed things up return unless is_a_JPEG($dpic); # mapivi just needs the comments (COM), EXIF (APP1), IPTC (APP13) and size (SOF) segments my $meta = new Image::MetaData::JPEG($dpic, $what, $option); print "getMetaData: Kind:$what pic:$dpic\n" if $verbose; warn "Error: " . Image::MetaData::JPEG::Error() unless $meta; return $meta; } ############################################################## # check is file is a raw picture file based on file suffix ############################################################## sub is_raw_file { my $file = shift; my $is_raw = 0; if (-f $file) { my ($basename,$dir,$suffix) = fileparse($file, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) $is_raw = 1 if (isInList(lc($suffix), \@raw_suffix_lc)); } return $is_raw; } ############################################################## # check if the given file has a RAW file with same name # and in same folder ############################################################## sub has_raw_file { my $basename = shift; # the basename is the complete path with filename but without suffix my $rc = 0; foreach my $raw_suffix (@raw_suffix) { # check for different suffix and suffix in lower case if ((-f $basename.$raw_suffix) or (-f $basename.lc($raw_suffix))) { $rc = 1; last; } } return $rc; } ############################################################## # return true if a copy of the given file in the origs folder # with either the same name or the "-bak" suffix exists # the second return value is the file name if found, else # it is undef ############################################################## sub has_orig_file { my ($pic,$dir,$suffix) = @_; my $rc = 0; my $file = undef; my $orig = $dir.$conf{origs_folder_name}{value}.'/'.$pic.$suffix; if (-f $orig) { $rc = 1; $file = $orig; } else { my $origbak = buildBackupName($orig); if (-f $origbak) { $rc = 1; $file = $origbak; } } return ($rc, $file); } ############################################################## # getAllFileInfo ############################################################## sub getAllFileInfo { my $dpic = shift; my $bpic = buildBackupName($dpic); my $size = ''; my $w = 0; my $h = 0; my ($pic,$dir,$suffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $basename = "$dir/$pic"; $size = basename($dpic)."\n"; $size .= int($searchDB{$dpic}{SIZE}/1024).'kB' if $searchDB{$dpic}{SIZE}; $size .= '[bak]' if (-f $bpic); # show that there is a backup file $size .= '[orig]' if (has_orig_file($pic,$dir,$suffix)); # show that there is a original file $size .= '[raw]' if (has_raw_file($basename)); # show that there is a raw file $size .= '[XMP]' if ((-f $basename.'.xmp') or (-f $basename.'.XMP')); # show that there is a XMP sidecar file $size .= '[WAV]' if ((-f $basename.'.wav') or (-f $basename.'.WAV')); # show that there is a WAV audio file $size .= "\n".date_iso_to_relative(getDateTimeISOString($searchDB{$dpic}{MOD})) if ($config{ShowFileDate} and defined $searchDB{$dpic}{MOD}); $w = $searchDB{$dpic}{PIXX} if $searchDB{$dpic}{PIXX}; $h = $searchDB{$dpic}{PIXY} if $searchDB{$dpic}{PIXY}; # MP = MegaPixel my $p = sprintf "%.2f", ($w*$h/1000000); $size .= "\n${w}x$h (${p}MP)"; if ($config{BitsPixel}) { my $bitPix = getBitPix($dpic); $bitPix = sprintf "%.2f", $bitPix; $size .= "\n${bitPix}b/p"; } $size .= "\n".getAspectRatio($w, $h) if ($config{AspectRatio} and ($w > 0) and ($h > 0)); if (-l $dpic) { $size .= "\n(Link)"; } $size .= " Viewed ".$searchDB{$dpic}{POP}.' times' if (($config{trackPopularity}) and (defined $searchDB{$dpic}{POP})); return $size; } ############################################################## # getAspectRatio ############################################################## sub getAspectRatio { my $w = shift; my $h = shift; return '' if (($h == 0) or ($w == 0)); my $aspectdelta = 1 + ($config{AspectSloppyFactor} / 100); # delta factor for aspect ratio my $r = $w/$h; # aspect ratio my $ratio = ''; if (($r <= $aspectdelta*4/3) and ($r >= (4/3)/$aspectdelta)) { $ratio = "[4:3]"; } elsif (($r <= $aspectdelta*3/4) and ($r >= (3/4)/$aspectdelta)) { $ratio = "[3:4]"; } elsif (($r <= $aspectdelta*2/3) and ($r >= (2/3)/$aspectdelta)) { $ratio = "[2:3]"; } elsif (($r <= $aspectdelta*3/2) and ($r >= (3/2)/$aspectdelta)) { $ratio = "[3:2]"; } elsif (($r <= $aspectdelta*5/4) and ($r >= (5/4)/$aspectdelta)) { $ratio = "[5:4]"; } elsif (($r <= $aspectdelta*4/5) and ($r >= (4/5)/$aspectdelta)) { $ratio = "[4:5]"; } elsif (($r <= $aspectdelta*7/5) and ($r >= (7/5)/$aspectdelta)) { $ratio = "[7:5]"; } elsif (($r <= $aspectdelta*5/7) and ($r >= (5/7)/$aspectdelta)) { $ratio = "[5:7]"; } elsif (($r <= $aspectdelta*16/9) and ($r >= (16/9)/$aspectdelta)) { $ratio = "[16:9]"; } elsif (($r <= $aspectdelta*9/16) and ($r >= (9/16)/$aspectdelta)) { $ratio = "[9:16]"; } elsif ($w == $h) { $ratio = "[1:1]"; } else { if ($w > $h) { $ratio = sprintf "[%.2f:1]", ($w/$h); } else { $ratio = sprintf "[1:%.2f]", ($h/$w); } } return $ratio; } ############################################################## # removeIPTC ############################################################## sub removeIPTC { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $rc = $top->messageBox(-icon => 'question', -message => langf("Please press Ok to remove all IPTC info of the %d selected pictures. There is no undo!", scalar @sellist), -title => lang('Remove all IPTC info?'), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my $errors = ''; my $i = 0; my $pw = progressWinInit($top, "Remove IPTC info"); foreach my $dpic (@sellist){ last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Removing IPTC info ($i/".scalar @sellist.") ...", $i, scalar @sellist); next unless (-f $dpic); next if (!checkWriteable($dpic)); my $meta = getMetaData($dpic, "APP13"); $meta->remove_app13_info(-1, 'IPTC'); # remove all APP13 IPTC segments unless ($meta->save()) { $errors .= "removeIPTC: save $dpic failed!\n"; } updateOneRow($dpic, $picLB); if ($dpic eq $actpic) { showImageInfo($dpic); #showImageInfoCanvas($dpic); } } progressWinEnd($pw); log_it("ready! (removed IPTC info of $i/".scalar @sellist.")"); showText("Errors while removing IPTC infos", $errors, NO_WAIT) if ($errors ne ''); return; } ############################################################## # cutString - cat a string to a given length, remove newline # and carriage return and add e.g. dots if cut # examples: cutString("elephant",20,"..") -> "elephant" # cutString("elephant", 7,"..") -> "eleph.." # cutString("elephant",-7,"..") -> "..phant" ############################################################## sub cutString { my $str = shift; # input string my $len = shift; # the max length my $dot = shift; # the dots (e.g. ".." or "...") return unless (defined $str); return if ($str eq ''); my $dotlen = length($dot); my $out = $str; if (length($dot) >= abs($len)) { warn "cutString: lenght of dots is longer or equal than length"; return $out; } if ($len >= 0) { $out = substr($out, 0, ($len-$dotlen)).$dot if (length($out) > $len); } else { $out = $dot.substr($out, ($len+$dotlen), length($str)) if (length($out) > -$len); } $out =~ s/\n//g; # remove newlines $out =~ s/\r//g; # remove \r (carriage return) return $out; } ############################################################## # formatString - cuts and formats a string to # a width of $linelenght chars and a length of # $line_nr_limit lines. # this function wont work as expected with # comments containing a lot of nearly empty lines ############################################################## sub formatString { my $string = shift; my $linelenght = shift; my $line_nr_limit = shift; # use -1 if there should be no line nr limit return '' if ((!defined $string) or ($string eq '')); local($Text::Wrap::columns) = $linelenght+1; local($Text::Wrap::huge) = 'overflow'; # sanitize and wrap string $string =~ s/\r//g; # cut \r (carriage return) $string =~ tr[\200-\377][\000-\177]; # remove the eight bit $string =~ tr/\n -~//cd; # remove non-printable characters (but not \n) $string = wrap('','',$string); # limit the number of lines (cut off the rest) if ($line_nr_limit > 0) { # split up in an array of single lines my @l = split /\n/, $string; my $max = $line_nr_limit; $max = @l if (@l < $max); $string = ''; # rebuild string by using the first $max lines for ( 0 .. ($max - 1)) { $string .= sprintf "%s\n", $l[$_]; } $string =~ s/\n+$//; # cut off trailing newline(s) } return $string; } ############################################################## ############################################################## sub get_list_size { my $list_ref = shift; my $size = 0; my $size_str = ''; foreach my $dpic (@{$list_ref}) { $size += getFileSize($dpic, NO_FORMAT); } $size_str = computeUnit($size) if $size; return $size_str; } ############################################################## # getFileSize - get the size in kB of a file, even if it is a link ############################################################## sub getFileSize { my $dpic = shift; my $format = shift; # NO_FORMAT = return size unformated in Bytes (integer) FORMAT = with "kB" added (string) my $size = ''; return $quickSortHashSize{$dpic} if ($quickSortSwitch and defined $quickSortHashSize{$dpic}); if (!-f $dpic) { warn "getFileSize: $dpic is no file!"; if ((defined $format) and ($format == NO_FORMAT)) { return 0; } else { return ''; } } if (-l $dpic) { $size = (lstat (getLinkTarget($dpic)))[7]; } else { $size = (lstat $dpic)[7]; } if ((defined $format) and ($format == FORMAT)) { $size = int($size/1024).'kB' if $size; } $quickSortHashSize{$dpic} = $size if $quickSortSwitch; return $size; } ############################################################## # makeDir - create the directory for storing the # thumbnail pictures or EXIF infos ############################################################## sub makeDir { my $dir = shift; my $ask = shift; # ASK = ask before creating a dir, NO_ASK return 1 if (-d $dir); if ( ($ask == ASK) and $config{AskMakeDir} ) { my $rc = checkDialog("Create new folder?", "Mapivi would like to create this folder:\n$dir\nContinue?", \$config{AskMakeDir}, "ask every time", '', 'OK', 'Cancel'); return if ($rc ne 'OK'); } # 0755 = rwxr.xr.x eval { mkpath($dir, 0, oct(755)) }; # 0 = no output, 0755 = access rights if ($@) { $top->messageBox(-icon => 'warning', -message => "makeDir: can not create $dir: $@", -title => 'Error', -type => 'OK'); return 0; } return 1; } ############################################################## # aNewerThanb - true if file a is newer than file b, or if # file a exists and file b does not ############################################################## sub aNewerThanb { my $afile = shift; my $bfile = shift; if (-f $afile) { if (-f $bfile) { # compare modification times return (lstat $afile)[9] > (lstat $bfile)[9]; } return 1; } return 0; } ############################################################## # nextPic - get the index of the next picture in the directory ############################################################## sub nextPic { my $actpic = shift; my @pics = $picLB->info('children'); # if there are no pics return an empty string return '' if (@pics == 0); # if there is no actpic we start with the first return $pics[0] if ($actpic eq ''); # try to get the next pic my $next = $picLB->info('next', $actpic); # if there is no next pic unless ($next) { # we have reached the end and start again with the first picture beep() if ($config{BeepWhenLooping}); $next = $pics[0]; } return $next; } ############################################################## # nextSelectedPic - get the index of the next selected picture # in the directory ############################################################## sub nextSelectedPic { my $actpic = shift; my @pics = $picLB->info('children'); my @sel = $picLB->info('selection'); # if there are no pics return an empty string return '' if (@pics == 0); return '' if (@sel == 0); my $start = 0; my $next = ''; my $nextsel = ''; foreach my $dpic (@pics) { # skip all pics until we reach the actual picture $start = 1 if ($dpic eq $actpic); next unless $start; # get the next picture $next = $picLB->info('next', $dpic); # check if it is selected if ($next and isInList($next, \@sel)) { $nextsel = $next; last; } } # if there is no next pic if ($nextsel eq '') { # we have reached the end and start again with the first selected picture #beep() if ($config{BeepWhenLooping}); $nextsel = $sel[0]; } return $nextsel; } ############################################################## # prevPic - show the previous picture in the directory ############################################################## sub prevPic { my $actpic = shift; my @pics = $picLB->info('children'); # if there are no pics return an empty string return '' if (@pics == 0); # if there is no actpic we start with the first return $pics[-1] if ($actpic eq ''); # try to get the previous pic my $prev = $picLB->info('prev', $actpic); # if there is no prev pic unless ($prev) { # we have reached the start and jump to the last picture beep() if ($config{BeepWhenLooping}); $prev = $pics[-1]; } return $prev; } ############################################################## # gotoPic ############################################################## sub gotoPic { my $lb = shift; return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off my @childs = $lb->info('children'); return if (!@childs); my $goto = ''; my $rc = myEntryDialog("Go to picture/select pictures", "Please enter a part of the name or the index number of the picture(s) to select/show.\nIndex number are entered like this: /number.\nUse /c to switch to case sensitive and /s if the filename starts with the search string.\n\nExamples:\nabc show and select all pictures containing abc (any case)\n/10 show picture number 10\n/sabc show and select all pictures starting with abc (any case)\n/cABC show and select all pictures containing an upper case ABC\n/s/cABC show and select all pictures starting with an upper case ABC", \$goto); return if (($rc ne 'OK') or ($goto eq '')); if ($goto =~ m/(\/)(\d+)/) { # $goto is a number if (($2 > 0) and ($2 < @childs + 1)) { # saved here for undo function @savedselection2 = @savedselection; @savedselection = $lb->info('selection'); $lb->selectionClear(); showPic($childs[$2-1]) if ($lb == $picLB); } else { log_it("number $2 is out of range!"); } } else { # $goto is a string my @pics; my $case = "i"; my $start = ".*"; if ($goto =~ m/.*\/c/) { $case = ''; $goto =~ s/\/c//; } if ($goto =~ m/.*\/s/) { $start = "^"; $goto =~ s/\/s//; } foreach (@childs) { if (basename($_) =~ m/(?$case)$start$goto.*/) { push @pics, $_; } } if (@pics) { # saved here for undo function @savedselection2 = @savedselection; @savedselection = $lb->info('selection'); $lb->selectionClear(); showPic($pics[0]) if ($lb == $picLB); reselect($lb, @pics); log_it("selected ".scalar @pics." pictures matching \"$goto\""); } else { log_it("string $goto was not found in the picture names"); } } } ############################################################## # showImageInfo - display infos and comment of given image # if available ############################################################## sub showImageInfo { my $dpic = shift; if ((not defined $dpic) or (not -f $dpic)) { $widthheight = ''; $size = ''; #$exif = ''; $rating_but->configure(-image => $mapivi_icons{Rating0}); $commentText->delete( 0.1, 'end') if ($config{ShowCommentField}); } else { my $meta = getMetaData($dpic, "COM|APP13|APP1|SOF", 'FASTREADONLY'); ($width, $height) = getSize($dpic, $meta); $widthheight = $width.'x'.$height; if ($config{ShowCommentField}) { my $comment = getComment($dpic, LONG, $meta); # does not work! mh 14.07.03 # # determine the height of the textbox by counting the number of lines # my $height = ($comment =~ tr/\n//); # $height++; # $height = 10 if ($height > 10); # not to big, we have scrollbars # print "h = $height\n"; # $commentText->configure(-height => $height); $commentText->delete( 0.1, 'end'); # remove old comment $commentText->insert('end', $comment); # insert new comment } update_IPTC_frame_content($dpic); my $star_icon = iptc_rating_star_icons($dpic); $rating_but->configure(-image => $mapivi_icons{$star_icon}); $size = getFileSize($dpic, FORMAT); } setTitle(); # also update the canvas text showImageInfoCanvas($dpic); # if ($dpic eq $actpic); } ############################################################## # update or clear IPTC Headline and Caption entry in picture frame ############################################################## sub update_IPTC_frame_content { my $dpic = shift; # optional if (($config{ShowIPTCFrame}) and (defined $titleText)) { $titleText->delete( 0.1, 'end'); # remove old headline $captionText->delete( 0.1, 'end'); # remove old caption if ((defined $dpic) and (-f $dpic)) { my $headline = getIPTCHeadline($dpic); $titleText->insert('end', $headline); # insert new headline my $caption = getIPTCCaption($dpic); $captionText->insert('end', $caption); # insert new caption } } return; } ############################################################## # showImageInfoCanvas - display infos on the canvas ############################################################## # ToDo: check if either showImageInfo could call showImageInfoCanvas or other way round # and fix all calls correspondingly sub showImageInfoCanvas { my $dpic = shift; $c->delete('withtag', 'TEXT'); # remove picture info text $c->delete('withtag', 'GPS'); # remove GPS button update_IPTC_frame_content($dpic); return 0 unless (defined $dpic); return 0 unless (-f $dpic); if ($config{ShowPicInfo}) { # update balloon info for displayed picture my $balloonmsg = makeBalloonMsg($dpic); # bind the balloon to the canvas $balloon->attach($c->Subwidget('canvas'), -balloonposition => 'mouse', -msg => {'pic' => $balloonmsg} ); } return 1 unless ($config{ShowInfoInCanvas}); GPS_button($c, $dpic); my $info = lang('File').': '.basename($dpic)."\n"; $info .= lang('Path').': '.dirname($dpic)."\n\n"; my $meta = getMetaData($dpic, "COM|APP13|APP1|SOF", 'FASTREADONLY'); my $exif = formatString(date_iso_to_relative(getShortEXIF($dpic, NO_WRAP, $meta)), 80, -1); my $comm = formatString(getComment($dpic, LONG, $meta), 80, -1); my $iptc = formatString(getIPTC($dpic, LONG, $meta), 80, -1); my $xmp = formatString(xmp_get($dpic), 80, -1); #my $iptcE= formatString(iptc_get($dpic), 80, -1); # iptc extracted by ExifTool #my $exifE= formatString(exif_get($dpic), 80, -1); # exif extracted by ExifTool $info .= "EXIF:\n$exif\n" if ($exif ne ''); $info .= "\nIPTC:\n$iptc\n" if ($iptc ne ''); $info .= "\nXMP:\n$xmp\n" if ($xmp ne ''); #$info .= "\nIPTC (from ExifTool):\n$iptcE\n" if ($iptcE ne ''); #$info .= "\nEXIF (from ExifTool):\n$exifE\n" if ($exifE ne ''); $info .= "\nComment:\n$comm" if ($comm ne ''); return 1 if ($info eq ''); # show image info on canvas white font with black shadow $c->createText( 5, 5, -font => $font, -text => $info, -anchor => 'nw', -fill => 'black', -tags => ['TEXT']); my $id = $c->createText( 4, 4, -font => $font, -text => $info, -anchor => 'nw', -fill => $conf{color_fg}{value}, -tags => ['TEXT']); my ($x1, $y1, $x2, $y2) = $c->bbox($id); $c->createText( 4, $y2+4, -font => $small_font, -text => lang("F3: show/hide overlay text"), -anchor => 'nw', -fill => 'gray60', -tags => ['TEXT']); return 1; } ############################################################## # display any text with shadow on any canvas # based on showImageInfoCanvas # uses TEXT, METAINFO and SHADOW as tags ############################################################## sub show_text_on_canvas { my $c = shift; # canvas widget my $text = shift; $c->delete('withtag', 'TEXT'); # remove picture info text if ($conf{show_micro_meta}{value}) { return 1 if (not defined $text or $text eq ''); # show image info on canvas white font with black shadow (shadow offset = 1 pixel) $c->createText( 5, 5, -font => $font, -text => $text, -anchor => 'nw', -fill => 'black', -tags => ['TEXT','SHADOW']); my $id = $c->createText( 4, 4, -font => $font, -text => $text, -anchor => 'nw', -fill => 'gray60', -tags => ['TEXT','METAINFO']); # old fill: $conf{color_fg}{value} # change font size to fill window width adapt_font_size($c, 'METAINFO', 'SHADOW'); # add info text (always in small size) my ($x1, $y1, $x2, $y2) = $c->bbox($id); $c->createText( 4, $y2+4, -font => $small_font, -text => lang("F3: show/hide overlay text"), -anchor => 'nw', -fill => 'gray60', -tags => ['TEXT']); } return 1; } ############################################################## ############################################################## sub adapt_font_size { my $c = shift; # canvas widget my $tag = shift; # tag of text item on canvas my $tag2 = shift; # tag of shadow text item on canvas my $font_size_min = 10; my $font_size = $font_size_min; my $quit = 0; # flag to exit loop $top->update; my $c_w = $c->width; return if (not defined $c_w); my @ids = $c->find('withtag', $tag); return if (not @ids); # assumption: there is just one canvas element with this tag my $border = 3; #0.1 * $c->Width; # keep a 10% border around the text for my $i (1..10) { # max 10 iterations to avoid endless loops my ($x1, $y1, $x2, $y2) = $c->bbox($ids[0]); last if (not defined ($x1)); # calc distance between right text border and right canvas corner my $distance = $c_w - $border - $x2; #print "adapt_FontSize: $i $font_size distance: $distance canvas:$c_w x2:$x2\n"; # Flux Capacitor ;-) if ($distance > 100) { # a lot of space -> increase font size $font_size = round($font_size*1.2); } elsif ($distance > 50) { # some space left $font_size += 1; } elsif ($distance < -100) { # text is much too wide $font_size = round($font_size*0.8); } elsif ($distance < 0) { # text is a litte too wide $font_size -= 1; } else { # size if fine, leave loop last; } # minimum and maximum font sizes if ($font_size < $font_size_min) { $font_size = $font_size_min; #print " reached minimum font size\n"; $quit = 1; # leave loop after font change } if ($font_size > $conf{font_size_big}{value}) { $font_size = $conf{font_size_big}{value}; #print " reached maximum font size\n"; $quit = 1; # leave loop after font change } # make and apply new font $font_big = $top->Font(-family => $config{'FontFamily'}, -size => $font_size); $c->itemconfigure($ids[0], -font => $font_big); # update also shadow text my @ids2 = $c->find('withtag', $tag2); $c->itemconfigure($ids2[0], -font => $font_big) if (@ids2); $c->update; last if ($quit); } return 1; } ############################################################## # returns string with very short picture meta info # only EXIF and IPTC # todo: maybe add some selected XMP values later ############################################################## sub get_meta_micro { my $dpic = shift; my $meta = getMetaData($dpic, "COM|APP13|APP1|SOF", 'FASTREADONLY'); my $micro = formatString(date_iso_to_relative(getMicroEXIF($dpic, $meta)), 80, -1); $micro .= "\n" if ($micro ne ''); $micro .= formatString(getIPTC($dpic, MICRO, $meta), 80, -1); return $micro; } ############################################################## # adds a GPS button on the given canvas depending # on the existence of EXIF GPS coordinates in the given picture ############################################################## sub GPS_button { my $c = shift; # canvas widget my $dpic = shift; # displayed picture # get GPS info from picture my ($lat, $lon, $lat_ref, $lon_ref) = gps_get($dpic); # add GPS button if both coordinates are available if (defined $lat and defined $lon) { $lat *= -1 if (defined $lat_ref and $lat_ref eq 'South'); $lon *= -1 if (defined $lon_ref and $lon_ref eq 'West'); # add GPS button in the upper right corner of the canvas $c->createImage(($c->width - 20), 10, -image => $mapivi_icons{'Location'}, -tag => ['GPS'], -anchor => 'ne'); $c->bind(+'GPS', '', sub { #web_browser_open('http://maps.google.com/maps?q='."$lat,$lon"); # due to the & in the open street web address we need additional quotes " web_browser_open('http://www.openstreetmap.org/"?mlat='.$lat.'&mlon='.$lon.'"'); }); $c->bind(+'GPS', '', sub { $c->configure(-cursor => 'hand2'); # workaround because balloon doesn't work log_it('click here to display GPS position ('.$lat.', '.$lon.') in web browser'); }); $c->bind(+'GPS', '', sub { $c->configure(-cursor => 'arrow'); # workaround because balloon doesn't work log_it(''); }); # 2010-10 doesn't work, reason unclear: #$balloon->attach($c->Subwidget('canvas'), -balloonposition => 'mouse', -msg => {'GPS' => 'click here to display GPS position in web browser'} ); } } ############################################################## ############################################################## sub web_browser_open { my $url = shift; my $command = "$conf{web_browser}{value} $url"; # instead of the & for UNIX windows needs a "start" in front of the application to run in the background if ($EvilOS) { $command = "start $url"; } else { $command .= " 2>&1 1>/dev/null &"; } log_it("Open $url in browser ..."); $top->Busy; (system "$command") == 0 or warn "$command failed: $!"; log_it("Ready! ($url opened)"); $top->Unbusy; } ############################################################## # showZoomInfo - calculate the zoom factor of the displayed # pic by messuring the size of the file # and the size on the canvas ############################################################## sub showZoomInfo { my $dpic = shift; my $id = shift; if (-f $dpic) { my ($width, $height) = getSize($dpic); my ($x1, $y1, $x2, $y2) = $c->bbox($id); if ((defined $x2) and (defined $x1) and ($x2 - $x1 != 0)) { my $z = $width/($x2 - $x1); if ($z > 0) { # avoid divison by zero $zoomFactorStr = int(1/$z * 100)."%"; if ($verbose) { my $wz = $photos{$dpic}->width; print "showZoomInfo: id=$id z = $z zoomFactorStr = $zoomFactorStr x1:$x1 x2:$x2 w:$width wz:$wz\n"; } return; } } } $zoomFactorStr = "?%"; } ############################################################## # handleNonJPEG ############################################################## sub handleNonJPEG { my $dir = shift; my @pics = @_; my $changed = 0; # counter return 0 if ((defined $nonJPEGdirNoAskAgain{"$dir"}) and ($nonJPEGdirNoAskAgain{"$dir"} == 1)); # open window my $myDiag = $top->Toplevel(); $myDiag->title(lang('Non-JPEG pictures')); $myDiag->Label(-text => "There are ".scalar @pics." non-JPEG pictures in folder ".basename($dir).".\nShould I convert these pictures to JPEG format?\n(After convertion these pictures will be visible in Mapivi.)")->pack(-fill => 'x', -padx => 3, -pady => 3); my $qS = labeledScale($myDiag, 'top', 40, "Quality of JPEG picture when converting", \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); my $removeOrig = 0; $myDiag->Checkbutton(-variable => \$removeOrig, -text => "Remove the original pictures after conversion")->pack(-anchor=>'w'); my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 0, -pady => 0); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $myDiag->withdraw(); $myDiag->destroy(); $changed = convertToJPEG($dir, $removeOrig, @pics); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); $ButF->Button(-text => lang("Show picture list"), -command => sub { my $info = "Non-JPEG pictures in $dir:\n\n"; foreach (sort @pics) { my $size = getFileSize("$dir/$_", NO_FORMAT); $info .= sprintf "%-45s %12s Bytes\n", $_, $size; } showText("Non-JPEG pictures", $info, WAIT); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); my $xBut = ButF->Button(-text => lang('Cancel'), -command => sub { # save dir in hash, so we don't bother the user again if he reopens the dir $nonJPEGdirNoAskAgain{"$dir"} = 1; $myDiag->withdraw(); $myDiag->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); bind_exit_keys_to_button($myDiag, $xBut); $myDiag->Popup(-popover => 'cursor'); repositionWindow($myDiag); if ($EvilOS) { # sometimes to dialog disappears when clicked on, so we need a short grab $myDiag->grab; $myDiag->after(50, sub{$myDiag->grabRelease}); } $myDiag->waitWindow; my $reread = ($changed > 0) ? 1 : 0; return $reread; } ############################################################## # convertToJPEG - convert the piclist to JPEG format ############################################################## sub convertToJPEG { my $dir = shift; my $del = shift; # delete orig after conversion (bool) my @pics = @_; my $converted = 0; foreach (@pics) { my $dpic = "$dir/$_"; my $tpic = $dpic; $tpic =~ s/($nonJPEGsuffixes)$/jpg/i; print "convertToJPEG: $_ -> $tpic\n" if $verbose; if (-f $tpic) { $top->messageBox(-icon => 'warning', -message => "$tpic exists - skipping!", -title => 'Warning', -type => 'OK'); next; } log_it("converting $_ to JPEG $tpic ..."); my $command = "convert"; $command .= " -quality ".$config{PicQuality}." \"$dpic\" \"$tpic\""; $top->Busy; #(system "$command") == 0 or warn "$command failed: $!"; execute($command); $top->Unbusy; $converted++ if ((-f $tpic) and (!-z $tpic)); if (($del) and ((-f $tpic) and (!-z $tpic))) { removeFile($dpic); } } return $converted; } ############################################################## # showNonJPEGS - show all non JPEG files of the actual folder # todo: rename to show_hidden_files and show the diff between # files in folder and actually displayed pictures ############################################################## sub showNonJPEGS { my @files = getFiles($actdir); # put just the files not matching jpg, jpeg, JPG or JPEG in the file list my @nonjpeg = sort(grep {!m/.*\.jp(g|eg)$/i} @files); #my $info = "There are ".scalar @nonjpeg." non-JPEGs in $actdir:\n\n"; #foreach (sort @nonjpeg) { # my $size = getFileSize("$actdir/$_", NO_FORMAT); # $info .= sprintf "%-45s %12s Bytes\n", $_, $size; #} #showText("Non-JPEGs", $info, WAIT); if (@nonjpeg) { # open window my $myDiag = $top->Toplevel(); $myDiag->title(lang('Hidden files')); $myDiag->iconimage($mapiviicon) if $mapiviicon; $myDiag->Label(-anchor => 'w', -justify => 'left', -text => lang('List of hidden files in folder ').$actdir)->pack(-fill => 'x', -padx => 3, -pady => 3); my $listBoxY = @nonjpeg; $listBoxY = 30 if ($listBoxY > 30); # maximum 30 entries my $listBox = $myDiag->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 80, -height => $listBoxY, )->pack(-expand => 1, -fill =>'both', -padx => 3, -pady => 3); $listBox->insert('end', @nonjpeg); #$listBox->bind('', sub { # @$sellist = $listBox->curselection(); # } ); my $ubutF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $ubutF->Button(-text => lang('Select all'), -command => sub { $listBox->selectionSet(0, 'end'); })->pack(-side => 'left', -padx => 3, -pady => 3); $ubutF->Button(-text => lang('Select videos'), -command => sub { $listBox->selectionClear(0, 'end'); my @list = $listBox->get(0, 'end'); my $index = 0; foreach my $file (@list) { print "check file: $file\n"; if (is_a_video($file)) { $listBox->selectionSet($index); } $index++; } })->pack(-side => 'left', -padx => 3, -pady => 3); $ubutF->Button(-text => lang('Select none'), -command => sub { $listBox->selectionClear(0, 'end'); })->pack(-side => 'left', -padx => 3, -pady => 3); my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $playB = $ButF->Button(-text => lang('Play video'), -command => sub { my @sellist = $listBox->curselection(); return unless checkSelection($myDiag, 1, 0, \@sellist, lang("video(s)")); my $command = $conf{video_player}{value}.' '; foreach my $file (@sellist) { $command .= '"'.$actdir.'/'.$nonjpeg[$file].'" '; } print "video command: - $command -\n"; execute($command); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($playB, -msg => lang("Play selected videos with ").$conf{video_player}{value}.lang("\nTool can be changed in Options->Tools.")); my $editB = $ButF->Button(-text => lang('Edit picture'), -command => sub { my @sellist = $listBox->curselection(); return unless checkSelection($myDiag, 1, 0, \@sellist, lang("picture(s)")); my @piclist; foreach my $file (@sellist) { push @piclist, $actdir.'/'.$nonjpeg[$file]; } edit_pic($myDiag, @piclist); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($editB, -msg => lang("Edit selected pictures with ").$conf{external_pic_editor}{value}.lang("\nTool can be changed in Options->Tools.")); $myDiag->bind('', sub { $editB->Invoke; } ); $ButF->Button(-text => lang('Delete'), -command => sub { my @sellist = $listBox->curselection(); my $rc = myButtonDialog("Really delete?", "Press Ok to delete these ".scalar @sellist." files in $actdir.\nThere is no undelete!", undef, 'OK', 'Cancel'); if ($rc eq 'OK') { foreach my $file (@sellist) { log_it("removing $nonjpeg[$file]"); removeFile($actdir.'/'.$nonjpeg[$file]); } # reread files and update lists @files = getFiles($actdir); @nonjpeg = sort(grep {!m/.*\.jp(g|eg)$/i} @files); $listBox->delete(0, 'end'); # clear all $listBox->insert('end', @nonjpeg); # insert new list } })->pack(-side => 'left', -expand => 0, -padx => 3, -pady => 3); my $XB = $ButF->Button(-text => lang('Close'), -command => sub { $myDiag->destroy(); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $XB->bind('', sub { $XB->Invoke; } ); $myDiag->Popup; if ($EvilOS) { # sometimes to dialog disappears when clicked on, so we need a short grab $myDiag->grab; $myDiag->after(50, sub{$myDiag->grabRelease}); } $XB->focus; $myDiag->waitWindow(); } } ############################################################## # convertNonJPEGS ############################################################## sub convertNonJPEGS { my @files = getFiles($actdir); # put just the files not matching jpg, jpeg, JPG or JPEG in the file list my @nonjpeg = grep {!m/.*\.jp(g|eg)$/i} @files; handleNonJPEG($actdir, @nonjpeg); updateThumbs(); } ############################################################## # getPics - returns the piclist of the given dir ############################################################## sub getPics { my $dir = shift; my $with_path = shift; # bool: WITH_PATH or JUST_FILE my $check_for_non_jpegs = shift; # bool: CHECK_JPEG or NO_CHECK_JPEG my @other; my @files = getFiles($dir); # are there non-JPEG pictures in this directory? if ($check_for_non_jpegs == CHECK_JPEG) { @other = grep {m/.*\.($nonJPEGsuffixes)$/i} @files; my @otherNoJPEG; foreach (@other) { $_ =~ m/(.*)\.($nonJPEGsuffixes)$/i; # separate the name from the suffix my $jpeg = "$1.jpg"; # built the corresponding jpeg file name if (!-f "$dir/$jpeg") { # if this doesn't exists push @otherNoJPEG, $_ # we push it to this list } } # are there some non-JPEGs without corresponding JPEGs? if (@otherNoJPEG > 0) { my $reread = handleNonJPEG($dir, @otherNoJPEG); # ask the user to convert them @files = getFiles($dir) if $reread; # reread file list if necessary } } my @pics; if ($config{supportOtherPictureFormats}) { # add "|(avi)" below to include AVI videos # see also @raw_suffix at top of this file! .NEF .CRW .CR2 .DNG .NRW # 2016-10: GIMP *.xcf works fine for e.g. thumbnail generation, but commands like # ImageMagick -rotate destroy the GIMP file (file size -> 0kB). Thus dangerous! @pics = grep {m/.*\.(jp(g|eg))|(crw)|(cr2)|(dng)|(nrw)|(gif)|(xpm)|(ppm)|(xbm)|(ti(f|ff))|(svg)|(png)|(bmp)|(nef)|(raw)$/i} @files; } else { # put just the files matching jpg, jpeg, JPG or JPEG in the file list @pics = grep {m/.*\.jp(g|eg)$/i} @files; } # if we are in the actual dir, display the number of non-JPEG files if ($dir eq $actdir) { $otherFiles = @files - @pics; $otherFiles = '' if ($otherFiles == 0); } $dir =~ s|/*$||; # remove trailing slashes if ($with_path == WITH_PATH) { foreach (@pics) { $_ = "$dir/$_"; } # add the path to each file } return @pics; } ############################################################## # sortPics - sorts a list of pictures according to $sortby ############################################################## sub sortPics { my $sortby = shift; my $sortreverse = shift; my $pics = shift; # reference on array to sort # todo: check if this causes problems in light table (no sorting possible?) if ($act_modus == COLLECTION) { #print "mode = COLLECTION: skipping sortPics()!\n"; return; } print "sortby = $sortby\n" if $verbose; my $str = langf("Sorting %d pictures by %s",scalar(@$pics),$sortby); $str .= ' '.lang('(reverse)') if $sortreverse; log_it("$str ..."); clearQuickSortHashes(); # remove old values $quickSortSwitch = 1; # activate quick sort/buffering if ($sortby eq 'name') { # sort alphabetical with no case @$pics = sort { uc(basename($a)) cmp uc(basename($b)) } @$pics; } elsif ($sortby eq 'date') { # sort by file date and name #@$pics = sort { getFileDate($b, NO_FORMAT) <=> getFileDate($a, NO_FORMAT) || #uc($a) cmp uc($b) } @$pics; @$pics = sort { $searchDB{$b}{MOD} <=> $searchDB{$a}{MOD} || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'exifdate') { #@$pics = sort { getEXIFDate($b) cmp getEXIFDate($a) || #uc($a) cmp uc($b) } @$pics; @$pics = sort { $searchDB{$b}{TIME} <=> $searchDB{$a}{TIME} || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'aperture') { @$pics = sort { getEXIFAperture($a, NUMERIC) <=> getEXIFAperture($b, NUMERIC) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'exposuretime') { @$pics = sort { getEXIFExposureTime($a, NUMERIC) <=> getEXIFExposureTime($b, NUMERIC) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'model') { @$pics = sort { uc(getEXIFModel($a)) cmp uc(getEXIFModel($b)) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'artist') { @$pics = sort { uc(getEXIFArtist($a)) cmp uc(getEXIFArtist($b)) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'size') { #@$pics = sort { getFileSize($a, NO_FORMAT) <=> getFileSize($b, NO_FORMAT) || #uc($b) cmp uc($a) } @$pics; @$pics = sort { $searchDB{$b}{SIZE} <=> $searchDB{$a}{SIZE} || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'pixel') { @$pics = sort { getPixels($a) <=> getPixels($b) || uc($b) cmp uc($a) } @$pics; } elsif ($sortby eq 'bitpix') { @$pics = sort { getBitPix($a) <=> getBitPix($b) || uc($b) cmp uc($a) } @$pics; } elsif ($sortby eq 'urgency') { @$pics = sort { getIPTCurgencyDB($a) <=> getIPTCurgencyDB($b) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'popularity') { @$pics = sort { $searchDB{$b}{POP} <=> $searchDB{$a}{POP} || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'flag') { @$pics = sort { getFlag($b) <=> getFlag($a) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'byline') { @$pics = sort { uc(getIPTCByLine($a)) cmp uc(getIPTCByLine($b)) || uc($a) cmp uc($b) } @$pics; } elsif ($sortby eq 'random') { fisher_yates_shuffle($pics); #@$pics = @$pics; } else { my $sort = 'undefined!'; $sort = $sortby if (defined $sortby); warn "sortPics: error: wrong sort: $sort - sorting by name"; @$pics = sort { uc($a) cmp uc($b); } @$pics; } clearQuickSortHashes(); # free mem $quickSortSwitch = 0; # stop quick search if ($sortreverse and ($sortby ne 'random')) { @$pics = reverse @$pics; } } ############################################################## # get FLAG info from database, retun 0 if no flag is set ############################################################## sub getFlag { my $dpic = shift; my $flag = 0; $flag = $searchDB{$dpic}{FLAG} if (defined $searchDB{$dpic}{FLAG}); return $flag; } ############################################################## # clearQuickSortHashes - reset all sort hashes ############################################################## sub clearQuickSortHashes { undef %quickSortHash; undef %quickSortHashSize; undef %quickSortHashPixel; undef %quickSortHashBitsPixel; } ############################################################## # getFileDate - parameter: file (with absolute path) # format ############################################################## sub getFileDate { my $dpic = shift; my $format = shift; # FORMAT = the date is returned in this date format (dd.mm.yyyy hh:mm:ss); NO_FORMAT return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); unless (-f $dpic) { warn "$dpic is no file!" if $verbose; return 0; } my $filedate = (lstat $dpic)[9]; # 9 is the modify time $filedate = getDateTimeDINString($filedate) if ((defined $format) and ($format == FORMAT)); $quickSortHash{$dpic} = $filedate if $quickSortSwitch; return $filedate; } ############################################################## # getEXIFDate - parameter: file (with absolute path) # image info (optional) # returns yyyy:mm:dd hh:mm:ss ############################################################## sub getEXIFDate { my $dpic = shift; my $er = shift; # optional, the EXIF hash ref if available return '' unless (is_a_JPEG($dpic)); return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); if (!defined($er)) { my $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') ; $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); if (!defined($er)) { warn "$dpic has no exif info" if $verbose; return ''; } } my $date = []; my $datestr = ''; if (defined $er->{'SUBIFD_DATA'}->{DateTimeOriginal}) { $datestr = ${$er->{'SUBIFD_DATA'}->{DateTimeOriginal}}[0]; } elsif (defined $er->{'SUBIFD_DATA'}->{DateTimeDigitized}) { $datestr = ${$er->{'SUBIFD_DATA'}->{DateTimeDigitized}}[0]; } elsif (defined $er->{'IFD0_DATA'}->{DateTime}) { $datestr = ${$er->{'IFD0_DATA'}->{DateTime}}[0]; } else { } $datestr =~ tr/\000/ /; # remove null termination (\000) chars $datestr =~ s/( )*$//g; # remove trailing space printf "getEXIFDate: -%s- (%s)\n", $datestr, basename($dpic) if $verbose; $quickSortHash{$dpic} = $datestr if $quickSortSwitch; return $datestr; } ############################################################## # getEXIFModel - parameter: file (with absolute path) # image info (optional) ############################################################## sub getEXIFModel { my $dpic = shift; my $er = shift; # optional, the EXIF hash ref if available return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); unless (defined($er)) { my $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') ; if (defined $meta) { $er = $meta->get_Exif_data('ALL', "TEXTUAL"); if (! defined $er) { warn "$dpic has no exif info"; return; } } else { warn "$dpic has no meta data" unless (defined $er); return; } } my $maker = ''; if (defined $er->{'IFD0_DATA'}->{'Make'}) { $maker = ${$er->{'IFD0_DATA'}->{'Make'}}[0]; $maker =~ s/( co\.,ltd)//i; # some companies are a little to verbose here, $maker =~ s/( co\., ltd\.)//i; $maker =~ s/( optical)//i; # so we try to short some words $maker =~ s/( electric)//i; $maker =~ s/(\.)//i; $maker =~ s/( corporation)//i; $maker =~ s/(eastman kodak company)/KODAK/i; $maker =~ s/(hewlett-packard company)/Hewlett-Packard/i; $maker =~ s/(konica)/Konica/i; $maker =~ s/(pentax)/Pentax/i; $maker =~ s/(nikon)/Nikon/i; } my $model = ''; if (defined $er->{'IFD0_DATA'}->{'Model'}) { $model = ${$er->{'IFD0_DATA'}->{'Model'}}[0]; $model =~ s/(digital camera )//i; # uh, really! :) - ok it could also be a scanner ... $model =~ s/(digital camera)//i; # sometimes with trailing space, sometimes not ... $model =~ s/(digital science )//i; # this is really to verbose ... $model =~ s/(digital science)//i; # sometimes with trailing space, sometimes not ... $model =~ s/( digital)//i; # $model =~ s/(kodak )//i; # hello! we already had this in the Make field ... $model =~ s/(canon )//i; $model =~ s/(konica )//i; $model =~ s/(pentax )//i; $model =~ s/(nikon )//i; $model =~ s/(sigma )//i; $model =~ s/(HP )//; } # store result for quick access $quickSortHash{$dpic} = "$maker $model" if $quickSortSwitch; return if ($maker eq '' and $model eq ''); # return undef return "$maker $model"; } ############################################################## # getEXIFArtist - parameter: file (with absolute path) # image info (optional) ############################################################## sub getEXIFArtist { my $dpic = shift; my $er = shift; # optional, the EXIF hash ref if available return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); unless (defined($er)) { my $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') ; $er = $meta->get_Exif_data('ALL', "TEXTUAL"); warn "$dpic has no exif info" unless (defined $er); } my $artist = ''; if (defined $er->{'IFD0_DATA'}->{Artist}) { $artist = ${$er->{'IFD0_DATA'}->{Artist}}[0]; } $quickSortHash{$dpic} = $artist if $quickSortSwitch; print "Artist: $artist pic:$dpic\n" if $verbose; return $artist; } ############################################################## # getEXIFAperture - parameter: file (with absolute path) # format (boolean) # image info (optional) ############################################################## sub getEXIFAperture { my $dpic = shift; my $format = shift; # NUMERIC or STRING my $er = shift; # optional, the EXIF hash ref if available return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); unless (defined($er)) { my $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') ; $er = $meta->get_Exif_data('ALL', "TEXTUAL") if (defined $meta); warn "$dpic has no exif info" unless (defined $er); } # FNumber: The actual F-number (F-stop) of lens when the image was taken. my $aperture = 0; if (defined $er->{'SUBIFD_DATA'}->{FNumber}) { $aperture = calc($er->{'SUBIFD_DATA'}->{FNumber}); } elsif (defined $er->{'SUBIFD_DATA'}->{ApertureValue}) { $aperture = calc($er->{'SUBIFD_DATA'}->{ApertureValue}); } else { } $aperture = sprintf("F%02.1f ", $aperture) if (($format == STRING) and ($aperture != 0)); $quickSortHash{$dpic} = $aperture if $quickSortSwitch; return $aperture; } ############################################################## # getEXIFExposureTime - parameter: file (with absolute path) # format (boolean) # image info (optional) ############################################################## sub getEXIFExposureTime { my $dpic = shift; my $format = shift; # STRING -> return a string ("1/20s "), NUMERIC -> return a value (0,05) my $er = shift; # optional, EXIF hash ref my $exti = ''; # exposure time as string my $extiN = 0; # exposure time as number return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); unless (defined($er)) { my $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') ; $er = $meta->get_Exif_data('ALL', "TEXTUAL"); if ($verbose) { warn "$dpic has no exif info" unless (defined $er); } } if (defined $er->{'SUBIFD_DATA'}->{'ExposureTime'}) { my $time = $er->{'SUBIFD_DATA'}->{'ExposureTime'}; warn "getEXIFExposureTime: not enough numbers!" if (@{$time} < 2); # this should not happen if ($$time[1] == 0) { warn "error ".basename($dpic)." wrong EXIF exposure time t0:$$time[0] t1:$$time[1]"; $format == STRING ? return '' : return 0; } if (($$time[0]/$$time[1]) >= 1) { # handle long time exposure (e.g. 800/100) $exti = sprintf "%.2f",($$time[0]/$$time[1]); $extiN = $exti; } else { # handle everything faster than one second if ($$time[0] != 1) { # some cameras use the format 10/600 if ($$time[0] == 0) { print "error ".basename($dpic)." div by zero exti:$exti t0: $$time[0] t1:$$time[1]\n" if $verbose; $exti = "1/$$time[1]?"; $extiN = 0; } else { $exti = "1/".int($$time[1]/$$time[0]); # instead of 1/60 so we have to normalize this $extiN = 1/int($$time[1]/$$time[0]); } } else { $exti = "1/".$$time[1]; $extiN = 1/$$time[1]; } } } elsif (defined $er->{'SUBIFD_DATA'}->{'ShutterSpeedValue'}) { my $time = $er->{'SUBIFD_DATA'}->{'ShutterSpeedValue'}; $exti = ($$time[0]/$$time[1]); $exti = int(2**$exti); $extiN = 1/$exti; $exti = "1/".$exti; } else { $exti = ''; $extiN = 0; } my $rc = 0; if ($format == STRING) { if ($exti eq '') { $rc = ''; } else { $rc = $exti."s "; # add the time unit (s = second) } } else { #$format == NUMERIC $rc = $extiN; } $quickSortHash{$dpic} = $rc if $quickSortSwitch; return $rc; } ############################################################## # getFiles - returns the filelist of the given dir ############################################################## sub getFiles { my $dir = shift; print " getFiles: in $dir\n" if $verbose; my @fileDirList = readDir($dir); my @fileList; foreach (@fileDirList) { # put only files which are not empty into the filelist push @fileList, $_ if ((-f "$dir/$_") and (!-z "$dir/$_")); } return @fileList; } ############################################################## # getDirs - returns the sorted dir list of the given dir ############################################################## sub getDirs { my $dir = shift; my @fileDirList = readDir($dir); my @dirList; foreach (@fileDirList) { next if (($_ eq '.') or ($_ eq '..')); my $item = Encode::encode('iso-8859-1', "$dir/$_"); #my $d2 = Encode::encode('iso-8859-1', $d); #print "getDirs: encoded: $item"; #if (-d $item) { print " is a dir\n"; } #else { print " is not a dir\n"; } push @dirList, $item if (-d $item); } @dirList = sort { uc($a) cmp uc($b) } @dirList; return @dirList; } ############################################################## # getDirsRecursive - returns all subdirs of the given dir # $dir is also included in list # mapivi and gimp subdirs are skipped # dirs starting with "." are skipped ############################################################## sub getDirsRecursive { my $dir = shift; my @dirs; find(sub { if (-d and ($_ !~ m|^\.|) and ($_ ne $thumbdirname) and ($_ ne $exifdirname)) { push @dirs, $File::Find::name; } }, $dir); return @dirs; } ############################################################## # readDir - reads the contents of the given directory ############################################################## sub readDir { my $dir = shift; $dir = Encode::encode('iso-8859-1', $dir); if (! -d $dir) { warn "readDir: $dir is no dir!: $!" unless (($dir =~ m/.*$thumbdirname$/)); return 0; } my @fileDirList; # open the directory if (!opendir ACTDIR, $dir) { warn "Can't open folder $dir: $!"; return 0; } # show no files starting with a '.', but '..' @fileDirList = grep /^(\.\.)|^[^\.]+.*$/, readdir ACTDIR; closedir ACTDIR; return @fileDirList; } ############################################################## # quitMain ############################################################## sub quitMain { log_it(lang('Saving for exit').' ...'); $top->update(); my $ok = saveAllConfig(); return 0 if (not $ok); diff_database_statistic() if $conf{show_statistic}{value}; freeMem(); exit; } ############################################################## # freeMem ############################################################## sub freeMem { # clean up all photo objects log_it("free mem ..."); foreach ($top->imageNames) { if (defined $_) { print "cleaning up: $_\n" if $verbose; $_->delete; } else { warn "image $_ is not defined!"; } } log_it("exit ..."); } ############################################################## # saveAllConfig ############################################################## sub saveAllConfig { # check if the light table window is still open and ask to save it. if (Exists($ltw)) { my $ok = light_table_close(); return 0 if (not $ok); } log_it("saving configuration ..."); $config{Geometry} = $top->geometry; saveAdjusterPos(); $config{LastDir} = $actdir if (-d $actdir); $config{ActPic} = $actpic; # we don't want to start in full screen mode # so if we've been in fullscreen mode, we save the settings from before the fullscreen switch if ((defined $top->{my_fullscreen_flag}) and ($top->{my_fullscreen_flag} == 1)) { print "saveAllConfig called in full screen mode\n" if $verbose; $config{Geometry} = $top->{my_last_geometry}; } else { print "saveAllConfig called in normal screen mode\n" if $verbose; } log_it("saving options to $configFile ..."); # old config saveConfig($configFile, \%config); # new config my ($ok, $err) = configuration_store($conf_file, \%conf); if (not $ok) { log_it($err); warn $err; } if ($config{SaveDatabase}) { log_it("saving search database ..."); nstore(\%searchDB, $searchDBfile) or warn "could not store searchDB in file $user_data_path/SearchDataBase: $!"; } log_it("saving dir folder hotlist ..."); nstore(\%dirHotlist, "$user_data_path/hotlist") or warn "could not store $user_data_path/hotlist: $!"; my $datetime = getDateTimeShortString(time()); # save a copy of the old hash in the trash # todo: remove very old backups log_it("saving dir check list ..."); mycopy("$user_data_path/dirProperties", "$trashdir/dirProperties-$datetime", OVERWRITE) if (-f "$user_data_path/dirProperties"); nstore(\%dirProperties, "$user_data_path/dirProperties") or warn "could not store $user_data_path/dirProperties: $!"; nstore(\%ignore_keywords, "$user_data_path/keywords_ignore") or warn "could not store $user_data_path/keywords_ignore: $!"; nstore(\%hot_keywords, "$user_data_path/keywords_hot") or warn "could not store $user_data_path/keywords_hot: $!"; save_slideshows(); if (MatchEntryAvail) { log_it("saving entry values ..."); nstore(\%entryHistory, $file_Entry_values) or warn "could not store $file_Entry_values: $!"; } # save the mode of the trees (opened, closed branches) if ($nav_F->{key_frame}) { saveTreeMode($nav_F->{key_frame}->{tree}); if (defined $nav_F->{key_frame}->{tree}->{m_mode}) { nstore($nav_F->{key_frame}->{tree}->{m_mode}, "$user_data_path/keywordMode") or warn "could not store $user_data_path/keywordMode: $!"; } } log_it("saving categories ..."); saveArrayToFile("$user_data_path/categories", \@precats); log_it("saving keywords ..."); saveArrayToFile("$user_data_path/keywords", \@prekeys); log_it(lang('Ready!')); return 1; } ############################################################## # persist slideshows hash to a file ############################################################## sub save_slideshows { #nstore(\%slideshows, "$user_data_path/slideshows") or warn "could not store $user_data_path/slideshows: $!"; my $ok; my $result = eval { nstore( \%slideshows, $collectionsFile ) }; if( $@ ) { $ok = 0; warn "Serious error from Storable: $@"; } elsif( not defined $result ) { $ok = 0; warn "I/O error from Storable: $!"; } else { $ok = 1; } return $ok; } ############################################################## # getComment - returns a string containing all Comments # (if available) of the given pic (up to 64K per # block, nr of blocks is not limited, so this can # get pretty huge!) ############################################################## sub getComment { my $dpic = shift; my $format = shift; # LONG or SHORT my $meta = shift; # optional, the Image::MetaData::JPEG object of $dpic if available return '' unless is_a_JPEG($dpic); # todo support GIF and PNG comments my @comments = getComments($dpic, $meta); return '' if (@comments <= 0); my $comment = ''; # put the comments togehter, adding a newline after each comment foreach (@comments) { $comment .= "$_\n"; } $comment =~ s/\r*//g; # remove \r (carriage return) $comment =~ s/\n+$//; # cut off last newline(s) $comment = formatString($comment, $config{LineLength}, $config{LineLimit}) if ($format == SHORT); print "getComment: $comment $dpic\n" if $verbose; return $comment; } ############################################################## # getComments - returns an array containing all Comments # (if available) of the given pic (up to 64K per # block, nr of blocks is not limited, so this can # get pretty huge!) ############################################################## sub getComments { my $dpic = shift; my $meta = shift; # optional, the Image::MetaData::JPEG object of $dpic if available $meta = getMetaData($dpic, "COM", 'FASTREADONLY') unless (defined($meta)); my @coms = (); if ($meta) { @coms = $meta->get_comments(); #print "getComments: $dpic:\n"; foreach (@coms) { print " com: $_\n"; } print "\n"; #foreach (@coms) { # if (Encode::is_utf8($_)) { # $_ = decode("utf8", $_); # #print "getComments: decoded UTF8: $_\n"; # } #} } else { warn "*** getComments: no meta for $dpic available!" if ($verbose); } #foreach (@coms) { print "getComments: $_\n"; } return @coms; } ############################################################## # getShortEXIF - returns a string containing some of the # EXIF-Data (if available) of the given pic # if wrap is true the string is broken in # several lines (for thumbnail view) ############################################################## sub getShortEXIF { my $dpic = shift; my $wrap = shift; # WRAP or NO_WRAP my $meta = shift; # optional my $exif = ''; return $exif unless is_a_JPEG($dpic); $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') unless (defined($meta)); # add a symbol ([s]) to the exif column for each picture with saved EXIF data $exif .= '[s] ' if (-f dirname($dpic)."/$exifdirname/".basename($dpic)) ; return unless (defined($meta)); my $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); # er = exif hash ref todo: use IMAGE_DATA instead of ALL? return $exif unless (defined $er); # Some cameras store settings in Maker Notes, so it is important to know the maker of the camera. my $make = ''; $make = ${$er->{IFD0_DATA}->{Make}}[0] if (defined $er->{IFD0_DATA}->{Make}); # check for thumbnail add a [t] if there is one $exif .= '[t] ' if (defined $er->{ROOT_DATA}->{ThumbnailData}); my $datestr = ''; $datestr = getEXIFDate($dpic, $er); if ($datestr ne '') { if (my ($y, $M, $d, $h, $m, $s) = $datestr =~ m/(\d\d\d\d):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)/) { #$exif .= "$d.$M.$y $h:$m:$s "; # german date format #$exif .= "$M/$d/$y $h:$m:$s "; # american date format $exif .= "$y-$M-$d $h:$m:$s "; # ISO 8601 date format $exif .= "\n" if ($wrap and $exif ne ''); } else { warn "picture has an unusual EXIF date: \"$datestr\" ($dpic)\n" if $config{MetadataWarn}; } } if (defined $er->{SUBIFD_DATA}->{FocalLength}) { my $flength = int(calc($er->{SUBIFD_DATA}->{FocalLength})); $exif .= $flength."mm "; } if (defined $er->{SUBIFD_DATA}->{FocalLengthIn35mmFilm}) { $exif .= "(".join('', @{$er->{SUBIFD_DATA}->{FocalLengthIn35mmFilm}})."mm) "; } my $aperture = getEXIFAperture($dpic, STRING, $er); $exif .= $aperture if ($aperture ne '0'); $exif .= getEXIFExposureTime($dpic, STRING, $er); if (defined $er->{SUBIFD_DATA}->{ExposureBiasValue}) { my $bias = calc($er->{SUBIFD_DATA}->{ExposureBiasValue}); if (($bias eq '-') and $config{MetadataWarn}) { warn "unusal EXIF ExposureBiasValue (".$er->{SUBIFD_DATA}->{ExposureBiasValue}.") in picture $dpic\n"; } $exif .= sprintf("+%1.1f ", $bias) if (($bias ne '-') and ($bias > 0)); $exif .= sprintf( "%1.1f ", $bias) if (($bias ne '-') and ($bias < 0)); } my $iso = get_ISO_value($dpic, $er, $meta, $make); $exif .= "ISO$iso " if ($iso ne ''); $exif .= "\n" if ($wrap and $exif ne ''); my $exposureStr = get_EXIF_exposure($er, $meta, $make); $exif .= $exposureStr.' ' if ($exposureStr ne ''); if (defined $er->{SUBIFD_DATA}->{Flash}) { if (${$er->{SUBIFD_DATA}->{Flash}}[0] & 1) { $exif .= 'flash '; } } if ($conf{exif_plus}{value}) { # show contrast sharpness saturation metering white balance lens my $exifplus = get_EXIF_plus($dpic); $exifplus = formatString($exifplus, 80, -1) if ($wrap); $exif .= $exifplus if ($exifplus ne ''); } my $exmod = getEXIFModel($dpic, $er); $exif .= "\n$exmod" if (defined $exmod); my ($lat, $lon, $latRef, $lonRef) = gps_get($dpic); if (defined $lat and defined $lon) { $latRef = '?' if (!defined $latRef); $lonRef = '?' if (!defined $lonRef); # %.1s truncates string, while %1s does not $exif .= sprintf(" GPS: %.4f%.1s %.4f%.1s",$lat, $latRef, $lon, $lonRef); } $exif =~ tr/\000/ /; # remove null termination (\000) chars $exif =~ s/( )+/ /g; # replace more than one space with one space my $tmp = $exif; $tmp =~ s/\n//g; # remove newlines $tmp =~ s/\s//g; # remove whitespaces # if there are just newlines and spaces we return an empty string $exif = '' if ($tmp eq ''); return $exif; } ############################################################## # geMicroEXIF - returns a string containing only the most # relevant EXIF-Data ############################################################## sub getMicroEXIF { my $dpic = shift; my $meta = shift; # optional my $exif = ''; return $exif unless is_a_JPEG($dpic); $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY') unless (defined($meta)); return unless (defined($meta)); my $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); # er = exif hash ref todo: use IMAGE_DATA instead of ALL? return $exif unless (defined $er); # Some cameras store settings in Maker Notes, so it is important to know the maker of the camera. my $make = ''; $make = ${$er->{IFD0_DATA}->{Make}}[0] if (defined $er->{IFD0_DATA}->{Make}); my $datestr = ''; $datestr = getEXIFDate($dpic, $er); if ($datestr ne '') { if (my ($y, $M, $d, $h, $m, $s) = $datestr =~ m/(\d\d\d\d):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)/) { $exif .= "$y-$M-$d $h:$m:$s "; # ISO 8601 date format } else { warn "picture has an unusual EXIF date: \"$datestr\" ($dpic)\n" if $config{MetadataWarn}; } } if (defined $er->{SUBIFD_DATA}->{FocalLength}) { my $flength = int(calc($er->{SUBIFD_DATA}->{FocalLength})); $exif .= $flength."mm "; } my $aperture = getEXIFAperture($dpic, STRING, $er); $exif .= $aperture if ($aperture ne '0'); $exif .= getEXIFExposureTime($dpic, STRING, $er); if (defined $er->{SUBIFD_DATA}->{ExposureBiasValue}) { my $bias = calc($er->{SUBIFD_DATA}->{ExposureBiasValue}); if (($bias eq '-') and $config{MetadataWarn}) { warn "unusal EXIF ExposureBiasValue (".$er->{SUBIFD_DATA}->{ExposureBiasValue}.") in picture $dpic\n"; } $exif .= sprintf("+%1.1f ", $bias) if (($bias ne '-') and ($bias > 0)); $exif .= sprintf( "%1.1f ", $bias) if (($bias ne '-') and ($bias < 0)); } my $iso = get_ISO_value($dpic, $er, $meta, $make); $exif .= "ISO$iso " if ($iso ne ''); my $exposureStr = get_EXIF_exposure($er, $meta, $make); $exif .= $exposureStr.' ' if ($exposureStr ne ''); my $exmod = getEXIFModel($dpic, $er); $exif .= "$exmod" if (defined $exmod); $exif =~ tr/\000/ /; # remove null termination (\000) chars $exif =~ s/( )+/ /g; # replace more than one space with one space my $tmp = $exif; $tmp =~ s/\n//g; # remove newlines $tmp =~ s/\s//g; # remove whitespaces # if there are just newlines and spaces we return an empty string $exif = '' if ($tmp eq ''); return $exif; } ############################################################## ############################################################## sub get_EXIF_exposure { my ($er, $meta, $make) = @_; my $exposureStr = ''; # Canon places specific exposure program in maker note. if ($make =~ m/Canon/) { my $seg = $meta->retrieve_app1_Exif_segment(); if ($seg) { my $makernote = $seg->get_Exif_data('MAKERNOTE_DATA', 'TEXTUAL'); if (exists $makernote->{CameraSettings}) { my %CanonExp = ( 0 => 'Easy shooting', 1 => 'Program', 2 => 'Shutter priority', 3 => 'Aperture priority', 4 => 'Manual', 5 => 'Auto-DEP', 6 => 'DEP' ); my %CanonEasy = ( 0 => 'Auto', 1 => 'Manual', 2 => 'Landscape', 3 => 'Fast shutter', 4 => 'Slow shutter', 5 => 'Night', 6 => 'B/W', 7 => 'Sepia', 8 => 'Portrait', 9 => 'Sports', 10 => 'Macro/Close-Up', 11 => 'Pan focus' ); my $exp = $makernote->{CameraSettings}[20]; if (defined $exp) { $exposureStr = $CanonExp{$exp} if (defined $CanonExp{$exp}); if ($exp == 0) { # Find more specific "Easy shooting" mode $exp = $makernote->{CameraSettings}[11]; $exposureStr = '\$' . $exp; $exposureStr = $CanonEasy{$exp} if (defined $CanonEasy{$exp}); } } } } } # if its no Canon we look in the ExposureProgram tag if (($exposureStr eq '') and (defined $er->{SUBIFD_DATA}->{ExposureProgram})) { my @ExposureProgram = ('Not defined', 'Manual', 'Program', 'Aperture priority', 'Shutter priority', 'Creative program', 'Action program', 'Portrait mode', 'Landscape mode'); my $prog = ${$er->{SUBIFD_DATA}->{ExposureProgram}}[0]; $exposureStr = $ExposureProgram[$prog] if ($prog > 0); } # if there is also nothing defined, we take the ExposureMode tag if ($exposureStr eq '') { # some camera uses this tag instead of ExposureProgram if (defined $er->{SUBIFD_DATA}->{ExposureMode}) { my @ExposureMode = ('Auto exposure', 'Manual exposure', 'Auto bracket'); my $mode = ${$er->{SUBIFD_DATA}->{ExposureMode}}[0]; $exposureStr = $ExposureMode[$mode] if ($mode >= 0); } } return $exposureStr; } ############################################################## # returns additional EXIF information using exiftool: contrast, # sharpness, saturation, metering, white balance, Metering, # focus dist, DOF, ... ############################################################## sub get_EXIF_plus { my ($dpic) = shift; my $exifplus = ''; #if ($verbose and (defined $er->{SUBIFD_DATA}->{OwnerName})) { print "*** Owner $dpic: ".join('', @{$er->{'SUBIFD_DATA'}->{'OwnerName'}})."\n"; } #if ($verbose and (defined $er->{SUBIFD_DATA}->{UserComment})) { print "*** EXIF comment $dpic: -".join('', @{$er->{'SUBIFD_DATA'}->{'UserComment'}})."-\n"; } # what to get and in which order my @items = qw(MeteringMode WhiteBalance Contrast Sharpness Saturation ColorSpace FocusDistance DOF LensID ShutterCount); # how to label my %itemlabel = ( # labels to display 'MeteringMode' => 'Metering:', 'WhiteBalance' => 'WB:', 'Sharpness' => 'Sharp:', 'Contrast' => 'Contrast:', 'Saturation' => 'Sat:', 'FocusDistance' => 'Dist:', 'DOF' => 'DOF:', 'ShutterCount' => '#', ); my $exifTool = new Image::ExifTool; my $info = $exifTool->ImageInfo($dpic, '*:*'); foreach my $item (@items) { if (defined $$info{$item}) { if (defined $itemlabel{$item}) { $exifplus .= $itemlabel{$item}; } $exifplus .= $$info{$item}." "; } } return $exifplus; } ############################################################## ############################################################## sub get_ISO_value { my ($dpic, $er, $meta, $make) = @_; my $iso = ''; if (defined $er->{SUBIFD_DATA}->{ISOSpeedRatings}) { $iso = ${$er->{SUBIFD_DATA}->{ISOSpeedRatings}}[0]; } else { # Same as ISOSpeedRatings. Only Kodak's camera uses this tag instead of ISOSpeedRating if (defined $er->{SUBIFD_DATA}->{ExposureIndex}) { $iso = calc($er->{SUBIFD_DATA}->{ExposureIndex}); } else { # Nikon and Canon hide the ISO settings in the Makernotes my $seg = $meta->retrieve_app1_Exif_segment(); if ($seg) { my $makernote = $seg->get_Exif_data('MAKERNOTE_DATA', 'TEXTUAL'); if ($make =~ m/Canon/) { if (exists $makernote->{CameraSettings}) { my $iso_int = $makernote->{CameraSettings}[16]; if ($iso_int == 15) { $iso = '-Auto'; } elsif (16 <= $iso_int and $iso_int <= 19) { $iso = (50 * (1 << ($iso_int - 16))); } } } elsif (exists $makernote->{ISOSetting}) { $iso = ${$makernote->{ISOSetting}}[1]; } } } } # if nothing helps we use exiftool if ((!defined $iso) or ($iso eq '')) { my $exifTool = new Image::ExifTool; my $info = $exifTool->ImageInfo($dpic, '*:ISO'); # 'Nikon:ISO' $iso = $$info{ISO} if defined $$info{ISO}; } # this part will repair Nikon D70 files (ISO info is just available in the Makernotes) # by setting the ISO value in the right EXIF tag (ISOSpeedRatings) # but Mapivi won't modify the users pictures without asking that's why this is commented out #if (($iso_value > 1) and ($iso_value < 30000)) { #print "adding ISO value $iso_value to $dpic\n"; ## the other $meta is read only #my $meta2= new Image::MetaData::JPEG($dpic, 'APP1$'); #my $hash = $meta2->set_Exif_data({'ISOSpeedRatings' => $iso_value}, 'IMAGE_DATA', 'ADD'); #if (%$hash) { # print "ISO record rejected\n"; #} #else { # unless ($meta2->save()) { # print "Save ISO failed for $dpic\n"; # } #} return $iso; } ############################################################## # getEXIFMeta ############################################################## sub getEXIFMeta { my $dpic = shift; my $exif = ''; return $exif unless is_a_JPEG($dpic); my $pic = basename($dpic); my $meta = getMetaData($dpic, 'APP1$', 'FASTREADONLY'); my $hash_ref = $meta->get_Exif_data('ALL', "TEXTUAL"); #if (defined $hash_ref->{APP1}->{ThumbnailData}) { #printf "[t] %s\n", basename($dpic); #} #return unless ($verbose); my $num = $meta->retrieve_app1_Exif_segment(-1); print "getEXIFMeta: $pic has $num EXIF APP1 segments\n" if $verbose; my $ref = $meta->retrieve_app1_Exif_segment(); unless (defined $ref) { print "getEXIFMeta: $pic has no EXIF APP1 segments\n" if $verbose; return $exif; } while (my ($d, $h) = each %$hash_ref) { while (my ($t, $a) = each %$h) { my $a2 = ''; foreach (@$a) { $_ =~ tr/ -~//cd; # remove all non-printable chars $a2 .= sprintf "%-5s", $_; } $a2 = cutString($a2, 30 , '..'); $exif .= sprintf "%-25s\t%-25s\t-> %-s\n", $d, $t, $a2; } } return $exif; } ############################################################## # calc - make a number from an array ref containing two numbers # input e.g. [28, 10] -> output: 2.8 ############################################################## sub calc { my $value = shift; if (@{$value} != 2) { warn "calc: no separator -> no values! or division by zero\n" if $config{MetadataWarn}; return join("/", $value); } if ($$value[1] == 0) { if ($$value[0] == 0) { return 0; } else { warn "calc: division by zero" if $config{MetadataWarn}; return 0; } } return ($$value[0] / $$value[1]); #return the calculated number } ############################################################## # displayEXIFData - displays all EXIF-Data in a window ############################################################## sub displayEXIFData { my $lb = shift; my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); return unless askSelection(\@sellist, 10, "EXIF info"); my $selected = @sellist; log_it("displaying EXIF data of $selected pictures"); my $pw = progressWinInit($lb, "Display EXIF data"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Display EXIF data ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $title = "EXIF info of ".basename($dpic); my $exif = "EXIF info of $dpic\n"; my $exifs = date_iso_to_relative(getShortEXIF($dpic, NO_WRAP)); $exif .= "compact EXIF info:\n$exifs\n\n" if ($exifs ne ''); $exif .= "\ndetailed EXIF info (from Image::ExifTool):\n"; $exif .= exif_get($dpic); # exif extracted by ExifTool $exif .= "\ndetailed EXIF info (from Image::Info):\n"; my $ii = getImageInfo($dpic); foreach (sort { uc($a) cmp uc($b); } keys %{$ii}) { next if (($_ eq "MakerNote") or ($_ eq "ColorComponentsDecoded") or ($_ =~ m/^App.*/)); if (ref($ii->{$_}) eq "ARRAY") { # handle array entries $exif .= sprintf "%-25s ",$_; foreach (@{$ii->{$_}}) { if (ref($_) eq "ARRAY") { # handle array in array entries foreach (@{$_}) { $exif .= "$_, "; } } elsif (ref($_) eq "HASH") { # handle hash in array entries my %hash = %{$_}; foreach (sort keys %hash) { $exif .= "$_=".$hash{$_}.", "; } } else { # handle normal strings in array entries $exif .= "$_, "; } } $exif =~ s/, $//; # remove trailing comma and space } else { # handle normal string entries $exif .= sprintf "%-25s %s",$_, $ii->{$_}; } $exif .= "\n"; } if ($config{EXIFshowApp}) { foreach (sort { uc($a) cmp uc($b); } keys %{$ii}) { next unless (($_ eq "MakerNote") or ($_ eq "ColorComponentsDecoded") or ($_ =~ m/^App.*/)); if (ref($ii->{$_}) eq "ARRAY") { # handle array entries $exif .= sprintf "%-25s ",$_; foreach (@{$ii->{$_}}) { if (ref($_) eq "ARRAY") { # handle array in array entries foreach (@{$_}) { $exif .= "$_, "; } } elsif (ref($_) eq "HASH") { # handle hash in array entries my %hash = %{$_}; foreach (sort keys %hash) { $exif .= "$_=".$hash{$_}.", "; } } else { # handle normal strings in array entries $exif .= "$_, "; } } $exif =~ s/, $//; # remove trailing comma and space } else { # handle normal string entries my $part = sprintf "%-25s %s",$_, $ii->{$_}; $part =~ s/\n//g; $exif .= $part; } $exif .= "\n"; } } $exif .= "\ndetailed EXIF info (from Image::MetaData::JPEG):\n"; $exif .= getEXIFMeta($dpic); $exif =~ tr/\n -~//cd; # remove non-printable characters (but not \n) showText($title, $exif, NO_WAIT, getThumbFileName($dpic)); } progressWinEnd($pw); log_it("ready! ($i of $selected displayed)"); } ############################################################## # date_iso_to_relative # input: any strings (also multiline strings with newline) # output: same string, but contained iso date string (format: yyyy-mm-dd) # is replaced with relative date string (e.g. "today" or "yesterday") # if found and applicable ############################################################## sub date_iso_to_relative { my $string = shift; return $string if (not defined $string); # $1 = pre string # $2 = years # $3 = months # $4 = days # $5 = post string # regex modifier /s = treat newline like any char (.) if ($string =~m/(.*)(\d\d\d\d)-(\d\d)-(\d\d)(.*)/s) { my $relative = undef; #print "date_iso_to_relative: found ISO date $4.$3.$2 \n"; # todo: make a sub from the next 3 statements, it is used quite often in mapivi my (undef,undef,undef,$d,$m,$y) = getDateTime(time()); if ($y == $2 and $m == $3 and $d == $4) { $relative = 'today'; } else { # get yesterdays date my ($ny,$nm,$nd) = date_relative($y,$m,$d,-1); if ($ny == $2 and $nm == $3 and $nd == $4) { $relative = 'yesterday'; } } if (defined $relative) { $string = $1.$relative.$5; } } return $string; } ############################################################## # adds or subtracts $relative_days from given date and returns new date ############################################################## sub date_relative { my ($y,$m,$d,$relative_days) = @_; my $ctime = timelocal(12,12,12,$d,($m-1),$y); my $seconds = 24 * $relative_days * 60 * 60; my (undef,undef,undef,$nd,$nm,$ny) = getDateTime($ctime + $seconds); #print "date_relative $y-$m-$d -> $ny-$nm-$nd\n"; return ($ny,$nm,$nd); } ############################################################## # removeEXIFData - remove all EXIF data in all selected pictures ############################################################## sub removeEXIFData { my $mode = shift; # 'all' or 'thumb' if (!defined $mode) { warn "removeEXIFData: Missing a mode, should be \"thumb\" or \"all\"!"; return; } my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $text; if ($mode eq 'all') { $text = "Remove all EXIF infos (picture and camera data and embedded thumbnail picture) of $selected selected pictures."; } elsif ($mode eq 'thumb') { $text = "Remove the embedded EXIF thumbnails and other non-camera settings from the EXIF headers of $selected selected pictures."; } else { warn "removeEXIFData: Wrong mode ($mode), should be \"thumb\" or \"all\"!"; return; } my $rc = $top->messageBox(-icon => 'question', -message => "$text\nOk to continue?", -title => "Question", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); log_it("removing EXIF data of $selected pictures"); my $i = 0; my $errors = ''; my $pw = progressWinInit($top, "Remove EXIF data"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Remove EXIF data ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); next if (!removeEXIF($dpic, $mode, \$errors)); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i of $selected infos removed)"); showText("Errors while removing EXIF data", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # removeEXIF ############################################################## sub removeEXIF { my $dpic = shift; my $mode = shift; my $errors = shift; # reference my $meta = getMetaData($dpic, "APP1"); unless ($meta) { $$errors .= "No EXIF data in $dpic\n"; return 0; } if ($mode eq "all") { $meta->remove_app1_Exif_info(-1); } elsif ($mode eq "thumb") { my $nothumb = ''; my $hash = $meta->set_Exif_data(\$nothumb, 'THUMBNAIL', 'REPLACE'); $$errors .= "Thumbnail record rejected for $dpic\n" if (keys %$hash); } else { die; } unless ($meta->save()) { $$errors .= "Save failed $dpic\n"; return 0; } return 1; } ############################################################## # getEXIFThumb - extract the embedded EXIF thumbnail ############################################################## sub getEXIFThumb { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $rc = $top->messageBox(-icon => 'question', -message => "Extract embedded EXIF thumbnails of $selected selected pictures and write them to a (new created) subfolder \"EXIFThumbs/\" in the current folder.\nShould I continue?", -title => "Question", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); log_it("extracting embedded EXIF thumbnails of $selected pictures"); if (!-d "$actdir/EXIFThumbs") { if ( !mkdir "$actdir/EXIFThumbs", oct(755)) { warn "makedir: can not create $actdir/EXIFThumbs: $!"; return; } } my $i = 0; my $errors = ''; my $pw = progressWinInit($top, "Extracting EXIF thumbnails"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Extracting EXIF thumbnail ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); my $dthumb = "$actdir/EXIFThumbs/$pic"; next if (!getRealFile(\$dpic)); extractThumb($dpic, $dthumb, \$errors); } progressWinEnd($pw); log_it("ready! ($i of $selected thumbs extracted)"); showText("Errors while saving EXIF thumbnail", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # setEXIFDate - adjust the date and time field in the EXIF header ############################################################## sub setEXIFDate { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist); my $selected = @sellist; my $count = 0; if (!$config{setEXIFDateAskAgain}) { my $rc = checkDialog("Change EXIF date/time?", "Change the date and time info (EXIF: DateTimeOriginal) of $selected selected pictures. The previous information will be lost!\nShould I continue?", \$config{setEXIFDateAskAgain}, "don't ask again", '', 'OK', 'Cancel'); return if ($rc ne 'OK'); } my $datetime = $config{EXIFDateAbs}; my $rc = setEXIFDateDialog(\$datetime); return if ($rc ne 'OK'); if (($config{EXIFAbsRel} eq 'abs') and !($datetime =~ m/\d{4}:\d{2}:\d{2}-\d{2}:\d{2}:\d{2}/)) { $top->messageBox(-icon => 'warning', -message => "Sorry, but $datetime has a wrong format!\nShould be: yyyy:mm:dd-hh:mm:ss Aborting.", -title => 'Error', -type => 'OK'); return; } $config{EXIFDateAbs} = $datetime if ($config{EXIFAbsRel} eq 'abs'); log_it("changing the date and time of $selected pictures"); my $i = 0; my $errors = ''; my $pw = progressWinInit($top, "Changing EXIF date and time"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Changing EXIF date and time ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); if ($config{EXIFAbsRel} eq 'abs') { # nothing to do, we just use $datetime $datetime =~ s/-/ /; # replace just the "-" with a space between date and time } elsif ($config{EXIFAbsRel} eq 'rel') { my $exif = getEXIFDate($dpic); if (defined($exif) and ($exif =~ m/(\d\d\d\d):(\d\d):(\d\d)\s(\d\d):(\d\d):(\d\d)/)) { my $mon = $2; my $year = $1; $mon--; $year -= 1900; if ($mon >= 0 and $mon <= 11) { # calculate the time as value in seconds since the Epoch (Midnight, January 1, 1970) my $ctime = timelocal($6,$5,$4,$3,$mon,$year); my $hours = $config{EXIFyears} * 365 * 24 + $config{EXIFdays} * 24 + $config{EXIFhours}; my $seconds = $hours * 60 * 60 + $config{EXIFmin} * 60 + $config{EXIFsec}; if ($config{EXIFPlusMin} eq "+") { $ctime = $ctime + $seconds; } else { $ctime = $ctime - $seconds; } $datetime = getDateTimeEXIFString($ctime); } else { $errors .= "Wrong month in EXIF date in $dpic\n"; next; } } else { $errors .= "No EXIF date in $dpic\n"; next; } } else { warn "setEXIFDate: wrong value: ", $config{EXIFAbsRel}; return 0; # should not happen } print "set EXIF datetime: $datetime to $dpic\n" if $verbose; next if (not setEXIFDatePic($dpic,$datetime,\$errors)); $count++; # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i/$selected)"); showText("Errors while adjusting EXIF date", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # setEXIFDatePic - set the date/time in the EXIF header for a single picture ############################################################## sub setEXIFDatePic { my $dpic = shift; # file to process my $datetime = shift; # format: yyyy:mm:dd hh:mm:ss my $errors = shift; # string ref my $meta = getMetaData($dpic, 'APP1$'); if (not defined $meta) { $$errors .= "No meta info available: $dpic\n"; return 0; } #date time format: 2007:04:04 11:12:13 my $hash = $meta->set_Exif_data({'DateTime' => $datetime, 'DateTimeOriginal' => $datetime, 'DateTimeDigitized' => $datetime}, 'IMAGE_DATA', 'ADD'); if (keys %$hash) { $$errors .= "DateTime record rejeced: $dpic\n"; return 0; } unless ($meta->save()) { $$errors .= "Save failed $dpic\n"; return 0; } return 1; } ############################################################## # setEXIFDate - set the year in the EXIF header from file name ############################################################## sub setEXIFDate_from_file_name { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $count = 0; my $rc = $top->messageBox(-icon => 'warning', -message => "This function will extract the year from the file name and set the EXIF date to January, 1st of this year and the time to 12:00:00 in ".scalar @sellist." pictures.\nThe file name must start with the four year digits, else the picture is ignored.\nExample: The EXIF date of a picture named 2009_pic2.jpg will be set to 2009:01:01 12:00:00.\nOk to continue?", -title => "Warning", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); if (!$config{setEXIFDateAskAgain}) { my $rc = checkDialog("Change EXIF date/time?", "Change the date and time info (EXIF: DateTimeOriginal) of $selected selected pictures. The previous information will be lost!\nShould I continue?", \$config{setEXIFDateAskAgain}, "don't ask again", '', 'OK', 'Cancel'); return if ($rc ne 'OK'); } log_it("changing the date and time of $selected pictures"); my $i = 0; my $errors = ''; my $pw = progressWinInit($top, "Changing EXIF date and time"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Changing EXIF date and time ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); if ($pic =~ m|^(\d\d\d\d).*|) { my $datetime = "$1:01:01 12:00:00"; print "set EXIF datetime: $datetime to $dpic\n"; # if $verbose; next if (not setEXIFDatePic($dpic,$datetime,\$errors)); $count++; # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } else { $errors .= "$pic doesn't start with 4 digits. Skipping.\n"; } } progressWinEnd($pw); log_it("ready! ($i/$selected)"); showText("Errors while adjusting EXIF date", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # remap_abs_rel ############################################################## sub remap_abs_rel { my $tf = shift; my $af = shift; my $rf = shift; if ($config{EXIFAbsRel} eq 'abs') { $rf->packForget if ($rf->ismapped); $af->pack(-in => $tf, -fill => 'both', -padx => 3, -pady => 3) unless ($af->ismapped); } else { $af->packForget if ($af->ismapped); $rf->pack(-in => $tf, -fill => 'both', -padx => 3, -pady => 3) unless ($rf->ismapped); } } ############################################################## # setEXIFDateDialog - get the date/time info from the user # returns 'OK' or 'Cancel' ############################################################## sub setEXIFDateDialog { my $datetime = shift; # var ref date time string (absolute) my $rc = 'Cancel'; # open window my $dtw = $top->Toplevel(); $dtw->title('Set EXIF date and time'); $dtw->iconimage($mapiviicon) if $mapiviicon; $dtw->Label(-text => "You may set the date and time to an absolute or relative value")->pack(-anchor => 'w'); # frame for the absolute/relative radio buttons my $arf = $dtw->Frame(-bd => 1, -relief => 'raised')->pack(-fill => 'both', -padx => 3, -pady => 3); # frame for the time/date adjustment my $tf = $dtw->Frame(-bd => 1, -relief => 'raised')->pack(-fill => 'both', -padx => 3, -pady => 3); my $af = $tf->Frame(); my $rf = $tf->Frame(); $arf->Radiobutton(-text => "use absolute value", -variable => \$config{EXIFAbsRel}, -value => 'abs', -command => sub { remap_abs_rel($tf, $af, $rf); })->pack(-anchor => 'w', -side => 'left'); $arf->Radiobutton(-text => "use relative value", -variable => \$config{EXIFAbsRel}, -value => 'rel', -command => sub {remap_abs_rel($tf, $af, $rf); })->pack(-anchor => 'w', -side => 'left'); remap_abs_rel($tf, $af, $rf); ######### absolute $af->Label(-text => "Please enter the new date and time to store\nin the selected pictures\n(please use the format: yyyy:mm:dd-hh:mm:ss\nexample: 2009:11:21-11:07:59)", -justify => 'left')->pack(-anchor => 'w'); my $entry = $af->Entry(-textvariable => \$$datetime, -width => 40, )->pack(-fill => 'x', -padx => 3, -pady => 3); # todo that's not enough to switch when focusIn #$entry->bind('', sub { $config{EXIFAbsRel} = 'abs'; $af->update(); } ); $entry->selectionRange(0,'end'); # select all $entry->icursor('end'); $entry->xview('end'); ######### relative $rf->Radiobutton(-text => "+ (add time)", -variable => \$config{EXIFPlusMin}, -value => "+")->pack(-anchor => 'w'); $rf->Radiobutton(-text => "- (subtract time)", -variable => \$config{EXIFPlusMin}, -value => "-", -command => sub {$config{EXIFAbsRel} = 'rel'})->pack(-anchor => 'w'); labeledScale($rf, 'top', 8, 'years', \$config{EXIFyears}, 0, 100, 1); labeledScale($rf, 'top', 8, 'days', \$config{EXIFdays}, 0, 365, 1); labeledScale($rf, 'top', 8, 'hours', \$config{EXIFhours}, 0, 24, 1); labeledScale($rf, 'top', 8, 'minutes', \$config{EXIFmin}, 0, 59, 1); labeledScale($rf, 'top', 8, 'seconds', \$config{EXIFsec}, 0, 59, 1); my $OKB; $entry->bind('', sub { $OKB->Invoke; } ); $entry->focus; my $ButF = $dtw->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $rc = 'OK'; $dtw->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $XBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 'Cancel'; $dtw->destroy(); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($dtw, $XBut); $dtw->Popup; $dtw->waitWindow(); return $rc; } ############################################################## # showEXIFThumb - displays the embedded EXIF thumbnail ############################################################## sub showEXIFThumb { my $noThumbIn = ''; my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); return unless askSelection(\@sellist, 10, "EXIF thumbnail"); if (!-d $trashdir) { # we need the trash dir for the temp files $top->messageBox(-icon => 'warning', -message => "Trash folder $trashdir not found!\nPlease create this folder (shell: mkdir $trashdir) and retry.\n\nAborting.", -title => "No trash folder", -type => 'OK'); return; } my $pw = progressWinInit($top, "Show EXIF thumbnail"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Show EXIF thumbnail ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); my $exifthumb = "$trashdir/EXIFthumb-$pic"; if (-f $exifthumb) { $top->messageBox(-icon => 'warning', -message => "There is something wrong, $exifthumb already exists.\nPlease delete it first.\nSkipping!", -title => 'Warning', -type => 'OK'); next; } my $errors = ''; extractThumb($dpic, $exifthumb, \$errors); if (!-f $exifthumb) { $noThumbIn .= "$pic\n"; next; } showPicInOwnWin($exifthumb); # show the thumb # remove the thumb removeFile($exifthumb); } progressWinEnd($pw); showText("No EXIF thumbnail", "Sorry, there seems to be no embedded EXIF thumbnail in the following pictures:\n\n$noThumbIn" ,NO_WAIT) if ($noThumbIn ne ''); log_it("ready! ($i of ".scalar @sellist." thumbs)"); } { # encapsulate data structure with access methods my $copyEXIFDataSource; # local variable for copy/pasteEXIFdata source ############################################################## # copyEXIFData - copy the EXIF info from one picture to others ############################################################## sub copyEXIFData { my @sellist = $picLB->info('selection'); if (@sellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select exactly one picture (the picture from which the EXIF info should be taken) for this function!", -title => 'Error', -type => 'OK'); return; } $copyEXIFDataSource = $sellist[0]; # save source pic to global variable log_it(lang("Copy meta information from ").basename($copyEXIFDataSource)); return; # that's all for now ;-) } ############################################################## # pasteEXIFData - paste the EXIF info from one picture to others ############################################################## sub pasteEXIFData { my @sellist = $picLB->info('selection'); my $selected = @sellist; my $errors = ''; my $i = 0; return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if ((not defined $copyEXIFDataSource) or (not -f $copyEXIFDataSource)) { $top->messageBox(-icon => 'warning', -message => "Please select a source picture (the picture from which the EXIF info should be copied) first by using EXIF info->copy from!", -title => 'Error', -type => 'OK'); return; } # get EXIF info and thumbnail for dialog my $exif = getShortEXIF($copyEXIFDataSource, WRAP); my $EXIFthumb = ''; # temp file holding the embedded EXIF thumbnail $EXIFthumb = "$user_data_path/".basename($copyEXIFDataSource); extractThumb($copyEXIFDataSource, $EXIFthumb, \$errors); my $message = "Copy the EXIF infos:\ -------------\ $exif\ -------------\ and the embedded thumbnail from\ \"".basename($copyEXIFDataSource)."\"\ to $selected selected pictures.\ The original EXIF infos and thumbnails of these pictures will be lost!\ Ok to continue?"; my $rc = myButtonDialog('Copy EXIF data', $message, $EXIFthumb, 'OK', 'Cancel'); removeFile($EXIFthumb); # remove temp thumbnail file return if ($rc ne 'OK'); log_it("transfering EXIF infos from ".basename($copyEXIFDataSource)." to $selected pictures"); my $pw = progressWinInit($picLB, 'Copy EXIF data'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "transfering EXIF info ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my $rc = copyEXIF( $copyEXIFDataSource, $dpic); $errors .= "$rc\n" if ($rc ne "1"); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); #showImageInfoCanvas($dpic) if ($dpic eq $actpic); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); } progressWinEnd($pw); log_it("ready! ($i/$selected copied)"); showText('Errors while copying EXIF infos', $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # copyThumbnail ############################################################## sub copyThumbnail { my @sellist = $picLB->info('selection'); my $selected = @sellist; return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if ((!defined $copyEXIFDataSource) or (!-f $copyEXIFDataSource)) { $top->messageBox(-icon => 'warning', -message => 'Please select a source picture first. This picture will be used as thumbnail, you may use "Save thumbnail ..." first. Than choose EXIF info->copy from!', -title => 'No source picture', -type => 'OK'); return; } my $size = getFileSize($copyEXIFDataSource, NO_FORMAT); # file size in bytes if ($size > 65535) { $top->messageBox(-icon => 'warning', -message => "Sorry, the thumbnail picture is too big ($size bytes). The thumbnail picture must have less than 65535 bytes.", -title => "Thumbnail too big", -type => 'OK'); return; } my $message = "Copy this thumbnail from\ \"".basename($copyEXIFDataSource)."\"\ to $selected selected pictures.\ The original thumbnails of these pictures will be lost!\ Ok to continue?"; my $rc = myButtonDialog("Copy EXIF data", "$message", $copyEXIFDataSource, 'OK', 'Cancel'); return if ($rc ne 'OK'); log_it("transfering thumbnail to $selected pictures"); my $errors = ''; my $i = 0; my $pw = progressWinInit($top, "Copy thumbnail"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "transfering thumbnail ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my $rc = writeThumb($dpic, $copyEXIFDataSource); $errors .= "$rc\n" if ($rc ne '1'); updateOneRow($dpic, $picLB); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); } progressWinEnd($pw); log_it("ready! ($i/$selected thumbnails transfered)"); showText("Errors while transfering thumbnails", $errors, NO_WAIT) if ($errors ne ''); } } # end encapsulation ############################################################## # copyEXIF ############################################################## sub copyEXIF { my $from = shift; my $to = shift; if (!-f $from) { warn "copyEXIF: file $from does not exists!\n"; return; } if (!-f $to) { warn "copyEXIF: file $to does not exists!\n"; return; } # from file my $meta = getMetaData($from, '^APP1$', 'FASTREADONLY'); return "Could not get EXIF info of source $from!" unless (defined $meta); # to file my $meta2 = getMetaData($to, '^APP1$'); return "Could not get EXIF info of target $to!" unless (defined $meta2); # find the EXIF segment my $seg = extract_app1_Exif_segment($meta); return "Could not get EXIF segment of source $from!" unless (defined $seg); # insert the segment and save the picture insert_app1_Exif_segment($meta2, $seg); my $result = $meta2->save(); return "save failed for $to" unless ($result); return 1; } ############################################################## # extract_app1_Exif_segment - sub supplied from Stefano Bettelli ############################################################## sub extract_app1_Exif_segment { my ($this) = @_; my $segment = $this->retrieve_app1_Exif_segment(); return unless $segment; # this removes the segment from the picture (in memory) # you could skip this if the picture is no more used @{$this->{segments}} = grep { $_ != $segment } @{$this->{segments}}; # this unlinks the picture from the segment, orphaning it $segment->{parent} = undef; return $segment; } ############################################################## # insert_app1_Exif_segment - sub supplied from Stefano Bettelli ############################################################## sub insert_app1_Exif_segment { my ($this, $segment) = @_; # this locates or produces an Exif segment my $old = $this->provide_app1_Exif_segment(); for (@{$this->{segments}}) { # looking for the segment to replace ... next unless $_ == $old; # tell the segment it now belongs to the picture $segment->{parent} = $this; # tell the picture it now owns the segment $_ = $segment; last; } } ############################################################## # restoreComments - remove existing comments and store the # given list of comments ############################################################## sub restoreComments { my $dpic = shift; my @comments = @_; my $meta = getMetaData($dpic, "COM"); if ($meta) { # remove all existing comments, we want to restore exactly $meta->remove_all_comments(); # write the old comments back if (@comments) { foreach (@comments) { $meta->add_comment($_); } } unless ($meta->save()) { warn "restoreComments: save $dpic failed!"; } } } ############################################################## # EXIFsave - make a new subdir .exif, copy the thumbnail of # the selected pics to this dir, copy the EXIF # info from the original pics to the thumbs ############################################################## sub EXIFsave { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); # make EXIF subdir return if (!makeDir("$actdir/$exifdirname", ASK)); my $errors = ''; my $i = 0; my $pw = progressWinInit($top, "Save EXIF infos"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Saving EXIF info ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); my $exiffile = "$actdir/$exifdirname/$pic"; # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $meta = getMetaData($dpic, '^APP1$', 'FASTREADONLY'); unless (defined $meta) { $errors .= "Could not get EXIF info of $pic!\n"; next; } my $seg = extract_app1_Exif_segment($meta); unless (defined $seg) { $errors .= "Could not get EXIF segment of $pic!\n"; next; } unless (nstore($seg, $exiffile)) { $errors .= "could not store EXIF segment in file $exiffile: $!\n"; next; } updateOneRow($dpic, $picLB); # display the new exif info (flag [s] is now set) showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i/".scalar @sellist." saved)"); showText("Errors while saving EXIF infos", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # EXIFrestore - copy the saved EXIF info back to the selected # pics ############################################################## sub EXIFrestore { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if (!-d "$actdir/$exifdirname") { $top->messageBox(-icon => 'warning', -message => "Found no saved EXIF infos in this folder!", -title => "No EXIF infos", -type => 'OK'); return; } # message for one picture my $message = "Restore saved EXIF infos to ".basename($sellist[0]).".\nThe actual EXIF infos of this picture will be lost!\nOk to continue?"; # message for more than one picture if (@sellist > 1) { $message = "Restore saved EXIF infos\nto the ".scalar @sellist." pictures.\nThe actual EXIF infos of this picture will be lost!\nOk to continue?" } return if (myButtonDialog("Restore EXIF data", "$message", undef, 'OK', 'Cancel') ne 'OK'); my $errors = ''; my $i = 0; my $pw = progressWinInit($top, "Restore EXIF info"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Restore EXIF info ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); my $exiffile = "$actdir/$exifdirname/$pic"; unless (-f $exiffile) { $errors .= "Found no saved EXIF infos for $dpic!\n"; next; } # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my $meta = getMetaData($dpic, '^APP1$'); unless (defined $meta) { $errors .= "Could not get EXIF info of $dpic!\n"; next; } # load stored EXIF segment from the file my $exif = retrieve($exiffile); unless (defined $exif) { $errors .= "could not retrieve saved EXIF info\n"; next; } insert_app1_Exif_segment($meta, $exif); unless ($meta->save()) { $errors .= "save failed for $dpic\n"; next; } updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); } progressWinEnd($pw); log_it("ready! ($i/".scalar @sellist."restored)"); showText("Errors while restoring EXIF data", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # EXIFremoveSaved - remove the saved exif info file ############################################################## sub EXIFremoveSaved { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if (!-d "$actdir/$exifdirname") { $top->messageBox(-icon => 'warning', -message => "Sorry, but there are no saved EXIF infos in this folder!", -title => "no EXIF infos", -type => 'OK'); return; } my $rc = $top->messageBox(-icon => 'warning', -message => "Remove the saved EXIF infos and the embedded thumbnails of ".scalar @sellist." pictures.\nOk to continue?", -title => "Warning", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my $i = 0; my $pw = progressWinInit($top, "Remove saved EXIF infos"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Removing saved EXIF info ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); my $exifthumb = "$actdir/$exifdirname/$pic"; if ((!-f $exifthumb) and (@sellist == 1)) { # show this info only when removing from one file $top->messageBox(-icon => 'warning', -message => "Sorry, but there are no saved EXIF infos for $pic!", -title => "no EXIF infos", -type => 'OK'); next; } # check if file is a link and get the real target next if (!getRealFile(\$dpic)); # remove the saved EXIF info file removeFile($exifthumb ); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i/".scalar @sellist." exif removed)"); } ############################################################## # copyComment - copy the comment from one picture to others ############################################################## sub copyComment { my $direction = shift; # "from" or "to" if (!defined $direction) { warn "copyComment: Missing a direction, should be \"from\" or \"to\"!"; return; } my @sellist = $picLB->info('selection'); my $selected = @sellist; my $i = 0; if ($direction eq 'from') { # set the copy source if (@sellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select exactly one picture (the picture from which comments should be taken) for this function!", -title => 'Error', -type => 'OK'); return; } $copyCommentSource = $sellist[0]; # save source pic to global variable log_it("copy source set to ".basename($copyCommentSource)); return; # that's all for now ;-) } elsif ($direction eq 'to') { return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if ((!defined $copyCommentSource) or (!-f $copyCommentSource)) { $top->messageBox(-icon => 'warning', -message => "Please select a source picture (the picture from which comments should be taken) first, and than choose comments->copy from!", -title => 'Error', -type => 'OK'); return; } my $com = getComment($copyCommentSource, SHORT); my $thumb = getThumbFileName($copyCommentSource); my $message = "Add the comments:\ -------------\ $com\ -------------\ from\ \"".basename($copyCommentSource)."\"\ to $selected selected pictures.\ The original comments won't be lost!\ Ok to continue?"; my $rc = myButtonDialog("Copy comments", $message, $thumb, 'OK', 'Cancel'); return if ($rc ne 'OK'); log_it("transfering comments to $selected pictures"); my $pw = progressWinInit($top, "Transfer comments"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "transfering comments ($i/$selected) ...", $i, $selected); next if (!checkWriteable($dpic)); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my @comments = getComments($copyCommentSource); my $meta = getMetaData($dpic, "COM"); next unless ($meta); # add the comments foreach (@comments) { $meta->add_comment($_); } unless ($meta->save()) { warn "copyComment: save $dpic failed!"; } updateOneRow($dpic, $picLB); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); } # foreach end progressWinEnd($pw); } else { warn "copyComment: Wrong direction ($direction), should be \"from\" or \"to\"!"; return; } log_it("ready! ($i of $selected copied)"); } ############################################################## # displayIPTCData - displays all IPTC-Data in a window ############################################################## sub displayIPTCData { my $lb = shift; my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); return unless askSelection(\@sellist, 10, "IPTC info"); my $i = 0; my $pw = progressWinInit($lb, "Display IPTC data"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; my $iptc = ''; progressWinUpdate($pw, "displaying IPTC data ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $title = "IPTC/IIM information of $pic"; $iptc = getIPTC($dpic, LONG); if ($iptc eq '') { $iptc = "Found no IPTC/IIM information in \"$pic\"\n"; } showText($title, $iptc, NO_WAIT, getThumbFileName($dpic)); } progressWinEnd($pw); if ($lb == $picLB) { log_it("ready! ($i/".scalar @sellist." IPTC displayed)"); } } ############################################################## # saveIPTC - save IPTC info hash as template to a file ############################################################## sub saveIPTC { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 1, \@sellist, lang("picture(s)")); my $dpic = $sellist[0]; my ($ok, $iptc) = get_IPTC_info($dpic); if (not $ok) { $top->messageBox(-icon => 'warning', -message => "Could not open IPTC segment of $dpic!", -title => "Save IPTC info", -type => 'OK'); return; } if (!-d $iptcdir) { if ( !mkdir $iptcdir, oct(755) ) { $top->messageBox(-icon => 'warning', -message => "Error making IPTC template folder $iptcdir: $!", -title => "Save IPTC template", -type => 'OK'); return; } } my $types = [ ['IPTC Template', '.iptc2',], ['All Files', '*',], ]; my $file = $top->getSaveFile(-title => 'Save IPTC template (please use the .iptc2 suffix)', -defaultextension => 'iptc2', -initialfile => "template.iptc2", -initialdir => $iptcdir, -filetypes => $types); return if ((!defined $file) or ($file eq '')); my $rc = nstore($iptc, $file) or warn "could not store IPTC in file $file: $!"; log_it("IPTC template saved ($rc)"); } { # encapsulate data structure with access methods my $iptcCopySource; # local variable to store path and picture name for copyIPTC/pasteIPTC ############################################################## # copyFromIPTC ############################################################## sub copyIPTC { my @sellist = $picLB->info('selection'); if (@sellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select exactly one picture.", -title => "Copy IPTC info", -type => 'OK'); return; } $iptcCopySource = $sellist[0]; log_it("IPTC copy from $iptcCopySource"); } ############################################################## # pasteIPTC ############################################################## sub pasteIPTC { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if (not defined $iptcCopySource or not -f $iptcCopySource) { $top->messageBox(-icon => 'warning', -message => "Please select a copy source picture first.", -title => "Paste IPTC info", -type => 'OK'); return; } # get IPTC info from source picture (set by copyIPTC) my $meta = getMetaData($iptcCopySource, 'APP13'); my $iptcCopy = $meta->get_app13_data('TEXTUAL', 'IPTC'); unless (defined $iptcCopy) { $top->messageBox(-icon => 'warning', -message => "There is no IPTC info in source picture $iptcCopySource! Stopping.", -title => "Paste IPTC info", -type => 'OK'); return; } applyIPTC($picLB, $iptcCopy, \@sellist); my $sel_pics = scalar(@sellist); log_it(langf("Meta information from %s added to %d picture(s).",basename($iptcCopySource),$sel_pics)); } } ############################################################## # mergeIPTC - merge a IPTC info hash template to a file ############################################################## sub mergeIPTC { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $types = [ ['IPTC Template', '.iptc2',], ['All Files', '*',], ]; my $file = $top->getOpenFile(-title => 'Merge IPTC template', -defaultextension => 'iptc2', -initialdir => $iptcdir, -filetypes => $types); return if ((!defined $file) or ($file eq '') or (!-f $file)); my $iptc = retrieve($file); unless (defined $iptc) { warn langf("Could not retrieve %s",$file); return; } applyIPTC($picLB, $iptc, \@sellist); } ############################################################## # applyIPTC - apply a IPTC info hash to a list of pics ############################################################## sub applyIPTC { my $lb = shift; # reference to listbox widget my $iptc = shift; # reference to an IPTC hash as provided by Image::MetaData::JPEG my $piclist = shift; # picture list reference my $errors = ''; my $pw = 0; $pw = progressWinInit($lb, 'Apply IPTC template') if (@$piclist > 1); my $i = 0; foreach my $dpic (@$piclist) { last if ($pw and progressWinCheck($pw)); $i++; progressWinUpdate($pw, "applying IPTC template ($i/".scalar @$piclist.") ...", $i, scalar @$piclist) if $pw; my ($ok, $error) = applyIPTCint($dpic, $iptc); if ($ok) { my $dirthumb = getThumbFileName($dpic); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch($dirthumb); updateOneRow($dpic, $lb); showImageInfoCanvas($dpic) if ($dpic eq $actpic); } else { $errors .= $error; } } progressWinEnd($pw) if $pw; log_it("ready! ($i of ".scalar @$piclist." processed)"); showText('Errors while applying IPTC infos', $errors, NO_WAIT) if ($errors ne ''); } ############################################################## ############################################################## sub applyIPTCint { my $dpic = shift; my $iptc = shift; # reference to an IPTC hash as provided by Image::MetaData::JPEG my $error = ''; if (not checkWriteable($dpic)) { $error .= "File $dpic is not writable! Skipping.\n"; return (0, $error); } my $meta = getMetaData($dpic, 'APP13'); if (not defined $meta) { $error .= "Could not get IPTC info of $dpic!"; return (0, $error); } # todo, we could also use UPDATE or REPLACE here $meta->set_app13_data($iptc, 'ADD', 'IPTC'); # make the SupplementalCategories and Keywords unique and sorted uniqueIPTC($meta); if (not $meta->save()) { $error .= "Saving IPTC info failed for $dpic\n"; return (0, $error); } return (1, $error); } ############################################################## # uniqueArray ############################################################## sub uniqueArray { my $listR = shift; my %d; # build a hash foreach (@{$listR}) { $d{$_} = 1; } @{$listR} = (sort { uc($a) cmp uc($b); } keys %d); } ############################################################## # uniqueIPTC - remove double entries from SupplementalCategories # and Keywords and sort them alphabetically # !Function will not save IPTC! ############################################################## sub uniqueIPTC { my $meta = shift; my $iptc = $meta->get_app13_data('TEXTUAL', 'IPTC'); # replace (german) umlaute by corresponding letters and # replace all non-printable chars, but not newline etc. if ($config{onlyASCII}) { foreach my $key (keys %{$iptc}) { if (${$iptc->{$key}}[0]) { # test some encodings #my $utf8code = Encode::decode('utf8',${$iptc->{$key}}[0]); #my $isocode = Encode::decode('iso-8859-1',${$iptc->{$key}}[0]); #if ($utf8code =~ m/Ö/) { # print "Found Ö in utf8code $utf8code ${$iptc->{$key}}[0]\n"; #} #if ($isocode =~ m/Ö/) { # print "Found Ö in isocode $isocode ${$iptc->{$key}}[0]\n"; #} # end test encodings ${$iptc->{$key}}[0] =~ s/([$umlaute])/$umlaute{$1}/g; ${$iptc->{$key}}[0] =~ tr/\n\t\r\f -~//cd; } } } my %d; # build a hash foreach (@{$iptc->{SupplementalCategory}}) { $_ =~ tr/ -~//cd; # replace all non-printable chars $d{$_} = 1; } @{$iptc->{SupplementalCategory}} = (sort { uc($a) cmp uc($b); } keys %d); %d = (); # completely empty %d foreach (@{$iptc->{Keywords}}) { $_ =~ tr/ -~//cd; # replace all non-printable chars (Picasa adds one to each keyword) $d{$_} = 1; } @{$iptc->{Keywords}} = (sort { uc($a) cmp uc($b); } keys %d); $meta->set_app13_data($iptc, 'REPLACE', 'IPTC'); } ############################################################## # editIPTCCategories ############################################################## sub editIPTCCategories { my $lb = shift; if (Exists($catw)) { $catw->deiconify; $catw->raise; $catw->focus; return; } # open window $catw = $lb->Toplevel(); $catw->withdraw; $catw->title('Categories'); $catw->iconimage($mapiviicon) if $mapiviicon; my $cattree; my $XBut = $catw->Button(-text => lang('Close'), -command => sub { saveTreeMode($cattree); nstore($cattree->{m_mode}, "$user_data_path/categoryMode") or warn "could not store $user_data_path/categoryMode: $!"; $catw->destroy; })->pack(-expand => 0,-fill => 'x',-padx => 1,-pady => 1); my $af = $catw->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $af->Radiobutton(-text => "all", -variable => \$config{CategoriesAll}, -value => 1)->pack(-side => 'left'); $af->Radiobutton(-text => "join", -variable => \$config{CategoriesAll}, -value => 2)->pack(-side => 'left'); $af->Radiobutton(-text => "last", -variable => \$config{CategoriesAll}, -value => 0)->pack(-side => 'left'); my $addB = $af->Button(-text => lang('add'), -command => sub { my @cats = $cattree->info('selection'); return unless checkSelection($catw, 1, 0, \@cats); my @sellist = $lb->info('selection'); return unless checkSelection($catw, 1, 0, \@sellist); my $warning = ''; my @catlist; foreach my $cat (@cats) { my @items; if ($config{CategoriesAll} == 1) { # all, separated @items = getAllItems($cat); } elsif ($config{CategoriesAll} == 2) { # all, joined @items = getAllItems($cat); my $joined = join('.', @items); if (length($joined) > 32) { $warning .= "Category $joined has ".length($joined)." characters"; next; } undef @items; push @items, $joined; } elsif ($config{CategoriesAll} == 0) { # last @items = getLastItem($cat); } else { warn "editIPTCCategories: should never be reached ($config{CategoriesAll})!"; } push @catlist, @items; } if (@catlist) { my $iptc = { SupplementalCategory => \@catlist }; applyIPTC($lb, $iptc, \@sellist); } if ($warning ne '') { $warning = "IPTC supp. categories are limited to 32 characters. Please shorten category.\n$warning"; showText("Warnings while adding keywords", $warning, NO_WAIT); } } )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); $balloon->attach($addB, -msg => "Add the selected categories to the selected pictures"); my $rmB = $af->Button(-text => lang('remove'), -command => sub { my @cats = $cattree->info('selection'); return unless checkSelection($catw, 1, 0, \@cats); my @sellist = $lb->info('selection'); return unless checkSelection($catw, 1, 0, \@sellist); my $pw = progressWinInit($catw, "Remove category"); my $i = 0; my $sum = @sellist; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "removing category ($i/$sum) ...", $i, $sum); foreach my $cat (@cats) { last if progressWinCheck($pw); progressWinUpdate($pw, "removing category $cat ($i/$sum) ...", $i, $sum); my $item; if ($config{CategoriesAll} == 2) { # all, joined my @items = getAllItems($cat); $item = join('.', @items); } else { # last $item = getLastItem($cat); } print "remove category $item ($cat) from $dpic\n" if $verbose; removeIPTCItem($dpic, 'SupplementalCategory', $item); updateOneRow($dpic, $lb); } } progressWinEnd($pw); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); $balloon->attach($rmB, -msg => "Remove the selected categories from the selected pictures"); $cattree = $catw->Scrolled('Tree', -separator => '/', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 25, -height => 25, )->pack(-expand => 1, -fill =>'both', -padx => 4, -pady => 2); $balloon->attach($cattree, -msg => "Double click on a category to add it to the selected pictures.\nIt's possible to edit the categories, use the\nright mouse button to open the edit menu."); # try to get the saved mode if (-f "$user_data_path/categoryMode") { my $hashRef = retrieve("$user_data_path/categoryMode"); warn "could not retrieve mode" unless defined $hashRef; $cattree->{m_mode} = $hashRef; } $cattree->bind('', sub { $addB->Invoke; }); addTreeMenu($cattree, \@precats); insertTreeList($cattree, @precats); bind_exit_keys_to_button($catw, $XBut); $catw->Popup; $catw->waitWindow; } ############################################################## # format given keyword list (reference) according to the configured format (all, joined, or last) # return formated list ############################################################## sub keyword_format { my $keys = shift; # list reference my $warning = shift; # string reference my @keylist; foreach my $key (@$keys) { my @items; if ($config{KeywordsAll} == 1) { # all, separated @items = getAllItems($key); } elsif ($config{KeywordsAll} == 2) { # all, joined @items = getAllItems($key); my $joined = join('.', @items); if (length($joined) > 64) { $$warning .= "Keyword $joined has ".length($joined)." characters"; next; } undef @items; push @items, $joined; } elsif ($config{KeywordsAll} == 0) { # last @items = getLastItem($key); } else { warn "keyword_format: \$config{KeywordsAll} has wrong value: $config{KeywordsAll} should never be reached!"; } push @keylist, @items; } return @keylist; } ############################################################## # editCommentKeywords ############################################################## sub editCommentKeywords { my $lb = shift; if (Exists($keycw)) { $keycw->deiconify; $keycw->raise; $keycw->focus; return; } # open window $keycw = $top->Toplevel(); $keycw->withdraw; $keycw->title('Keywords for comments'); $keycw->iconimage($mapiviicon) if $mapiviicon; my $keytree; my $XBut = $keycw->Button(-text => lang('Close'), -command => sub { saveTreeMode($keytree); nstore($keytree->{m_mode}, "$user_data_path/keywordMode") or warn "could not store $user_data_path/keywordMode: $!"; $keycw->destroy; })->pack(-expand => 0,-fill => 'x',-padx => 1,-pady => 1); my $af = $keycw->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $af->Radiobutton(-text => "all", -variable => \$config{KeywordsAll}, -value => 1)->pack(-side => 'left'); $af->Radiobutton(-text => "last", -variable => \$config{KeywordsAll}, -value => 0)->pack(-side => 'left'); my $addB = $af->Button(-text => 'add', -command => sub { my @keys = $keytree->info('selection'); return unless checkSelection($keycw, 1, 0, \@keys, lang("keyword(s)")); my @sellist = $lb->info('selection'); return unless checkSelection($keycw, 1, 0, \@sellist, lang("keyword(s)")); my $comment; foreach my $key (@keys) { my @items; if ($config{KeywordsAll}) { @items = getAllItems($key); } else { @items = getLastItem($key); } $comment .= "$_ " foreach (@items); } # todo add to end of existing comment or as new comment foreach my $dpic (@sellist) { # todo progressbar addCommentToPic($comment, $dpic, TOUCH); updateOneRow($dpic, $lb); showImageInfo($dpic) if ($dpic eq $actpic); } } )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); $balloon->attach($addB, -msg => "Add the selected keywords to the selected pictures"); $keytree = $keycw->Scrolled('Tree', -separator => '/', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 25, -height => 25, )->pack(-expand => 1, -fill =>'both', -padx => 4, -pady => 2); $balloon->attach($keytree, -msg => "Double click on a keyword to insert it.\nIt's possible to edit the keywords, use the\nright mouse button to open the edit menu."); # try to get the saved mode if (-f "$user_data_path/keywordMode") { my $hashRef = retrieve("$user_data_path/keywordMode"); warn "could not retrieve mode" unless defined $hashRef; $keytree->{m_mode} = $hashRef; } $keytree->bind('', sub { $addB->Invoke; }); addTreeMenu($keytree, \@prekeys); insertTreeList($keytree, @prekeys); bind_exit_keys_to_button($keycw, $XBut); $keycw->Popup; $keycw->waitWindow; } ############################################################## # addTreeMenu - add a menu to a tree widget to edit a tree ############################################################## sub addTreeMenu { my $tree = shift; # tree widget my $listRef = shift; # the list displayed in the tree my $hot = shift; # the hotlist widget (optional, if available the additional menu entry "add to clipboard" will be shown) my $menu = $tree->Menu(-title => lang('Tree edit menu')); if (defined $hot and Exists($hot)) { $menu->command(-label => lang('add to clipboard'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 0, \@keys); add_keyword_to_hotlist($hot, \@keys); }); $menu->separator; } $menu->command(-label => lang('add new item'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 1, \@keys); my $item = ''; my $parent = ''; $parent = $keys[0] if (@keys); if ($parent !~ m/.*\/.*/) { $parent = ''; } else { # cut of last element $parent = $1 if ($parent =~ m/(.*\/).*/); $parent .= '/' if (($parent ne '') and ($parent !~ m/.*\/$/)); } my $rc = myEntryDialog(lang('New item'), langf("Please enter the new item (below %s)",$parent), \$item); return if ($rc ne 'OK'); return if ($item eq ''); # avoid slash and backslash if (($item =~ m|.*\/.*|) or ($item =~ m|.*\\.*|)) { $tree->messageBox(-icon => 'info', -message => 'Sorry, but the slash (/) and the backslash (\) are not allowed.', -title => 'Wrong character', -type => 'OK'); return; } # avoid double entries if (isInList($parent.$item, $listRef)) { $tree->messageBox(-icon => 'info', -message => "Sorry, but $parent$item is already in the list.", -title => 'Double entry', -type => 'OK'); return; } push @{$listRef}, $parent.$item; insertTreeList($tree, @{$listRef}); # show new keywords filter_tree_open($tree, '', $item); }); $menu->command(-label => lang('add new sub item'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 1, \@keys); my $item = ''; my $parent = $keys[0]; my $rc = myEntryDialog(lang('New sub item'), langf("Please enter the new sub item (below %s)",$parent), \$item); return if ($rc ne 'OK'); return if ($item eq ''); # avoid slash and backslash if (($item =~ m|.*\/.*|) or ($item =~ m|.*\\.*|)) { $tree->messageBox(-icon => 'info', -message => 'Sorry, but the slash (/) and the backslash (\) are not allowed.', -title => 'Wrong character', -type => 'OK'); return; } $parent .= '/' if (($parent ne '') and ($parent !~ m/.*\/$/)); # avoid double entries if (isInList($parent.$item, $listRef)) { $tree->messageBox(-icon => 'info', -message => "Sorry, but $parent$item is already in the list.", -title => 'Double entry', -type => 'OK'); return; } push @{$listRef}, $parent.$item; insertTreeList($tree, @{$listRef}); # show new keywords filter_tree_open($tree, '', $item); }); $menu->separator; $menu->command(-label => lang('rename or move item'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 1, \@keys); my $parent = $keys[0]; my $rc = myEntryDialog('Rename item', "Please enter the new name for item $parent", \$parent); return if ($rc ne 'OK'); return if ($parent eq ''); $parent =~ s|^/||; # cut leading slash foreach my $t (0 .. @{$listRef}-1) { # find index and change list entry if ($$listRef[$t] =~ m/^$keys[0](.*)/) { print "rename: $$listRef[$t] ($t) to $parent$1\n" if $verbose; $$listRef[$t] = $parent.$1; } } insertTreeList($tree, @{$listRef}); }); $menu->separator; $menu->command(-label => lang('delete item(s)'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 0, \@keys); for my $t (reverse 0 .. (scalar @{$listRef} - 1) ) { foreach my $key (@keys) { if ($$listRef[$t] =~ m/^$key.*/) { print " trow out: $$listRef[$t] ($t) key = $key\n" if $verbose; splice @{$listRef}, $t, 1; # remove it from list } } } insertTreeList($tree, @{$listRef}); }); $menu->separator; $menu->command(-label => lang('search for item(s)'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 0, \@keys); my @keywords; # here we consider all selected keywords for the search foreach (@keys) { push @keywords, split(/\//, $_); } my @keywords_ex; my @list = get_pics_with_keywords(\@keywords, \@keywords_ex); my $title = 'Keywords: '; $title .= "$_ " foreach (@keywords); showThumbList(\@list, $title); }); $menu->separator; $menu->command(-label => lang('collapse all'), -command => sub { tree_fold(CLOSE, $tree); }); $menu->command(-label => lang('collapse to first sub level'), -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($tree, 1, 0, \@keys); for my $key (@keys) { my @childs = $tree->child_entries($key, 1); # path, depth foreach my $child (@childs) { $tree->close($child); } } }); $menu->command(-label => lang('expand all'), -command => sub { tree_fold(OPEN, $tree); }); #$menu->command(-label => lang("expand item(s)"), -command => sub { # my @keys = $tree->info('selection'); # return unless checkSelection($tree, 1, 0, \@keys); # for my $key (@keys) { # print "expand: $key\n"; # my @childs = $tree->child_entries($key, 1); # path, depth # foreach my $child (@childs) { # print " child: $child\n"; # $tree->open($child); # } # } #}); $tree->bind('', sub { $menu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); } ############################################################## ############################################################## sub add_keyword_to_hotlist { my ($hot, $keys) = @_; foreach my $key (@{$keys}) { $hot_keywords{$key}++; } $hot->delete(0, 'end'); $hot->insert('end', (sort keys %hot_keywords)); return; } ############################################################## # getLastItem - returns the last item of a scalar separated with # a slash: family/Miller/Robert -> Robert ############################################################## sub getLastItem { my $item = shift; my @names = split /\//, $item; my $name = $names[-1]; $name = $item if ((!defined $name) or ($name eq '')); return $name; } ############################################################## # getAllItems - returns a list of all items of a scalar # separated with a slash: # family/Miller/Robert -> family, Miller, Robert ############################################################## sub getAllItems { my $item = shift; return split /\//, $item; } ############################################################## # insertTreeList ############################################################## sub insertTreeList { my $tree = shift; my %mode; saveTreeMode($tree); %mode = %{$tree->{m_mode}} if (defined $tree->{m_mode}); $tree->delete('all'); # insert the list (@_) foreach (sort { uc($a) cmp uc($b); } @_ ) { my @names = split /\//, $_; my $name = $names[-1]; $name = $_ if ((!defined $name) or ($name eq '')); $tree->add($_, -text=>$name); } $tree->autosetmode; # reset mode to the the old setting for the first 3 levels foreach ($tree->info('children')) { $tree->close($_) if ((defined $mode{$_}) and ($mode{$_} eq 'open')); $tree->open($_) if ((defined $mode{$_}) and ($mode{$_} eq 'close')); foreach ($tree->info('children', $_)) { $tree->close($_) if ((defined $mode{$_}) and ($mode{$_} eq 'open')); $tree->open($_) if ((defined $mode{$_}) and ($mode{$_} eq 'close')); foreach ($tree->info('children', $_)) { $tree->close($_) if ((defined $mode{$_}) and ($mode{$_} eq 'open')); $tree->open($_) if ((defined $mode{$_}) and ($mode{$_} eq 'close')); } } } } ############################################################## # saveTreeMode - save the mode (open, close status) of the first 3 # levels of a tree in $widget->{m_mode} # {m_mode} is mapivi private data stored in the # widget hash ############################################################## sub saveTreeMode { my $tree = shift; print "saveTreeMode: Error no widget\n" unless Exists($tree); my %mode; %mode = %{$tree->{m_mode}} if (defined $tree->{m_mode}); # save mode (open, close) of existing items for the first 3 levels foreach ($tree->info('children')) { $mode{$_} = $tree->getmode($_); foreach ($tree->info('children', $_)) { $mode{$_} = $tree->getmode($_); foreach ($tree->info('children', $_)) { $mode{$_} = $tree->getmode($_); } } } $tree->{m_mode} = \%mode; } ############################################################## # removeIPTCItem ############################################################## sub removeIPTCItem { my $dpic = shift; my $kind = shift; my $item = shift; if (($kind ne 'Keywords') and ($kind ne 'SupplementalCategory')) { warn "removeIPTCItem: $kind is wrong kind"; return; } print "removeIPTCItem: kind:$kind item:$item pic:$dpic\n" if $verbose; my $meta = getMetaData($dpic, 'APP13'); unless (defined $meta) { print "removeIPTCItem: Could not create IPTC info for $dpic!\n"; return; } my $iptc = $meta->get_app13_data('TEXTUAL', 'IPTC'); my %d; # build a hash foreach (@{$iptc->{$kind}}) { $d{$_} = 1; } return unless (defined $d{$item}); delete $d{$item}; # remove item from list @{$iptc->{$kind}} = (sort { uc($a) cmp uc($b); } keys %d); $meta->set_app13_data($iptc, 'REPLACE', 'IPTC'); if ($meta->save()) { my $dirthumb = getThumbFileName($dpic); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch($dirthumb); } else { print "removeIPTCItem: save failed for $dpic\n"; } } #my %get_encoding_name_from_tag = ( # "0x1b0x250x47" => "UTF8", # stolen from Image::ExifTool (thanks to Phil Harvey) #------------------------------------------------------------------------------ # Print conversion for CodedCharacterSet # Inputs: 0) value sub PrintCodedCharset { my $val = shift; return $iptcCharset{$val} if $iptcCharset{$val}; $val =~ s/(.)/ $1/g; $val =~ s/ \x1b/, ESC/g; $val =~ s/^,? //; return $val; } ############################################################## # getIPTC - returns all IPTC-Data of the given picture ############################################################## sub getIPTC { # the pic with complete path my $dpic = shift; # bool, if = LONG a better complete readable output, # if = SHORT a compact but complete IPTC info for e.g. the search database # if = MICRO compact, only values no key names my $format = shift; my $meta = shift; # optional, the Image::MetaData::JPEG object of $pic if available my $iptc = ''; my $indent = ''; # space to indent multi key values return $iptc unless is_a_JPEG($dpic); my $shortkey; # todo: is , 'FASTREADONLY' here possible? $meta = getMetaData($dpic, 'APP13') unless (defined($meta)); if ($meta) { my $seg = $meta->retrieve_app13_segment(undef, 'IPTC'); if ($seg) { my $hashref = $seg->get_app13_data('TEXTUAL', 'IPTC'); foreach my $key (@IPTCAttributes) { # this causes trouble (cuts off the rest) because it's binary next if ($key eq "RecordVersion"); if (defined($hashref->{$key})) { if ($format == LONG) { my $shortkey = $key; $shortkey = substr($shortkey, 0, 13)."." if (length($shortkey) > 14); $iptc .= sprintf "%-14s: ", $shortkey; #$iptc .= sprintf "%-31s: ", $key; $indent = "\n "; #$indent = "\n "; } elsif ($format == MICRO) { # show only content, no key names $indent = " "; } else { my $shortkey = $key; $shortkey =~ s/SupplementalCategory/SuppCategories/; $shortkey = substr($shortkey, 0, 7)."." if (length($shortkey) > 8); $iptc .= sprintf "%-8s: ", $shortkey; $indent = " "; } # add IPTC value for (@{$hashref->{$key}}) { # show rating/urgency using stars (*) at least in MICRO output # todo : could make sense also for the other outputs, but check consequences first # e.g. on IPTC values of search db!!! if (($format == MICRO) and ($key eq 'Urgency')) { $iptc .= iptc_rating_stars_urg($_).$indent; } else { $iptc .= "$_$indent"; } } $iptc =~ s/$indent$//; # remove last indent (newline/space after last value) $iptc .= "\n"; # newline for each defined IPTC attribute } } # add Coded Character Set info my $hash_1 = $seg->get_app13_data('TEXTUAL', 'IPTC_1'); if (defined $hash_1->{'CodedCharacterSet'}) { my $encoding = PrintCodedCharset(${$hash_1->{'CodedCharacterSet'}}[0]); if (($format == LONG)) { $iptc .= sprintf "%-31s: ", 'CodedCharacterSet'; } elsif ($format == MICRO) { # show only content, no key names } else { $iptc .= 'CCharSet: '; } $iptc .= "$encoding\n"; #print "found Coded character set in $dpic: [$encoding][${$hash_1->{'CodedCharacterSet'}}[0]]\n"; } } } $iptc =~ s/\s+$//; # cut trailing whitespace $iptc =~ tr/\n -~//cd; # remove all non-printable chars, but not newline return $iptc; } ############################################################## # getShortIPTC - get just one attribute of the IPTC comment # I decided to use the caption/abstract, but # I am not sure if this is the best attribute # here? # if there is no file or no IPTC info in the file # an empty string is returned ############################################################## sub getShortIPTC { my $dpic = shift; # optional, if set to LONG the complete contents of the @iptcs attributes # (see below) will be returned # else (SHORT) it will be cut to fit in the hlist my $format = shift; # LONG or SHORT return '' unless (-f $dpic); my $info = getIPTC($dpic, SHORT); $info = formatString($info, $config{LineLength}, $config{LineLimit}) if ((defined $format) and ($format == SHORT)); return $info; } ############################################################## # getImageInfo - returns a hash containing the image info ############################################################## sub getImageInfo { my $pic = shift; if (!-f $pic) { return ''; } my $ii = image_info($pic); if (!$ii) { return ''; } if ($ii->{Errno} and $ii->{Errno} ne "0") { return ''; } return $ii; } ############################################################## # getNearestItem - finds the nearest item to the mouse pointer # in a listbox ############################################################## sub getNearestItem { my($LB) = @_; my ($X,$Y) = $LB->pointerxy(); my $y = $LB->rooty(); my $yy = $Y - $y; return ($LB->nearest($yy)); } ############################################################## # processARGV - handels the command line arguments (if any) ############################################################## sub processARGV { getopts('iv'); # sets $opt_i if switch -i is found - import pictures #getopts('v'); # sets $opt_v if switch -v is found - verbose logging #getopts('h'); # sets $opt_h if switch -h is found - help $verbose = 1 if ($opt_v); my $nr = @ARGV; if ($nr < 1) { # no arguments - open the last dir $actdir = $config{LastDir}; dirSave($actdir); return; } if ($nr > 1) { # too many arguments print "Mapivi error: to many command line options\n"; printUsage(); exit; } #if ($opt_h) { # -h flag -> help # printUsage(); # exit; #} my $item = abs_path($ARGV[0]); #print "processARGV: -e $item = ", -e $item, "\n"; $item = Encode::encode('iso-8859-1', $item); #print "processARGV: item: $item item2: $item2\n"; #print "processARGV: -e $item = ", -e $item, "\n"; if (-f $item) { $actpic = $item; $actdir = dirname($item); } elsif (-d $item) { $actdir = $item; } else { printUsage(); exit; } dirSave($actdir); } ############################################################## # re-read all meta information from picture files ############################################################## sub reread_pics { my $lb = shift; my @sellist = $lb->info('selection'); # check selection args: widget, min, max, listref, itemkind (e.g. "picture") return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); $lb->Busy; log_it("Reading meta-info of ".scalar @sellist." files ..."); foreach my $dpic (@sellist) { updateOneRow($dpic, $lb); } $lb->Unbusy; log_it("ready! (re-read ".scalar @sellist." files)"); } ############################################################## # open_pic_folder - open the folder containing the selected pictures ############################################################## sub open_pic_folder { my $lb = shift; my @sellist = $lb->info('selection'); # check selection args: widget, min, max, listref, itemkind (e.g. "picture") return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my %folders; # check for all possible folders foreach my $dpic (@sellist) { $folders{dirname($dpic)}++; } my $folder; # all selected pictures are located in the same folder if (scalar keys %folders == 1) { $folder = $_ foreach (keys %folders); } # picture from different folders have been selected else { # todo: let the user choose which folder to open # take the folder of the first pic $folder = dirname($sellist[0]); my $rc = $top->messageBox(-icon => 'question', -message => "The selected pictures are stored in ".scalar(keys(%folders))." different folders. Proceed with first folder: $folder?", -title => "Proceed?", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } openDirPost($folder); } ############################################################## # openDir - let the user select a new dir and open it # with a dir dialog ############################################################## sub openDir { my $dir = dirDialog($actdir); openDirPost($dir); } ############################################################## # openDirPost - things to do when opening a new dir ############################################################## sub openDirPost { my $dir = shift; $dir = Encode::encode('iso-8859-1', $dir); $dir =~ s|\\|/|g; # perl likes slashes (/ = UNIX style) instead of backslash (\ = Windows style) $dir =~ s/\/\//\//g; # replace all double slashes (//) with single slashes (/) return unless (defined $dir); return unless (-d $dir); $actdir = $dir; my $path = cutString($dir, -22, '..'); log_it(lang('Opening')." $path ..."); $actpic = ''; # reset var $actpic - needed to get a correct window title setDirProperties(); dirSave($dir); clearLabels(); clear_canvas_thumbs($c); showImageInfoCanvas(); setTitle(); $commentText->delete( 0.1, 'end') if ($config{ShowCommentField} and defined $commentText); # update_IPTC_frame_content(''); # now included in showImageInfoCanvas $dirtree->configure(-directory => $actdir); # Set the folder exists &Tk::DirTree::chdir ? $dirtree->chdir($actdir) : $dirtree->set_dir($actdir); selectDirInTree($actdir); # switch display modus to folder $act_modus = FOLDER; updateThumbs(); } ############################################################## # setDirProperties ############################################################## sub setDirProperties { $dirPropSORT = 0; $dirPropMETA = 0; $dirPropPRIO = 0; $dirPropSORT = $dirProperties{$actdir}{SORT} if (defined $dirProperties{$actdir}{SORT}); $dirPropMETA = $dirProperties{$actdir}{META} if (defined $dirProperties{$actdir}{META}); $dirPropPRIO = $dirProperties{$actdir}{PRIO} if (defined $dirProperties{$actdir}{PRIO}); } ############################################################## # showDirProperties ############################################################## sub showDirProperties { if (Exists($dpw)) { $dpw->deiconify; $dpw->raise; $dpw->focus; return; } # open window $dpw = $top->Toplevel(); $dpw->withdraw; $dpw->title(lang('Folder Checklist')); $dpw->iconimage($mapiviicon) if $mapiviicon; my $topf = $dpw->Frame()->pack(); my $topf2 = $dpw->Frame()->pack(); my $window_label = '...'; my $dplb = $dpw->Scrolled("HList", -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 1, -columns => 5, -scrollbars => 'osoe', #-selectmode => "dragdrop", todo -selectmode => "extended", -background => $conf{color_bg}{value}, #8fa8bf -width => 40, -height => 60, )->pack(-expand => 1, -fill => 'both'); my $count = 0; $dplb->{dircol} = $count; $dplb->header('create', $count++, -text => lang('Folder'), -headerbackground => $conf{color_entry}{value}); $dplb->{sortcol} = $count; $dplb->header('create', $count++, -text => lang('1 Sort'), -headerbackground => $conf{color_entry}{value}); $dplb->{metacol} = $count; $dplb->header('create', $count++, -text => lang('2 Meta'), -headerbackground => $conf{color_entry}{value}); $dplb->{priocol} = $count; $dplb->header('create', $count++, -text => lang('3 Rating'), -headerbackground => $conf{color_entry}{value}); $dplb->{commcol} = $count; $dplb->header('create', $count++, -text => lang('Comment'), -headerbackground => $conf{color_entry}{value}); my $Xbut = $topf->Button(-text => lang("Close"), -command => sub { $dpw->withdraw; $dpw->destroy; } )->pack(-side => 'left', -expand => 0,-fill => 'x',-padx => 3,-pady => 3); my $upd_but = $topf->Button(-text => lang("Update"), -command => sub { my @dirs = $dplb->info('selection'); my $last = $dirs[-1]; $dplb->delete("all"); insertDirProperties($dplb, \$window_label); reselect($dplb, @dirs); $dplb->see($last) if ($dplb->info("exists", $last));; })->pack(-side => 'left', -expand => 0,-padx => 3,-pady => 3); $topf->Checkbutton(-text => lang("Show unfinished"), -variable => \$config{ShowUnfinishedDirs}, -command => sub { $upd_but->Invoke; }, )->pack(-side => 'left', -expand => 0,-fill => 'x',-padx => 3,-pady => 3); $topf->Checkbutton(-text => lang("Show finished"), -variable => \$config{ShowFinishedDirs}, -command => sub { $upd_but->Invoke; }, )->pack(-side => 'left', -expand => 0,-fill => 'x',-padx => 3,-pady => 3); $topf->Button(-image => $mapivi_icons{Help}, -pady => 0, -padx => 0, -command => sub { showText("Help for Folder Checklist", "The purpose of this list is to give an overview about all picture folders.\nThe green folders are finshed (all three merkers are set), the blue folders are unfinsihed, the red folders are missing (may have been deleted or renamed).\n\nDouble click on any folder to open it in the main window or use the context menu to set or reset any markers.", NO_WAIT); })->pack(-expand => 0, -side => 'left', -fill => 'x', -padx => 3, -pady => 3); $topf2->Label(-textvariable => \$window_label)->pack(-side => 'left', -expand => 0,-fill => 'x',-padx => 3,-pady => 3); my $dpmenu = $dpw->Menu(-title => lang("Folder Checklist Menu")); $dpmenu->command(-label => lang("Open folder"), -command => sub { my @dirs = $dplb->info('selection'); return unless checkSelection($dpw, 1, 1, \@dirs); if (-d $dirs[0]) { openDirPost($dirs[0]); # show main window $top->deiconify; $top->raise; } else { $dplb->messageBox(-icon => 'info', -message => "Sorry, but this folder is currently not available!", -title => "Folder not available", -type => 'OK'); } } ); $dpmenu->command(-label => lang("Add all sub folders to list"), -command => sub { my @dirs = $dplb->info('selection'); return unless checkSelection($dpw, 1, 1, \@dirs); @dirs = getDirsRecursive($dirs[0]); my $nr = 0; foreach (@dirs) { # todo skip empty dirs if (!defined $dirProperties{$_}) { print "adding $_\n" if $verbose; $dirProperties{$_}{SORT} = 0 ; $dirProperties{$_}{META} = 0 ; $dirProperties{$_}{PRIO} = 0 ; $nr++; } } $upd_but->Invoke; $dplb->messageBox(-icon => 'info', -message => "Added $nr folders.", -title => "Added sub folders", -type => 'OK'); } ); $dpmenu->command(-label => lang("Remove selected from list"), -command => sub { my @dirs = $dplb->info('selection'); return unless checkSelection($dpw, 1, 0, \@dirs); foreach my $dir (@dirs) { delete $dirProperties{$dir}; $dplb->delete("entry", $dir) if ($dplb->info('exists', $dir)); } } ); $dpmenu->command(-label => lang("Edit folder comment"), -command => sub { my @dirs = $dplb->info('selection'); return unless checkSelection($dpw, 1, 1, \@dirs); my $text = ''; $text = $dirProperties{$dirs[0]}{COMM} if (defined $dirProperties{$dirs[0]}{COMM}); my $rc = myTextDialog("Edit comment", "Please edit comment of $dirs[0]", \$text); return if ($rc ne 'OK'); # replace (german) umlaute by corresponding letters $text =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $dirProperties{$dirs[0]}{COMM} = $text; $dplb->itemConfigure($dirs[0], $dplb->{commcol}, -text => $dirProperties{$dirs[0]}{COMM}, -style => $fileS); } ); my $sort_menu = $dpmenu->cascade(-label => lang("1 Sort")); my $meta_menu = $dpmenu->cascade(-label => lang("2 Meta")); my $prio_menu = $dpmenu->cascade(-label => lang("3 Rating")); my $all_menu = $dpmenu->cascade(-label => lang("All")); $sort_menu->command(-label => lang("set"), -command => sub { setProperty($dplb, 'SORT', 1); } ); $sort_menu->command(-label => lang("reset"), -command => sub { setProperty($dplb, 'SORT', 0); } ); $meta_menu->command(-label => lang("set"), -command => sub { setProperty($dplb, 'META', 1); } ); $meta_menu->command(-label => lang("reset"), -command => sub { setProperty($dplb, 'META', 0); } ); $prio_menu->command(-label => lang("set"), -command => sub { setProperty($dplb, 'PRIO', 1); } ); $prio_menu->command(-label => lang("reset"), -command => sub { setProperty($dplb, 'PRIO', 0); } ); $all_menu->command( -label => lang("set"), -command => sub { setProperty($dplb, 'ALL', 1); } ); $all_menu->command( -label => lang("reset"), -command => sub { setProperty($dplb, 'ALL', 0); } ); $dplb->bind('', sub { $dpmenu->Popup(-popover => "cursor", -popanchor => "nw"); } ); $dplb->bind('', sub { my @dirs = $dplb->info('selection'); return unless checkSelection($dpw, 1, 1, \@dirs); if (-d $dirs[0]) { openDirPost($dirs[0]); # show main window $top->deiconify; $top->raise; } else { $dplb->messageBox(-icon => 'info', -message => "Sorry, but this folder is currently not available!", -title => "Folder not available", -type => 'OK'); } } ); bind_exit_keys_to_button($dpw, $Xbut); $dpw->Popup; my $ws = 0.7; # window size is 70% of screen my $w = int($ws * $dpw->screenwidth); my $h = int($ws * $dpw->screenheight); my $x = int(($dpw->screenwidth - $w)/3); my $y = int(($dpw->screenheight - $h)/3); $dpw->geometry("${w}x${h}+${x}+${y}"); insertDirProperties($dplb, \$window_label); $dpw->waitWindow; return; } ############################################################## # insertDirProperties ############################################################## sub insertDirProperties { my $lb = shift; my $labelref = shift; # todo: for a bright background we should use #009 #090 #900 my $normal_S = $lb->ItemStyle('text', -anchor=>'nw', -foreground=>'#88F', -background=>$conf{color_bg}{value}); my $finished_S = $lb->ItemStyle('text', -anchor=>'nw', -foreground=>'#8F8', -background=>$conf{color_bg}{value}); my $not_avail_S = $lb->ItemStyle('text', -anchor=>'nw', -foreground=>'#F88', -background=>$conf{color_bg}{value}); my $last_time; my $finished = 0; my $unfinished = 0; foreach my $dir (sort { uc($a) cmp uc($b); } keys %dirProperties) { my $style = $normal_S; if (defined $dirProperties{$dir}{SORT} and defined $dirProperties{$dir}{META} and defined $dirProperties{$dir}{PRIO} and $dirProperties{$dir}{SORT} == 1 and $dirProperties{$dir}{META} == 1 and $dirProperties{$dir}{PRIO} == 1) { $style = $finished_S; $finished++; } else { $unfinished++; } next if (!$config{ShowFinishedDirs} and $style == $finished_S); next if (!$config{ShowUnfinishedDirs} and $style != $finished_S); $style = $not_avail_S unless (-d $dir); # create new row $lb->add($dir); $lb->itemCreate($dir, $lb->{dircol}, -text => $dir, -style => $style); $lb->itemCreate($dir, $lb->{sortcol}, -text => $dirProperties{$dir}{SORT}, -style => $comS); $lb->itemCreate($dir, $lb->{metacol}, -text => $dirProperties{$dir}{META}, -style => $iptcS); $lb->itemCreate($dir, $lb->{priocol}, -text => $dirProperties{$dir}{PRIO}, -style => $comS); $lb->itemCreate($dir, $lb->{commcol}, -text => $dirProperties{$dir}{COMM}, -style => $iptcS); # show progress every 0.5 seconds - idea from Slaven if (!defined $last_time || Tk::timeofday()-$last_time > 0.5) { $lb->update; $last_time = Tk::timeofday(); } } # total number of folders my $total = keys %dirProperties; my $percent_finished = $finished/$total*100; my $percent_unfinished = $unfinished/$total*100; $$labelref = langf("%d (%2.1f%%) finished and %d (%2.1f%%) unfinished folders", $finished, $percent_finished, $unfinished, $percent_unfinished); } ############################################################## # showDirSizes ############################################################## sub showDirSizes { if (Exists($dsw)) { $dsw->deiconify; $dsw->raise; $dsw->focus; return; } my @dirs = @_; # just one dir at the moment, because the dir tree is configured to single selection # will contain all dirs my @alldirs; my $break = 0; my $pw = progressWinInit($top, lang("Calculate folder size")); foreach my $dir (@dirs) { progressWinUpdate($pw, "Collecting folders below $dir ...", 0, scalar @dirs); # the selected folder should also been shown push @alldirs, $dir; # thumbnail folders and folders starting with "." are skipped push @alldirs, getDirsRecursive($dir); } my %dirsize; my %files; my $max = 0; my $allsize = 0; my $dirCount = 0; my $i = 0; foreach my $dir (@alldirs) { if (progressWinCheck($pw)) { $break = 1; last; } $i++; progressWinUpdate($pw, "in folder $dir ($i/".scalar @alldirs.") ...", $i, scalar @alldirs); my $dirsize = 0; $dirCount++; # get non-empty files only my @files = getFiles($dir); foreach my $file (@files) { $dirsize += getFileSize("$dir/$file", NO_FORMAT); } $dirsize{$dir} = $dirsize; # thumbnail folders like (.thumbs) are already excluded, see above $files{$dir} = scalar(@files); $max = $dirsize if ($dirsize > $max); $allsize += $dirsize; } progressWinEnd($pw); return if ($break); # open window $dsw = $top->Toplevel(); $dsw->title(lang('Folder Sizes')); $dsw->iconimage($mapiviicon) if $mapiviicon; my $label = "Starting soon"; my $Xbut = $dsw->Button(-text => lang('Close'), -command => sub { $dsw->withdraw; $dsw->destroy; } )->pack(-expand => 0,-fill => 'x',-padx => 1,-pady => 1); $dsw->Label(-textvariable => \$label, )->pack(-expand => 0,-fill => 'x',-padx => 1,-pady => 1); my $dc_width = 700; my $dc = $dsw->Scrolled('Canvas', -scrollbars => 'osoe', -width => $dc_width, -height => 400, -relief => 'sunken', -bd => $config{Borderwidth})->pack(-expand => 1,-fill => 'both',-padx => 1, -pady => 1); my $height = 16; $dc->configure(-scrollregion => [0, 0, $dc_width, ($#alldirs * $height)]); $max = 1 if ($max <= 0); # avoid divison by zero my $scale = ($dc_width - 2)/$max; my $y = 2; my $x = 2; my $file_total = 0; foreach my $dir (sort keys %dirsize) { $dc->createRectangle( $x, $y, $x + ($dirsize{$dir} * $scale), $y+$height, -tags => ['RECT'], #-outline => undef, -outline => 'black', -fill => 'goldenrod3', ); my $filestr = sprintf "%5s", $files{$dir}; $filestr .= ' '.lang('files'); my $text = sprintf "%6s", computeUnit($dirsize{$dir}); $dc->createText( $x+1, $y+1, -text => $filestr, -anchor => 'nw'); $dc->createText( $x+80, $y+1, -text => $text, -anchor => 'nw'); $dc->createText( $x+130, $y+1, -text => $dir, -anchor => 'nw'); $y += $height; $file_total += $files{$dir}; } $max = computeUnit($max); $allsize = computeUnit($allsize); $label = scalar(@alldirs)." folders, total: $file_total files with $allsize, biggest folder size: $max"; $dsw->waitWindow; } ############################################################## # setProperty ############################################################## sub setProperty { my $lb = shift; my $prop = shift; my $value = shift; my @dirs = $lb->info('selection'); return unless checkSelection($dpw, 1, 0, \@dirs); if ((!defined $value) or ($value < 0) or ($value > 1)) { warn "wrong value $value"; return; } if ((!defined $prop) or (($prop ne 'SORT') and ($prop ne 'META') and ($prop ne 'PRIO') and ($prop ne 'ALL'))) { warn "wrong property $prop"; return; } foreach my $dir (@dirs) { # set property to given value unless ($prop eq 'ALL') { $dirProperties{$dir}{$prop} = $value; } else { $dirProperties{$dir}{SORT} = $value; $dirProperties{$dir}{META} = $value; $dirProperties{$dir}{PRIO} = $value; } # show changed property my $style = $iptcS; $style = $exifS if (defined $dirProperties{$dir}{SORT} and defined $dirProperties{$dir}{META} and defined $dirProperties{$dir}{PRIO} and $dirProperties{$dir}{SORT} == 1 and $dirProperties{$dir}{META} == 1 and $dirProperties{$dir}{PRIO} == 1); $lb->itemConfigure($dir, $lb->{dircol}, -text => $dir, -style => $style); $lb->itemConfigure($dir, $lb->{sortcol}, -text => $dirProperties{$dir}{SORT}, -style => $comS); $lb->itemConfigure($dir, $lb->{metacol}, -text => $dirProperties{$dir}{META}, -style => $iptcS); $lb->itemConfigure($dir, $lb->{priocol}, -text => $dirProperties{$dir}{PRIO}, -style => $comS); $lb->itemConfigure($dir, $lb->{commcol}, -text => $dirProperties{$dir}{COMM}, -style => $iptcS); } } ############################################################## # selectDirInTree ############################################################## sub selectDirInTree { my $dir = shift; $dir = Encode::encode('iso-8859-1', $dir); $dir =~ s/\//\\/g if $EvilOS; # windows needs backslashes $dirtree->selectionClear(); if ($dirtree->info('exists', $dir)) { $dirtree->selectionSet($dir); $dirtree->show('entry', $dir); } else { print "selectDirInTree: neither dir $dir does not exists!\n"; # debugging } } ############################################################## # dirSave - save the last used dirs, build a hotlist of # often used dirs and update the dir menu ############################################################## sub dirSave { my $dir = shift; return if ($dir eq $trashdir); # check if dir is already in history list my $i = 0; foreach (@dirHist) { if ($_ eq $dir) { splice @dirHist, $i, 1; # throw old entry away last; } $i++; } # add dir to history list push @dirHist, $dir; # no more than 10 entries in history list if (@dirHist > 10) { shift @dirHist; } # count the number of accesses to each dir if (defined $dirHotlist{$dir}) { $dirHotlist{$dir}++; } else { $dirHotlist{$dir} = 1; } updateDirMenu(); } ############################################################## # clearLabels - clear the labels containing infos about the # actual picture ############################################################## sub clearLabels { # show index number in window $nrof = '0/0 (0)'; $widthheight = ''; $size = ''; $zoomFactorStr = ''; $rating_but->configure(-image => $mapivi_icons{Rating0}); } ############################################################## # dirDialog - open a window and a dir tree ############################################################## sub dirDialog { my $dir = shift; $dir = Encode::encode('iso-8859-1', $dir); if ($EvilOS) { if ($win32FOAvail) { print "FileOp is available!\n" if $verbose; # this is untested!!! todo $dir = BrowseForFolder("Choose folder", "CSIDL_DESKTOP"); } else { # windows, but no win32 FileOp available print "FileOp is not available!\n" if $verbose; $dir = $top->chooseDirectory(-title => "Select folder", -initialdir => $dir); } # At least under Windows XP both encodings seem to work: 'iso-8859-1' 'windows-1252' # todo: the correct encoding should be determined using: # I18N::Langinfo->import(qw(langinfo CODESET)); # $codeset = langinfo(CODESET()); # note the () $dir = Encode::encode('windows-1252', $dir); $dir = '' unless (defined $dir); $dir = '' unless (-d $dir); $dir =~ s|\\|/|g; # perl likes slashes (UNIX style) instead of backslash (Windows style) } else { # non windows system # code based on Tk::chooseDirectory my $t = $top->Toplevel; $t->withdraw; $t->title('Open folder ...'); $t->iconimage($mapiviicon) if $mapiviicon; my $ok = 0; # flag: "1" means OK, "0" means cancelled # Create Frame widget before the DirTree widget, so it's always visible # if the window gets resized. my $f = $t->Frame->pack(-fill => 'x', -side => "bottom"); my $d; my $mkdB = $t->Button(-text => lang('Make new folder'), -command => sub { my $new_dir = makeNewDir($dir, $d); if (-d $new_dir) { $d->see($new_dir) } })->pack(-fill => 'x'); $balloon->attach($mkdB, -msg => "The new folder will be created in the selected folder.\nPlease select a folder in the tree."); $d = $t->Scrolled('DirTree', -scrollbars => 'osoe', -showhidden => $config{ShowHiddenDirs}, -selectmode => 'browse', -exportselection => 1, -browsecmd => sub { # this function will show all subdirs when pressing on the + sign $dir = shift; $dir = Encode::encode('iso-8859-1', $dir); return if (@_ >= 1); if (!-d $dir) { print "dirDialog: $dir does not exists!\n"; return; } $t->Busy; my @dirs = getDirs($dir); $t->Unbusy; return if (@dirs < 1); $t->Busy; my $lastdir = $dir.'/'.$dirs[-1]; if ($d->info('exists', $lastdir)) { $d->see($lastdir) if (-d $lastdir); } $t->Unbusy; }, # With this version of -command a double-click will # select the folder -command => sub { $ok = 1; $t->destroy; }, # With this version of -command a double-click will # open a folder. Selection is only possible with # the Ok button. #-command => sub { $d->opencmd($_[0]) }, )->pack(-fill => 'both', -expand => 1); # Set the initial folder exists &Tk::DirTree::chdir ? $d->chdir($dir) : $d->set_dir($dir); $f->Button(-text => lang('Ok'), -command => sub { $ok = 1; $t->destroy; })->pack(-side => 'left',-fill => 'x', -expand => 1); $f->Button(-text => lang('Cancel'), -command => sub { $ok = 0; $t->destroy; })->pack(-side => 'left',-fill => 'x', -expand => 1); # file and dir requester should always be big! (50% of screenwidth and 90% of screenheight) my $w = int(0.5 * $t->screenwidth); my $h = int(0.9 * $t->screenheight); $t->geometry("${w}x${h}+0+0"); $t->deiconify; $t->raise; $f->waitWindow(); $t->destroy() if (Exists($t)); $dir = '' if ($ok != 1); } #print "dirDialog: \"$dir\"\n"; return $dir; } ############################################################## # printUsage - show the user how to use mapivi ############################################################## sub printUsage { print "\nUsage: mapivi [-i] [file|folder]\n"; print "\n -i start with import wizard\n"; } ############################################################## # touch - set the modification date of the given file to the # actual date and time ############################################################## sub touch { my $file = shift; my $now = time; utime($now, $now, $file); } ############################################################## # addComment - add a comment to all selected pics in the given # listbox ############################################################## sub addComment { my $lb = shift; # the reference to the active listbox widget my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); log_it("adding comments to ".scalar @sellist." pictures"); # check if some files are links return if (!checkLinks($lb, @sellist)); my $info = "Please enter comment to add to the ".scalar @sellist." selected pictures"; my $text = ''; my $thumb = ''; # if just one pic should be commented we show the thumbnail and the real name if (@sellist == 1) { $thumb = getThumbFileName($sellist[0]); $info = "Please enter comment to add to ".basename($sellist[0]); } my $rc = myTextDialog("Add comment", $info, \$text, $thumb); return if ($rc ne 'OK' or $text eq ''); # replace (german) umlaute by corresponding letters # (a lot of programs seem to have problems with Umlauten in comments) $text =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $config{Comment} = $text; # save changed comment to global config hash my $pw = progressWinInit($lb, "Add comment"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "adding comment ($i/".scalar @sellist.") ...", $i, scalar @sellist); next if (!checkWriteable($dpic)); addCommentToPic($text, $dpic, TOUCH); # touch thumbnail updateOneRow($dpic, $lb); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i of ".scalar @sellist." commented)"); } ############################################################## # black and white conversion preview # takes one picture as input, scales it to $config{FilterPrevSize} # and converts it to different black and white versions # therefore all entries of %channel_mixer are used. # Each converted picture is then displayed in a new window. ############################################################## sub grayscale_preview { my $dpic = shift; return unless -f $dpic; # check if ImageMagick convert version is at least or bigger than 6 if ((`convert 2>&1` =~ m/.*ImageMagick (\d+)\.(\d+)\.(\d+).*/) and ($1 < 6)) { $top->messageBox(-icon => 'warning', -message => "Sorry, but for these function you need at least ImageMagick (convert) in version 6.x.x. You have $1.$2.$3.\nPlease download at http://www.imagemagick.org.", -title => "Wrong ImageMagick version ($1.$2.$3)", -type => 'OK'); return; } my ($pic,$dir,$suffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $preview_start_pic = $trashdir.'/'.$pic.'-start'; return if (!mycopy($dpic, $preview_start_pic, OVERWRITE)); return if (!resizePic($preview_start_pic, $config{FilterPrevSize}, $config{FilterPrevSize}, 80)); # save actual values my $red = $config{ChannelRed}; my $green = $config{ChannelGreen}; my $blue = $config{ChannelBlue}; my $versions = scalar(keys(%channel_mixer)); my $message = langf("Converting %s using all %d presets. Press %s to stop.\n",$pic,$versions,lang("Cancel")); my $pw = progressWinInit($top, lang('Black and white preview')); my $i = 0; foreach my $key (sort keys %channel_mixer) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, $message.langf("Converting to %s (%d/%d) ...",$key,$i,$versions),$i,$versions); my $key_no_whitespace = $key; $key_no_whitespace =~ s/\s+//g; my $preview_pic = $trashdir.'/'.$pic.'_'.$key_no_whitespace.$suffix; last if (!mycopy($preview_start_pic, $preview_pic, OVERWRITE)); # grayscalePicInt is controlled by $config{ChannelRed|Green|Blue} $config{ChannelRed} = @{$channel_mixer{$key}}[0]; $config{ChannelGreen} = @{$channel_mixer{$key}}[1]; $config{ChannelBlue} = @{$channel_mixer{$key}}[2]; grayscalePicInt($preview_pic, PREVIEW); showPicInOwnWin($preview_pic); } # restore old values $config{ChannelRed} = $red; $config{ChannelGreen} = $green; $config{ChannelBlue} = $blue; progressWinEnd($pw); } ############################################################## # grayscalePic ############################################################## sub grayscalePic { my $lb = shift; # the reference to the active listbox widget # check if ImageMagick convert version is at least or bigger than 6 if ((`convert 2>&1` =~ m/.*ImageMagick (\d+)\.(\d+)\.(\d+).*/) and ($1 < 6)) { $top->messageBox(-icon => 'warning', -message => "Sorry, but for these function you need at least ImageMagick (convert) in version 6.x.x. You have $1.$2.$3.\nPlease download at http://www.imagemagick.org.", -title => "Wrong ImageMagick version ($1.$2.$3)", -type => 'OK'); return; } my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); # check if some files are links return if (!checkLinks($lb, @sellist)); my $rc = 0; # open window my $win = $top->Toplevel(); $win->title(lang('Convert to black and white')); $win->iconimage($mapiviicon) if $mapiviicon; my $topF = $win->Frame()->pack(-expand => 1, -fill =>'both', -padx => 5); my $picF = $topF->Frame(-height => $config{FilterPrevSize}, -width => $config{FilterPrevSize})->pack(-side => 'left', -expand => 1, -fill =>'both'); my $presetF = $topF->Frame()->pack(-side => 'left', -expand => 1, -fill =>'both'); $win->{status} = $picF->Label(-textvariable => \$win->{label})->pack(); my $w = 18; labeledScale($win, 'top', $w, lang("Red channel (%)"), \$config{ChannelRed}, -100, 200, 1); labeledScale($win, 'top', $w, lang("Green channel (%)"), \$config{ChannelGreen}, -100, 200, 1); labeledScale($win, 'top', $w, lang("Blue channel (%)"), \$config{ChannelBlue}, -100, 200, 1); my $original_pic = $sellist[0]; my $preview_start_pic = $trashdir.'/'.basename($original_pic).'-start'; my $preview_pic = $trashdir.'/'.basename($original_pic); my $preview_photo; my $update_button = $win->Button(-text => lang("Update"), -command => sub { $win->Busy; $win->{label} = lang("Processing preview ..."); $win->update; return if (!mycopy($preview_start_pic, $preview_pic, OVERWRITE)); grayscalePicInt($preview_pic, PREVIEW); $preview_photo = $win->Photo(-file => $preview_pic, -gamma => $config{Gamma}); if (not $win->{photo}) { $win->{photo} = $picF->Label(-image => $preview_photo, -relief => 'sunken', )->pack(-padx => 3, -pady => 3); } else { $win->{photo}->configure(-image => $preview_photo); } $win->{label} = lang("Preview finished"); $win->Unbusy; })->pack(); $presetF->Label(-text => lang('Presets'))->pack(); my $preset_list = $presetF->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 20, -height => 10, )->pack(-expand => 1, -fill =>'both', -padx => 2, -pady => 2); $preset_list->insert('end', (sort keys %channel_mixer)); $preset_list->bind('', sub { my ($preset) = $preset_list->curselection(); my $key = $preset_list->get($preset); $config{ChannelRed} = @{$channel_mixer{$key}}[0]; $config{ChannelGreen} = @{$channel_mixer{$key}}[1]; $config{ChannelBlue} = @{$channel_mixer{$key}}[2]; $update_button->invoke(); } ); $win->Checkbutton(-variable => \$config{ChannelBright}, -text => lang("Keep brightness"))->pack(-anchor=>'w', -padx => 5, -pady => 3); my $decoF = $win->Frame()->pack(-fill =>'x', -padx => 5); $decoF->Checkbutton(-variable => \$config{ChannelDeco}, -anchor => 'w', -text => lang("Add border or text (not visible in preview)"))->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1); $decoF->Button(-text => lang("Options"), -anchor => 'w', -command => sub {decorationDialog(scalar @sellist,0); })->pack(-side => 'left', -anchor => 'w', -padx => 3); buttonBackup($win, 'top'); my $qs = labeledScale($win, 'top', 18, lang("Quality (%)"), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qs); $win->Label(-text => langf("Convert %d picture(s) to black and white.\nPress OK to continue.",scalar(@sellist)))->pack(); my $but_frame = $win->Frame()->pack(-fill =>'x'); my $ok_but = $but_frame->Button(-text => 'OK', -command => sub { $rc = 1; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); my $x_but = $but_frame->Button(-text => lang('Cancel'), -command => sub { $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 3); $win->Popup(-popover => 'cursor'); repositionWindow($win); return if (!mycopy ($original_pic, $preview_start_pic, OVERWRITE)); return if (!resizePic($preview_start_pic, $config{FilterPrevSize}, $config{FilterPrevSize}, 80)); return if (!mycopy ($preview_start_pic, $preview_pic, OVERWRITE)); $update_button->invoke(); $win->waitWindow; $preview_photo->delete if $preview_photo; grayscale_pics($lb, \@sellist) if ($rc); return; } ############################################################## # convert a list of pictures to black and white ############################################################## sub grayscale_pics { my $lb = shift; # the reference to the active listbox widget my $pic_list = shift; # array ref log_it("converting ".scalar @$pic_list." picture(s) to black and white"); my $pw = progressWinInit($lb, lang('Convert to black and white')); my $i = 0; foreach my $dpic (@$pic_list) { last if progressWinCheck($pw); progressWinUpdate($pw, "converting ($i/".scalar @$pic_list.") this may take a while ...", $i, scalar @$pic_list); next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); grayscalePicInt($dpic, NO_PREVIEW); $i++; progressWinUpdate($pw, "converting ($i/".scalar @$pic_list.") ...", $i, scalar @$pic_list); updateOneRow($dpic, $lb); showImageInfo($dpic) if ($dpic eq $actpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one } progressWinEnd($pw); reselect($lb, @$pic_list); log_it("ready! ($i of ".scalar @$pic_list." converted)"); generateThumbs(ASK, SHOW); } ############################################################## # grayscalePicInt - $dpic will be overwritten! ############################################################## sub grayscalePicInt { my $dpic = shift; my $preview = shift; my $sum = 100; if ($config{ChannelBright}) { $sum = $config{ChannelRed}+$config{ChannelGreen}+$config{ChannelBlue}; } $sum = 1 if ($sum == 0); # avoid division by zero my $command = "convert "; $command .= " \"$dpic\" -fx \"(r*$config{ChannelRed}+g*$config{ChannelGreen}+b*$config{ChannelBlue})/$sum\" "; # windows needs the " instead of ' #\'(r*$config{ChannelRed}+g*$config{ChannelGreen}+b*$config{ChannelBlue})/$sum\' "; $command .= makeDrawOptions($dpic) if ($config{ChannelDeco} and !$preview); $command .= " \"$dpic\" "; print "grayscalePicInt: command: $command\n" if $verbose; execute($command); } ############################################################## # updateOneRow - update the (changed) metainfo of one picture # in the given listbox and store them in the # search database ############################################################## sub updateOneRow { my $dpic = shift; # pic with path my $lb = shift; # the listbox reference # reselect does not work for the light table return if (ref($lb) eq 'Tk::Canvas'); return unless (-f $dpic); # check if listbox entry exists unless ($lb->info('exists', $dpic)) { #warn "entry $dpic not found in listbox!"; return; } my $meta = addToSearchDB($dpic); # save meta data of picture into the search data base my $com = $searchDB{$dpic}{COM}; my $exif = date_iso_to_relative($searchDB{$dpic}{EXIF}); my $iptc = displayIPTC($dpic); $com = formatString($com, $config{LineLength}, $config{LineLimit}); # format the comment for the list $iptc = formatString($iptc, $config{LineLength}, $config{LineLimit}); # format the IPTC info for the list my $rating_size = get_rating_and_size($dpic, $lb); # update the metainfo in the listbox $lb->itemConfigure($dpic, $lb->{thumbcol}, -text => getThumbCaption($dpic)) if (defined $lb->{thumbcol}); $lb->itemConfigure($dpic, $lb->{comcol}, -text => $com) if (defined $lb->{comcol}); $lb->itemConfigure($dpic, $lb->{exifcol}, -text => $exif) if (defined $lb->{exifcol}); $lb->itemConfigure($dpic, $lb->{iptccol}, -text => $iptc) if (defined $lb->{iptccol}); #$lb->itemConfigure($dpic, $lb->{filecol}, -text => $size) if (defined $lb->{filecol}); $lb->itemConfigure($dpic, $lb->{filecol}, -itemtype => "image", -image => $rating_size, -style => $fileS) if (defined $lb->{filecol}); } ############################################################## # addCommentToPic - add a comment to a single picture ############################################################## sub addCommentToPic { my $com = shift; my $dpic = shift; my $touch = shift; # TOUCH = touch thumbnail, NO_TOUCH return if (!-f $dpic); # check if file is a link and get the real target return if (!getRealFile(\$dpic)); my $meta = getMetaData($dpic, "COM"); return unless ($meta); printf "addCommentToPic: %-30s %s\n", cutString($com,30,".."), $dpic if $verbose; #$com = encode("utf8", $com); $meta->add_comment($com); unless ($meta->save()) { warn "addCommentToPic: save $dpic failed!"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)) if ($touch == TOUCH); addToSearchDB($dpic); } ############################################################## # replaceComment - search/replace a string in a comment to all # selected pics in the given listbox ############################################################## sub replaceComment { my $lb = shift; # the reference to the active listbox widget my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); log_it("replacing comments in ".scalar @sellist." pictures"); # check if some files are links return if (!checkLinks($lb, @sellist)); my $info = "Please enter the string to replace in the ".scalar @sellist." selected pictures"; my $stext = $config{SearchPattern}; # search string my $rtext = ''; # replace string # if just one pic should be commented we show the real name if (@sellist == 1) { $info = "Please enter the string to replace in ".basename($sellist[0]); } my $test = 1; my $i = 0; while ($test) { # todo: one search/replace dialog with upper/lower case support my $rc = myReplaceDialog("Replace comment", $info, \$stext, \$rtext); return if (($rc eq 'Cancel') or ($stext eq '')); $test = 0 if ($rc eq 'OK'); $config{SearchPattern} = $stext; # replace (german) umlaute by corresponding letters # (a lot of programs seem to have problems with Umlauten in comments) $stext =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $rtext =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); my $spat = makePattern($stext); $config{Comment} = $rtext; # save changed comment to global config hash my $nocom = ''; my $nostr = ''; my $countComments = 0; my $countFiles = 0; my $pw = progressWinInit($lb, "Replace comments"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "replacing comments ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); print "replaceComment: pic:$pic\n" if $verbose; next if (!checkWriteable($dpic)); my $meta = getMetaData($dpic, "COM"); unless ($meta) { $nocom .= "$dpic\n"; next; } my @com = getComments($dpic, $meta); # get all comments from the file unless (@com) { $nocom .= "$dpic\n"; next; } my $replace = 0; for my $j (0 .. $#com) { if ($com[$j] =~ m/$spat/) { # todo handle lower/uppercase unless ($test) { print "replacing $stext with $rtext in $pic: -$com[$j]- " if $verbose; $com[$j] =~ s/$spat/$rtext/g; print "to -$com[$j]-\n" if $verbose; $meta->set_comment($j, $com[$j]); } $replace++; $countComments++; } } if ($replace > 0) { unless ($test) { unless ($meta->save()) { warn "replaceComment: save $pic failed!"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $lb); } $countFiles++; } else { $nostr .= "$dpic\n"; } } progressWinEnd($pw); # short the strings for better output my $stextd = cutString($stext, 20, ".."); my $rtextd = cutString($rtext, 20, ".."); my $text = "Replaced "; $text = "Test mode:\nMapivi would replace " if $test; $text .= "the string \"$stextd\" with \"$rtextd\"\nin $countComments comments of $countFiles pictures\n\n"; if (($nocom ne '') or ($nostr ne '')) { $text .= "Found no comments in these pictures:\n$nocom\n" if ($nocom ne ''); $text .= "Found no string matching \"$stextd\" in these pictures:\n$nostr\n" if ($nostr ne ''); } showText("Replace comment log", $text, WAIT); } log_it("ready! ($i of ".scalar @sellist." pictures processed)"); } ############################################################## # nameToComment - add the filename as comment to all selected # pictures ############################################################## sub nameToComment { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $dia = $top->DialogBox(-title => "Add filename to comment", -buttons => ['OK', 'Cancel']); $dia->add("Label", -text => "This function will add a comment containing\nthe individual filename of $selected pictures!", -bg => $conf{color_bg}{value}, -justify => 'left')->pack; $dia->add("Checkbutton", -text => "Remove suffix (.jpg)", -variable => \$config{NameComRmSuffix})->pack; my $rc = $dia->Show(); $top->focusForce; return if ($rc ne 'OK'); log_it("adding filename as comment of $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, "Adding file name as comment"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Adding file name ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); my $com = $pic; next if (!checkWriteable($dpic)); if (($config{NameComRmSuffix}) and ($pic =~ /(.*)(\.jp(g|eg))/i)) { $com = $1; # remove .jp(e)g suffix } # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $meta = getMetaData($dpic, "COM"); next unless ($meta); $meta->add_comment($com); unless ($meta->save()) { warn "nameToComment: save $pic failed!"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i of $selected processed)"); } ############################################################## # showComment - show the comment of all selected pictures ############################################################## sub showComment { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); return unless askSelection(\@sellist, 10, "comment"); my $selected = @sellist; my $nocomment = ''; log_it("displaying JPEG comments of $selected pictures"); my $pw = progressWinInit($top, "Display comments"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "displaying comment ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my @comments = getComments($dpic); my $comment = ''; foreach (@comments) { $comment .= "$_\n"; } my $plural; (@comments > 1) ? ($plural = "s") : ($plural = ''); if ($comment ne '') { showText("$pic contains ".scalar @comments." comment$plural", $comment, NO_WAIT, getThumbFileName($dpic)); } else { $nocomment .= "$pic\n"; } } progressWinEnd($pw); if ($nocomment ne '') { showText("no comments", "no comments in:\n$nocomment", NO_WAIT); } log_it("ready! ($i of $selected displayed)"); } my %logo; # hash to hold all logo data # todo: just a workaround until settings are saved to the Mapivi configuration ############################################################## ############################################################## sub logo_set_defaults { my $logo = shift; # hash ref $$logo{text} = $conf{logo_text}{value}; $$logo{size_x} = 500; $$logo{size_y} = 100; $$logo{color_bg} = 'white'; $$logo{font_size} = $conf{logo_font_size}{value}; $$logo{offset_x} = 25; $$logo{offset_y} = 65; $$logo{shadow} = $conf{logo_shadow}{value}; $$logo{offset_shadow} = 5; $$logo{font} = $conf{logo_font}{value}; $$logo{outfile} = "$actdir/logo.jpg"; $$logo{color_font} = $conf{logo_font_color}{value}; $$logo{color_shadow} = $conf{logo_shadow_color}{value}; } ############################################################## ############################################################## sub logo_save_defaults { my $logo = shift; # hash ref # we don't save all, but the most important settings $conf{logo_text}{value} = $$logo{text}; $conf{logo_font_size}{value} = $$logo{font_size}; $conf{logo_shadow}{value} = $$logo{shadow}; $conf{logo_font}{value} = $$logo{font}; $$logo{outfile} = "$actdir/logo.jpg"; $conf{logo_font_color}{value} = $$logo{color_font}; $conf{logo_shadow_color}{value} = $$logo{color_shadow}; } ############################################################## # generate a text logo using image magick - dialog window ############################################################## sub logo_generate_win { # open window my $win = $top->Toplevel(); $win->title('Generate Text Logo'); $win->iconimage($mapiviicon) if $mapiviicon; my $preview_button; my @imfonts = getImageMagickFonts(); logo_set_defaults(\%logo) if (not defined $logo{text}); # set font to a valid value (in this case the first font in the list) # if font is not available $logo{font} = $imfonts[0] if (not isInList($logo{font}, \@imfonts)); # text frame my $textf = $win->Frame(-bd => 1, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 6); my $eframe = labeledEntry($textf, 'top', 19, "Logo text", \$logo{text}); $eframe->{entry}->bind('', sub { $logo{size_x} = int($logo{font_size}*(length($logo{text})*0.7)+1) + $logo{offset_x}; $preview_button->Invoke(); }); # font frame my $fontf = $win->Frame(-bd => 1, -relief => 'groove')->pack(-expand => 0, -fill => 'x', -padx => 3, -pady => 3); my $ff = $fontf->Frame()->pack(-fill => 'x', -padx => 6, -pady =>3); $ff->Label(-text => 'Font', -width => 19, -anchor => 'w')->pack(-side => 'left'); $ff->Button(-textvariable => \$logo{font}, -command => sub { my ($ok, $font) = image_magick_select_font(); if ($ok) { $logo{font} = $font; $preview_button->Invoke(); } })->pack(-expand => 0, -side => 'left', -fill => 'x'); labeledScale($fontf, 'top', 19, 'Font size (point)', \$logo{font_size}, 6, 300, 1, sub { $logo{offset_x} = int($logo{font_size}*0.2); $logo{offset_y} = int($logo{font_size}*0.9); $logo{size_x} = int($logo{font_size}*(length($logo{text})*0.7)+1) + $logo{offset_x}; $logo{size_y} = int($logo{font_size}*1.3); $logo{offset_shadow} = int($logo{font_size}*0.06);}); labeledEntryColor($fontf,'top',19,"Font color",'Set',\$logo{color_font}); my $bgce = labeledEntryColor($fontf,'top',19,"Background color",'Set',\$logo{color_bg}); $balloon->attach($bgce, -msg => "Select logo background color.\nHint: Enter string \"none\" to get a transparent background.\nTransparent colors are not supported by JPEG, but e.g. by PNG pictures."); # file name labeledEntry($win, 'top', 19, "Logo file", \$logo{outfile}); # shadow frame #my $shadf = $win->Frame(-bd => 1, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); # shadow options frame (optinal) my $shadfopt = $win->Frame(-bd => 1, -relief => 'groove'); my $show_shadfopt = 0; my $show_shadfoptb; $show_shadfoptb = $win->Checkbutton(-text => "Show shadow settings", -variable => \$show_shadfopt, -command => sub { if ($show_shadfopt) { $shadfopt->pack(-after => $show_shadfoptb, -fill => 'x', -padx => 3, -pady => 3); } else { $shadfopt->packForget(); }})->pack(-anchor => 'w'); $shadfopt->Checkbutton(-text => "Add a shadow", -variable => \$logo{shadow})->pack(-anchor => 'w'); labeledScale($shadfopt, 'top', 19, 'Shadow offset (pixel)', \$logo{offset_shadow}, 0, 100, 1); labeledEntryColor($shadfopt,'top',19,"Shadow color",'Set',\$logo{color_shadow}); # position frame my $posf = $win->Frame(-bd => 1, -relief => 'groove'); my $show_pos = 0; my $show_posb; $show_posb = $win->Checkbutton(-text => "Show size/position settings", -variable => \$show_pos, -command => sub { if ($show_pos) { $posf->pack(-after => $show_posb,-fill => 'x', -padx => 3, -pady => 3); } else { $posf->packForget(); }})->pack(-anchor => 'w'); my $maxsize = 3000; labeledScale($posf, 'top', 19, 'Logo width (pixel)', \$logo{size_x}, 10, $maxsize, 1); labeledScale($posf, 'top', 19, 'Logo height (pixel)', \$logo{size_y}, 10, $maxsize, 1); my $xs = labeledScale($posf, 'top', 19, 'offset x', \$logo{offset_x}, 0, $maxsize/2, 1); $balloon->attach($xs, -msg => "Offset from first letter to left side of logo in pixel"); my $ys = labeledScale($posf, 'top', 19, 'offset y', \$logo{offset_y}, 0, $maxsize/2, 1); $balloon->attach($ys, -msg => "Offset of text baseline from top of logo in pixel"); # preview frame my $c; # canvas widget my $pf = $win->Frame()->pack(-expand => 1, -fill => 'both', -padx => 3); $preview_button = $pf->Button(-image => $mapivi_icons{'Update'}, -command => sub { $c->delete('all'); my $outfile = $logo{outfile}; # save file name ... $logo{outfile} = "$trashdir/logoXYZ554.jpg"; # will be overwritten logo_generate(\%logo); if (-f $logo{outfile}) { $win->{preview} = $win->Photo(-file => $logo{outfile}); if ($win->{preview}) { # insert pic $c->createImage(0,0, -image => $win->{preview}, -anchor => 'nw'); my ($w, $h) = getSize($logo{outfile}); $c->configure(-scrollregion => [0, 0, $w, $h]); } else { print "Could not create photo object from $logo{outfile}\n"; } } else { print "Logo preview file $logo{outfile} not available.\n"; } $logo{outfile} = $outfile; # ... restore file name })->pack(-side => 'left'); $c = $pf->Scrolled("Canvas", -width => 600, -height => 250, -scrollbars => 'osoe')->pack(-expand => 1, -fill => 'both', -side => 'left'); # button frame my $butF1 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); # OK Button $butF1->Button(-text => lang('Ok'), -command => sub { if (-f $logo{outfile}) { my $rc = $win->Dialog( -title => "File exists", -text => "$logo{outfile} already exists!", -buttons => ['Overwrite', 'Cancel'])->Show(); return if ($rc ne 'Overwrite'); } logo_generate(\%logo); log_it("Logo generated"); deleteCachedPics($logo{outfile}); # force reloading my $dir = dirname($logo{outfile}); if ($actdir ne $dir) { openDirPost($dir); } else { updateThumbs(); } showPic($logo{outfile}); # clean up preview photo object (free mem) $win->{preview}->delete if ($win->{preview}); logo_save_defaults(\%logo); # close window $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $butF1->Button(-text => lang('Cancel'), -command => sub { # clean up preview photo object (free mem) $win->{preview}->delete if ($win->{preview}); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->Popup(); $preview_button->Invoke(); #repositionWindow($win); $win->waitWindow; } ############################################################## # generate a text logo using image magick - overwrites outfile without warning!!! so better check before! ############################################################## sub logo_generate { my $logo = shift; # hash ref my $font_coords = "+".($$logo{offset_x}+0)."+".($$logo{offset_y}+0); # force numeric context (+0) my $shadow_coords = "+".($$logo{offset_x}+$$logo{offset_shadow})."+".($$logo{offset_y}+$$logo{offset_shadow}); my $command = "convert -size $$logo{size_x}x$$logo{size_y} xc:\"$$logo{color_bg}\" -font \"$$logo{font}\" "; $command .= "-pointsize $$logo{font_size} "; # shadow $command .= "-fill \"$$logo{color_shadow}\" -annotate $shadow_coords \"$$logo{text}\" -blur 0x4 " if ($$logo{shadow}); # text $command .= "-fill \"$$logo{color_font}\" -annotate $font_coords \"$$logo{text}\" "; # outfile $command .= "\"$$logo{outfile}\""; print "com=$command\n" if $verbose; execute($command); my $comment = "Logo generated by Mapivi $version ($mapiviURL):\n".$command; addCommentToPic($comment, $logo{outfile}, NO_TOUCH); } ############################################################## # addDecoration ############################################################## sub addDecoration { return if (!checkExternProgs("addDecoration", "mogrify")); my $index = shift; my @sellist; if ((defined $index) and ($index >= 0) and ($index < $picLB->info('children'))) { push @sellist, $index; } else { @sellist = $picLB->info('selection'); } my $selected = @sellist; return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); log_it("adding decorations to $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); return if (!decorationDialog($selected,1)); my $pw = progressWinInit($top, "Adding decoration"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "adding decorations ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); my $command = "mogrify ".makeDrawOptions($dpic)."-quality ".$config{PicQuality}." \"$dpic\""; execute($command); addDropShadow($dpic); deleteCachedPics($dpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one updateOneRow($dpic, $picLB); } progressWinEnd($pw); reselect($picLB, @sellist); log_it("ready! ($i of $selected)"); generateThumbs(ASK, SHOW); } ############################################################## # addDropShadow - to be called after makeDrawOptions and # mogrify # operates on the pic directly # a backup has to be made before ############################################################## sub addDropShadow { my $dpic = shift; return unless (-f $dpic); return unless ($config{DropShadow}); my $b4 = $config{DropShadowWidth} * 4; my $b3 = $config{DropShadowWidth} * 3; my $command = "convert -depth 8 -colors 1 -gamma 0 \"$dpic\" -bordercolor \"".$config{DropShadowBGColor}."\" -border ${b4}x${b4} -gaussian 0x".$config{DropShadowBlur}." -shave ${b3}x${b3} - | composite -quality ".$config{PicQuality}." -gravity northwest \"$dpic\" - \"$dpic\""; #(system "$command") == 0 or warn "$command failed: $!"; print "addDropShadow: $command\n" if $verbose; execute($command); } ############################################################## # makeDrawOptions ############################################################## sub makeDrawOptions { my $dpic = shift; my $command = ''; my $x = $config{CopyX}; my $y = $config{CopyY}; if ($config{BorderAdd}) { $command .= '-bordercolor "'.$config{BorderColor1}.'" -border '.$config{BorderWidth1x}.'x'.$config{BorderWidth1y}.' '; $command .= '-bordercolor "'.$config{BorderColor2}.'" -border '.$config{BorderWidth2x}.'x'.$config{BorderWidth2y}.' ' if (($config{BorderWidth2x} > 0) or ($config{BorderWidth2y} > 0)); $command .= '-bordercolor "'.$config{BorderColor3}.'" -border '.$config{BorderWidth3x}.'x'.$config{BorderWidth3y}.' ' if (($config{BorderWidth3x} > 0) or ($config{BorderWidth3y} > 0)); $command .= '-bordercolor "'.$config{BorderColor4}.'" -border '.$config{BorderWidth4x}.'x'.$config{BorderWidth4y}.' ' if (($config{BorderWidth4x} > 0) or ($config{BorderWidth4y} > 0)); } if ($config{CopyAdd}) { if ($config{CopyTextOrLogo} eq "text") { # text $command .= "-gravity $config{CopyPosition} "; my $geo1 = ($x+5).",".($y+5); my $geo2 = "$x,$y"; print "drawoptions: x = $x y = $y geo1 = $geo1 geo2 = $geo2\n" if $verbose; $command .= "-font \"$config{CopyFontFamily}\" -pointsize $config{CopyFontSize} "; $command .= "-fill \"$config{CopyFontColBG}\" -annotate $geo1 \"$config{Copyright}\" " if $config{CopyFontShadow}; $command .= "-fill \"$config{CopyFontColFG}\" -annotate $geo2 \"$config{Copyright}\" "; print "com=$command\n"; } else { # logo image my ($lw, $lh) = getSize($config{CopyrightLogo}); my ($pw, $ph) = getSize($dpic); if ($config{BorderAdd}) { # calc new size of pic (including borders) $pw += 2 * $config{BorderWidth1x}; $pw += 2 * $config{BorderWidth2x}; $pw += 2 * $config{BorderWidth3x}; $ph += 2 * $config{BorderWidth1y}; $ph += 2 * $config{BorderWidth2y}; $ph += 2 * $config{BorderWidth3y}; } if ($config{CopyPosition} eq 'NorthEast') { $x = $pw - $lw - $x; } elsif ($config{CopyPosition} eq 'North') { $x = $pw/2 - $lw/2 - $x; } elsif ($config{CopyPosition} eq 'SouthWest') { $y = $ph - $lh - $y; } elsif ($config{CopyPosition} eq 'South') { $y = $ph - $lh - $y; $x = $pw/2 - $lw/2 - $x; } elsif ($config{CopyPosition} eq 'SouthEast') { $y = $ph - $lh - $y; $x = $pw - $lw - $x; } $x = int($x); $y = int($y); my $geo = "$x,$y"; $command .= "-draw \"image Over $geo $lw,$lh '".$config{CopyrightLogo}."'\" "; } } print "command == $command\n";# if $verbose; return $command; } ############################################################## # buildBackupName ############################################################## sub buildBackupName { my $dpic = shift; my ($pic,$dir,$suffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $bpic = "${dir}${pic}-bak$suffix"; return $bpic; } ############################################################## # makeBackup ############################################################## sub makeBackup { my $dpic = shift; return 0 if (!-f $dpic); return 1 if (!$config{MakeBackup}); my $dir = dirname($dpic); my $dthumb = getThumbFileName($dpic); my $bpic = buildBackupName($dpic); # make a backup file if (!mycopy($dpic, $bpic, ASK_OVERWRITE)) { my $rc = $top->messageBox(-icon => 'question', -message => "Proceed anyway?", -title => "Proceed?", -type => 'OKCancel'); if ($rc =~ m/Ok/i) { return 1; } else { return 0; } } # copy the thumbnail too mycopy($dthumb, getThumbFileName($bpic), OVERWRITE); if (!-f $bpic) { warn "backup failed, there is no $bpic, giving up ..."; return 0; } else { # copy meta info in search database (needed e.g. for nr. of views) $searchDB{$bpic} = $searchDB{$dpic}; # insert backup bpic in listbox with thumbnail after dpic addOneRow($picLB, $bpic, 1, $dpic); } return 1; } ############################################################## ############################################################## sub get_image_magick_version { # get ImageMagick version if (`convert 2>&1` =~ m/.*ImageMagick (\d+)\.(\d+)\.(\d+).*/) { return (1,$1,$2,$3); } return 0; } ############################################################## ############################################################## sub image_magick_select_font { # open window my $win = $top->Toplevel(); $win->title('Select Font'); $win->iconimage($mapiviicon) if $mapiviicon; my @imfonts = getImageMagickFonts(); my $w = 300; my $h = 26; $win->Label(-text => 'Click on font to select')->pack(-padx => 3, -pady => 3); my $c = $win->Scrolled("Canvas", -width => $w+20, -height => 600, -scrollbars => 'osoe')->pack(-expand => 1, -fill => 'both'); $c->configure(-cursor => "hand2"); $win->Popup; my %logo; # data structure $logo{size_x} = $w; $logo{size_y} = $h; $logo{font_size} = int($h*4/5); $logo{offset_x} = 2; $logo{offset_y} = int($h*0.8); $logo{shadow} = 0; $logo{offset_shadow} = 5; $logo{color_font} = 'black'; $logo{color_shadow} = 'gray20'; my $y = 0; my %pics; # hash to store all photo object todo: delete after usage my $pw = progressWinInit($win, 'Building font preview ...'); my $i = 0; my $ok = 0; my $sel_font = ''; foreach my $font (@imfonts) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Font: $font ($i/".scalar @imfonts.") ...", $i, scalar @imfonts); $logo{text} = $font; $logo{font} = $font; $logo{outfile} = "$trashdir/$font.jpg"; if (not -f $logo{outfile}) { logo_generate(\%logo); } $pics{$font} = $win->Photo(-file => $logo{outfile}) if (-f $logo{outfile}); if ($pics{$font}) { my $id = $c->createImage(0, $y, -image => $pics{$font}, -anchor => "nw", -tags => "$font"); my ($x1, $y1, $x2, $y2) = $c->bbox($id); $c->configure(-scrollregion => [0, 0, $x2, $y2]); $c->bind(+"$font", '', sub { $sel_font = $font; $ok = 1; $win->destroy;}); $y += $h+1; } } progressWinEnd($pw); $win->waitWindow(); return ($ok, $sel_font); } ############################################################## # getImageMagickFonts - get the font families supported by IM ############################################################## sub getImageMagickFonts { return if (!checkExternProgs('getImageMagickFonts', 'identify')); my ($ok, $im1, $im2, $im3) = get_image_magick_version(); if (not $ok) { warn "Could not get Image Magick version number!\n"; return; } # the API changed with version 6.3.5-7 my $fonts; if ($im1*100+$im2*10+$im3 <= 635) { print "old image magick version <= 635 $im1.$im2.$im3\n" if $verbose; $fonts = `identify -list type`; } else { print "new image magick version > 635 $im1.$im2.$im3\n" if $verbose; $fonts = `identify -list font`; } my %families; my @lines = split(/\n/, $fonts); foreach my $line (@lines) { #print "line = $line\n"; # \s = whitespace \S = non-whitespece \d = number if ($line =~ m |(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)|) { $families{$2} = 1; } if ($line =~ m |Font: (\S+)|) { $families{$1} = 1; } } my @font_families = sort keys(%families); #print "font_families: $_\n" foreach (@font_families); return @font_families; } my $decoW; ############################################################## # decorationDialog ############################################################## sub decorationDialog { if (Exists($decoW)) { $decoW->deiconify; $decoW->raise; return; } my $pics = shift; my $QandB = shift; # bool - show Quality-Scale and Backup-Checkbutton my $rc = 0; my $max = 1000; #my @fontFamilies = sort $top->fontFamilies; my @fontFamilies = getImageMagickFonts(); # open window $decoW = $top->Toplevel(); $decoW->title('Add border/copyright/shadow'); $decoW->iconimage($mapiviicon) if $mapiviicon; my $addF = $decoW->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-anchor => 'w', -fill => 'x', -padx => 5, -pady => 3); $addF->Label(-text => "Process $pics pictures", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w', -fill => 'x', -padx => 5, -pady => 3); $addF->Label(-text => "Add ", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); $addF->Checkbutton(-text => "border ", -variable => \$config{BorderAdd})->pack(-side => 'left', -anchor => 'w'); $addF->Checkbutton(-text => "copyright info ", -variable => \$config{CopyAdd})->pack(-side => 'left', -anchor => 'w'); $addF->Checkbutton(-text => "drop shadow", -variable => \$config{DropShadow})->pack(-side => 'left', -anchor => 'w'); my $notebook = $decoW->NoteBook(#-width => 500, -background => $conf{color_bg}{value}, # background of active page (including its tab) -inactivebackground => $conf{color_entry}{value}, # tabs of inactive pages -backpagecolor => $conf{color_bg}{value}, # background behind notebook )->pack(-expand => "yes", -fill => 'both', -padx => 5, -pady => 5); my $cF = $notebook->add("border", -label => "Border"); my $bF = $notebook->add("copy", -label => "Copyright"); my $dF = $notebook->add("shadow", -label => "Drop shadow"); if ($QandB) { my $qS = labeledScale($decoW, 'top', 19, lang("Quality (%)"), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); buttonBackup($decoW, 'top'); buttonComment($decoW, 'top'); } # ### copyright ### my $pfa = $bF->Frame()->pack(-anchor => 'w'); $pfa->Label(-text => "Position in picture", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w', -padx => 3); my $pf = $pfa->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-side => 'left'); my $pfn = $pf->Frame()->pack(); my $pfs = $pf->Frame()->pack(); foreach my $gravity (qw(NorthWest North NorthEast)) { my $but = $pfn->Radiobutton(-text => '', -variable => \$config{CopyPosition}, -value => $gravity)->pack(-side => 'left'); $balloon->attach($but, -msg => "Align the copyright text or logo in $gravity position"); } foreach my $gravity (qw(SouthWest South SouthEast)) { my $but = $pfs->Radiobutton(-text => '', -variable => \$config{CopyPosition}, -value => $gravity)->pack(-side => 'left'); $balloon->attach($but, -msg => "Align the copyright text or logo in $gravity position"); } labeledScale($bF, 'top', 17, "x offset", \$config{CopyX}, 0, $max, 1); labeledScale($bF, 'top', 17, "y offset", \$config{CopyY}, 0, $max, 1); my $ctF = $bF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-anchor => 'w', -fill => 'x',-padx => 5, -pady => 5); my $clF = $bF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-anchor => 'w', -fill => 'x', -padx => 5, -pady => 5); $ctF->Radiobutton(-text => "add copyright text", -variable => \$config{CopyTextOrLogo}, -value => "text")->pack(-anchor => 'w'); labeledEntry($ctF, 'top', 17, "Copyright text", \$config{Copyright}); my $fontF = $ctF->Frame(-bd => 0)->pack(-anchor => 'w', -padx => 5, -pady => 3); my $fontF2 = $ctF->Scrolled('Pane', -bd => $config{Borderwidth}, -relief => 'groove', -scrollbars => 'osoe', -height => 80, -width => 480)->pack(-anchor => 'w', -padx => 5, -pady => 3); $fontF->Label(-text => "Font family", -bg => $conf{color_bg}{value})->pack(-side => 'left'); my $fontL = $fontF2->Label(-textvariable => \$config{Copyright}, -bg => $conf{color_bg}{value})->pack(-side => 'left'); $fontF->Optionmenu(-textvariable => \$config{CopyFontFamily}, -options => \@fontFamilies, -command => sub { $decoW->Busy; my $font = $top->Font(-family => $config{CopyFontFamily}, -size => $config{CopyFontSize}); $fontL->configure(-font => $font) if (ref($font) eq 'HASH'); $fontL->update(); $decoW->Unbusy; })->pack(-side => 'left', -anchor => 'w'); $fontF->Label(-text => "Font size", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $fontF->Scale( -variable => \$config{CopyFontSize}, -from => 5, -to => 200, -resolution => 1, -sliderlength => 30, -orient => 'horizontal', -showvalue => 0, -width => 15, -bd => $config{Borderwidth}, -command => sub { $decoW->Busy; my $font = $top->Font(-family => $config{CopyFontFamily}, -size => $config{CopyFontSize}); $fontL->configure(-font => $font); $fontL->update(); $decoW->Unbusy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $fontF->Label(-textvariable => \$config{CopyFontSize})->pack(-side => 'left'); labeledEntryColor($ctF, 'top', 17, "Foreground color", 'Set', \$config{CopyFontColFG}); $ctF->Checkbutton(-variable => \$config{CopyFontShadow}, -anchor => 'w', -text => "Add a shadow to the copyright text" )->pack(-anchor => 'w', -padx => 5, -pady => 3); labeledEntryColor($ctF, 'top', 17, "Shadow color", 'Set', \$config{CopyFontColBG}); $clF->Radiobutton(-text => "add copyright logo (image)", -variable => \$config{CopyTextOrLogo}, -value => "logo")->pack(-anchor => 'w'); labeledEntryButton($clF,'top',17,"path/name of logo",'Set',\$config{CopyrightLogo}); # ### border ### $cF->Label(-text => "Add one or several borders around pictures", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); my $wi = 25; my $bF1 = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $bF1->Label(-text => "Border 1 - innermost border", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); labeledScale($bF1, 'top', $wi, "Border width x-direction", \$config{BorderWidth1x}, 0, $max, 1); labeledScale($bF1, 'top', $wi, "Border width y-direction", \$config{BorderWidth1y}, 0, $max, 1); labeledEntryColor($bF1, 'top', $wi, "Color", 'Set', \$config{BorderColor1}); my $bF2 = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $bF2->Label(-text => "Border 2 - border around border 1 (use width 0 to disable)", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); labeledScale($bF2, 'top', $wi, "Border width x-direction", \$config{BorderWidth2x}, 0, $max, 1); labeledScale($bF2, 'top', $wi, "Border width y-direction", \$config{BorderWidth2y}, 0, $max, 1); labeledEntryColor($bF2, 'top', $wi, "Color", 'Set', \$config{BorderColor2}); my $bF3 = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $bF3->Label(-text => "Border 3 - border around border 2 (use width 0 to disable)", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); labeledScale($bF3, 'top', $wi, "Border width x-direction", \$config{BorderWidth3x}, 0, $max, 1); labeledScale($bF3, 'top', $wi, "Border width y-direction", \$config{BorderWidth3y}, 0, $max, 1); labeledEntryColor($bF3, 'top', $wi, "Color", 'Set', \$config{BorderColor3}); my $bF4 = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $bF4->Label(-text => "Border 4 - border around border 3 (use width 0 to disable)", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); labeledScale($bF4, 'top', $wi, "Border width x-direction", \$config{BorderWidth4x}, 0, $max, 1); labeledScale($bF4, 'top', $wi, "Border width y-direction", \$config{BorderWidth4y}, 0, $max, 1); labeledEntryColor($bF4, 'top', $wi, "Color", 'Set', \$config{BorderColor4}); # ### drop shadow ### $dF->Label(-text => "Add a drop shadow to the pictures", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); $dF->Label(-text => "(conversion may take some time)", -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 3); labeledScale($dF, 'top', 17, "Border width", \$config{DropShadowWidth}, 1, $max, 1); labeledScale($dF, 'top', 17, "Shadow blur", \$config{DropShadowBlur}, 1, 9, 1); labeledEntryColor($dF, 'top', 17, "Background color", 'Set', \$config{DropShadowBGColor}); my $ButF = $decoW->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $decoW->withdraw(); $decoW->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $decoW->withdraw(); $decoW->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($decoW, $Xbut); $decoW->Popup; $decoW->waitWindow; return $rc; } my $colw; ############################################################## # colorDialog ############################################################## sub colorDialog { if (Exists($colw)) { $colw->deiconify; $colw->raise; return; } my $rc = 0; # open window $colw = $top->Toplevel(); $colw->title('Color options'); $colw->iconimage($mapiviicon) if $mapiviicon; foreach (qw(Brightness Saturation Hue)) { labeledScale($colw, 'top', 16, "$_ (%)", \$config{"Pic$_"}, 0, 200, 1); } labeledScale($colw, 'top', 16, "Gamma", \$config{PicGamma}, 0.1, 10.0, 0.01); $colw->Button(-text => "Reset", -command => sub { foreach (qw(Brightness Saturation Hue)) { $config{"Pic$_"} = 100; } $config{PicGamma} = 1.00; })->pack(-anchor => 'w', -padx => 3, -pady => 3); my $OKB = $colw->Button(-text => "Close", -command => sub { $rc = 1; $colw->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $colw->bind('', sub { $OKB->Invoke; }); $colw->bind('', sub { $OKB->Invoke; }); $colw->Popup; $colw->waitWindow; } my $uw; ############################################################## # unsharpDialog ############################################################## sub unsharpDialog { if (Exists($uw)) { $uw->deiconify; $uw->raise; return; } my $rc = 0; # open window $uw = $top->Toplevel(); $uw->title('Unsharp mask options'); $uw->iconimage($mapiviicon) if $mapiviicon; my $usr =labeledScale($uw, 'top', 16, "Radius (pixel)", \$config{UnsharpRadius}, 0, 10, 1); $balloon->attach($usr, -msg => "The radius of the Gaussian, in pixels, not counting the center pixel. Use a radius of 0 and the function selects a suitable radius for you (default 0)"); my $uss = labeledScale($uw, 'top', 16, "Sigma (pixel)", \$config{UnsharpSigma}, 0.1, 10, 0.1); $balloon->attach($uss, -msg => "The standard deviation of the Gaussian,\nin pixels (default 1.0)"); my $usa = labeledScale($uw, 'top', 16, "amount (%)", \$config{UnsharpAmount}, 0, 100, 0.1); $balloon->attach($usa, -msg => "The percentage of the difference between the original\nand the blur image that is added back into the original\n(default 1.0)"); my $ust = labeledScale($uw, 'top', 16, "Threshold (frac)", \$config{UnsharpThreshold}, 0, 10, 0.01); $balloon->attach($ust, -msg => "The threshold, as a fraction of MaxRGB,\nneeded to apply the difference amount\n(default 0.05)"); $uw->Button(-text => "Default", -command => sub { $config{UnsharpRadius} = 0; $config{UnsharpSigma} = 1.0; $config{UnsharpAmount} = 1.0; $config{UnsharpThreshold} = 0.05; })->pack(-anchor => 'w', -padx => 3, -pady => 3); my $OKB = $uw->Button(-text => "Close", -command => sub { $rc = 1; $uw->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $uw->bind('', sub { $OKB->Invoke; }); $uw->bind('', sub { $OKB->Invoke; }); $uw->Popup; $uw->waitWindow; } my $lw; ############################################################## # levelDialog ############################################################## sub levelDialog { if (Exists($lw)) { $lw->deiconify; $lw->raise; return; } my $rc = 0; # open window $lw = $top->Toplevel(); $lw->title('Levels'); $lw->iconimage($mapiviicon) if $mapiviicon; my $lws = labeledScale($lw, 'top', 18, "White point (%)", \$config{LevelWhite}, 0, 100, 1); $balloon->attach($lws, -msg => "White point specifies the lightest color in the image. Colors brighter than the white point are set to the maximum quantum value."); my $lms = labeledScale($lw, 'top', 18, "Mid point (gamma)", \$config{LevelGamma}, 0.1, 10.0, 0.1); $balloon->attach($lms, -msg => "Mid point specifies a gamma correction to apply to the image."); my $lbs = labeledScale($lw, 'top', 18, "Black point (%)", \$config{LevelBlack}, 0, 100, 1); $balloon->attach($lbs, -msg => "The black point specifies the darkest color in the image. Colors darker than the black point are set to zero."); $lw->Button(-text => "Reset", -command => sub { $config{LevelWhite} = 100; $config{LevelGamma} = 1.0; $config{LevelBlack} = 0; })->pack(-anchor => 'w', -padx => 3, -pady => 3); my $OKB = $lw->Button(-text => "Close", -command => sub { $rc = 1; $lw->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $lw->bind('', sub { $OKB->Invoke; }); $lw->bind('', sub { $OKB->Invoke; }); $lw->Popup; $lw->waitWindow(); } ############################################################## ############################################################## sub get_IPTC_info { my $dpic = shift; my $ok = 0; # default return value is false my $iptc = {}; # hash reference # get IPTC data my $meta = getMetaData($dpic, 'APP13'); if ($meta) { $ok = 1; if (defined $meta->get_app13_data('TEXTUAL', 'IPTC')) { $iptc = $meta->get_app13_data('TEXTUAL', 'IPTC'); warn "IPTC segment of $dpic has errors!" if ($iptc->{error}); } } else { warn "get_IPTC_info: got no meta info of $dpic\n"; } print "get_IPTC_info $dpic\n" if $verbose; return ($ok, $iptc, $meta); } ############################################################## # get_IPTC_intersection ############################################################## sub get_IPTC_intersection { my $lb = shift; my $pic_list = shift; # pictures to intersect, array ref # use the IPTC info of the first picture as master my $first_pic = @{$pic_list}[0]; my ($ok, $iptc_i) = get_IPTC_info($first_pic); return $iptc_i if not $ok; my $i = 0; my $pw; # show a progressbar only if there are more than 5 pictures selected $pw = progressWinInit($lb, 'Analyzing IPTC data ...') if (@{$pic_list} > 5); foreach my $dpic (@{$pic_list}) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Collecting common data ($i/".scalar @{$pic_list}.") ...", $i, scalar @{$pic_list}); next if ($dpic eq $first_pic); # the first pic is already processed above as master my ($ok, $iptc) = get_IPTC_info($dpic); next if not $ok; # compare each key from the master foreach my $key (keys %{$iptc_i}) { my $ref = ref($iptc_i->{$key}); my $nr = scalar @{$iptc_i->{$key}}; # if key doesn't exists in one of the pictures we remove this key unless (exists $iptc->{$key}) { delete $iptc_i->{$key}; next; } # get the intersection of the key content (this works for single elements and lists) my @intersection = listIntersection($iptc_i->{$key}, $iptc->{$key}); # if there is something left we take the intersection if (@intersection) { $iptc_i->{$key} = \@intersection; } # else we remove the key else { delete $iptc_i->{$key}; } } } progressWinEnd($pw); return $iptc_i; } ############################################################## # editIPTC - edit IPTC info of one or multiple pictures ############################################################## sub editIPTC { my $lb = shift; my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); # check if some files are links return if (!checkLinks($lb, @sellist)); # init with the first picture in list my $dpic = $sellist[0]; my $pic = basename($dpic); # take the first picture as master for the IPTC data my ($ok, $iptcm) = get_IPTC_info($dpic); # $iptcm = IPTC master, hash reference if (not $ok) { log_it("Could not open IPTC segment of $pic"); return; } # handle several pictures: the IPTC dialog should just show common elements if (@sellist > 1) { $iptcm = get_IPTC_intersection($lb, \@sellist, $iptcm); } my @keywords_common = (); my @suppcats_common = (); foreach (@{$iptcm->{Keywords}}) { $_ =~ tr/ -~//cd; # remove non-printables (Picasa adds one to each keyword) } ${$iptcm->{Caption}}[0] =~ tr/\n\t\r\f -~//cd if (${$iptcm->{Caption}}[0]); # replace all non-printable chars, but not newline etc. # these are the common items (e.g. common keywords of all selected pictures) @keywords_common = @{$iptcm->{Keywords}} if (exists $iptcm->{Keywords}); @suppcats_common = @{$iptcm->{SupplementalCategory}} if (exists $iptcm->{SupplementalCategory}); my $rc = iptcDialog($iptcm, $pic, scalar @sellist); return if ($rc ne 'OK'); # after user interaction in the dialog my @keywords_master = (); @keywords_master = @{$iptcm->{Keywords}} if (exists $iptcm->{Keywords}); my @suppcats_master = (); @suppcats_master = @{$iptcm->{SupplementalCategory}} if (exists $iptcm->{SupplementalCategory}); # to remove keywords and categories we need to figure out what has been removed by the user my @keywords_removed = diffList(\@keywords_common, \@keywords_master); my @suppcats_removed = diffList(\@suppcats_common, \@suppcats_master); my $IPTC_action = $config{IPTC_action}; # if we edit a single picture we always use the replace mode $IPTC_action = 'REPLACE' if (@sellist == 1); my $errors = ''; my $i = 0; my $pw = progressWinInit($lb, "Writing IPTC info"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Writing IPTC info ($i/".scalar @sellist.") ...", $i, scalar @sellist); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my $meta = getMetaData($dpic, 'APP1'); # APP1 includes EXIF and IPTC (APP13) my $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); my $iptc; # copy (clone) master iptc hash to picture iptc hash $iptc = dclone($iptcm); if (($config{IPTCdateEXIF}) or ($config{IPTCtimeEXIF})) { my $date = getEXIFDate($dpic, $er); # date format YYYY:MM:DD HH:mm:SS my ($ok, $IPTCdate, $IPTCtime) = EXIFtoIPTCdatetime($date); if ($ok) { # according to IPTC - NAA INFORMATION INTERCHANGE MODEL, Version No. 4, 1999, http://www.iptc.org/IIM/ ${$iptc->{DateCreated}}[0] = $IPTCdate if ($config{IPTCdateEXIF}); # format CCYYMMDD ${$iptc->{TimeCreated}}[0] = $IPTCtime if ($config{IPTCtimeEXIF}); # format HHMMSS+HHMM } else { warn "picture has an unusual EXIF date: \"$date\" ($dpic)\n" if $config{MetadataWarn}; } } if ($config{IPTCbylineEXIF}) { if (defined $er) { my $owner = getEXIFowner($er); if ($owner ne '') { print "*** Writing \"$owner\" to $dpic\n" if $verbose; ${$iptc->{ByLine}}[0] = $owner; } } } if ($config{IPTCaddMapivi}) { ${$iptc->{OriginatingProgram}}[0] = 'Mapivi'; ${$iptc->{ProgramVersion}}[0] = $version; } # make some corrections for keywords and supp cats # according to the documentation of Image::MetaData::JPEG this should not be needed if ((@sellist > 1) and (($IPTC_action eq 'UPDATE') or ($IPTC_action eq 'ADD'))) { # todo problem is still, that removed elements (where nothing is left, e.g. a headline) are not removed in Update mode my $seg = $meta->retrieve_app13_segment(undef, 'IPTC'); # todo should be possible without segement if ($seg) { my $hashref = $seg->get_app13_data('TEXTUAL', 'IPTC'); my @keywords; # take the original items and add the items from the dialog (master) push @keywords, @{$hashref->{Keywords}} if (defined($hashref->{Keywords})); push @keywords, @keywords_master; # then remove items which have been removed in the dialog @keywords = diffList(\@keywords, \@keywords_removed); #@keywords = ('') unless (@keywords); $iptc->{Keywords} = \@keywords; my @suppcats; # take the original items and add the items from the dialog (master) push @suppcats, @{$hashref->{SupplementalCategory}} if (defined($hashref->{SupplementalCategory})); push @suppcats, @suppcats_master; # then remove items which have been removed in the dialog @suppcats = diffList(\@suppcats, \@suppcats_removed); $iptc->{SupplementalCategory} = \@suppcats; } } $meta->set_app13_data($iptc, $IPTC_action, 'IPTC'); uniqueIPTC($meta); if ($meta->save()) { # success # if urgency / rating is not equal 0 we also modify the XMP rating tag if (exists $iptcm->{Urgency} and ${$iptcm->{Urgency}}[0] != 0) { xmp_set_rating($dpic, ${$iptcm->{Urgency}}[0]) if $conf{xmp_rating}{value}; } } else { $errors .= "save failed for $dpic\n"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding touch(getThumbFileName($dpic)); updateOneRow($dpic, $lb); if ($dpic eq $actpic) { showImageInfoCanvas($dpic); # update_IPTC_frame_content($dpic); } } progressWinEnd($pw); log_it("ready! ($i/".scalar @sellist." written)"); showText("Errors while editing IPTC info", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## # returns EXIF owner, or artist, or user comment, or empty string # input: EXIF data of meta data # here is how to get $er: # my $meta = getMetaData($dpic, 'APP1'); # APP1 includes EXIF and IPTC (APP13) # my $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); ############################################################## sub getEXIFowner { my $er = shift; my $owner = ''; if (defined $er->{SUBIFD_DATA}->{OwnerName}) { $owner = join('', @{$er->{SUBIFD_DATA}->{OwnerName}}); } elsif (defined $er->{IFD0_DATA}->{Artist}) { $owner = join('', @{$er->{IFD0_DATA}->{Artist}}); } elsif (defined $er->{SUBIFD_DATA}->{UserComment}) { $owner = join('', @{$er->{SUBIFD_DATA}->{UserComment}}); } else { } if ($owner ne '') { $owner =~ tr/ -~//cd; # remove non-printable characters (but not \n) $owner =~ s/ASCII//g; # cut 'ASCII' $owner =~ s/^\s+//; # cut leading white $owner =~ s/\s+$//; # cut trailing white } return $owner; } ############################################################## # convert EXIT date/time to IPTC format ############################################################## sub EXIFtoIPTCdatetime { my $date = shift; # EXIF date my ($IPTCdate, $IPTCtime); my $ok = 0; if (my ($y, $M, $d, $h, $m, $s) = $date =~ m/(\d\d\d\d):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)/) { $ok = 1; my $time = timelocal($s,$m,$h,$d,($M-1),($y-1900)); my $diff = ((localtime($time))[2] - (gmtime($time))[2]); # RJW: Correct timezone calculation in case of migration over # 24 hour border if ( $diff > 12 ) { $diff -= 24; } elsif ( $diff < -12 ) { $diff += 24; } my $GMToffset = sprintf("%+03d00", $diff); $IPTCdate = $y.$M.$d; $IPTCtime = $h.$m.$s.$GMToffset; } return ($ok, $IPTCdate, $IPTCtime); } ############################################################## # setIPTCurgency - set the urgency flag to a given value (0 .. 8) ############################################################## sub setIPTCurgency { my $lb = shift; # the reference to the active listbox widget my $urgency = shift; return unless (defined($urgency)); return if (($urgency < 0) or ($urgency > 9)); # 9 is used to clear the urgency flag my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); # check if some files are links return if (!checkLinks($lb, @sellist)); $urgency = '' if ($urgency == 9); # 9 is used to clear the urgency flag my $msg = "Writing IPTC urgence $urgency"; $msg = "Deleting IPTC urgence flag" if ($urgency eq ''); my $errors = ''; my $i = 0; my $pw; $pw = progressWinInit($lb, $msg) if (@sellist > 1); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "$msg ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my $ok = set_IPTC_urgency_file($dpic, $urgency, \$errors); if ($ok) { # urgency changed successfully! # set also XMP rating, if option is set xmp_set_rating($dpic, $urgency) if $conf{xmp_rating}{value}; print "saved IPTC urgency $urgency to $pic\n" if $verbose; # touch the thumbnail pic (set actual time stamp), to suppress rebuilding touch(getThumbFileName($dpic)); updateOneRow($dpic, $lb); if ($dpic eq $actpic) { #showImageInfoCanvas($dpic); showImageInfo($dpic); } } } progressWinEnd($pw); $msg = "Urgency $urgency written to"; $msg = "Removed urgency flag in" if ($urgency eq ''); log_it("ready! $msg $i of ".scalar @sellist." pictures"); showText("Errors and infos while saving IPTC urgency", $errors, NO_WAIT) if ($errors ne ''); } ############################################################## ############################################################## sub set_IPTC_urgency_file { my $dpic = shift; my $urgency = shift; my $errors = shift; # reference to string, errors will be added my ($ok, $iptc, $meta) = get_IPTC_info($dpic); if (not $ok) { $$errors .= "Could not open IPTC segment of $dpic\n"; return 0; } if ($config{UrgencyChangeWarning} and (defined $iptc->{Urgency}) and (${$iptc->{Urgency}}[0] ne $urgency)) { $$errors .= "Info: Rating (Urgency) changed from ".iptc_rating_stars_urg(${$iptc->{"Urgency"}}[0])." to ".iptc_rating_stars_urg($urgency)." $dpic\n"; } $iptc->{Urgency} = $urgency; $meta->set_app13_data($iptc, 'REPLACE', 'IPTC'); if (!$meta->save()) { $$errors .= "save failed for $dpic\n"; return 0; } return 1; } ############################################################## # getIPTCurgencyDB - get the urgency flag of a given file from # the search database # returns 9 if there is no file or no urgency ############################################################## sub getIPTCurgencyDB { my $dpic = shift; my $urgency = 9; $urgency = $searchDB{$dpic}{URG} if (defined $searchDB{$dpic}{URG}); return $urgency; } ############################################################## # getIPTCurgency - get the urgency flag of a given file # returns 9 if there is no file or no urgency ############################################################## sub getIPTCurgency { my $dpic = shift; my $meta = shift; # optional, the Image::MetaData::JPEG object of $dpic if available my $urgency = 9; return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); return 9 unless (-f $dpic); $meta = getMetaData($dpic, "APP13", 'FASTREADONLY') unless (defined($meta)); return 9 unless ($meta); my $seg = $meta->retrieve_app13_segment(undef, 'IPTC'); # todo should be possible without segement return 9 unless ($seg); my $hashref = $seg->get_app13_data("TEXTUAL", 'IPTC'); if (defined($hashref->{Urgency})) { $urgency = ${$hashref->{Urgency}}[0]; $urgency = 8 if ($urgency =~ /l/i); $urgency = 1 if ($urgency =~ /h/i); $urgency = 9 if ($urgency !~ /\d/); $urgency = 9 if ( ($urgency > 9) or ($urgency < 0) ); } $quickSortHash{$dpic} = $urgency if $quickSortSwitch; print "getIPTCurgency: -$urgency- $dpic\n" if $verbose; return $urgency; } ############################################################## # getIPTCkeywords - get the keywords of a given file # returns empty list if there is no file or # no keyword ############################################################## sub getIPTCkeywords { my $dpic = shift; my $meta = shift; # optional, the Image::MetaData::JPEG object of $dpic if available my @keywords = (); return @keywords unless (-f $dpic); $meta = getMetaData($dpic, 'APP13', 'FASTREADONLY') unless (defined($meta)); return @keywords unless ($meta); my $seg = $meta->retrieve_app13_segment(undef, 'IPTC'); # todo should be possible without segement return @keywords unless ($seg); my $hashref = $seg->get_app13_data('TEXTUAL', 'IPTC'); if (defined($hashref->{Keywords})) { @keywords = @{$hashref->{Keywords}}; } foreach (@keywords) { # translate it to a string if it is non-printing #my $key = $_; #$key =~ s/[\000-\037\177-\377]/sprintf "\\%02x",ord($&)/e; #print "key = -$key-\n"; $_ =~ tr/ -~//cd; # replace all non-printable chars (Picasa adds one to each keyword) } return @keywords; } ############################################################## # getIPTCByLine - get the by-line info of a given file ############################################################## sub getIPTCByLine { my $dpic = shift; my $byline = ''; return $quickSortHash{$dpic} if ($quickSortSwitch and defined $quickSortHash{$dpic}); return $byline unless (-f $dpic); my $meta = getMetaData($dpic, "APP13", 'FASTREADONLY'); return $byline unless ($meta); my $seg = $meta->retrieve_app13_segment(undef, 'IPTC'); # todo should be possible without segement return $byline unless ($seg); my $hashref = $seg->get_app13_data("TEXTUAL", 'IPTC'); $byline = ${$hashref->{ByLine}}[0] if (defined($hashref->{ByLine})); $quickSortHash{$dpic} = $byline if $quickSortSwitch; print "getIPTCByLine: $byline ($dpic)\n" if $verbose; return $byline; } ############################################################## # getIPTCAttr - get an IPTC attribute of a given file # returns empty string if attribute is not defined ############################################################## sub getIPTCAttr { my $dpic = shift; my $name = shift; my $val = ''; if (-f $dpic) { my $meta = getMetaData($dpic, 'APP13', 'FASTREADONLY'); if ($meta) { my $seg = $meta->retrieve_app13_segment(undef, 'IPTC'); # todo should be possible without segement if ($seg) { my $hashref = $seg->get_app13_data('TEXTUAL', 'IPTC'); if (defined($hashref->{$name})) { $val = ${$hashref->{$name}}[0]; print "getIPTCAttr: $name=$val ($dpic)\n" if $verbose; } } } } return $val; } ############################################################## # getIPTCObjectName - get the object name of a given file ############################################################## sub getIPTCObjectName { my $dpic = shift; return getIPTCAttr($dpic, "ObjectName"); } ############################################################## # getIPTCHeadline - get the headline of a given file ############################################################## sub getIPTCHeadline { my $dpic = shift; return getIPTCAttr($dpic, "Headline"); } ############################################################## # getIPTCCaption - get the caption of a given file ############################################################## sub getIPTCCaption { my $dpic = shift; return getIPTCAttr($dpic, "Caption/Abstract"); } ############################################################## # getIPTCByLineTitle - get the by-line title of a given file ############################################################## sub getIPTCByLineTitle { my $dpic = shift; return getIPTCAttr($dpic, "ByLineTitle"); } ############################################################## # getIPTCSublocation - get the sublocation of a given file ############################################################## sub getIPTCSublocation { my $dpic = shift; return getIPTCAttr($dpic, "SubLocation"); } ############################################################## # getIPTCCity - get the city of a given file ############################################################## sub getIPTCCity { my $dpic = shift; return getIPTCAttr($dpic, "City"); } ############################################################## # getIPTCProvince - get the province/state of a given file ############################################################## sub getIPTCProvince { my $dpic = shift; return getIPTCAttr($dpic, "Province/State"); } ############################################################## # getIPTCCountryCode - get the country code of a given file ############################################################## sub getIPTCCountryCode { my $dpic = shift; return getIPTCAttr($dpic, "Country/PrimaryLocationCode"); } ############################################################## # iptcDialog ############################################################## sub iptcDialog { my $iptc = shift; my $picname = shift; my $nr = shift; # number of pics my $rc = 'Cancel'; my @tag_list; # used to store all IPTC tags which are already displayed, all others will go to the misc tab # open window my $t = $top->Toplevel(); $t->title("Edit IPTC/IIM information of $nr pictures ($picname)"); $t->iconimage($mapiviicon) if $mapiviicon; $t->geometry($conf{iptc_geometry}{value}); # we use a pane to support small screen resolutions, the user is still able to scroll e.g. to the buttons at the bottom of the window my $pane = $t->Scrolled('Pane', -scrollbars => 'osoe')->pack(-expand => 1, -fill => 'both'); my $notebook = $pane->NoteBook(-background => $conf{color_bg}{value}, # background of active page (including its tab) -inactivebackground => $conf{color_entry}{value}, # tabs of inactive pages -backpagecolor => $conf{color_bg}{value}, # background behind notebook )->pack(-expand => 1, -fill => 'both', -padx => 5, -pady => 5); my $aN = $notebook->add('stan', -label => 'Standard'); my $bN = $notebook->add('misc', -label => 'Misc'); my $cN = $notebook->add('opt', -label => 'Options'); $notebook->raise($config{IPTCLastPad}); my $w = 11; my $ent; ####### Standart IPTC tags ############# # left and right frame on standard tab my $aF = $aN->Frame(-bd => 0)->pack(-side => 'left', -expand => 1, -fill => 'both', -padx => 3, -pady => 0); my $bF = $aN->Frame(-bd => 0)->pack(-side => 'left', -expand => 1, -fill => 'both', -padx => 3, -pady => 0); my @alist = ('Headline', 'ObjectName'); foreach (@alist) { $ent = labeledEntry($aF,'top',$w,$_,\${$iptc->{$_}}[0], 5); if (defined $iptcHelp{$_}) { $balloon->attach($ent, -msg => formatString($_.":\n".$iptcHelp{$_}, 80, -1)) if (Exists $ent); } } push @tag_list, @alist; # add already displayed elements to the list ####### Caption ############# my $capF = $aF->Frame(-bd => 0)->pack(-anchor=>'w', -expand => 1, -fill => 'both', -padx => 3, -pady => 3); $capF->Label(-text => 'Caption/Abstract', -bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 2, -pady => 2); my $caption = $capF->Scrolled("Text", -scrollbars => 'osoe', -wrap => 'word', -width => 60, -height => 6, )->pack(-expand => 1, -fill => 'both', -padx => 2, -pady => 2); $caption->insert('end', ${$iptc->{'Caption/Abstract'}}[0]); $caption->see(0.1); push @tag_list, 'Caption/Abstract'; # add already displayed elements to the list ####### Urgency ############# my $oF = $aF->Frame(-bd => 0)->pack(-anchor=>'w', -padx => 3, -pady => 6); $balloon->attach($oF, -msg => "Rating/Urgency\n0 = no\n1 = High ********\n2 = *******\n3 = ******\n4 = *****\n5 = Normal ****\n6 = ***\n7 = **\n8 = Low *"); $oF->Label(-text => "Rating/Urgency", -bg => $conf{color_bg}{value}, -width => 15, -anchor => 'w')->pack(-side => 'left', -anchor => 'w', -padx => 2, -pady => 2); $oF->Optionmenu(-variable => \${$iptc->{Urgency}}[0], -textvariable => \${$iptc->{Urgency}}[0], -options => [0,1,2,3,4,5,6,7,8])->pack(-side => 'left', -anchor => 'w', -padx => 0); push @tag_list, 'Urgency'; # add already displayed elements to the list if ($config{IPTCProfessional}) { ####### Writer/Editor and Credit ############# labeledDoubleEntry($aF, 'top', $w, 'Writer/Editor', 'Credit', \${$iptc->{'Writer/Editor'}}[0], formatString("Writer/Editor:\n".$iptcHelp{'Writer/Editor'}, 80, -1), \${$iptc->{'Credit'}}[0], formatString("Credit:\n".$iptcHelp{'Credit'}, 80, -1)); push @tag_list, ('Writer/Editor', 'Credit'); # add already displayed elements to the list } ####### BylineTitle and Byline ############# # !!! todo byline and bylinetitle are repeatable use e.g. .= "$_, " for @{$iptc->{$_}}; labeledDoubleEntry($aF, 'top', $w, 'ByLineTitle', 'Name', \${$iptc->{ByLineTitle}}[0], formatString("ByLineTitle:\n".$iptcHelp{'ByLineTitle'}, 80, -1), \${$iptc->{ByLine}}[0], formatString("ByLine:\n".$iptcHelp{'ByLine'}, 80, -1)); push @tag_list, ('ByLineTitle', 'ByLine'); # add already displayed elements to the list ####### EditStatus etc. ############## if ($config{IPTCProfessional}) { @alist = ('EditStatus', 'SpecialInstructions', 'Contact', 'Source', 'CopyrightNotice'); foreach (@alist) { $ent = labeledEntry($aF,'top',$w,$_,\${$iptc->{$_}}[0]); if (defined $iptcHelp{$_}) { # todo this cuts very long desc because of config{LineLimit} $balloon->attach($ent, -msg => formatString($_.":\n".$iptcHelp{$_}, 80, -1)) if (Exists $ent); } } push @tag_list, @alist; # add already displayed elements to the list } ####### Location ############## my $locF = $aF->Frame(-relief => 'raised')->pack(-anchor=>'w', -padx => 3, -pady => 3, -fill => 'x'); $locF->Label(-text => 'Location')->pack(-anchor => 'w', -padx => 2, -pady => 2); labeledDoubleEntry($locF, 'top', $w, 'Country', 'Code', \${$iptc->{'Country/PrimaryLocationName'}}[0], formatString("Country/PrimaryLocationName:\n".$iptcHelp{'Country/PrimaryLocationName'}, 80, -1), \${$iptc->{'Country/PrimaryLocationCode'}}[0], formatString("Country/PrimaryLocationCode:\n".$iptcHelp{'Country/PrimaryLocationCode'}, 80, -1)); labeledDoubleEntry($locF, 'top', $w, 'Province/State', 'City', \${$iptc->{'Province/State'}}[0], formatString("Province/State:\n".$iptcHelp{'Province/State'}, 80, -1), \${$iptc->{'City'}}[0], formatString("City:\n".$iptcHelp{'City'}, 80, -1) ); $ent = labeledEntry($locF,'top',$w,'SubLocation',\${$iptc->{'SubLocation'}}[0]); if (defined $iptcHelp{'SubLocation'}) { # todo this cuts very long desc because of config{LineLimit} $balloon->attach($ent, -msg => formatString("SubLocation:\n".$iptcHelp{'SubLocation'}, 80, -1)) if (Exists $ent); } push @tag_list, ('SubLocation', 'City', 'Province/State', 'Country/PrimaryLocationName', 'Country/PrimaryLocationCode'); ####### Date and Time ############ if ($config{IPTCProfessional}) { @alist = ('ReleaseDate', 'ReleaseTime', 'DateCreated', 'TimeCreated'); my $dateF = $aF->Frame(-bd => $config{Borderwidth}, -relief => 'raised')->pack(-anchor=>'w', -padx => 3, -pady => 3, -fill => 'x'); $dateF->Label(-text => 'Date and time')->pack(-anchor => 'w', -padx => 2, -pady => 2); labeledDoubleEntry($dateF, 'top', $w, 'Date created', 'Time', \${$iptc->{DateCreated}}[0], formatString("DateCreated:\n".$iptcHelp{'DateCreated'}, 80, -1), \${$iptc->{TimeCreated}}[0], formatString("TimeCreated:\n".$iptcHelp{'TimeCreated'}, 80, -1)); labeledDoubleEntry($dateF, 'top', $w, 'Date released', 'Time', \${$iptc->{ReleaseDate}}[0], formatString("ReleaseDate:\n".$iptcHelp{'ReleaseDate'}, 80, -1), \${$iptc->{ReleaseTime}}[0], formatString("ReleaseTime:\n".$iptcHelp{'ReleaseTime'}, 80, -1)); push @tag_list, @alist; # add already displayed elements to the list } ####### Keywords ############ my $keyword_frame = $bF->Frame(-bd => 0)->pack(-expand => 1, -fill => 'both', -padx => 0, -pady => 0); # get the keywords (list ref) doubleList($keyword_frame, \@prekeys, \@{$iptc->{Keywords}}, 'keywords'); push @tag_list, 'Keywords'; ####### Categories ########## my $category_frame; if ($config{IPTCProfessional} == 1) { $category_frame = $bF->Frame(-bd => 0)->pack(-expand => 1, -fill => 'both', -padx => 0, -pady => 0); $ent = labeledEntry($category_frame,'top',$w,'Category',\${$iptc->{Category}}[0]); if (defined $iptcHelp{Category}) { $balloon->attach($ent, -msg => formatString("Category:\n".$iptcHelp{Category}, 80, -1)); # todo } # supp categories ### doubleList($category_frame, \@precats, \@{$iptc->{SupplementalCategory}}, 'supplemental categories'); push @tag_list, ('Category', 'SupplementalCategory'); } ####### Misc ################# my $p = $bN->Scrolled("Pane", -scrollbars => "oe", -height => 300)->pack(-fill => 'both', -expand => 1); # build a frame, a label and an entry for every tag which is not yet displayed foreach (@IPTCAttributes) { next if (isInList($_, \@tag_list)); $ent = labeledEntry($p,'top',40,$_,\${$iptc->{$_}}[0]); if (defined $iptcHelp{$_}) { $balloon->attach($ent, -msg => formatString($_.":\n".$iptcHelp{$_}, 80, -1)); # todo } } ###### bottom frame my $exf = $pane->Frame()->pack(-anchor=>'w'); #my $exf2 = $t->Frame()->pack(-anchor=>'w'); my $edb = $exf->Checkbutton(-variable => \$config{IPTCdateEXIF}, -text => "EXIF date -> creation date ")->pack(-anchor => 'w', -side => 'left'); $balloon->attach($edb, -msg => 'This option will copy EXIF date, to the IPTC date created tag.'); my $etb = $exf->Checkbutton(-variable => \$config{IPTCtimeEXIF}, -text => "EXIF time -> creation time ")->pack(-anchor => 'w', -side => 'left'); $balloon->attach($etb, -msg => 'This option will copy EXIF time, to the IPTC time created tag.'); my $IbEo = $exf->Checkbutton(-variable => \$config{IPTCbylineEXIF}, -text => "EXIF owner -> ByLine ")->pack(-anchor => 'w', -side => 'left'); $balloon->attach($IbEo, -msg => 'This option will copy the content of EXIF Owner, or if not available the content of EXIF Artist, or if not available the content of EXIF UserComment to the IPTC ByLine tag.'); my $IMap = $exf->Checkbutton(-variable => \$config{IPTCaddMapivi}, -text => "Add Mapivi infos")->pack(-anchor => 'w', -side => 'left'); $balloon->attach($IMap, -msg => 'This option will insert Mapivi in the IPTC OriginatingProgram tag and the actual Mapivi version into the ProgramVersion tag.'); my $umlautB = $exf->Checkbutton(-variable => \$config{onlyASCII}, -text => 'Use only ASCII')->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 1); $balloon->attach($umlautB, -msg => "Remove non-ASCII chars and convert german umlaute (e.g. ä -> ae)\nNon-ASCII chars like umlaute often cause problems in other tools,\nso it's saver to remove or convert them to plain ASCII where possible."); my $optF = $cN->Frame()->pack(); $optF->Label(-text => 'IPTC dialog layout')->pack(-anchor => 'w'); $optF->Radiobutton(-text => 'Simple', -variable => \$config{IPTCProfessional}, -value => 0)->pack(-anchor => 'w'); $optF->Radiobutton(-text => 'Professional without Category', -variable => \$config{IPTCProfessional}, -value => 2)->pack(-anchor => 'w'); $optF->Radiobutton(-text => 'Professional with Category', -variable => \$config{IPTCProfessional}, -value => 1)->pack(-anchor => 'w'); $cN->Label(-text => 'Note: According to the IPTC standard Categories are deprecated.')->pack(); $cN->Label(-text => 'Please choose IPTC dialog layout, close dialog and open it again to see changes.')->pack(); my $f = $pane->Frame()->pack(-anchor=>'w',-fill => 'x', -expand => 0); # edit mode buttons only for more than one pictures if ($nr > 1) { my $rf = $f->Frame()->pack(-side => 'left', -anchor=>'w', -fill => 'x', -expand => 0); my $radioB = $rf->Label(-text => 'Edit mode')->pack(-side => 'left', -anchor => 'w'); $rf->Radiobutton(-text => lang('Add'), -variable => \$config{IPTC_action}, -value => 'ADD')->pack(-side => 'left', -anchor => 'w'); $rf->Radiobutton(-text => lang('Update'), -variable => \$config{IPTC_action}, -value => 'UPDATE')->pack(-side => 'left', -anchor => 'w'); $rf->Radiobutton(-text => lang('Replace'), -variable => \$config{IPTC_action}, -value => 'REPLACE')->pack(-side => 'left', -anchor => 'w'); $balloon->attach($rf, -msg => 'Add: new records are added and nothing is deleted; however, if you try to add a non-repeatable record which is already present, the newly supplied value ejects (replaces) the pre-existing value. Update: new records replace those characterised by the same tags, but the others are preserved. This makes it possible to modify some repeatable IPTC records without deleting the other tags. Replace: all records present in the IPTC sub folder are deleted before inserting the new ones.'); } my $okb = $f->Button(-text => lang('OK'), -command => sub { # get the caption ${$iptc->{'Caption/Abstract'}}[0] = $caption->get(0.1, 'end'); ${$iptc->{'Caption/Abstract'}}[0] =~ s/\s+$//; # remove trailing whitespace $config{IPTCLastPad} = $notebook->raised(); if (Exists $keyword_frame) { saveTreeMode($keyword_frame->{m_tree}); # todo nstore($keyword_frame->{m_tree}->{m_mode}, "$user_data_path/keywordMode") or warn "could not store $user_data_path/keywordMode: $!"; } if (Exists $category_frame) { saveTreeMode($category_frame->{m_tree}); # todo nstore($category_frame->{m_tree}->{m_mode}, "$user_data_path/categoryMode") or warn "could not store $user_data_path/categoryMode: $!"; } $conf{iptc_geometry}{value} = $t->geometry; # save window size $t->destroy; # close window $rc = 'OK'; } )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 3, -pady => 3); $balloon->attach($okb, -msg => "You can press Control-x to close the dialog (like OK button)"); $t->bind('', sub { $okb->Invoke; }); my $Xbut = $f->Button(-text => lang('Cancel'), -command => sub { $config{IPTCLastPad} = $notebook->raised(); $conf{iptc_geometry}{value} = $t->geometry; # save window size $t->destroy; # close window $rc = 'Cancel'; } )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 3, -pady => 3); $balloon->attach($Xbut, -msg => "You can press ESC to close the dialog (like Cancel button)"); bind_exit_keys_to_button($t, $Xbut); $t->waitWindow; return $rc; } ############################################################## # doubleList - mega widget containing two listboxes, an entry # and some buttons ############################################################## sub doubleList { my $widget = shift; # mother widget my $l1 = shift; # predefined list ref my $l2 = shift; # real list ref my $name = shift; # build a frame for the keywords/categories my $f = $widget->Frame(-bd => $config{Borderwidth}, -relief => 'raised')->pack(-expand => 1, -fill => 'both', -anchor=>'w', -padx => 3, -pady => 3); $f->Label(-text => $name, -bg => $conf{color_bg}{value})->pack(-anchor=>'w', -padx => 2, -pady => 2); my $fc1 = $f->Frame()->pack(-expand => 1, -fill => 'both', -side => 'left', -anchor=>'n'); my $fc2 = $f->Frame()->pack(-expand => 0, -fill => 'x', -side => 'left', -anchor=>'n'); my $fc3 = $f->Frame()->pack(-expand => 1, -fill => 'both', -side => 'left', -anchor=>'n'); $fc1->Label(-text => "common tags", -bg => $conf{color_bg}{value})->pack(-anchor=>'w', -padx => 2, -pady => 2); my $catLB2; my $category = ''; my $fcent = $fc1->Entry(-textvariable => \$category, -width => 20)->pack(-fill => 'x', -padx => 2, -pady => 2); $fcent->bind('', sub { return if ($category eq ''); # check if keyword/category is allready in list return if isInList($category, $l2); push @$l2, $category; $category = ''; @$l2 = sort { uc($a) cmp uc($b) } @$l2; $catLB2->delete(0, 'end'); $catLB2->insert('end', @$l2); }); my $tree = $fc1->Scrolled('Tree', -separator => '/', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 26, -height => 14, )->pack(-expand => 1, -fill => 'both', -padx => 2, -pady => 2); $widget->{m_tree} = $tree; $balloon->attach($tree, -msg => "Double click on a item to insert it.\nIt's possible to edit the items, use the\nright mouse button to open the edit menu."); # try to get the saved mode my $modeRef; if ($name eq 'keywords' and -f "$user_data_path/keywordMode") { $modeRef = retrieve("$user_data_path/keywordMode"); } if ($name eq 'supplemental categories' and -f "$user_data_path/categoryMode") { $modeRef = retrieve("$user_data_path/categoryMode"); } $tree->{m_mode} = $modeRef if (defined $modeRef); addTreeMenu($tree, $l1); insertTreeList($tree, @$l1); # $tree->bind("", sub { # my @keys = $keytree->info('selection'); # return unless checkSelection($myDiag, 1, 0, \@keys); # $entry->insert("insert", getLastItem($keys[0])." "); # }); $fc2->Label(-text => "command", -bg => $conf{color_bg}{value})->pack(-expand => 0, -anchor=>'w', -padx => 2, -pady => 2); my $all = 0; my $all_ref = \$all; $all_ref = \$config{CategoriesAll} if ($name eq 'supplemental categories'); $all_ref = \$config{KeywordsAll} if ($name eq 'keywords'); my $addB = $fc2->Button(-text => "add", -command => sub { my @keys = $tree->info('selection'); return unless checkSelection($widget, 1, 0, \@keys); my @keylist; my $warning = ''; my @items; foreach my $key (@keys) { if ($$all_ref == 1) { # all, separated push @items, getAllItems($key); } elsif ($$all_ref == 2) { # all, joined my $joined = join('.', getAllItems($key)); if (length($joined) > 64) { $warning .= "Keyword $joined has ".length($joined)." characters"; next; } push @items, $joined; } elsif ($$all_ref == 0) { # last push @items, getLastItem($key); }else { warn "doubleList: should never be reached!"; } } foreach my $item (@items) { next if isInList($item, $l2); # make @$l2 unique push @$l2, $item; # by adding just new items @$l2 = sort { uc($a) cmp uc($b) } @$l2; # sort alphabetical $catLB2->delete(0, 'end'); $catLB2->insert('end', @$l2); } } )->pack(-expand => 0, -fill => 'x', -padx => 1, -pady => 1); $balloon->attach($addB, -msg => "Add the selected items to the picture"); my $fc2a = $fc2->Frame()->pack(); $fc2a->Radiobutton(-text => "all", -variable => $all_ref, -value => 1)->pack(-anchor => 'w'); $fc2a->Radiobutton(-text => "join", -variable => $all_ref, -value => 2)->pack(-anchor => 'w'); $fc2a->Radiobutton(-text => "last", -variable => $all_ref, -value => 0)->pack(-anchor => 'w'); $balloon->attach($fc2a, -msg => "$name add mode\nExample $name: Friend/Bundy/Kelly\nmode all: three $name: Friend, Bundy and Kelly\nmode join: one $name: Friend.Bundy.Kelly\nmode last: one $name: Kelly"); my $rmB = $fc2->Button(-text => "remove", -command => sub { my @sellist = $catLB2->curselection(); if (@sellist < 1) { print "nothing selected\n" if $verbose; return; } # delete the selected elements in reverse order foreach (reverse @sellist) { splice @$l2, $_, 1; } $catLB2->delete(0, 'end'); $catLB2->insert('end', @$l2); })->pack(-expand => 0, -fill => 'x', -padx => 1, -pady => 1); $balloon->attach($rmB, -msg => "Remove the selected items from the picture"); $tree->bind('', sub { $addB->Invoke(); } ); $fc3->Label(-text => "tags of picture", -bg => $conf{color_bg}{value})->pack(-anchor=>'w'); $catLB2 = $fc3->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 25, -height => 14, )->pack(-expand => 1, -fill =>'both', -padx => 2, -pady => 2); $catLB2->insert('end', @$l2); $catLB2->bind('', sub { $rmB->Invoke(); } ); } ############################################################## # removeAllComments ############################################################## sub removeAllComments { my $ask = shift; unless ($ask == ASK or $ask == NO_ASK) { warn "removeAllComments called with wrong argument: $ask"; return; } my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; if ($ask == ASK) { my $rc = $top->messageBox(-icon => 'question', -message => "Ok to remove all comments of $selected selected pictures?\nThere is no undo!", -title => "Remove all comments?", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } log_it("removing comments ..."); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, "Remove all comments"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "removing all comments ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); next if (!checkWriteable($dpic)); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $meta = getMetaData($dpic, 'COM'); next unless ($meta); $meta->remove_all_comments(); unless ($meta->save()) { warn "removeAllComments: save $pic failed!"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! (removed comments in $i of $selected pictures)"); } ############################################################## # editComment ############################################################## sub editComment { my $lb = shift; # the reference to the listbox widget to update my @sellist = $lb->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; log_it("editing comments from $selected pictures"); # check if some files are links return if (!checkLinks($lb, @sellist)); my $pw = progressWinInit($lb, "Edit comments"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "editing comment ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); my $dirthumb = getThumbFileName($dpic); next if (!checkWriteable($dpic)); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my @comsellist = (); my $text = ''; my @comments = getComments($dpic); if (@comments <= 0) { next; # no comment -> no edit } elsif (@comments == 1) { $text = $comments[0]; # one comment -> select the first $comsellist[0] = 0; } else { # more than one comment, let the user select one comment to edit my $nr = @comments; my @shortComments; foreach (@comments) { push @shortComments, cutString($_, 80, "..."); } next if (!mySelListBoxDialog("Edit comment of $pic", "Please select one of the $nr comments to edit", SINGLE, "Edit", \@comsellist, @shortComments)); if (@comsellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select just one comment.", -title => "Wrong selection", -type => 'OK'); next; } $text = $comments[$comsellist[0]]; } my $rc = myTextDialog("Edit comment", "Please edit comment of $pic", \$text, $dirthumb); next if ($rc ne 'OK'); # replace (german) umlaute by corresponding letters $text =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $config{Comment} = $text; # save changed comment to global config hash my $meta = getMetaData($dpic, "COM"); next unless ($meta); $meta->set_comment($comsellist[0], $text); unless ($meta->save()) { warn "editComment: save $pic failed!"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch($dirthumb); updateOneRow($dpic, $lb); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i of $selected edited)"); } ############################################################## # joinComments ############################################################## sub joinComments { my $ask = shift; unless ($ask == ASK or $ask == NO_ASK) { warn "joinComments called with wrong argument: $ask"; return; } my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $separator = "\n"; if ($ask == ASK) { my $rc = myButtonDialog('Join comments?', "Ok to join all comments to one comment in each of the ".scalar @sellist." selected pictures?\n\n(Some programms are only able to display the fist comment of a JPEG picture.\nPictures with no or just one comment will be skipped.)\nPlease choose the desired separator when joining the comments.", undef, 'Space', 'Newline', 'Nothing', 'Cancel'); return if ($rc =~ m/Cancel/i); $separator = ' ' if ($rc =~ m/Space/i); $separator = '' if ($rc =~ m/Nothing/i); } log_it("joining comments from ".scalar @sellist." pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, "Join comments"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "joining comments ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); next if (!checkWriteable($dpic)); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $meta = getMetaData($dpic, "COM"); next unless ($meta); my $nr = $meta->get_number_of_comments(); next if ($nr <= 1); # no or just one comment -> no join my $com = getComments($dpic, 0); if ((defined $com) and (length $com > $maxCommentLength)) { # a JPEG comment may have max 64kB my $rc = $top->messageBox(-icon => 'warning', -message => "The joined comments of $dpic are too long (".length $com." characters).\nJPEG-Comments may only be up to 64K.\nOK will skip this picture, Cancel will abort the operation.", -title => "Comment to big", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); next; } # join comments with configurable separator string $meta->join_comments($separator); unless ($meta->save()) { warn "editComment: save $pic failed!"; } # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); } progressWinEnd($pw); log_it("ready! ($i of ".scalar @sellist." joined)"); } ############################################################## # checkTempFile - check if temp file exists # returns 0 if it exists # returns 1 if not ############################################################## sub checkTempFile { my $tmpfile = shift; if (-f $tmpfile) { $top->messageBox(-icon => 'warning', -message => "Temporary file $tmpfile already exists. Skipping!", -title => 'Error', -type => 'OK'); return 0; } return 1; } ############################################################## # removeComment - remove a JPEG comment from a picture # if there is more than one comment in the # picture the user can # choose which to delete # if the same comment is selected in two pics # we ask, if we should delete this one in all ############################################################## sub removeComment { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $doForAll = 0; my @removedComments; log_it("removing comments from $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, "Remove comments"); my $i = 0; my $j = 0; # the real number of changed pictures foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "removing comment ($i/$selected) ...", $i, $selected); my $pic = basename($dpic); next if (!checkWriteable($dpic)); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my @comments = getComments($dpic); next if (@comments <= 0); # let the user select the comments to delete my @comsellist = (); # normal modus - let the user select what to remove if (!$doForAll) { my @shortComments; foreach (@comments) { push @shortComments, cutString($_, 80, "..."); } next if (!mySelListBoxDialog("Remove comments", "Please select comment(s) to remove from $pic", MULTIPLE, "Remove", \@comsellist, @shortComments)); } # comment remove wizard mode :) - we choose the right comment to delete else { for (0 .. $#comments) { # search in all comments if ($comments[$_] eq $removedComments[-1]) { # for the magic comment $comsellist[0] = $_; # remember the index last; } } } if ( (@comsellist == 1) and ($doForAll == 0) ) { # if just one comment is removed push @removedComments, $comments[$comsellist[0]]; # remember the removed comments if (@removedComments >= 2) { # when we collected at least two ... if ($removedComments[-1] eq $removedComments[-2]) { # and they are the same ... if ($i < @sellist) { # and there is still some work to be done ... my $com = $removedComments[-1]; $com = substr($com, 0, 100)."..." if (length($com) > 103); my $rc = $top->messageBox(-icon => 'question', -message => "You've selected the same comment two times. Should I remove this comment:\n-------------\n$com\n-------------\nfrom the rest (".(@sellist - $i).") of the selected pictures?", -title => "Comment remove wizard", -type => 'OKCancel'); $doForAll = 1 if ($rc =~ m/Ok/i); } } } } # this can only happen in wizard mode (for pictures not containing the comment to remove) next if (@comsellist == 0); my $meta = getMetaData($dpic, 'COM'); next unless ($meta); # delete the selected elements in reverse order, the unselected stay in the @comments foreach (reverse @comsellist) { $meta->remove_comment($_); } unless ($meta->save()) { warn "editComment: save $pic failed!"; } $j++; # count the modified pics # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! (removed comments in $j of $selected pictures)"); } ############################################################## # rotate - rotate all selected pictures by 90, 180 or 270 # degrees or do a flip transformation ############################################################## sub rotate { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; return if (!checkExternProgs("rotate", "jpegtran")); my $deg = shift; # 90, 180, 270, auto, clear, horizontal or vertical if ($deg eq "auto") { return if (!checkExternProgs("auto rotate", "jhead")); my $usage = `jhead -h 2>&1`; if ($usage !~ m/.*-autorot.*/) { $top->messageBox(-icon => 'warning', -message => "Sorry, but your version of jhead does not support automatic rotation!\nTry to get a newer version at: ".$exprogsres{jhead}, -title => "Wrong jhead version", -type => 'OK'); return; } } elsif ($deg eq "clear") { return if (!checkExternProgs("clear rotate", "jhead")); my $usage = `jhead -h 2>&1`; if ($usage !~ m/.*-norot.*/) { $top->messageBox(-icon => 'warning', -message => "Sorry, but your version of jhead does not support the clearing of the rotation tag!\nTry to get a newer version at: ".$exprogsres{jhead}, -title => "Wrong jhead version", -type => 'OK'); return; } } else { } my $errors = ''; log_it("rotating $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, "rotate pictures"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; my ($ok, $error) = rotate_pic($dpic, $deg); $errors .= $error; progressWinUpdate($pw, "rotating ($i/$selected) ...", $i, $selected); updateOneRow($dpic, $picLB); deleteCachedPics($dpic); showPic($dpic) if ($ok and ($dpic eq $actpic)); # redisplay the picture if it is the actual one } progressWinEnd($pw); reselect($picLB, @sellist); log_it("ready! ($i of $selected rotated)"); showText("Errors while rotating pictures", $errors, NO_WAIT) if ($errors ne ''); generateThumbs(ASK, SHOW); } ############################################################## # rotate a single pic by 90, 180, 270, degree or # auto, clear, flip horizontal or flip vertical # !!!!!! WARNING !!!!!!! # assumes that external progs are already checked! # (checkExternProgs("rotate", "jpegtran")) , ... jhead # see also rotate() ############################################################## sub rotate_pic { my $dpic = shift; my $deg = shift; my @allowed_args = qw(90 180 270 auto clear horizontal vertical); my $valid = 0; foreach (@allowed_args) { if ($deg eq $_) { $valid = 1; last; } } if (not $valid) { return (0, "rotate_pic called with invalid argument. Must be one of ".join(", ", @allowed_args)."\n"); } my $pic = basename($dpic); my $dirtpic = dirname($dpic)."/$pic"."-cjpg"; # temporary file if (not checkWriteable($dpic)) { return (0, "File $dpic is not writable! Exit rotate.\n"); } # check if temp file exists if (-f $dirtpic) { return (0, "Temp file $dirtpic exists! Please delete first. Exit rotate.\n"); } my $transform = "-rotate $deg"; if (($deg eq 'horizontal') or ($deg eq 'vertical')) { $transform = "-flip $deg"; } my $command = ''; if ($deg eq 'auto') { # auto if (is_a_JPEG($dpic)) { # call external command jhead and auto rotate the file directly $command = "jhead -autorot \"$dpic\" "; } else { return (0, "auto rotation is only supported for JPEGs ($dpic)\n"); } } elsif ($deg eq 'clear') { # clear if (is_a_JPEG($dpic)) { # call external command jhead and clear the rotation flag of the file directly $command = "jhead -norot \"$dpic\" "; } else { return (0, "clear rotation is only supported for JPEGs ($dpic)\n"); } } else { if (is_a_JPEG($dpic)) { my $trim = ''; $trim = "-trim " if $config{jpegtranTrim}; # call external command jpegtran and rotate to the temp file $command = "jpegtran -copy all $transform $trim -outfile \"$dirtpic\" \"$dpic\" "; } else { $transform = "-rotate $deg"; if ($deg eq "horizontal") { $transform = "-flip"; } if ($deg eq "vertical") { $transform = "-flop"; } $command = "mogrify $transform \"$dpic\" "; } } if ($command ne '') { execute($command); # now overwrite the original pic with the temp file and delete the temp file # (only needed for jpegtran; not needed for jhead and mogrify) if ($command =~ m/jpegtran .*/) { rotateThumb($dirtpic, $transform) if ($config{RotateThumb}); if (not overwrite($dpic, $dirtpic)) { return (0, "Could not overwrite $dpic!\n"); } } } else { return (0, "rotate_pic: Found no command for $dpic\n"); } return (1, ''); } ############################################################## # rotateThumb ############################################################## sub rotateThumb { my $dpic = shift; my $pic = basename($dpic); my $tmppic = "$trashdir/$pic"; my $tmppic2 = "$trashdir/$pic.tcjpeg"; my $transform = shift; print "rotateThumb: $pic $transform\n" if $verbose; my $errors = ''; extractThumb($dpic, $tmppic, \$errors); return unless (-f $tmppic); # there is no EXIF thumbnail my $trim = ''; $trim = "-trim " if $config{jpegtranTrim}; my $command = "jpegtran -copy all $transform $trim -outfile \"$tmppic2\" \"$tmppic\" "; execute($command); removeFile($tmppic); writeThumb($dpic, $tmppic2); removeFile($tmppic2); } ############################################################## # extractThumb - extract embedded thumbnail picture and save # it into a file ############################################################## sub extractThumb { my $dpic = shift; # picture file with path my $dthumb = shift; # thumbnail file with path (will be overwritten!) my $errors = shift; # reference to error text scalar # extract information from an embedded thumbnail image using ExifTool my $exifTool = new Image::ExifTool; my $info = $exifTool->ImageInfo($dpic, 'thumbnailimage'); if ($info) { my $thumbInfo = $exifTool->ImageInfo($$info{ThumbnailImage}); if (defined $thumbInfo and defined ${$$info{ThumbnailImage}}) { my $thumbfile; if (open($thumbfile,'>',$dthumb)) { binmode $thumbfile; print $thumbfile ${$$info{ThumbnailImage}}; close $thumbfile; } else { $$errors .= "Couldn't open thumb $dthumb for write access\n"; } } else { $$errors .= "No thumbnail in $dpic info block\n"; } } else { $$errors .= "No thumbnail in $dpic\n"; } } ############################################################## # writeThumb - returns 1 if OK, else an error string ############################################################## sub writeThumb { my $dpic = shift; # the picture file with path to which the thumb will be written my $dthumb = shift; # the thumbnail file name with path my $error = 1; my $image = new Image::MetaData::JPEG($dpic, 'APP1'); return "Could not read meta data of $dpic" unless ($image); my $thimage = new Image::MetaData::JPEG($dthumb); return "Could not read meta data of $dthumb" unless ($thimage); my $data = "dummy"; unless ($thimage->save(\$data)) { return "Could not build thumbnail for $dthumb"; } my $hash = $image->set_Exif_data(\$data, 'THUMBNAIL'); return "JPEG thumbnail rejected for $dpic" if (keys %$hash); my $result = $image->save(); return "save failed for $dpic" unless ($result); return 1; } ############################################################## # buildEXIFThumb ############################################################## sub buildEXIFThumb { my $rc = $top->messageBox(-icon => "question", -message => "This function will (re)build the embedded EXIF thumbnail of the selected pictures.\nThe original EXIF thumnail (if existent) will be overwritten!\nOk to continue?", -title => "(Re)Build EXIF thumbnail", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my @sellist = $picLB->info('selection'); my $selected = @sellist; return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); log_it("(re)building EXIF thumbnail in $selected pictures"); my $i = 0; my $pw = progressWinInit($top, "(Re)build EXIF thumbnail"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); progressWinUpdate($pw, "(Re)building EXIF thumbnail ($i/$selected) ...", $i, $selected); $i++; my $pic = basename($dpic); my $thumb = "$trashdir/$pic-exifthumb"; if (-f $thumb) { warn "the temp file $thumb exists - skipping!"; next; } # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my $command = "convert -size 160x160 -geometry 160x160 -quality 75 -sharpen 0.4 -filter Lanczos \"$dpic\" \"$thumb\""; $top->Busy; execute($command); $top->Unbusy; if (!-f $thumb) { warn "file $thumb not generated - skipping!"; next; } my $errors; removeEXIF($thumb, 'all', \$errors); my $size = getFileSize($thumb, NO_FORMAT); # file size in bytes if ($size > 65535) { $top->messageBox(-icon => 'warning', -message => "Sorry, builded EXIF thumbnail picture is too big ($size bytes). The thumbnail picture must have less than 65535 bytes.\nSkipping picture ...", -title => "Thumbnail too big", -type => 'OK'); next; } writeThumb($dpic, $thumb); removeFile($thumb); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); log_it("ready! ($i/$selected EXIF thumbnails (re)builded)"); } ############################################################## # reselect - selects the index in the given list, if they exist # and shows the selection information in the status # bar ############################################################## sub reselect { my $lb = shift; # reselect does not work for the light table return if (ref($lb) eq 'Tk::Canvas'); foreach (@_) { $lb->selectionSet($_) if ($lb->info("exists", $_)); } showNrOf() if ($lb == $picLB); } ############################################################## # after moving or deleting pictures we try to select the picture # after the last selected file ############################################################## sub select_next { my $lb = shift; my $select = shift; # name of picture to select if (not $select) { # when the last picture has been deleted there is no next picture # so we simply select the last picture which is available my @childs = $lb->info('children'); $select = $childs[-1]; } if ($lb->info("exists", $select)) { if ($lb == $picLB) { showPic($select); } else { # just select it selectThumb($lb, $select); } } } ############################################################## # rotateAny - rotate all selected pictures in any angle ############################################################## sub rotateAny { return if (!checkExternProgs('rotateAny', 'mogrify')); my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; log_it("rotating $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $doforall = 0; my $degree = 0; my $color = 'gray30'; my $pw = progressWinInit($top, "Rotate pictures"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; if (!$doforall) { last if (!rotateDialog(\$degree, \$color, \$doforall, $dpic, $selected)); } progressWinUpdate($pw, "rotating ($i/$selected) ...", $i, $selected); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); my $command = "mogrify -rotate $degree -bordercolor \"$color\" -background \"$color\" -quality $config{PicQuality} "; $command .= "-unsharp ".$config{UnsharpRadius}.'x'.$config{UnsharpSigma}."+".$config{UnsharpAmount}."+".$config{UnsharpThreshold}." " if $config{Unsharp}; $command .= "\"$dpic\" "; print "$command\n" if $verbose; execute($command); addProcessInfoToPicComment($command, $dpic); updateOneRow($dpic, $picLB); deleteCachedPics($dpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one } progressWinEnd($pw); reselect($picLB, @sellist); log_it("ready! ($i of $selected rotated)"); generateThumbs(ASK, SHOW); } my $rotw; ############################################################## # rotateDialog ############################################################## sub rotateDialog { my $deg = shift; # reference my $col = shift; # reference my $doforall = shift; # reference my $pic = shift; # the preview pic my $nr = shift; # the number of pics my $preview_size = 400; if (Exists($rotw)) { $rotw->deiconify; $rotw->raise; return; } my $orig = "$trashdir/".basename($pic); my $new = "$trashdir/x-".basename($orig); unless (mycopy($pic, $orig, OVERWRITE)) { warn "rotateDialog: copy error $pic -> $orig ($new)\ncopy"; return 0; } my ($w, $h) = getSize($orig); if ($w > $preview_size or $h > $preview_size) { log_it("rotate: resizing preview picture ..."); my $command = "mogrify -geometry ${preview_size}x${preview_size} -quality 80 \"$orig\""; $top->Busy; execute($command); $top->Unbusy; log_it(lang('Ready!')); } return 0 unless (-f $orig); # open window $rotw = $top->Toplevel(); $rotw->title("Rotate picture"); $rotw->iconimage($mapiviicon) if $mapiviicon; my $rc = 0; my $preview; $preview = $rotw->Photo(-file => "$orig", -gamma => $config{Gamma}) if (-f $orig); my $fc = $rotw->Frame()->pack(); my $prevC; $prevC = $fc->Scrolled('Canvas', -scrollbars => 'osoe', -width => $preview_size, -height => $preview_size, -relief => 'sunken', -bd => $config{Borderwidth})->pack(-side => 'left', -padx => 3, -pady => 3,-anchor => 'w') if $preview; my $horizont = 0; my $vertical = 0; $fc->Scale(-variable => \$horizont, -length => $preview_size, -from => 0, -to => $preview_size, -resolution => 1, -sliderlength => 10, -orient => 'vertical', -width => 10, -bd => 1, -showvalue => 0, -relief => 'groove', -command => sub { drawHorizont($prevC, $horizont, $vertical); } )->pack(-side => 'left', -padx => 3,-pady => 3); $rotw->Scale(-variable => \$vertical, -length => $preview_size, -from => 0, -to => $preview_size, -resolution => 1, -sliderlength => 10, -orient => 'horizontal', -width => 10, -bd => 1, -showvalue => 0, -relief => 'groove', -command => sub { drawHorizont($prevC, $horizont, $vertical); } )->pack(-anchor => 'w', -padx => 3,-pady => 3); $prevC->createImage(0, 0, -image => $preview, -tag => "image", -anchor => "nw"); my $f1 = $rotw->Frame()->pack(-anchor => 'w'); my $auto = 0; $f1->Checkbutton(-text => "auto update", -variable => \$auto)->pack(-side => 'left', -expand => 1, -fill => 'x'); $f1->Button(-text => "--", -command => sub { $$deg--; rotUpdate($prevC, $preview, $orig, $new, $deg, $col) if $auto; })->pack(-side => 'left', -expand => 1, -fill => 'x'); $f1->Button(-text => "-", -command => sub { $$deg -= 0.1; rotUpdate($prevC, $preview, $orig, $new, $deg, $col) if $auto; })->pack(-side => 'left', -expand => 1, -fill => 'x'); $f1->Label(-textvariable => $deg, -relief => 'sunken', -width => 5)->pack(-side => 'left', ); $f1->Button(-text => "+", -command => sub { $$deg += 0.1; rotUpdate($prevC, $preview, $orig, $new, $deg, $col) if $auto; })->pack(-side => 'left', -expand => 1, -fill => 'x'); $f1->Button(-text => "++", -command => sub { $$deg++; rotUpdate($prevC, $preview, $orig, $new, $deg, $col) if $auto; })->pack(-side => 'left', -expand => 1, -fill => 'x'); labeledScale($rotw, 'top', 26, "Angle (degrees, clockwise)", $deg, 0, 359.9, 0.1); my $qS = labeledScale($rotw, 'top', 26, lang("Quality (%)"), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); labeledEntryColor($rotw,'top',26,"Background color",'Set',$col); # check, if a new version of ImageMagick's mogrify with the unsharp option is available my $unsharp = 0; $unsharp = 1 if (`mogrify` =~ m/.*-unsharp.*/); # sharpen the image with an unsharp mask operator if ($unsharp) { my $umF = $rotw->Frame()->pack(-fill =>'x'); my $umcB = $umF->Checkbutton(-variable => \$config{Unsharp}, -anchor => 'w', -text => "Unsharp mask")->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1); $balloon->attach($umcB, -msg => "The -unsharp option sharpens an image. We convolve the image with a Gaussian operator of the given radius and standard deviation (sigma). For reasonable results, radius should be larger than sigma. Use a radius of 0 to have the method select a suitable radius."); $umF->Button(-text => "Options", -anchor => 'w', -command => sub { unsharpDialog(); })->pack(-side => 'left', -anchor => 'w', -padx => 3); } buttonBackup($rotw, 'top'); buttonComment($rotw, 'top'); if ($nr > 1) { $rotw->Checkbutton(-variable => \$$doforall, -anchor => 'w', -text => "use this setting for all pics" )->pack(-anchor => 'w'); } my $ButF = $rotw->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $rc = 1; $rotw->withdraw(); $rotw->destroy(); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => "Preview", -command => sub { rotUpdate($prevC, $preview, $orig, $new, $deg, $col); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3) if $preview; my $XBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $rotw->withdraw(); $rotw->destroy(); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($rotw, $XBut); $rotw->Popup; $rotw->waitWindow; # clean up $preview->delete; removeFile($orig); removeFile($new); return $rc; } ############################################################## # bind Ctrl-q and ESC key and window close button to invoke # given window exit button (e.g. "Cancel" button) ############################################################## sub bind_exit_keys_to_button { my $w = shift; # widget / window my $button = shift; # Exit button (e.g. Cancel) to invoke on quit # check arguments if (ref($w) ne 'Tk::Toplevel') { warn "bind_exit_keys_to_button called with wrong argument w: ".ref($w); return 0; } if (ref($button) ne 'Tk::Button') { warn "bind_exit_keys_to_button called with wrong argument button: ".ref($button); return 0; } $w->bind('', sub { $button->Invoke; }); $w->bind('', sub { $button->Invoke; }); $w->protocol("WM_DELETE_WINDOW" => sub { $button->Invoke; }); return 1; } ############################################################## # drawHorizont ############################################################## sub drawHorizont { my $canvas = shift; my $y = shift; # in percent of the canvas height my $x = shift; # in percent of the canvas width $canvas->delete('withtag', 'line'); $canvas->createLine( 0, $y, $canvas->width, $y, -tags => 'line', -fill => 'black', -dash => [6,4,2,4], ); $canvas->createLine( 0, $y, $canvas->width, $y, -tags => 'line', -fill => 'white', -dash => [2,6,2,4], ); $canvas->createLine( $x, 0, $x, $canvas->height, -tags => 'line', -fill => 'black', -dash => [6,4,2,4], ); $canvas->createLine( $x, 0, $x, $canvas->height, -tags => 'line', -fill => 'white', -dash => [2,6,2,4], ); } ############################################################## # rotUpdate - update the picture in the rotateDialog with the # new degree setting ############################################################## sub rotUpdate { my ($prevC, $preview, $orig, $new, $deg, $col) = @_; return if !mycopy($orig, $new, OVERWRITE); $rotw->Busy; # some versions of mogrify need bordercolor, some background so we supply both my $command = "mogrify -rotate $$deg -bordercolor \"$$col\" -background \"$$col\" \"$new\" "; execute($command); $preview->configure(-file => $new, -gamma => $config{Gamma}); my ($id) = $prevC->find('withtag', 'image'); my ($x1, $y1, $x2, $y2) = $prevC->bbox($id); $prevC->configure(-scrollregion => [0, 0, $x2-$x1, $y2-$y1]); $rotw->Unbusy; } ############################################################## # getRealFile - alters the path and file name to the real file # if it's a link, else do nothing # returns 1 if everything worked, else 0 ############################################################## sub getRealFile { my $dirfileR = shift; # reference to a file, which may be a link if (!-f $$dirfileR) { my $file = Encode::encode('iso-8859-1', $$dirfileR); warn "getRealFile: $$dirfileR ($file) is no file!"; return 0; } if (-l $$dirfileR) { my $linktargetfile = getLinkTarget($$dirfileR); if ($linktargetfile eq '') { warn "error in getLinkTarget! ($$dirfileR)"; return 0; } else { $$dirfileR = $linktargetfile; return 1; } } else { # no link, change nothing, return true return 1; } } ############################################################## # getLinkTarget - returns the file a link is pointing to # input (folder, link) or (dirlink) where # dirlink consists of folder and link # works with relative and absolute links ############################################################## sub getLinkTarget { my ($dir, $link); if (@_ == 2) { $dir = shift; $link = shift; } elsif (@_ == 1) { $dir = dirname($_[0]); $link = basename($_[0]); } else { warn "getLinkTarget: wrong # of parameters!"; return ''; } # change first to the start dir (to handle relative links) return '' if !changeDir($dir); my $linktargetfile = readlink $link; my $linktargetdir = dirname $linktargetfile; # change to link target, this should now work for relative and absolute links return '' if !changeDir($linktargetdir); # get the current dir my $cwd = cwd(); $linktargetfile = $cwd.'/'.basename($linktargetfile); return $linktargetfile; } ############################################################## # overwrite - takes two files a and b, deletes a and moves b # to a # the file names must include the absolute path ############################################################## sub overwrite { my $dpic = shift; my $dirtpic = shift; if (!-f $dirtpic) { warn "overwrite: $dirtpic not created. Giving up!"; return 0; } if (-l $dpic) { my $linktargetfile = getLinkTarget($dpic); $dpic = $linktargetfile; } return 0 if (! removeFile($dpic) ); if (!move($dirtpic, $dpic)) { $top->Dialog(-title => "Move $dirtpic", -text => "Couldn't move $dirtpic to $dpic: $!", -buttons => ['Ok'])->Show(); return 0; } return 1; } ############################################################## # myEntryDialog - get a string from the user # returns 'OK' or 'Cancel' ############################################################## # ToDo: should return true or false instead of Ok/Cancel sub myEntryDialog { my $title = shift; my $text = shift; my $varRef = shift; my $thumbnail = shift; # optional my $icon; my $rc = 'Cancel'; # open window my $myDiag = $top->Toplevel(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; my $f = $myDiag->Frame()->pack(-fill => 'both', -expand => 1); if ((defined $thumbnail) and (-f $thumbnail)) { $icon = $top->Photo(-file => $thumbnail, -gamma => $config{Gamma}); if ($icon) { $f->Label(-image => $icon, -bg => $conf{color_bg}{value}, -relief => 'sunken', )->pack(-side => 'left', -fill => 'x', -padx => 3, -pady => 3); } } # determine the heigt of the textbox by counting the number of lines my $height = ($text =~ tr/\n//); $height += 2; $height = 30 if ($height > 30); # not to big, we have scrollbars my $rotext = $f->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 80, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-side => 'right', -fill => 'both', -expand => 1, -padx => 3, -pady => 3); $rotext->insert('end', $text); my $OKB; my $entry = $myDiag->Entry(-textvariable => \$$varRef, -width => 40, )->pack(-fill => 'x', -padx => 3, -pady => 3); if ($$varRef =~ /(.*)(\.jp(g|eg))/i) { # if it is a jpeg image name $entry->selectionRange(0,length($1)); # select only the part before the suffix $entry->icursor(length($1)); } else { $entry->selectionRange(0,'end'); # else select all $entry->icursor('end'); } $entry->xview('end'); $entry->bind('', sub { $OKB->Invoke; } ); $entry->focus; my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $rc = 'OK'; $myDiag->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $XBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 'Cancel'; $myDiag->destroy; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($myDiag, $XBut); $myDiag->Popup; repositionWindow($myDiag); $myDiag->waitWindow(); $icon->delete if $icon; return $rc; } ############################################################## # myFontDialog - dialog to select a font family ############################################################## sub myFontDialog { my $widget = shift; my $title = shift; #my $text = shift; my $varRef = shift; my $size = shift; my $rc = 0; # open window my $myDiag = $widget->Toplevel(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; my $listBox = $myDiag->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 30, #-height => 40, )->pack(-side => 'left', -anchor => 'w', -expand => 1, -fill =>'y', -padx => 3, -pady => 3); my $f = $myDiag->Frame()->pack(-side => 'left', -expand => 1, -anchor => 'w', -fill =>'both'); my @fontFamilies = sort $top->fontFamilies; shift @fontFamilies unless ($fontFamilies[0]); $listBox->insert('end', @fontFamilies); foreach my $i (0 .. $#fontFamilies) { if ($fontFamilies[$i] eq $$varRef) { $listBox->selectionSet($i); $listBox->see($i); last; } } my $normalText = "This is a preview Text, here you can see how the selected font will look like.\n\nThe brown fox jumps over the brigde.\n\n1 :\n12 :\n123 :\n1234 :\n12345 :\nWWWWWWW:\niiiiiii:\n\040\041\042\043\044\045\046\047\050\051\052\053\054\055\056\057\040\041\042\043\044\045\046\047\050\051\052\053\054\055\056\057\060\061\062\063\064\065\066\067\070\071\072\073\074\075\076\077\100\n\101\102\103\104\105\106\107\110\111\112\113\114\115\116\117\120\121\122\123\124\125\126\127\130\131\132\133\134\135\136\137\140\141\142\143\144\145\146\147\150\151\152\153\154\155\156\157\160\161\162\163\164\165\166\167\170\171\172\173\174\175\176\n\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\n\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\n\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377"; my $ButF = $f->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $pane = $f->Scrolled('Pane', -bd => $config{Borderwidth}, -relief => 'groove', -scrollbars => 'osoe', -width => 1000)->pack(-expand => 1, -anchor => 'w', -fill => 'both', -padx => 3, -pady => 3); my $example = $pane->Label(-text => $normalText, -bg => $conf{color_bg}{value}, -justify => 'left')->pack(-fill => 'both', -expand => 1, -anchor => 'w'); $listBox->bind('', sub { my @sell = $listBox->curselection(); return unless @sell; my $actfont = $fontFamilies[$sell[0]]; return unless $actfont; $myDiag->Busy; my $font = $top->Font(-family => $actfont, -size => $size); $example->configure(-font => $font); $example->update(); $myDiag->Unbusy; } ); $ButF->Button(-text => lang('Next'), -command => sub { my @sell = $listBox->curselection(); return unless @sell; my $index = $sell[0]; $listBox->selectionClear(0, 'end'); $index++; $index = 0 if ($index >= @fontFamilies); $listBox->selectionSet($index); $listBox->see($index); my $actfont = $fontFamilies[$index]; return unless $actfont; $myDiag->Busy; my $font = $top->Font(-family => $actfont, -size => $size); $example->configure(-font => $font); $example->update(); $myDiag->Unbusy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Previous'), -command => sub { my @sell = $listBox->curselection(); return unless @sell; my $index = $sell[0]; $listBox->selectionClear(0, 'end'); $index--; $index = $#fontFamilies if ($index < 0); $listBox->selectionSet($index); $listBox->see($index); my $actfont = $fontFamilies[$index]; return unless $actfont; $myDiag->Busy; my $font = $top->Font(-family => $actfont, -size => $size); $example->configure(-font => $font); $example->update(); $myDiag->Unbusy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { my @sell = $listBox->curselection(); $$varRef = $fontFamilies[$sell[0]] if @sell; $rc = 1; $myDiag->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $myDiag->bind ('', sub { $OKB->Invoke; } ); $listBox->bind('', sub { $OKB->Invoke; } ); $OKB->focus; my $XBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $myDiag->destroy; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($myDiag, $XBut); my $ws = 0.5; my $w = int($ws * $myDiag->screenwidth); my $h = int($ws * $myDiag->screenheight); my $x = int(((1 - $ws) * $myDiag->screenwidth)/3); my $y = int(((1 - $ws) * $myDiag->screenheight)/3); #print "geo==${w}x${h}+${x}+${y}\n"; $myDiag->geometry("${w}x${h}+${x}+${y}"); $myDiag->Popup; repositionWindow($myDiag); $myDiag->waitWindow(); return $rc; } ############################################################## # myPicDialog - show some thumbnails and a text to the user # returns 'OK' or content of $button ############################################################## sub myPicDialog { my $title = shift; my $text = shift; my $button = shift; # optional button, if not needed set to '' my @thumbnails = @_; my @icons; my $rc = $button; # open window my $myDiag = $top->Toplevel(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; # determine the heigt of the textbox by counting the number of lines my $height = ($text =~ tr/\n//); $height += 2; $height = 50 if ($height > 50); # not to big, we have scrollbars my $rotext = $myDiag->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 40, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-fill => 'both', -expand => 1, -padx => 3, -pady => 3); $rotext->insert('end', $text); my $f = $myDiag->Frame()->pack; my $i = 0; # insert the thumbnails foreach (@thumbnails) { if ((defined $_) and (-f $_)) { $icons[$i] = $top->Photo(-file => "$_", -gamma => $config{Gamma}); if ($icons[$i]) { $f->Label(-image => $icons[$i], -bg => $conf{color_bg}{value}, -relief => 'sunken', )->pack(-side => 'left', -anchor => 'n', -fill => 'x', -padx => 3, -pady => 3); $i++; } } } my $bf = $myDiag->Frame()->pack(-expand => 1, -fill => 'x'); my $OKB = $bf->Button(-text => 'OK', -command => sub { $rc = 'OK'; $myDiag->destroy; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $OKB->focus; if ($button ne '') { $bf->Button(-text => $button, -command => sub { $rc = $button; $myDiag->destroy; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); } $myDiag->bind('', sub { $OKB->Invoke; }); $myDiag->Popup; repositionWindow($myDiag); $myDiag->waitWindow(); foreach (@icons) { $_->delete if $_; } # free memory return $rc; } ############################################################## # myButtonDialog - get a feedback from the user # you may specify as many buttons as you like # the return value will be the text of the button pressed # The first one is the default button # the last one is invoked when pressing Escape ############################################################## sub myButtonDialog { my $title = shift; my $text = shift; my $thumbnail = shift; my @buttons = @_; my $icon; my $rc = ''; # open window my $myDiag = $top->Toplevel(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; my $f = $myDiag->Frame()->pack(-fill => 'both', -expand => 1); if ((defined $thumbnail) and (-f $thumbnail)) { $icon = $top->Photo(-file => "$thumbnail", -gamma => $config{Gamma}); if ($icon) { $f->Label(-image => $icon, -bg => $conf{color_bg}{value}, -relief => 'sunken', )->pack(-side => 'left', -fill => 'x', -padx => 3, -pady => 3); } } # determine the heigt of the textbox by counting the number of lines my $height = ($text =~ tr/\n//); $height += 2; $height = 50 if ($height > 50); # not to big, we have scrollbars my $rotext = $f->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 90, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-side => 'right', -fill => 'both', -expand => 1, -padx => 3, -pady => 3); $rotext->insert('end', $text); my %buts; my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); # add the buttons foreach (@buttons) { my $name = $_; $buts{$name} = $ButF->Button(-text => "$name", -command => sub { $rc = "$name"; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); } # the first button gets the focus and is invoked with return $myDiag->bind('', sub { $buts{$buttons[0]}->Invoke; } ); $buts{$buttons[0]}->focus; # the last button is invoked with the Escape key $myDiag->bind('', sub { $buts{$buttons[-1]}->Invoke; }); $myDiag->Popup; repositionWindow($myDiag); $myDiag->waitVariable(\$rc); $icon->delete if $icon; $myDiag->destroy(); $top->focus; return $rc; } ############################################################## # checkDialog - a dialog with a Checkbutton (e.g. do not show # this again ...) ############################################################## sub checkDialog { my $title = shift; my $text = shift; my $check = shift; # var ref my $checkT = shift; # the text for the checkbutton my $thumbnail = shift; # !!! not optional, supply '' if there is no thumbnail to show my @buts = @_; # the button text, this text will be returned my $icon; my $rc; # open window my $myDiag = $top->Toplevel(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; my $f = $myDiag->Frame()->pack; if ((defined $thumbnail) and (-f $thumbnail)) { $icon = $top->Photo(-file => "$thumbnail", -gamma => $config{Gamma}); if ($icon) { $f->Label(-image => $icon, -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -fill => 'x', -padx => 3, -pady => 3); } } # determine the heigt of the textbox by counting the number of lines my $height = ($text =~ tr/\n//); $height += 2; $height = 50 if ($height > 50); # not to big, we have scrollbars my $rotext = $f->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 55, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-side => 'right', -fill => 'both', -expand => 1, -padx => 3, -pady => 3); $rotext->insert('end', $text); my $OKB; $myDiag->Checkbutton(-variable => \$$check, -text => $checkT, )->pack(-fill => 'x', -padx => 3, -pady => 3); my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); foreach my $text (@buts) { $ButF->Button(-text => "$text", -command => sub { $rc = "$text"; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); } $myDiag->Popup; repositionWindow($myDiag); $myDiag->waitVariable(\$rc); $icon->delete if $icon; $myDiag->withdraw(); $myDiag->destroy(); return $rc; } ############################################################## # myTextDialog - get a text from the user ############################################################## sub myTextDialog { my $title = shift; my $text = shift; my $varRef = shift; my $thumb = shift; # optional file name of thumbnail my ($rc, $icon); # open window my $myDiag = $top->Toplevel(); #$myDiag->grab(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; $myDiag->Label(-text => $text, -bg => $conf{color_bg}{value} )->pack(-fill => 'x', -padx => 3, -pady => 3); my $fl = $myDiag->Frame()->pack(-anchor => 'n', -side => 'left'); my $fm = $myDiag->Frame()->pack(-expand => 1, -fill => 'both', -anchor => 'n', -side => 'left'); my $fr = $myDiag->Frame()->pack(-expand => 1, -fill => 'both', -anchor => 'n', -side => 'left'); if ((defined $thumb) and (-f $thumb)) { $icon = $myDiag->Photo(-file => "$thumb", -gamma => $config{Gamma}); if ($icon) { $fl->Label(-image => $icon, -bg => $conf{color_bg}{value}, -relief => 'sunken', )->pack(-padx => 1, -pady => 2); } } my $topButF = $fm->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill =>'x', -padx => 3, -pady => 3); my $midF = $fm->Frame()->pack(-expand => 1, -fill => 'both', -padx => 0, -pady => 3); my $entry = $midF->Scrolled("Text", -scrollbars => 'osoe', -wrap => 'none', -width => 65, -height => 20, )->pack(-side => 'left', -expand => 1, -fill => 'both', -padx => 3, -pady => 3); $entry->insert('end', $$varRef); #$entry->selectionRange(0,'end'); $entry->see('end'); $entry->markSet("insert",'end'); my $keytree = $fr->Scrolled('Tree', -separator => '/', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 20, )->pack(-expand => 1, -fill => 'both', -padx => 2, -pady => 2); $balloon->attach($keytree, -msg => "Double click on a keyword to insert it.\nIt's possible to edit the keywords, use the\nright mouse button to open the edit menu."); # try to get the saved mode if (-f "$user_data_path/keywordMode") { my $hashRef = retrieve("$user_data_path/keywordMode"); warn "could not retrieve mode" unless defined $hashRef; $keytree->{m_mode} = $hashRef; } addTreeMenu($keytree, \@prekeys); insertTreeList($keytree, @prekeys); $keytree->bind("", sub { my @keys = $keytree->info('selection'); return unless checkSelection($myDiag, 1, 0, \@keys); $entry->insert("insert", getLastItem($keys[0])." "); $entry->focus; }); my $ButF = $fm->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $umlautB = $ButF->Checkbutton(-variable => \$config{ConvertUmlaut}, -text => 'convert german umlaute')->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 1); $balloon->attach($umlautB, -msg => "Convert german umlaute (e.g. ä -> ae)\nUmlaute often cause problems in other tools,\nso it's saver to convert them to plain ASCII."); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $$varRef = $entry->get(0.1, 'end'); trimComment($varRef); my $len = length($$varRef); if ($len >= $maxCommentLength) { $top->messageBox(-icon => 'warning', -message => "Sorry your comment is too long ($len characters).\nJPEG-Comments may only be up to 64K.\nPlease shorten your comment.", -title => "Comment to long", -type => 'OK'); return; } $rc = 'OK'; saveTreeMode($keytree); nstore($keytree->{m_mode}, "$user_data_path/keywordMode") or warn "could not store $user_data_path/keywordMode: $!"; $myDiag->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($OKB, -msg => "You can press Control-x to close the dialog (OK button)"); # key-desc,Ctrl-x,accept text and close (in text dialog) $myDiag->bind('', sub { $OKB->Invoke; }); $topButF->Label(-text => lang("Insert ..."), -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); my $crb = $topButF->Button(-text => lang('Copyright'), -command => sub { $entry->insert("insert", $config{Copyright}); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); $myDiag->bind('', sub { $crb->Invoke; }); $topButF->Button(-text => lang('File name'), -command => sub { $entry->insert("insert", basename($actpic)); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); $topButF->Button(-text => lang('Last comment'), -command => sub { $entry->insert("insert", $config{Comment}); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 1); $topButF->Button(-text => lang("File ..."), -command => sub { my $file = $myDiag->getOpenFile(-title => 'Add comment from file', -initialdir => $actdir); return if ((!defined $file) or ($file eq '') or (!-f $file)); my $fileH; if (!open($fileH, '<', $file)) { warn "Sorry, I couldn't open the file $file: $!"; return; } my $buffer; read $fileH, $buffer, 32768; # <- todo!!! close($fileH); $entry->insert("insert", $buffer); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 'Cancel'; saveTreeMode($keytree); nstore($keytree->{m_mode}, "$user_data_path/keywordMode") or warn "could not store $user_data_path/keywordMode: $!"; $myDiag->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $entry->focus; $myDiag->Popup(-popover => 'cursor'); repositionWindow($myDiag); $myDiag->waitWindow; $icon->delete if $icon; return $rc; } ############################################################## # myReplaceDialog - get two strings from the user ############################################################## sub myReplaceDialog { my $title = shift; my $text = shift; my $varARef = shift; my $varBRef = shift; my $rc = 'Cancel'; # open window my $win = $top->Toplevel(); #$win->grab(); $win->title($title); $win->iconimage($mapiviicon) if $mapiviicon; $win->Label(-text => $text, -bg => $conf{color_bg}{value} )->pack(-anchor=>'w', -padx => 3, -pady => 3); my $midF = $win->Frame()->pack(-expand => 1, -fill => 'both', -padx => 0, -pady => 0); $midF->Label(-text => "Replace this:", -bg => $conf{color_bg}{value} )->pack(-anchor=>'w', -padx => 3, -pady => 3); my $entryA = $midF->Scrolled("Text", -scrollbars => 'osoe', -wrap => 'none', -height => 4, -width => 80, )->pack(-side => 'top', -expand => 1, -fill => 'both', -padx => 3, -pady => 3); $midF->Label(-text => "with that:", -bg => $conf{color_bg}{value} )->pack(-anchor=>'w', -padx => 3, -pady => 3); my $entryB = $midF->Scrolled("Text", -scrollbars => 'osoe', -wrap => 'none', -height => 4, -width => 80, )->pack(-side => 'top', -expand => 1, -fill => 'both', -padx => 3, -pady => 3); $entryA->insert('end', $$varARef); $entryA->see('end'); $entryA->markSet("insert",'end'); $entryB->insert('end', $$varBRef); $entryB->see('end'); $entryB->markSet("insert",'end'); my $umlautB = $win->Checkbutton(-variable => \$config{ConvertUmlaut}, -text => "convert german umlaute")->pack(-anchor => 'w', -padx => 3, -pady => 3); $balloon->attach($umlautB, -msg => "Convert german umlaute (e.g. ä -> ae)\nUmlaute often cause problems in other tools,\nso it's saver to convert them to plain ASCII."); my $ButF = $win->Frame()->pack(-fill =>'x', -padx => 0, -pady => 0); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $$varARef = $entryA->get(0.1, 'end'); trimComment($varARef); my $len = length($$varARef); if ($len >= $maxCommentLength) { $top->messageBox(-icon => 'warning', -message => "Sorry your comment is too long ($len characters).\nJPEG-Comments may only be up to 64K.\nPlease shorten your comment.", -title => "Comment to long", -type => 'OK'); return; } $$varBRef = $entryB->get(0.1, 'end'); trimComment($varBRef); $len = length($$varBRef); if ($len >= $maxCommentLength) { $top->messageBox(-icon => 'warning', -message => "Sorry your comment is too long ($len characters).\nJPEG-Comments may only be up to 64K.\nPlease shorten your comment.", -title => "Comment to long", -type => 'OK'); return; } $rc = 'OK'; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($OKB, -msg => "You can press Control-x to close the dialog (OK button)"); $ButF->Button(-text => lang("Test"), -command => sub { $$varARef = $entryA->get(0.1, 'end'); trimComment($varARef); my $len = length($$varARef); if ($len >= $maxCommentLength) { $top->messageBox(-icon => 'warning', -message => "Sorry your comment is too long ($len characters).\nJPEG-Comments may only be up to 64K.\nPlease shorten your comment.", -title => "Comment to long", -type => 'OK'); return; } $$varBRef = $entryB->get(0.1, 'end'); trimComment($varBRef); $len = length($$varBRef); if ($len >= $maxCommentLength) { $top->messageBox(-icon => 'warning', -message => "Sorry your comment is too long ($len characters).\nJPEG-Comments may only be up to 64K.\nPlease shorten your comment.", -title => "Comment to long", -type => 'OK'); return; } $rc = "Test"; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $win->bind('', sub { $OKB->Invoke; }); $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 'Cancel'; $win->withdraw(); $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $entryA->focus; $win->Popup(-popover => 'cursor'); repositionWindow($win); $win->waitWindow; return $rc; } ############################################################## # trimComment ############################################################## sub trimComment { my $comRef = shift; $$comRef =~ s/\n*$//; # remove trailing newlines $$comRef =~ s/\r*//g; # remove \r (carriage return) #$$comRef =~ s/"/\\"/g; # replace " with \" $$comRef =~ s/\"/\'/g; # replace " with ' return; } ############################################################## # mySelListBoxDialog - let the user select some items of the # given list ############################################################## sub mySelListBoxDialog { my $title = shift; my $text = shift; my $mode = shift; #SINGLE (one selection) or MULTIPLE (several selections) my $OKBut = shift; my $sellist = shift; # output list (list reference) - the list with the selected items my @list = @_; # input list - the list to choose from my $rc = 0; # open window my $myDiag = $top->Toplevel(); $myDiag->title($title); $myDiag->iconimage($mapiviicon) if $mapiviicon; $myDiag->Label(-anchor => 'w', -justify => 'left', -text => $text, -bg => $conf{color_bg}{value})->pack(-fill => 'x', -padx => 3, -pady => 3); my $listBoxY = @list; $listBoxY = 30 if ($listBoxY > 30); # maximum 30 entries my $listBox = $myDiag->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 80, -height => $listBoxY, )->pack(-expand => 1, -fill =>'both', -padx => 3, -pady => 3); $listBox->configure(-selectmode => 'single') if ($mode == SINGLE); $listBox->insert('end', @list); $listBox->bind('', sub { @$sellist = $listBox->curselection(); $rc = 1; } ); # select all|none make only sense if multiple selection is possible if ($mode == MULTIPLE) { my $ubutF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $ubutF->Button(-text => lang('Select all'), -command => sub { $listBox->selectionSet(0, 'end'); })->pack(-side => 'left', -padx => 3, -pady => 3); $ubutF->Button(-text => lang('Select none'), -command => sub { $listBox->selectionClear(0, 'end'); })->pack(-side => 'left', -padx => 3, -pady => 3); } my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => $OKBut, -command => sub { @$sellist = $listBox->curselection(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $OKB->bind('', sub { $OKB->Invoke; } ); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($myDiag, $xBut); $myDiag->Popup; if ($EvilOS) { # sometimes to dialog disappears when clicked on, so we need a short grab $myDiag->grab; $myDiag->after(50, sub{$myDiag->grabRelease}); } $OKB->focus; $myDiag->waitVariable(\$rc); $myDiag->destroy() if Tk::Exists($myDiag); return $rc; } ############################################################## # createDirMenu ############################################################## sub createDirMenu { $dirMenu = $top->Menu(-title => lang("Folder Menu")); return; } ############################################################## # updateDirMenu ############################################################## sub updateDirMenu { return if (!defined($dirMenu)); # get number of items my $end = $dirMenu->index('end'); # first call to function - build up menu fixed part # less than 12 menu items (separators count too!) if ($end < 12) { $dirMenu->command(-image => compound_menu($top, lang('Open folder ...'), 'folder.png'), -command => sub { my $dir = getRightDir(); openDirPost($dir);}, -accelerator => "double click"); $dirMenu->command(-image => compound_menu($top, lang('Preview folder'), ''), -command => sub { my $dir = getRightDir(); my @list = getPics($dir, WITH_PATH, NO_CHECK_JPEG); sortPics($config{SortBy}, $config{SortReverse}, \@list); showThumbList(\@list, $dir); }, -accelerator => "middle click"); $dirMenu->command(-image => compound_menu($top, lang('Copy pictures to folder'), ''), -command => sub { copy_or_move_pics_to_folder(COPY); }); $dirMenu->command(-image => compound_menu($top, lang('Move pictures to folder'), ''), -command => sub { copy_or_move_pics_to_folder(MOVE); }); $dirMenu->command(-image => compound_menu($top, lang('Search in folder ...'), 'system-search.png'), -command => sub { my $tmp = $config{SearchOnlyInDir}; # save search mode $config{SearchOnlyInDir} = 1; # set to local search searchMetaInfo(); $config{SearchOnlyInDir} = $tmp; # restore search mode }); my $dir_size = $dirMenu->cascade(-image => compound_menu($top, lang('Folder size'), '')); $dir_size->cget(-menu)->configure(-title => lang('Folder size')); $dir_size->command(-label => lang("Calculate folder size"), -command => sub { calcDirSize(); } ); $dir_size->command(-label => lang("Display folder sizes (graphic)"), -command => sub { showDirSizes(getRightDir()); } ); $dirMenu->separator; $dirMenu->command(-image => compound_menu($top, lang('Rename folder ...'), ''), -command => sub { renameDir(); }); $dirMenu->command(-image => compound_menu($top, lang('New folder ...'), 'folder-new.png'), -command => sub { my $dir = getRightDir(); if (!-d $dir) { warn "dir $dir is no dir"; return; } makeNewDir($dir, $dirtree); }); $dirMenu->command(-image => compound_menu($top, lang('Delete folder ...'), ''), -command => sub { deleteDir(); }); $dirMenu->separator; my $dir_hot = $dirMenu->cascade(-image => compound_menu($top, lang('Folder list'), 'emblem-favorite.png')); $dir_hot->cget(-menu)->configure(-title => lang('Folder list')); $dir_hot->command(-label => lang("Add actual folder"), -command => sub { my $dir = getRightDir(); my $max = 0; foreach (sort {$dirHotlist{$b} <=> $dirHotlist{$a}} keys %dirHotlist) { $max = $dirHotlist{$_}; last; } $dirHotlist{$dir} = $max; log_it("added $dir to list!"); updateDirMenu(); }); $dir_hot->command(-label => lang("Remove actual folder"), -command => sub { my $dir = getRightDir(); delete $dirHotlist{$dir} if (defined($dirHotlist{$dir})); log_it("removed $dir from list!"); updateDirMenu(); }); } else { # clear dir menu index 13 to end (dynamic part) $dirMenu->delete(13, 'end'); } # add the dynamic part # add the 12 most wanted hotlist folders :) my @dirlist; foreach (sort {$dirHotlist{$b} <=> $dirHotlist{$a}} keys %dirHotlist) { # remove deleted dirs if (!-d $_) { delete $dirHotlist{$_}; next; # skip } next if ($_ eq $trashdir); push @dirlist, $_; last if (@dirlist > 11); } foreach (sort @dirlist) { my $dir = $_; # we need a local copy # this will add the number of accesses of the folder #$dirMenu->command(-label => "$_", -command => sub { openDirPost($dir); }, -accelerator => "($dirHotlist{$_})"); $dirMenu->command(-label => "$_", -command => sub { openDirPost($dir); }); } $dirMenu->separator; # add the last used folders foreach (reverse @dirHist) { next if (!-d $_); my $dir = $_; # we need a local copy $dirMenu->command(-label => "$dir", -command => sub { openDirPost($dir); }); } } ############################################################## # createThumbMenu ############################################################## sub createThumbMenu { $thumbMenu = $top->Menu(-title => lang("Thumbnail Menu")); addSelectMenu($thumbMenu); $thumbMenu->separator; addFileActionsMenu($thumbMenu, $picLB); $thumbMenu->separator; addPicProcessing($thumbMenu, $picLB); $thumbMenu->separator; addMetaInfoMenu($thumbMenu); $thumbMenu->separator; $thumbMenu->command(-image => compound_menu($top, lang('Open this folder'), 'folder.png'), -command => sub { open_pic_folder($picLB); }, -accelerator => '' ); $thumbMenu->command(-image => compound_menu($top, lang('Reload pictures'), 'view-refresh.png'), -command => \&updateThumbsPlus, -accelerator => ''); $thumbMenu->command(-image => compound_menu($top, lang('Reload picture meta information'), 'view-refresh.png'), -command => sub { reread_pics($picLB); }); $thumbMenu->command(-label => lang("Rebuild thumbnails ..."), -command => \&rebuildThumbs); #, -accelerator => ""); $thumbMenu->command(-label => lang("Add to collection"), -command => sub {light_table_add_from_lb($picLB);}, -accelerator => ""); $thumbMenu->command(-image => compound_menu($top, lang('Show GPS position in map'), 'internet-web-browser.png'), -command => sub { gps_map_open($picLB); } ); } ############################################################## # createPicMenu ############################################################## sub createPicMenu { $picMenu = $top->Menu(-title => lang('Picture Menu')); $picMenu->command(-label => lang('Reload picture'), -command => \&reloadPic, -accelerator => '' ); $picMenu->command(-label => lang('Show thumbnails in picture frame'), -command => sub { my @pics = $picLB->info('children'); show_canvas_thumbs($c, \@pics);}, -accelerator => '' ); $picMenu->command(-image => compound_menu($top, lang('Open pictures in new window'), 'image-x-generic.png'), -command => sub {showPicInOwnWin($actpic);}, -accelerator => '' ); $picMenu->separator; addPicProcessing($picMenu, $picLB); $picMenu->separator; addZoomMenu($picMenu); $picMenu->separator; add_window_layout_menu($picMenu); $picMenu->separator; $picMenu->command(-image => compound_menu($top, lang('Options ...'), 'preferences-system.png'), -command => \&options_edit, -accelerator => ""); $picMenu->command(-image => compound_menu($top, lang('Other options ...'), 'preferences-system.png'), -command => \&options); $picMenu->command(-image => compound_menu($top, lang('Fullscreen'), 'view-fullscreen.png'), -command => sub { fullscreen($top); } ); } ############################################################## # compoud_menu ############################################################## sub compound_menu { my $w = shift; my $text = shift; my $icon_name = shift; my $space = shift; # optional $space = 19 unless defined $space; my $compound_image = $w->Compound(); if (-f "$icon_path/$icon_name") { $compound_image->Image(-image => $top->Photo(-file => "$icon_path/$icon_name", -gamma => $config{Gamma})); $compound_image->Space(-width => 3); } else { $compound_image->Space(-width => $space); print "Mapivi info: icon $icon_path/$icon_name not found.\n" if ($icon_name ne ''); } $compound_image->Text(-text => $text, -foreground => $conf{color_menu_fg}{value} ); return $compound_image; } ############################################################## # createMenubar ############################################################## sub createMenubar { $menubar = $top->Menu; my $file_menu = $menubar->cascade(-label => lang("File")); # use "~File" for key shortcut $file_menu->cget(-menu)->configure(-title => "File menu"); my $edit_menu = $menubar->cascade(-label => lang("Edit")); $edit_menu->cget(-menu)->configure(-title => "Edit menu"); my $view_menu = $menubar->cascade(-label => lang("View")); $view_menu->cget(-menu)->configure(-title => "View menu"); my $sort_menu = $menubar->cascade(-label => lang('Sort')); $sort_menu->cget(-menu)->configure(-title => "Sort menu"); my $find_menu = $menubar->cascade(-label => lang("Search")); $find_menu->cget(-menu)->configure(-title => "Search menu"); my $opti_menu = $menubar->cascade(-label => lang("Options")); $opti_menu->cget(-menu)->configure(-title => "Options menu"); my $extr_menu = $menubar->cascade(-label => "Extra"); $extr_menu->cget(-menu)->configure(-title => "Extra menu"); my $plug_menu = $menubar->cascade(-label => "PlugIns"); $plug_menu->cget(-menu)->configure(-title => "PlugIn menu"); my $help_menu = $menubar->cascade(-label => lang("Help")); $help_menu->cget(-menu)->configure(-title => "Help menu"); #my $icon = ; $file_menu->command(-image => compound_menu($top, lang('Open folder ...'), 'folder.png'), -command => \&openDir, -accelerator => ""); #$file_menu->command(-image => compound_menu($top, 'open umlaut folder ...', ''), -command => sub { openDirPost("/home/herrmann/tmp/dirb/subdirä"); } ); $file_menu->command(-image => compound_menu($top, lang('Preview folder'), ''), -command => sub { my $dir = getRightDir(); my @list = getPics($dir, WITH_PATH, NO_CHECK_JPEG); sortPics($config{SortBy}, $config{SortReverse}, \@list); showThumbList(\@list, $dir); }, -accelerator => "middle click"); $file_menu->command(-image => compound_menu($top, lang('Search in folder ...'), ''), -command => sub { my $tmp = $config{SearchOnlyInDir}; # save search mode $config{SearchOnlyInDir} = 1; # set to local search searchMetaInfo(); $config{SearchOnlyInDir} = $tmp; # restore search mode }); my $dir_size = $file_menu->cascade(-image => compound_menu($top, lang('Folder size'), '')); $dir_size->command(-label => lang("Calculate folder size"), -command => sub { calcDirSize(); } ); $dir_size->command(-label => lang("Display folder sizes (graphic)"), -command => sub { showDirSizes(getRightDir()); } ); $file_menu->separator; $file_menu->command(-image => compound_menu($top, lang('Rename folder ...'), ''), -command => \&renameDir); $file_menu->command(-image => compound_menu($top, lang('New folder ...'), 'folder-new.png'), -command => sub { my $dir = getRightDir(); if (!-d $dir) { warn "dir $dir is no dir"; return; } makeNewDir($dir, $dirtree); } ); $file_menu->command(-image => compound_menu($top, lang('Delete folder ...'), ''), -command => \&deleteDir); $file_menu->command(-image => compound_menu($top, lang('Hot folders ...'), ''), -command => sub { $dirMenu->Popup(-popover => "cursor", -popanchor => "nw"); }, , -accelerator => ""); $file_menu->separator; addFileActionsMenu($file_menu, $picLB); $file_menu->separator; #my $trash_menu = $file_menu->cascade(-image => compound_menu($top, lang('Trash'), 'user-trash.png')); $file_menu->command(-image => compound_menu($top, lang('Empty trash ...'), 'user-trash.png'), -command => \&emptyTrash); #$trash_menu->command(-label => lang("Open trash in main window"), -command => [\&openDirPost, $trashdir]); $file_menu->command(-image => compound_menu($top, lang('Folder checklist ...'), ''), -command => sub { showDirProperties(); } ); $file_menu->command(-image => compound_menu($top, lang('Import wizard ...'), 'camera-photo.png'), -command => \&importWizard); $file_menu->separator; $file_menu->command(-image => compound_menu($top, lang('Picture collection').' ...', ''), -command => \&light_table_open_window); $file_menu->command(-image => compound_menu($top, lang('Convert non-JPEG pictures ...'), ''), -command => \&convertNonJPEGS); $file_menu->command(-image => compound_menu($top, lang('Reload pictures'), 'view-refresh.png'), -accelerator => "", -command => \&updateThumbsPlus); $file_menu->command(-image => compound_menu($top, lang('Smart update'), 'view-refresh.png'), -command => sub { smart_update(); }, -accelerator => ""); $file_menu->command(-image => compound_menu($top, lang("Rebuild thumbnails ..."), ''), -command => \&rebuildThumbs); #, -accelerator => ""); $file_menu->command(-image => compound_menu($top, lang('Build thumbnails ...'), ''), -command => \&buildThumbsRecursive); $file_menu->separator; $file_menu->command(-image => compound_menu($top, lang('Iconify'), 'user-desktop.png'), -accelerator => "", -command => sub { $top->iconify; }); $file_menu->command(-image => compound_menu($top, lang('Quit'), 'system-log-out.png'), -accelerator => "", -command => \&quitMain); addSelectMenu($edit_menu); $edit_menu->separator; addPicProcessing($edit_menu, $picLB); $edit_menu->separator; # add the comments, EXIF and IPTC menu addMetaInfoMenu($edit_menu); $view_menu->command(-image => compound_menu($top, lang('Next'), 'go-next.png'), -command => sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off showPic(nextPic($actpic)); }, -accelerator => ""); $view_menu->command(-image => compound_menu($top, lang('Previous'), 'go-previous.png'), -command => sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off showPic(prevPic($actpic));}, -accelerator => ""); $view_menu->separator; $view_menu->command(-image => compound_menu($top, lang('First'), 'go-first.png'), -command => sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off my @childs = $picLB->info('children'); return unless (@childs); showPic($childs[0]); }, -accelerator => ""); $view_menu->command(-image => compound_menu($top, lang('Last'), 'go-last.png'), -command => sub { return if (stillBusy()); # block, until last picture is loaded if ($slideshow == 1) { $slideshow = 0; slideshow(); } # switch slideshow off my @childs = $picLB->info('children'); return unless (@childs); showPic($childs[-1]); }, -accelerator => ""); $view_menu->separator; $view_menu->command(-image => compound_menu($top, lang('go to/select ...'), ''), -command => sub { gotoPic($picLB); }); $view_menu->separator; addZoomMenu($view_menu); $view_menu->separator; $view_menu->command(-image => compound_menu($top, lang('Open pictures in new window'), 'image-x-generic.png'), -command => sub { my @sellist = getSelection($picLB); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); show_multiple_pics(\@sellist, 0); }, -accelerator => ""); $view_menu->command(-image => compound_menu($top, lang('Open pictures in external viewer'), 'image-x-generic.png'), -command => sub{openPicInViewer($picLB);}, -accelerator => ""); $view_menu->command(-label => lang("Picture information"), -command => \&identifyPic); $view_menu->command(-label => lang("Histogram (ImageMagick)"), -command => sub { showHistogram($picLB); } ); $view_menu->command(-label => lang("Histogram (Mapivi)"), -command => sub { showHistogram2($picLB); } ); $view_menu->command(-label => lang("Show JPEG segments"), -command => \&showSegments); $view_menu->command(-image => compound_menu($top, lang('Start/stop slideshow'), 'media-playback-start.png'), -command => sub { if ($slideshow == 0) { $slideshow = 1; } else { $slideshow = 0; } slideshow(); }, -accelerator => ""); $view_menu->command(-label => lang("Picture as desktop background"), -command => \&setBackground); $view_menu->separator; add_window_layout_menu($view_menu); $view_menu->command(-label => lang("Fullscreen"), -command => sub { fullscreen($top); }, -accelerator => ""); $view_menu->separator; my $thumb_menu = $view_menu->cascade(-label => lang("Thumbnail table")); $thumb_menu->cget(-menu)->configure(-title => lang("Thumbnail table")); my $caption_menu = $thumb_menu->cascade(-label => lang("Thumbnail caption")); $caption_menu->cget(-menu)->configure(-title => "Thumbnail caption ..."); $caption_menu->radiobutton(-label => lang("None"), -variable => \$config{ThumbCapt}, -value => "none", -command => sub { updateThumbsPlus(); }); $caption_menu->radiobutton(-label => lang("File name without suffix"), -variable => \$config{ThumbCapt}, -value => "filename", -command => sub { updateThumbsPlus(); }); $caption_menu->radiobutton(-label => lang("File name with suffix"), -variable => \$config{ThumbCapt}, -value => "filenameSuffix", -command => sub { updateThumbsPlus(); }); $caption_menu->radiobutton(-label => lang("IPTC object name"), -variable => \$config{ThumbCapt}, -value => "objectname", -command => sub { updateThumbsPlus(); }); $thumb_menu->separator; $thumb_menu->checkbutton(-label => lang("Show file info"), -variable => \$config{ShowFile}, -command => \&toggleHeaders); $thumb_menu->checkbutton(-label => lang("Show IPTC"), -variable => \$config{ShowIPTC}, -command => \&toggleHeaders); $thumb_menu->checkbutton(-label => lang("Show comments"), -variable => \$config{ShowComment}, -command => \&toggleHeaders); $thumb_menu->checkbutton(-label => lang("Show EXIF"), -variable => \$config{ShowEXIF}, -command => \&toggleHeaders); $thumb_menu->checkbutton(-label => lang("Show folder"), -variable => \$config{ShowDirectory}, -command => \&toggleHeaders); $sort_menu->radiobutton(-label => lang("File name"), -variable => \$config{SortBy}, -value => "name", -command => sub { updateThumbsPlus(); }); $sort_menu->radiobutton(-label => lang("File date"), -variable => \$config{SortBy}, -value => "date", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("File size"), -variable => \$config{SortBy}, -value => "size", -command => \&updateThumbsPlus); $sort_menu->separator; $sort_menu->radiobutton(-label => lang("IPTC urgency/rating"), -variable => \$config{SortBy}, -value => "urgency", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("IPTC by-line"), -variable => \$config{SortBy}, -value => "byline", -command => \&updateThumbsPlus); $sort_menu->separator; $sort_menu->radiobutton(-label => lang("Flags"), -variable => \$config{SortBy}, -value => "flag", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("Number of views"), -variable => \$config{SortBy}, -value => "popularity", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("Number of pixels"), -variable => \$config{SortBy}, -value => "pixel", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("Number of bits per pixels (b/p)"), -variable => \$config{SortBy}, -value => "bitpix", -command => \&updateThumbsPlus) if ($config{BitsPixel}); $sort_menu->separator; $sort_menu->radiobutton(-label => lang("EXIF date"), -variable => \$config{SortBy}, -value => "exifdate", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("EXIF aperture"), -variable => \$config{SortBy}, -value => "aperture", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("EXIF exposure time"), -variable => \$config{SortBy}, -value => "exposuretime", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("EXIF camera maker/model"), -variable => \$config{SortBy}, -value => "model", -command => \&updateThumbsPlus); $sort_menu->radiobutton(-label => lang("EXIF artist"), -variable => \$config{SortBy}, -value => "artist", -command => \&updateThumbsPlus); $sort_menu->separator; $sort_menu->radiobutton(-label => lang("Sort randomly"), -variable => \$config{SortBy}, -value => "random", -command => \&updateThumbsPlus); $sort_menu->separator; $sort_menu->checkbutton(-label => lang("Sort reverse"), -variable => \$config{SortReverse}, -command => \&updateThumbsPlus); $find_menu->command(-image => compound_menu($top, lang('Search ...'), 'system-search.png'), -command => sub { $config{Layout} = 1 ; layout(1); $act_modus = SEARCH; $nav_F->{nav_tab}->raise('search');}); $find_menu->command(-image => compound_menu($top, lang('Advanced search ...'), 'system-search.png'), -command => \&searchMetaInfo, -accelerator => ''); $find_menu->command(-image => compound_menu($top, lang('Search by keywords (tag cloud) ...'), 'weather-overcast.png'), -command => \&keyword_browse, -accelerator => ''); $find_menu->command(-image => compound_menu($top, lang('Search by timeline ...'), 'x-office-calendar.png'), -command => \&database_info); $find_menu->command(-image => compound_menu($top, lang('Search by location ...'), 'applications-internet.png'), -command => sub { search_by_location($picLB); } ); $find_menu->command(-image => compound_menu($top, lang('Search duplicates ...'), ''), -command => \&findDups); #$find_menu->command(-label => "check for new keywords ...", -command => \&check_new_keywords); $find_menu->separator; my $find_special_menu = $find_menu->cascade(-image => compound_menu($top, lang('Special searches'), '')); $find_menu->command(-image => compound_menu($top, lang('Slideshow all pictures with filter').' '.lang("Settings"), ''), -command => sub {slideshow_all_pics(SETTINGS);}); $find_menu->command(-image => compound_menu($top, lang('Slideshow all pictures with filter').' '.lang("Start"), ''), -command => sub {slideshow_all_pics(START);}); $find_special_menu->command(-label => lang("TOP 100 (best rated)"), -command => \&showMostPopularPics); $find_special_menu->command(-label => lang("EXIF histogram"), -command => \&exif_histogram); $find_special_menu->command(-image => compound_menu($top, lang('Search for file name ...'), 'edit-find.png'), -command => sub { searchFileName($picLB);}); $find_special_menu->command(-image => compound_menu($top, lang('Search for date/time'), 'edit-find.png'), -command => sub { search_by_date_time($picLB);}); $find_menu->separator; $find_menu->command(-image => compound_menu($top, lang('Add to database ...'), 'list-add.png'), -command => \&buildDatabase); $find_menu->command(-image => compound_menu($top, lang('Clean database ...'), 'edit-clear.png'), -command => \&cleanDatabase); $find_menu->command(-image => compound_menu($top, lang('Check database ...'), ''), -command => \&checkDatabase); $find_menu->command(-image => compound_menu($top, lang('Edit database ...'), 'accessories-text-editor.png'), -command => \&editDatabase); $find_menu->command(-image => compound_menu($top, lang('Database information ...'), 'dialog-information.png'), -command => \&diff_database_statistic); $opti_menu->command(-image => compound_menu($top, lang('Options ...'), 'preferences-system.png'), -command => \&options_edit, -accelerator => ""); $opti_menu->command(-image => compound_menu($top, lang('Other options ...'), 'preferences-system.png'), -command => \&options); $opti_menu->command(-image => compound_menu($top, lang("Save options"), 'media-floppy.png'), -command => \&saveAllConfig); $extr_menu->command(-label => lang("Export filelist ..."), -command => \&exportFilelist); $extr_menu->command(-label => lang("Compare folders")." ...", -command => sub { dirDiffWindow(); } ); $extr_menu->command(-label => lang("Compare pictures"), -command => \&diffPics); $extr_menu->separator; $extr_menu->command(-label => lang("HDR ..."), -command => \&hdr_pic); #$extr_menu->command(-label => lang("Fuzzy border ..."), -command => \&fuzzyBorder); $extr_menu->command(-label => lang("Drop picture (lossless) ..."), -command => \&losslessWatermark); $extr_menu->command(-label => lang("Generate logo ..."), -command => \&logo_generate_win); $extr_menu->command(-label => lang("Make screenshot ..."), -command => \&screenshot); $extr_menu->separator; $extr_menu->command(-label => lang("Build thumbnails ..."), -command => \&buildThumbsRecursive); $extr_menu->command(-label => lang("Clean thumbnails ..."), -command => sub { cleanThumbDB(); } ); $extr_menu->command(-label => lang("Clean folder ..."), -command => sub { cleanDir($actdir); } ); $extr_menu->command(-label => lang("Edit entry history ..."), -command => sub { editEntryHistory(); } ); $extr_menu->command(-label => lang("Session info"), -command => sub { session_info(); } ); $extr_menu->separator; $extr_menu->command(-label => lang("Mapivi test suite"), -command => \&testSuite); $extr_menu->command(-label => lang("Translation scan"), -command => \&language_scan); # mh 2011-04-29 this takes too long (some minutes for 12 drive letters) #if (Win32DriveInfoAvail) { # $extr_menu->command(-label => "dirtree drive letter test", -command => sub{$dirtree->configure(-directory => "$_:") foreach (Win32::DriveInfo::DrivesInUse());}); #} #$extr_menu->command(-label => "test menu", -command => sub {my @list; push @list, "Hund/Katze/Maus"; add_new_keywords(\@list); }); my $xmp_menu = $extr_menu->cascade(-label => "XMP ..."); $xmp_menu->command(-label => lang("Copy IPTC urgency to XMP rating"), -command => sub { rating_iptc_to_xmp(); } ); $xmp_menu->command(-label => lang("Copy XMP rating to IPTC urgency"), -command => sub { rating_xmp_to_iptc(); } ); $xmp_menu->command(-label => lang("Copy XMP keywords to IPTC"), -command => sub { keywords_xmp_to_iptc_folder(); } ); makePlugInsMenu($plug_menu, $plugin_sys_path); # add system wide plugins to the menu $plug_menu->separator; makePlugInsMenu($plug_menu, $plugin_user_path); # add user specific plugins to the menu $help_menu->command(-image => compound_menu($top, lang('About'), 'dialog-information.png'), -command => \&about); $help_menu->command(-image => compound_menu($top, lang('Keys'), 'input-keyboard.png'), -command => \&showkeys); $help_menu->command(-image => compound_menu($top, lang('System information'), 'utilities-system-monitor.png'), -command => \&systemInfo); foreach my $file (qw(License Changes Tips FAQ)) { $help_menu->command(-image => compound_menu($top, $file, 'help-browser.png'), -command => [\&showFile, "$program_data_path/docs/${file}.txt"]) if (-f "$program_data_path/docs/${file}.txt"); } $help_menu->command(-image => compound_menu($top, lang('Mapivi Home'), 'dialog-information.png'), -command => sub {web_browser_open($mapiviURL);} ); $top->configure(-menu => $menubar) if $config{ShowMenu}; } ############################################################## ############################################################## sub add_window_layout_menu { my $menu = shift; my $layout_menu = $menu->cascade(-label => lang("Window layout")); $layout_menu->cget(-menu)->configure(-title => lang("Window layout")); $layout_menu->command(-label => lang('Toggle layout'), -command => sub { $config{Layout}++; layout(1); }, -accelerator => ''); $layout_menu->separator; $layout_menu->command(-image => compound_menu($top, lang("3 columns: Navigation Thumbnails Picture"), 'layout-ntp.png'), -command => sub { $config{Layout} = 0 ; layout(1); }, -accelerator => ""); $layout_menu->command(-image => compound_menu($top, lang("2 columns: Navigation Thumbnails"), 'layout-nt.png'), -command => sub { $config{Layout} = 1 ; layout(1); }, -accelerator => ""); $layout_menu->command(-image => compound_menu($top, lang("1 column: Thumbnails"), 'layout-t.png'), -command => sub { $config{Layout} = 2 ; layout(1); }, -accelerator => ""); $layout_menu->command(-image => compound_menu($top, lang("2 columns: Thumbnails Picture"), 'layout-tp.png'), -command => sub { $config{Layout} = 3 ; layout(1); }, -accelerator => ""); $layout_menu->command(-image => compound_menu($top, lang("1 column: Picture"), 'layout-p.png'), -command => sub { $config{Layout} = 4 ; layout(1); }, -accelerator => ""); $layout_menu->command(-image => compound_menu($top, lang("2 columns: Navigation Picture"), 'layout-tp.png'), -command => sub { $config{Layout} = 5 ; layout(1); }); $layout_menu->separator; $layout_menu->checkbutton(-label => lang("Menu bar"), -variable => \$config{ShowMenu}, -command => sub { showHideFrames(); }, -accelerator => ""); $layout_menu->checkbutton(-label => lang("Status bar"), -variable => \$config{ShowInfoFrame}, -command => sub { showHideFrames(); }, -accelerator => ""); $layout_menu->checkbutton(-label => lang("Picture metadata overlay"), -variable => \$config{ShowInfoInCanvas}, -command => sub { showImageInfoCanvas($actpic); }, -accelerator => ""); $layout_menu->checkbutton(-label => lang("IPTC box"), -variable => \$config{ShowIPTCFrame}, -command => sub { showHideFrames(); }, -accelerator => ""); $layout_menu->checkbutton(-label => lang("Comment box"), -variable => \$config{ShowCommentField}, -command => sub { showHideFrames(); }); $layout_menu->checkbutton(-label => lang("Display coordinates"), -variable => \$conf{show_coordinates}{value}); } ############################################################## # addPicProcessing ############################################################## sub addPicProcessing { my $menu = shift; my $widget = shift; # listbox or canvas my $rot_menu = $menu->cascade(-image => compound_menu($top, lang('Rotate ...'), 'transform-rotate.png')); $rot_menu->cget(-menu)->configure(-title => "rotation menu"); $rot_menu->command(-label => lang("Rotate 90 - right (lossless)"), -command => sub { rotate(90); }, -accelerator => "<9>"); $rot_menu->command(-label => lang("Rotate 180 (lossless)"), -command => sub { rotate(180); }, -accelerator => "<8>"); $rot_menu->command(-label => lang("Rotate 270 - left (lossless)"), -command => sub { rotate(270); }, -accelerator => "<7>"); $rot_menu->command(-label => lang("Flip horizontal (lossless)"), -command => sub { rotate("horizontal"); }); $rot_menu->command(-label => lang("Flip vertical (lossless)"), -command => sub { rotate("vertical"); }); $rot_menu->command(-label => lang("Auto rotate (lossless)"), -command => sub { rotate("auto"); }, -accelerator => "<0>"); $rot_menu->command(-label => lang("Clear rotate flag"), -command => sub { rotate("clear"); }); $rot_menu->command(-label => lang("Rotate ..."), -command => [\&rotateAny]); $menu->command(-image => compound_menu($top, lang('Change size/quality ...'), 'transform-scale.png'), -command => \&changeSizeQuality, -accelerator => "" ); $menu->command(-image => compound_menu($top, lang('Crop (lossless) ...'), 'edit-cut.png'), -command => sub { crop($widget); }, -accelerator => ""); $menu->command(-image => compound_menu($top, lang('Image processing ...'), 'camera-photo.png'), -command => \&filterPic); $menu->command(-image => compound_menu($top, lang('Image processing extern'), 'applications-graphics.png'), -command => sub { edit_pic($widget); }, -accelerator => ""); $menu->command(-image => compound_menu($top, lang('Collage/index print ...'), 'image-x-generic.png'), -command => sub { my @pics = getSelection($widget); indexPrint(\@pics); }); $menu->command(-image => compound_menu($top, lang('Passport print ...'), 'image-x-generic.png'), -command => sub { passport_print($widget); }); $menu->command(-image => compound_menu($top, lang("Convert to black and white"), 'image-x-generic-bw.png'), -command => sub { grayscalePic($widget); } ); $menu->command(-image => compound_menu($top, lang("Black and white preview"), 'image-x-generic-bw.png'), -command => sub { my @list = getSelection($widget); return unless checkSelection($top, 1, 1, \@list, lang("picture(s)")); grayscale_preview($list[0]); }); my $border_menu = $menu->cascade(-image => compound_menu($top, lang('Add border ...'), 'image-x-generic.png')); $border_menu->cget(-menu)->configure(-title => lang('Border menu')); $border_menu->command(-image => compound_menu($top, lang('Add border (lossless) ...'), 'image-x-generic.png'), -command => sub { losslessBorder(PIXEL); }, -accelerator => ""); $border_menu->command(-image => compound_menu($top, lang('Add border aspect ratio (lossless) ...'), 'image-x-generic.png'), -command => sub { losslessBorder(ASPECT_RATIO); } ); $border_menu->command(-image => compound_menu($top, lang('Add relative border (lossless) ...'), 'image-x-generic.png'), -command => sub { losslessBorder(RELATIVE); } ); $border_menu->command(-image => compound_menu($top, lang('Add border or copyright (lossy) ...'), 'image-x-generic.png'), -command => \&addDecoration); $border_menu->command(-image => compound_menu($top, lang("Fuzzy border (lossy) ..."), 'image-x-generic.png'), -command => \&fuzzyBorder); $menu->command(-image => compound_menu($top, lang('Extract JPEG preview from raw'), 'image-x-generic.png'), -command => sub { extract_jpeg($widget); }); } ############################################################## # addFileActionsMenu ############################################################## sub addFileActionsMenu { my $menu = shift; my $lb = shift; my $fop_menu = $menu->cascade(-image => compound_menu($top, lang('File operations ...'), '')); $fop_menu->command(-image => compound_menu($top, lang('Copy to ...'), 'edit-copy.png'), -command => sub { copyPicsDialog(COPY, $lb); } ); $fop_menu->command(-image => compound_menu($top, langf("Copy to \"%s\" folder", $conf{origs_folder_name}{value}), ''), -command => sub { copy_move_to_origs($lb, COPY); }); $fop_menu->command(-image => compound_menu($top, lang('Copy to print ...'), 'printer.png'), -command => sub { copyToPrint($lb); }, -accelerator => ""); $fop_menu->command(-image => compound_menu($top, lang('Link to ...'), ''), -command => sub { linkPicsDialog($lb); }) if (!$EvilOS); $fop_menu->command(-image => compound_menu($top, lang('Move to ...'), ''), -command => sub { movePicsDialog($lb); } ); $fop_menu->command(-image => compound_menu($top, langf("Move to \"%s\" folder", $conf{origs_folder_name}{value}), ''), -command => sub { copy_move_to_origs($lb, MOVE); }, -accelerator => "" ); $fop_menu->command(-image => compound_menu($top, lang('Rename ...'), ''), -command => sub { renamePic($lb); }, -accelerator => ""); $fop_menu->command(-image => compound_menu($top, lang('Smart rename ...'), ''), -command => sub { renameSmart($lb); }, -accelerator => ""); $fop_menu->separator; $fop_menu->command(-image => compound_menu($top, lang('Email to ...'), 'mail-message-new.png'), -command => sub { sendTo($lb); } ); $fop_menu->command(-image => compound_menu($top, lang('Convert ...'), ''), -command => sub { convertPics($lb); } ); $fop_menu->command(-image => compound_menu($top, lang('Make backup'), ''), -command => sub { copyPicsDialog(BACKUP, $lb); } ); $fop_menu->command(-image => compound_menu($top, lang('Make HTML ...'), 'applications-internet.png'), -command => sub { makeHTML($lb); }); $fop_menu->separator; $fop_menu->command(-image => compound_menu($top, lang('Delete to trash'), 'user-trash.png'), -accelerator => "", -command => sub { deletePics($lb, TRASH); } ); $fop_menu->command(-image => compound_menu($top, lang('Delete ...'), ''), -accelerator => "", -command => sub { deletePics($lb, REMOVE); } ); } ############################################################## # addSelectMenu ############################################################## sub addSelectMenu { my $menu = shift; my $sel_menu = $menu->cascade(-image => compound_menu($top, lang('Select ...'), '')); $sel_menu->command(-label => lang("Select all"), -accelerator => "", -command => sub {selectAll($picLB);} ); $sel_menu->command(-label => lang("Select all backups"), -command => \&selectBak ); $sel_menu->command(-label => lang("Invert selection"), -command => \&selectInv ); $sel_menu->command(-label => lang("Redo selection"), -command => sub { $picLB->selectionClear(); reselect($picLB, @savedselection2); } ); } ############################################################## # addZoomMenu ############################################################## sub addZoomMenu { my $menu = shift; $menu->checkbutton(-label => lang("Auto zoom (fit picture)"), -variable => \$config{AutoZoom}); my $zoom_menu = $menu->cascade(-label => lang("Zoom ...")); $zoom_menu->cget(-menu)->configure(-title => lang("Zoom menu")); $zoom_menu->command(-label => lang("fit"), -command => sub { $conf{zoom_fit_fill}{value} = FIT; fitPicture(); }, -accelerator => " or "); $zoom_menu->command(-label => lang("fill"), -command => sub { $conf{zoom_fit_fill}{value} = FILL; fitPicture(); }, -accelerator => ""); my $i; for ($i = 0; $i < (@frac); $i += 2) { my $z = $frac[$i]; my $s = $frac[$i+1]; my $l = sprintf "%4d%%",($z/$s*100); unless ($l =~ m/\w*100%/) { $zoom_menu->command(-label => $l, -command => sub { zoom($z, $s); } ); } else { $zoom_menu->command(-label => $l, -command => sub { zoom($z, $s); }, -accelerator => " or "); } } } ############################################################## # the flags (red, green, blue) are used to mark pictures e.g. # for further processing. They are not stored in the picture, # but in the search database as bits ############################################################## sub flag_toggle { my $lb = shift; my $flag = shift; return unless (defined($flag)); return if (($flag < 0) or ($flag > 8)); my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $pw = progressWinInit($top, lang("Set / reset flags")); my $picnr = 0; foreach my $dpic (@sellist) { last if (progressWinCheck($pw)); $picnr++; progressWinUpdate($pw, "Processing picture $picnr/".scalar(@sellist), $picnr, scalar(@sellist)); # init flag if it does not exists if (not exists $searchDB{$dpic}{FLAG}) { $searchDB{$dpic}{FLAG} = FLAG_RESET; } if ($flag == FLAG_RESET) { $searchDB{$dpic}{FLAG} = FLAG_RESET; } else { $searchDB{$dpic}{FLAG} ^= $flag; # binary XOR = toggle flag } updateOneRow($dpic, $lb); # currently (2011-08) not needed, because the flags are # only shown in the thumbnail table #if ($dpic eq $actpic) { # showImageInfoCanvas($dpic); # showImageInfo($dpic); #} } progressWinEnd($pw); log_it(langf("Ready! Flag %d toggled in %d pictures.",$flag, scalar(@sellist))); } ############################################################## # mark all selected pictures which are used in a picture collection # with the given flag color ############################################################## sub flag_set_collection { my $lb = shift; my $flag = shift; return unless (defined($flag)); return if (($flag < 1) or ($flag > 8)); my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $slideshow_pics = build_slideshow_pic_hash(); my $marked_pics = 0; my $pw = progressWinInit($top, lang("Set / reset flags")); my $picnr = 0; foreach my $dpic (@sellist) { last if (progressWinCheck($pw)); $picnr++; progressWinUpdate($pw, "Processing picture $picnr/".scalar(@sellist), $picnr, scalar(@sellist)); if (defined $$slideshow_pics{$dpic}) { print "$dpic is used in a slideshow!\n"; $searchDB{$dpic}{FLAG} |= $flag; # binary OR = set flag updateOneRow($dpic, $lb); $marked_pics++; } } progressWinEnd($pw); log_it(langf("Ready! %d of the %d selected pictures are used in a collection and are now marked with a flag.",$marked_pics, scalar(@sellist))); } ############################################################## # addMetaInfoMenu ############################################################## sub addMetaInfoMenu { my $menu = shift; my $flag_menu = $menu->cascade(-image => compound_menu($top, lang("Flags"), 'media-record16-red.png')); $flag_menu->cget(-menu)->configure(-title => lang("Flags")); $flag_menu->command(-image => compound_menu($top, lang('Red flag'), 'media-record16-red.png'), -command => sub { flag_toggle($picLB, FLAG_RED); }, -accelerator => ""); $flag_menu->command(-image => compound_menu($top, lang('Green flag'), 'media-record16-green.png'), -command => sub { flag_toggle($picLB, FLAG_GREEN); }, -accelerator => ""); $flag_menu->command(-image => compound_menu($top, lang('Blue flag'), 'media-record16-blue.png'), -command => sub { flag_toggle($picLB, FLAG_BLUE); }, -accelerator => ""); $flag_menu->separator; $flag_menu->command(-image => compound_menu($top, lang('Flag pictures used in collections'), 'media-record16-green.png'), -command => sub { flag_set_collection($picLB, FLAG_GREEN); }); $flag_menu->separator; $flag_menu->command(-image => compound_menu($top, lang('Reset all flags'), ''), -command => sub { flag_toggle($picLB, FLAG_RESET); }); my $iptc_menu = $menu->cascade(-image => compound_menu($top, 'IPTC', '')); $iptc_menu->cget(-menu)->configure(-title => "IPTC/IIM information"); $iptc_menu->command(-image => compound_menu($top, lang('Show'), ''), -command => sub { displayIPTCData($picLB); }, -accelerator => ""); $iptc_menu->command(-image => compound_menu($top, lang('Edit ...'), 'accessories-text-editor.png'), -command => sub { editIPTC($picLB); }, -accelerator => ""); $iptc_menu->command(-image => compound_menu($top, lang('Set location from GPS ...'), 'internet-web-browser.png'), -command => sub { gps_to_location($picLB); }); $iptc_menu->command(-image => compound_menu($top, lang('Remove ...'), ''), -command => \&removeIPTC); $iptc_menu->separator; $iptc_menu->command(-image => compound_menu($top, lang('Copy from ...'), 'edit-copy.png'), -command => \©IPTC); $iptc_menu->command(-image => compound_menu($top, lang('Copy to ...'), 'edit-paste.png'), -command => \&pasteIPTC); $iptc_menu->separator; $iptc_menu->command(-image => compound_menu($top, lang('Add/remove categories ...'), ''), -command => sub { editIPTCCategories($picLB); }); $iptc_menu->separator; $iptc_menu->command(-image => compound_menu($top, lang('Save template ...'), ''), -command => sub { saveIPTC($picLB); } ); $iptc_menu->command(-image => compound_menu($top, lang('Merge template ...'), ''), -command => \&mergeIPTC); $iptc_menu->separator; addRatingMenu($iptc_menu, $picLB); addRatingMenu($menu, $picLB); my $xmp_menu = $menu->cascade(-image => compound_menu($top, 'XMP', '')); $xmp_menu->cget(-menu)->configure(-title => 'XMP information'); $xmp_menu->command(-image => compound_menu($top, 'show all XMP information', ''), -command => sub { xmp_show($picLB); }, -accelerator => ""); $xmp_menu->command(-image => compound_menu($top, 'add title ...', ''), -command => sub { xmp_add_title($picLB); }); $xmp_menu->command(-image => compound_menu($top, 'edit title ...', ''), -command => sub { xmp_edit_title($picLB); }); $xmp_menu->command(-image => compound_menu($top, 'add keyword ...', ''), -command => sub { xmp_add_keyword($picLB); }); $xmp_menu->command(-image => compound_menu($top, 'copy XMP keywords to IPTC ...', ''), -command => sub { keywords_xmp_to_iptc($picLB); }); $xmp_menu->command(-image => compound_menu($top, 'Remove ...', ''), -command => sub { xmp_remove($picLB); }); $xmp_menu->separator; $xmp_menu->command(-image => compound_menu($top, 'PNG: show info', ''), -command => sub { png_show($picLB); }); my $exif_menu = $menu->cascade(-image => compound_menu($top, 'EXIF', '')); $exif_menu->cget(-menu)->configure(-title => "EXIF information"); $exif_menu->command(-image => compound_menu($top, lang('show info'), ''), -command => sub { displayEXIFData($picLB); }, -accelerator => ""); $exif_menu->command(-image => compound_menu($top, lang('show GPS position in map'), 'internet-web-browser.png'), -command => sub { gps_map_open($picLB); } ); $exif_menu->command(-image => compound_menu($top, lang('show thumbnail'), ''), -command => \&showEXIFThumb, -accelerator => ""); $exif_menu->command(-image => compound_menu($top, lang('save thumbnail ...'), ''), -command => \&getEXIFThumb); $exif_menu->command(-image => compound_menu($top, lang('(re)build thumbnail ...'), ''), -command => \&buildEXIFThumb); $exif_menu->separator; $exif_menu->command(-image => compound_menu($top, lang('Copy from ...'), 'edit-copy.png'), -command => \©EXIFData); $exif_menu->command(-image => compound_menu($top, lang('Copy to ...'), 'edit-paste.png'), -command => \&pasteEXIFData); $exif_menu->command(-image => compound_menu($top, lang('Copy thumbnail to ...'), ''), -command => \©Thumbnail); $exif_menu->separator; $exif_menu->command(-image => compound_menu($top, lang('save'), ''), -command => \&EXIFsave); $exif_menu->command(-image => compound_menu($top, lang('restore ...'), ''), -command => \&EXIFrestore); $exif_menu->command(-image => compound_menu($top, lang('remove saved info ...'), ''), -command => \&EXIFremoveSaved); $exif_menu->separator; $exif_menu->command(-image => compound_menu($top, lang('set date/time ...'), 'accessories-text-editor.png'), -command => \&setEXIFDate, -accelerator => ""); $exif_menu->command(-image => compound_menu($top, lang('set GPS position ...'), 'accessories-text-editor.png'), -command => sub {gps_set($picLB);}, -accelerator => ""); $exif_menu->command(-image => compound_menu($top, lang('set year from file name ...'), 'accessories-text-editor.png'), -command => \&setEXIFDate_from_file_name); $exif_menu->separator; $exif_menu->command(-image => compound_menu($top, lang('remove thumbnail ...'), ''), -command => [\&removeEXIFData, "thumb"]); $exif_menu->command(-image => compound_menu($top, lang('remove all ...'), ''), -command => [\&removeEXIFData, "all"]); my $comm_menu = $menu->cascade(-image => compound_menu($top, lang('Comments'), '')); $comm_menu->cget(-menu)->configure(-title => "Comment menu"); $comm_menu->command(-label => "show ...", -command => \&showComment, -accelerator => ""); $comm_menu->separator; $comm_menu->command(-label => "add ...", -command => sub{ addComment($picLB); }, -accelerator => ""); $comm_menu->command(-image => compound_menu($top, lang('Edit ...'), 'accessories-text-editor.png'), -command => sub{ editComment($picLB); }, -accelerator => ""); $comm_menu->command(-label => "join ...", -command => sub { joinComments(ASK); } ); $comm_menu->command(-label => "search/replace ...", -command => sub{ replaceComment($picLB); } ); $comm_menu->command(-label => 'add/remove keywords ...', -command => sub { editCommentKeywords($picLB); } ); $comm_menu->separator; $comm_menu->command(-label => "remove ...", -command => \&removeComment); $comm_menu->command(-label => "remove all ...", -command => sub { removeAllComments(ASK); } ); $comm_menu->separator; $comm_menu->command(-image => compound_menu($top, lang('Copy from ...'), 'edit-copy.png'), -command => [\©Comment, "from"]); $comm_menu->command(-image => compound_menu($top, lang('Copy to ...'), 'edit-paste.png'), -command => [\©Comment, "to"]); $comm_menu->separator; $comm_menu->command(-label => "add filename as comment ...", -command => [\&nameToComment, "to"]); } ############################################################## # addRatingMenu ############################################################## sub addRatingMenu { my $menu = shift; my $widget = shift; # e.g. $picLB my $iptc_urge = $menu->cascade(-image => compound_menu($top, lang('Rating'), '')); $iptc_urge->cget(-menu)->configure(-title => lang('Rating')); $iptc_urge->command(-image => compound_menu($top, '5 '.lang('stars'), 'rating-1.png'), -command => sub {setIPTCurgency($widget, 1);}, -accelerator => "<5>"); $iptc_urge->command(-image => compound_menu($top, '4 '.lang('stars'), 'rating-2.png'), -command => sub {setIPTCurgency($widget, 2);}, -accelerator => "<4>"); $iptc_urge->command(-image => compound_menu($top, '3 '.lang('stars'), 'rating-3.png'), -command => sub {setIPTCurgency($widget, 3);}, -accelerator => "<3>"); $iptc_urge->command(-image => compound_menu($top, '2 '.lang('stars'), 'rating-4.png'), -command => sub {setIPTCurgency($widget, 4);}, -accelerator => "<2>"); $iptc_urge->command(-image => compound_menu($top, '1 '.lang('star'), 'rating-5.png'), -command => sub {setIPTCurgency($widget, 5);}, -accelerator => "<1>"); $iptc_urge->command(-image => compound_menu($top, '2/3 '.lang('star'), 'rating-6.png'), -command => sub {setIPTCurgency($widget, 6);}, -accelerator => ""); $iptc_urge->command(-image => compound_menu($top, '1/2 '.lang('star'), 'rating-7.png'), -command => sub {setIPTCurgency($widget, 7);}, -accelerator => ""); $iptc_urge->command(-image => compound_menu($top, '1/3 '.lang('star'), 'rating-8.png'), -command => sub {setIPTCurgency($widget, 8);}, -accelerator => ""); $iptc_urge->command(-image => compound_menu($top, '0 '.lang('stars'), 'rating-0.png'),-command => sub {setIPTCurgency($widget, 0);}, -accelerator => ""); $iptc_urge->command(-image => compound_menu($top, lang('Remove rating'), 'rating-0.png'),-command => sub {setIPTCurgency($widget, 9);}, -accelerator => ""); } ############################################################## # makePlugInsMenu ############################################################## sub makePlugInsMenu { my $menu = shift; my $dir = shift; log_it("Adding PlugIns from $dir"); return if (not -d $dir); my @plugins = getFiles($dir); my $file; foreach my $plugin (@plugins) { if ($plugin =~ m/.*\.txt$/) { # process just the descriptions if (!open($file, '<', "$dir/$plugin")) { warn "read PlugIn description: Couldn't open $plugin: $!"; next; } while (<$file>) { chomp; # no newline s/^#.*//; # no comments (lines starting with #) s/^\s+//; # no leading white s/\s+$//; # no trailing white next unless length; # anything left? my ($prog, $menuitem, $update, $desc); # example line: # filelist-plugin.pl + write file list + 0 + this plugin will write a file list if ($_ =~ m|(.+)\s\+\s(.+)\s\+\s(\d)\s\+\s(.*)|) { # "\s\+\s" = " + " $prog = $1; $menuitem = $2; $update = $3; $desc = $4; } else { warn "warning: PlugIn $plugin has wrong line format!\n"; next; } if (length($menuitem) > 50) { warn "warning: PlugIn $plugin: menu entry is too long \"$menuitem\" (max. 50 chars allowed)!\n"; next; } print "PlugIn: -$prog-$menuitem-$update-$desc-\n" if $verbose; if (!-f "$dir/$prog") { # look for the corresponding plugin warn "warning: PlugIn $prog for description $plugin not fount in $dir\n"; next; } my $item = $menu->command(-label => "$menuitem", -command => sub { print "$prog $menuitem $desc\n" if $verbose; my @sellist = $picLB->info('selection'); #return unless checkSelection($top, 1, 0, \@sellist); my $command = "\"$dir/$prog\" "; foreach (@sellist) { $command .= "\"$_\" "; } print "com = $command\n" if $verbose; my $buffer = `$command`; # execute command showText("Output of PlugIn $menuitem", $buffer, NO_WAIT) if ($buffer ne ''); updateThumbsPlus() if $update; }); #$balloon->attach($item, -msg => "$desc"); # does not work :( log_it(" Added PlugIn \"$menuitem\" ($prog)"); } close $file; } } } ############################################################## # toggleHeaders - adjusts the width of the columns to zero # or the width needed ('') ############################################################## sub toggleHeaders { my @col = ($conf{color_bg}{value}, $conf{color_bg2}{value}); my $c = 1; if ($config{ShowFile}) { $picLB->columnWidth($picLB->{filecol},''); $fileS->configure(-background=>$col[$c%2]); $c++; } else { $picLB->columnWidth($picLB->{filecol},0); } if ($config{ShowIPTC}) { $picLB->columnWidth($picLB->{iptccol},''); $iptcS->configure(-background=>$col[$c%2]); $c++; } else { $picLB->columnWidth($picLB->{iptccol},0); } if ($config{ShowComment}) { $picLB->columnWidth($picLB->{comcol},''); $comS->configure(-background=>$col[$c%2]); $c++; } else { $picLB->columnWidth($picLB->{comcol},0); } if ($config{ShowEXIF}) { $picLB->columnWidth($picLB->{exifcol},''); $exifS->configure(-background=>$col[$c%2]); $c++; } else { $picLB->columnWidth($picLB->{exifcol},0); } if ($config{ShowDirectory}) { $picLB->columnWidth($picLB->{dircol},''); $dirS->configure(-background=>$col[$c%2]); $c++; } else { $picLB->columnWidth($picLB->{dircol},0); } } ############################################################## # calcDirSize ############################################################## sub calcDirSize { my $dir = getRightDir(); my $size = 0; my $files = 0; my $dirs = 0; my $break = 0; my $pw = progressWinInit($top, lang("Calculate folder size")); find(sub { if (progressWinCheck($pw)) { $break = 1; $File::Find::prune = 1; } # we don't know how long it will take, so we set total to zero progressWinUpdate($pw, "size $size Bytes", 0, 0); $files++ if -f; $dirs++ if -d; $size += -s; },$dir); progressWinEnd($pw); $dirs--; # we don't want to include the basedir my $msg = lang("Calculation finished."); if ($break) { $msg = lang("Warning: The calculation wasn't finished!\nReal size may be bigger than displayed."); } my $unitSize = computeUnit($size); # add thousands separators (split every 3 digits and add a separator (dot)) my $sep = '.'; $size =~ s/(?<=\d)(?=(?:\d{3})+\b)/$sep/g; showText(langf("Folder size of %s", basename($dir)), langf("%s\nThe folder size of \"%s\" including thumbnails is\n\n%s (%s Bytes)\n\n%d file(s)\n%d folder(s)", $msg, $dir, $unitSize, $size, $files, $dirs), NO_WAIT); } ############################################################## # buildThumbsRecursive - scans through all sub folders of # the actual dir an collects JPEG files # let the user select in which dirs # mapivi should build/refresh thumbnails ############################################################## sub buildThumbsRecursive { my $basedir = getRightDir(); log_it("Updating thumbnails ..."); my $rc = $top->messageBox(-icon => 'question', -message => "Mapivi will first scan through all sub folders of $basedir and collect all folders containing pictures.\nThen you are able to select in which folders mapivi should build/refresh thumbnails.", -title => "Build thumbnails in all sub folders", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my ($ok, $dirlist, $pic_count, $nr_of_pics_in_dir) = get_subdirs($basedir); return if (not $ok); my @tmplist; return if (!mySelListBoxDialog(lang("Select folders"), "Found ".scalar @{$dirlist}." folders with $pic_count JPEG pictures.\nThumbnails will be created/updated only in the selected folders.", MULTIPLE, "build thumbnails", \@tmplist, @{$dirlist})); return if (not @tmplist); # return if nothing is selected # copy the selected elements into the @sel_dirs list my @sel_dirs; my $sel_pic_count = 0; foreach (@tmplist) { push @sel_dirs, $$dirlist[$_]; # add number of pics in selected folders $sel_pic_count += $$nr_of_pics_in_dir{$$dirlist[$_]} } my $rebuild = 0; $rc = myButtonDialog('Update or rebuild thumbnails?', "Please select if you want to update or rebuild $sel_pic_count thumbnails.\nUpdate will just create thumbnails for modified and new pictures, rebuild will rebuild all thumbnails.", undef, 'Update', 'Rebuild', 'Cancel'); if ($rc eq 'Cancel') { return; } elsif ($rc eq 'Update') { $rebuild = 0; } elsif ($rc eq 'Rebuild') { $rebuild = 1; } else { warn "buildThumbsRecursive: Error wrong rc: $rc"; return; } my $i = 0; my $actdir_save = $actdir; my $pw = progressWinInit($top, "build/refresh thumbnails"); foreach my $dir (@sel_dirs) { last if progressWinCheck($pw); $i++; my $dirshort = cutString($dir, -40, "..."); progressWinUpdate($pw, "processing ($i/".scalar @sel_dirs.") $dirshort", $i, scalar @sel_dirs); log_it(" Updating thumbnails in $dirshort ..."); # to rebuild we simply remove all thumbnails and then call generateThumbs() if ($rebuild) { my $thumbdir = dirname(getThumbFileName("$dir/dummy.jpg")); my @thumbs = getPics($thumbdir, WITH_PATH, NO_CHECK_JPEG); #print "buildThumbsRecursive: removing ".scalar @thumbs." thumbs in $dirshort...\n"; foreach (@thumbs) { #print "buildThumbsRecursive: remove $_\n"; if ( unlink($_) != 1) { # unlink returns the number of successfull removed files warn "buildThumbsRecursive: could not remove $_"; } } } $actdir = $dir; # needed for generateThumbs() generateThumbs(NO_ASK, NO_SHOW, 1); # do not ask the user when making a thumbnail dir # do not show (and sort!) the generated thumbs # 1 = read the pics from $actdir, not from the listbox } progressWinEnd($pw); $actdir = $actdir_save; log_it(" Thumbnails are now up to date!"); } ############################################################## # find all sub folders of basedir containing pictures ############################################################## sub get_subdirs { my $basedir = shift; log_it("searching sub folders of $basedir ..."); my @dirlist; # no questions about NON-JPEGS while searching please! my $pic_count = 0; my %nr_of_pics_in_dir; my $break = 0; my $pw = progressWinInit($top, "Collect sub folders"); find(sub { if (progressWinCheck($pw)) { $break = 1; $File::Find::prune = 1; } # process just dirs containing pictures, but not .thumbs/ .xvpics/ etc. dirs if (-d and ($_ ne $thumbdirname) and ($_ ne ".xvpics") and ($_ ne $exifdirname)) { progressWinUpdate($pw, "collecting folders (may take some time), found ".scalar @dirlist." ...", 0, 0); my @pictestlist = getPics($File::Find::name, JUST_FILE, NO_CHECK_JPEG); # no sort needed if (@pictestlist > 0) { $pic_count += scalar @pictestlist; $nr_of_pics_in_dir{$File::Find::name} = scalar @pictestlist; push @dirlist, $File::Find::name; } } }, $basedir); progressWinEnd($pw); if ($break) { log_it(" user break while counting sub folders"); return (0, \@dirlist, $pic_count); } log_it(" found ".@dirlist." sub folders with $pic_count pictures in $basedir."); return (1, \@dirlist, $pic_count, \%nr_of_pics_in_dir); } ############################################################## # keywords_xmp_to_iptc_folder - scans through all sub folders of # the actual dir and copy the XMP # keywords (Subject and HierarchicalSubject) # to the IPTC keywords ############################################################## sub keywords_xmp_to_iptc_folder { my $basedir = getRightDir(); log_it(lang("Copy keywords XMP to IPTC ...")); my $rc = $top->messageBox(-icon => 'question', -message => langf("Mapivi will first scan through all sub folders of %s and collect all folders containing pictures.", $basedir)."\n".lang("Then you are able to select in which folders Mapivi should copy XMP keywords (Subject and HierachicalSubject) to IPTC keywords."), -title => lang("Copy keywords XMP to IPTC ..."), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my ($ok, $dirlist, $pic_count, $nr_of_pics_in_dir) = get_subdirs($basedir); return if (not $ok); my @tmplist; return if (!mySelListBoxDialog(lang("Select folders"), langf("Found %d folders with %d pictures.",scalar(@{$dirlist}),$pic_count)."\n".lang("IPTC tags will be set only in the selected folders."), MULTIPLE, lang("copy keywords"), \@tmplist, @{$dirlist})); return if (not @tmplist); # return if nothing is selected # copy the selected elements into the @sel_dirs list my @sel_dirs; my $sel_pic_count = 0; foreach (@tmplist) { push @sel_dirs, $$dirlist[$_]; # add number of pics in selected folders $sel_pic_count += $$nr_of_pics_in_dir{$$dirlist[$_]} } my $i = 0; my $copy_count = 0; my $pw = progressWinInit($top, lang("copy keywords in folders")); foreach my $dir (@sel_dirs) { last if progressWinCheck($pw); $i++; my $dirshort = cutString($dir, -40, "..."); progressWinUpdate($pw, langf("processing folder (%d/%d) %s",$i,$sel_pic_count,$dirshort),$i,$sel_pic_count); log_it(langf(" Copy keywords in %s ...", $dirshort)); my @pics = getPics($dir, WITH_PATH, NO_CHECK_JPEG); my ($ok, $folder_copy_count) = keywords_xmp_to_iptc_int($top, \@pics); $copy_count += $folder_copy_count; last if (not $ok); } progressWinEnd($pw); log_it(langf(" Copied XMP to IPTC keywords in %d pictures finished.", $copy_count)); } ############################################################## # keywords_xmp_to_iptc - copy the XMP # keywords (Subject and HierarchicalSubject) of the selected pics # to the IPTC keywords ############################################################## sub keywords_xmp_to_iptc { my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my ($ok, $copy_count) = keywords_xmp_to_iptc_int($lb, \@sellist); log_it(langf(" Copied XMP to IPTC keywords in %d pictures finished.", $copy_count)); } ############################################################## # keywords_xmp_to_iptc_int - copy the XMP # keywords (Subject and HierarchicalSubject) of all pics in list # to the IPTC keywords ############################################################## sub keywords_xmp_to_iptc_int { my $w = shift; # widget my $pics = shift; # ref to pic list my $pic_nr = scalar(@$pics); my $i = 0; my $ok = 1; my $copy_count = 0; my $pw = progressWinInit($w, lang("copy keywords")); foreach my $dpic (@$pics) { if (progressWinCheck($pw)) { $ok = 0; last; } $i++; progressWinUpdate($pw, langf("processing picture (%d/%d)",$i,$pic_nr),$i,$pic_nr); # get XMP info (hash ref) my $xmp = exiftool_get($dpic, "XMP:*"); #print "$dpic: \"".exiftool_tostring($xmp)."\"\n" if (defined $xmp); my @keys; # $xmp_keywords is a string. Example: "Nature|Animal|Dog Nature|Plant|Rose" # at least when filled in with bibble my $xmp_keywords = $$xmp{HierarchicalSubject}; if (defined $xmp_keywords) { # replace hierarchical delimiter "|" with "/" $xmp_keywords =~ s/\|/\//g; # split keywords at whitespace push @keys, split /\s/, $xmp_keywords; } # get XMP Subject (non-hierarchical keywords?) $xmp_keywords = $$xmp{Subject}; if (defined $xmp_keywords) { # replace hierarchical delimiter "." with "/" $xmp_keywords =~ s/\./\//g; # split keywords at comma with optional whitespace push @keys, split /\,\s*/, $xmp_keywords; } # if there are some XMP keywords, add them to IPTC if (@keys) { my @pic; push @pic, $dpic; add_keywords_to_pics($picLB, \@keys, \@pic); $copy_count++; } } progressWinEnd($pw); return ($ok, $copy_count); } ############################################################## # rating_xmp_to_iptc - scans through all sub folders of # the actual dir and copy the XMP Rating # to the IPTC urgency tag ############################################################## sub rating_xmp_to_iptc { my $basedir = getRightDir(); log_it(lang("Copy ratings XMP to IPTC ...")); my $rc = $top->messageBox(-icon => 'question', -message => langf("Mapivi will first scan through all sub folders of %s and collect all folders containing pictures.", $basedir)."\n".lang("Then you are able to select in which folders Mapivi should copy the XMP rating to IPTC urgency."), -title => lang("Copy ratings XMP to IPTC ..."), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my ($ok, $dirlist, $pic_count, $nr_of_pics_in_dir) = get_subdirs($basedir); return if (not $ok); my @tmplist; return if (!mySelListBoxDialog(lang("Select folders"), langf("Found %d folders with %d pictures.",scalar(@{$dirlist}),$pic_count)."\n".lang("IPTC tags will be set only in the selected folders."), MULTIPLE, lang("copy rating"), \@tmplist, @{$dirlist})); return if (not @tmplist); # return if nothing is selected # copy the selected elements into the @sel_dirs list my @sel_dirs; my $sel_pic_count = 0; foreach (@tmplist) { push @sel_dirs, $$dirlist[$_]; # add number of pics in selected folders $sel_pic_count += $$nr_of_pics_in_dir{$$dirlist[$_]} } # let the user select if mapivi should overwrite existing ratings my $overwrite = 0; my $but_cancel = lang("Cancel"); my $but_over = lang("Overwrite"); my $but_no_over = lang("No overwrite"); $rc = myButtonDialog(lang('Overwrite IPTC ratings?'), lang("Overwrite existing IPTC rating with XMP rating?\nHint: If no XMP rating is defined, the IPTC rating is not changed.\n").convert_xmp_to_iptc_text(), undef, $but_over, $but_no_over, $but_cancel); if ($rc eq $but_cancel) { log_it(lang("action canceled by user")); return; } elsif ($rc eq $but_over) { $overwrite = 1; } elsif ($rc eq $but_no_over) { $overwrite = 0; } else { warn "rating_xmp_to_iptc: Error wrong rc: $rc"; return; } my $i = 0; my $copycount = 0; my $error = ''; my $pw = progressWinInit($top, lang("copy rating")); foreach my $dir (@sel_dirs) { last if progressWinCheck($pw); my $dirshort = cutString($dir, -40, "..."); log_it(langf(" Copy ratings in %s ...", $dirshort)); my @pics = getPics($dir, WITH_PATH, NO_CHECK_JPEG); foreach my $dpic (@pics) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, langf("processing picture (%d/%d) %s",$i,$sel_pic_count,$dirshort), $i, $sel_pic_count); # get XMP rating my $xmp = exiftool_get($dpic, "XMP-xmp:*"); my $xmp_rating = $$xmp{Rating}; if (defined $xmp_rating) { if ($overwrite == 0) { # get IPTC rating only when user wants no overwritting my $urgency = $searchDB{$dpic}{URG}; if (defined $urgency) { $error .= "info: $dpic has already a IPTC rating of $urgency (XMP: $xmp_rating)\n"; next; } } my $ok = set_IPTC_urgency_file($dpic, convert_xmp_to_iptc($xmp_rating), \$error); if ($ok) { $copycount++; # touch the thumbnail pic (set actual time stamp), to suppress rebuilding touch(getThumbFileName($dpic)); if ($dpic eq $actpic) { #showImageInfoCanvas($dpic); showImageInfo($dpic); } } } } } progressWinEnd($pw); showText(lang('Copy rating XMP to IPTC errors and infos'), $error, NO_WAIT) if ($error); log_it(langf(" Copy of %d XMP ratings to IPTC finished.",$copycount)); } ############################################################## # rating_iptc_to_xmp - scans through all sub folders of # the actual dir and copy the IPTC urgency # to the XMP Rating tag ############################################################## sub rating_iptc_to_xmp { my $basedir = getRightDir(); log_it(lang("Copy ratings IPTC to XMP ...")); my $rc = $top->messageBox(-icon => 'question', -message => langf("Mapivi will first scan through all sub folders of %s and collect all folders containing pictures.", $basedir)."\n".lang("Then you are able to select in which folders Mapivi should copy the IPTC urgency to XMP rating."), -title => lang("Copy ratings IPTC to XMP ..."), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my ($ok, $dirlist, $pic_count, $nr_of_pics_in_dir) = get_subdirs($basedir); return if (not $ok); my @tmplist; return if (!mySelListBoxDialog(lang("Select folders"), langf("Found %d folders with %d pictures.",scalar(@{$dirlist}),$pic_count)."\n".lang("XMP tags will be set only in the selected folders."), MULTIPLE, lang("copy rating"), \@tmplist, @{$dirlist})); return if (not @tmplist); # return if nothing is selected # copy the selected elements into the @sel_dirs list my @sel_dirs; my $sel_pic_count = 0; foreach (@tmplist) { push @sel_dirs, $$dirlist[$_]; # add number of pics in selected folders $sel_pic_count += $$nr_of_pics_in_dir{$$dirlist[$_]} } # let the user select if mapivi should overwrite existing ratings my $overwrite = 0; my $but_cancel = lang("Cancel"); my $but_over = lang("Overwrite"); my $but_no_over = lang("No overwrite"); $rc = myButtonDialog(lang('Overwrite XMP ratings?'), lang("Overwrite existing XMP ratings with IPTC ratings?\nHint: If no IPTC rating is defined, the XMP rating is not changed.\n").convert_iptc_to_xmp_text(), undef, $but_over, $but_no_over, $but_cancel); if ($rc eq $but_cancel) { log_it(lang("action canceled by user")); return; } elsif ($rc eq $but_over) { $overwrite = 1; } elsif ($rc eq $but_no_over) { $overwrite = 0; } else { warn "rating_iptc_to_xmp: Error wrong rc: $rc"; return; } my $i = 0; my $copycount = 0; my $error = ''; my $pw = progressWinInit($top, lang("copy rating")); foreach my $dir (@sel_dirs) { last if progressWinCheck($pw); my $dirshort = cutString($dir, -40, "..."); log_it(langf(" Copy ratings in %s ...", $dirshort)); my @pics = getPics($dir, WITH_PATH, NO_CHECK_JPEG); foreach my $dpic (@pics) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, langf("processing picture (%d/%d) %s",$i,$sel_pic_count,$dirshort), $i, $sel_pic_count); # get IPTC rating my $urgency = $searchDB{$dpic}{URG}; if (defined $urgency) { if ($overwrite == 0) { # get XMP rating only when user wants no overwritting my $xmp = exiftool_get($dpic, "XMP-xmp:*"); my $xmp_rating = $$xmp{Rating}; if (defined $xmp_rating) { $error .= "info: $dpic has already a XMP rating of $xmp_rating (IPTC: $urgency)\n"; next; } } $error .= xmp_set_rating($dpic, $urgency); $copycount++; # touch the thumbnail pic (set actual time stamp), to suppress rebuilding touch(getThumbFileName($dpic)); if ($dpic eq $actpic) { #showImageInfoCanvas($dpic); showImageInfo($dpic); } } } } progressWinEnd($pw); showText(lang('Copy rating IPTC to XMP errors and infos'), $error, NO_WAIT) if ($error); log_it(langf(" Copy of %d IPTC ratings to XMP finished.",$copycount)); } ############################################################## # rebuildThumbs ############################################################## sub rebuildThumbs { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if ($config{AskDeleteThumb}) { my $rc = checkDialog("Delete thumbnails?", "Please press Ok to delete ".scalar @sellist." thumbnails.", \$config{AskDeleteThumb}, "ask every time", '', 'OK', 'Cancel'); return if ($rc ne 'OK'); } log_it("Rebuild thumbnails ..."); my $thumb; my $i = 0; my $removed = 0; my $pw = progressWinInit($top, "Delete thumbnails"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); # when the element is not available we jump out completly last if (!$picLB->info("exists", $dpic)); $i++; progressWinUpdate($pw, "delete thumbnail ($i/".scalar @sellist.") ...", $i, scalar @sellist); $thumb = getThumbFileName($dpic); if (-f $thumb) { if (!removeFile( $thumb)) { next; } else { $removed++; # delete was successfull, so we insert the defaultthumb $picLB->itemConfigure($dpic, $picLB->{thumbcol}, -image => $mapivi_icons{'EmptyThumb'}, -itemtype => "imagetext"); } } } progressWinEnd($pw); log_it("Removed $removed thumbnails, starting generation in background ..."); my $starttime = Tk::timeofday(); my $generated_thumbs = generateThumbs(ASK, SHOW); my $time = sprintf("%.1f",Tk::timeofday()-$starttime); log_it("Rebuild $generated_thumbs thumbnails in $time seconds ... Ready!"); } ############################################################## # copyPicsDialog - copy the selected pictures to a choosen dir ############################################################## sub copyPicsDialog { my $mode = shift; # constant COPY or BACKUP my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $targetdir; if ($mode == BACKUP) { $targetdir = $actdir; } elsif ($mode == COPY) { $targetdir = getDirDialog(lang("Select target folder")); } else { warn "copyPicsDialog: error wrong mode: $mode"; return; } return if ($targetdir eq ''); copyPics($targetdir, $mode, $lb, @sellist); } ############################################################## # copy or move selected pictures to selected folder ############################################################## sub copy_or_move_pics_to_folder { my $kind = shift; # COPY or MOVE my $lb = $picLB; my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $targetdir = getSelectedDir(); if (defined $targetdir and -d $targetdir) { if ($kind == COPY) { copyPics($targetdir, COPY, $lb, @sellist); } elsif ($kind == MOVE) { movePics($targetdir, $lb, @sellist) } else { warn "copy_or_move_pics_to_folder called with wrong kind: $kind"; } } else { log_it(lang("No folder selected. Please select a folder in the navigation frame first.")); } return; } ############################################################## # copyPics - copy the selected pictures to a choosen dir ############################################################## sub copyPics { my $targetdir = shift; my $mode = shift; # constant COPY or BACKUP my $lb = shift; # the reference to the active listbox or canvas widget my @sellist = @_; return unless (-d $targetdir); # check if target folder is writable # hint: -w File or directory is writable by this (effective) user or group # -W File or directory is writable by this real user or group if (not -w $targetdir) { showText('Error while copying', "Folder $targetdir is not writable!", NO_WAIT); return; } return if (@sellist < 1); makeDir(dirname(getThumbFileName("$targetdir/dummy.jpg")), ASK); my $string = 'copy'; my $errors = ''; my $i = 0; my $overwrite = OVERWRITE; my $n = 0; # count successfull copied pictures my $pw = progressWinInit($lb, "Copy pictures"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); my $pic = basename($dpic); $i++; my $tpic = "$targetdir/$pic"; my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); if ($mode == BACKUP) { $string = 'backup'; $tpic = buildBackupName($dpic); $thumbtpic = buildBackupName(getThumbFileName($dpic)); print "copyPics: duplicate mode $tpic\n" if $verbose; } progressWinUpdate($pw, "$string picture ($i/".scalar @sellist.") ...", $i, scalar @sellist); $overwrite = overwritePic($tpic, $dpic, (scalar(@sellist) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); # if the copy is successfull if (mycopy($dpic, $tpic, OVERWRITE)) { $n++; # copy the thumbnail picture if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { mycopy($thumbpic, $thumbtpic, OVERWRITE) } # copy XMP, WAV, RAW files do_other_files($lb, COPY, $dpic, $tpic, \$errors); # copy meta info in search database $searchDB{$tpic} = $searchDB{$dpic}; if (($mode == BACKUP) and (ref($lb) ne 'Tk::Canvas')) { hlistCopy($lb, $dpic, $tpic); # insert and show the backup in the listbox $lb->itemConfigure($tpic, $lb->{thumbcol}, -text => getThumbCaption($tpic)); my $rating_size = get_rating_and_size($tpic, $lb); $lb->itemConfigure($tpic, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); #$lb->itemConfigure($tpic, $lb->{filecol}, -text => getAllFileInfo($tpic)); } } } # foreach - end progressWinEnd($pw); log_it("ready! ($n/".scalar @sellist." copied)"); if ($errors ne '') { $errors = "These errors occured while copying ".scalar @sellist." selected pictures:\n$errors"; showText('Error while copying', $errors, NO_WAIT); } reselect($lb, @sellist); } ############################################################## # do_other_files - rename, copy, move XMP, WAV and RAW files ############################################################## sub do_other_files { my $lb = shift; # the reference to the active listbox or canvas widget my $action = shift; # COPY, MOVE or RENAME my $dpic = shift; my $ndpic = shift; my $error_ref = shift; # reference to error string to add warnings etc. return unless ($action == RENAME or $action == COPY or $action == MOVE); my @suffixes; # we have to support upper and lower case XMP suffix push @suffixes, @xmp_suffix if $config{XMP_file_operations}; push @suffixes, @wav_suffix if $config{WAV_file_operations}; push @suffixes, @raw_suffix if $config{RAW_file_operations}; return unless (@suffixes); # return if no other file types have to be considered my @lc_suffixes; push @lc_suffixes, lc($_) foreach (@suffixes); push @suffixes, @lc_suffixes; # add lower case suffixes @suffixes = reverse @suffixes; # lower case first (better for Windows) #print "do_other_files: $action - suffixes: $_\n" foreach (@suffixes); my ($name,$dir,$fsuffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $dpic_no_suffix = $dir.$name; my ($nname,$ndir,undef) = fileparse($ndpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $ndpic_no_suffix = $ndir.$nname; foreach my $suffix (@suffixes) { # handle pic01.xmp and pic01.jpg.xmp if ((-f $dpic_no_suffix.$suffix) or (-f $dpic_no_suffix.$fsuffix.$suffix)) { my ($s_file, $t_file); if (-f $dpic_no_suffix.$suffix) { $s_file = $dpic_no_suffix.$suffix; $t_file = "$ndpic_no_suffix$suffix"; } elsif (-f $dpic_no_suffix.$fsuffix.$suffix) { $s_file = $dpic_no_suffix.$fsuffix.$suffix; $t_file = "$ndpic_no_suffix$fsuffix$suffix"; } if (-f $t_file) { $$error_ref .= "$suffix file $t_file exists, file not "; $$error_ref .= "renamed!\n" if $action == RENAME; $$error_ref .= "copyed!\n" if $action == COPY; $$error_ref .= "moved!\n" if $action == MOVE; } else { ### RENAME ### if ($action == RENAME) { if (rename($s_file, $t_file)) { if (defined $lb) { # change entry path hlistEntryRename($lb, $s_file, $t_file); # change name $lb->itemConfigure($t_file, $lb->{thumbcol}, -text => getThumbCaption($t_file)); my $rating_size = get_rating_and_size($t_file, $lb); $lb->itemConfigure($t_file, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); } } } elsif ($action == MOVE) { ### MOVE ### if (move($s_file, $ndir)) { # on success remove file from listbox $lb->delete('entry', $s_file) if (defined $lb and $lb->info('exists', $s_file)); } } elsif ($action == COPY) { ### COPY ### mycopy ($s_file, $t_file, ASK_OVERWRITE); } # todo: when move and rename we should update the list box # eg.g with $lb->delete('entry', $dpic) if ($lb->info('exists', $dpic)); # or updateThumbsPlus(); showNrOf(); ar the end ... } } } } ############################################################## # delete_XMP_file - delete XMP file if any ############################################################## sub delete_XMP_file { # XMP files follow picture file operations if this option is set to 1 return unless $config{XMP_file_operations}; my $dpic = shift; my ($name,$dir,$suffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) my $dpic_no_suffix = "$dir/$name"; my $xmp_file = ''; # we have to support upper and lower case XMP suffix if ((-f $dpic_no_suffix.'.xmp')) { $xmp_file = $dpic_no_suffix.'.xmp'; } elsif ((-f $dpic_no_suffix.'.XMP')) { $xmp_file = $dpic_no_suffix.'.XMP'; } else { } if ($xmp_file ne '') { print "remove $xmp_file\n" if $verbose; removeFile($xmp_file); } return; } ############################################################## # linkPicsDialog - link the selected pictures to a choosen dir ############################################################## sub linkPicsDialog { my $widget = shift; if ($EvilOS) { $top->messageBox(-icon => 'warning', -message => "Sorry, but this OS does not support symbolic links.", -title => 'Error', -type => 'OK'); return; } my @sellist = getSelection($widget); return unless checkSelection($widget, 1, 0, \@sellist, lang("picture(s)")); my $targetdir = getDirDialog("Link pictures to"); return if ($targetdir eq ''); linkPics($targetdir, @sellist); return; } ############################################################## # linkPics - link the selected pictures to a choosen dir ############################################################## sub linkPics { my $targetdir = shift; my @sellist = @_; if ($EvilOS) { $top->messageBox(-icon => 'warning', -message => "Sorry, but this OS does not support symbolic links.", -title => 'Error', -type => 'OK'); return; } return unless (-d $targetdir); return if (@sellist < 1); makeDir(dirname(getThumbFileName("$targetdir/dummy.jpg")), ASK); my $i = 0; my $overwrite = OVERWRITE; my $n = 0; # count successfull copied pictures my $pw = progressWinInit($top, "Link pictures"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); my $pic = basename($dpic); $i++; progressWinUpdate($pw, "linking ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $tpic = "$targetdir/$pic"; # Do not link to a link. Always link to the original image. next if (!getRealFile(\$dpic)); my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); $overwrite = overwritePic($tpic, $dpic, (scalar(@sellist) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); if (mylink ($dpic, $tpic, 1)) { $n++; # if the link is created successfully, we COPY the thumbnail # should the thumb also be a link??? if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { mycopy($thumbpic, $thumbtpic, OVERWRITE) } } } # foreach - end progressWinEnd($pw); log_it("ready! ($n/".scalar @sellist." linked)"); reselect($picLB, @sellist); return; } ############################################################## # getDirDialog - let the user select a dir ############################################################## sub getDirDialog { my $title = shift; my $text = "Please choose a target folder from the list below or open the folder browser\nby either double clicking the first item or by clicking the OK button without a selection.\n\nFolders from favorite list and recently visited folders:"; my $browser = "Open folder browser"; my @list; # put the "Open folder browser" item at the first position push @list, $browser; push @list, ''; # add empty line as separator # add the actual selected folder my $selected_dir = getSelectedDir(); if (defined $selected_dir and -d $selected_dir) { push @list, $selected_dir; push @list, ''; # add empty line as separator } # add max 4 media folders (e.g. USB-Sticks, USB-HDD, ...) to the list push @list, get_media_folders(4); # add max 15 often accessed folders push @list, getHotlists(15); push @list, ''; # add empty line as separator # add the last used folders foreach (reverse @dirHist) { next if (not -d $_); push @list, $_; } my @sellist; return '' unless (mySelListBoxDialog($title, $text, SINGLE, 'OK', \@sellist, @list)); my $dir = ''; $dir = $list[$sellist[0]] if $sellist[0]; if (($dir eq '') or ($dir eq $browser)) { my $dsdir = dirDialog($actdir); if (defined $dsdir) { $dir = $dsdir; } } $dir =~ s/\/\//\//g; # replace all // with / if (-d $dir) { dirSave($dir); } else { $dir = ''; } return $dir; } ############################################################## # returns a list of folders below media_folder_name # list is trunkated to max_entries ############################################################## sub get_media_folders { my $max_entries = shift; my $media_base_folder = $conf{media_folder_path}{value}; my @folders; if (-d $media_base_folder) { @folders = getDirs($media_base_folder); if (@folders > $max_entries) { @folders = splice(@folders,0,$max_entries); } } return @folders; } ############################################################## ############################################################## sub getHotlists { my $max_entries = shift; my @list; # sort dirs hash by numerical value reverse (number of accesses) # %dirHotlist contains folders used as target in open dir, copy, link, move, ... operations foreach (sort { $dirHotlist{$b} <=> $dirHotlist{$a} } keys %dirHotlist) { next if (not -d $_); # skip non existing dirs next if ($_ eq $trashdir); # skip the trash push @list, $_; # add to list last if (@list > $max_entries); # max_entries should be enough } # remove duplicates and sort folder list alphabetical my %saw; @saw{@list} = (); @list = (); @list = sort keys %saw; return @list; } ############################################################## # copy_move_to_origs - copy or move the selected pictures to a sub folder # called e.g. "originals" (folder name can be changed by user) # see $conf{origs_folder_name}{value} # If folder does not exists, it will be created ############################################################## sub copy_move_to_origs { my $lb = shift; # the reference to the active listbox widget my $operation = shift; # either COPY or MOVE my $origs = $conf{origs_folder_name}{value}; my $operation_text = ''; # check if operation argument is valid and set text string if ($operation == COPY) { $operation_text = langf("Copy to \"%s\" folder", $origs); } elsif ($operation == MOVE) { $operation_text = langf("Move to \"%s\" folder", $origs); } else { warn "copy_move_to_origs: unsupported operation: $operation"; return; } if (($operation == MOVE) and (ref($lb) eq 'Tk::Canvas')) { warn "copy_move_to_origs: Move operation not allowed in Canvas widget (light table)"; return; } my $i = 0; my $errors = ''; my $success = 0; my $overwrite = OVERWRITE; my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); # picture to select after deletion (has to be defined before we manipulate the listbox!) # we try to select the picture which is shown after the last picture of the current selection my $select_after; $select_after = $lb->info('next', $sellist[-1]) if ($operation == MOVE); foreach my $dpic (@sellist) { $i++; my $dir = dirname($dpic); my $pic = basename($dpic); my $targetfile = $dir.'/'.$origs.'/'.$pic; if ($dir =~ m/.*\/$origs$/) { $errors .= "Picture $dpic is already located in $origs folder, skipping!\n"; next; } if (-d $dir) { my $origsdir = "$dir/$origs"; # if origs folder does not exists, create it if (!-d $origsdir) { if (!mkdir $origsdir, oct(750)) { $errors .= $operation_text." ($dpic): "; $errors .= langf("Error making folder %s: %s\n", $origsdir, $!); $errors .= lang("Operation cancelled\n"); last; # cancel function (don't copy or move any further files) } } if (-d $origsdir) { if (-f $targetfile) { # if the pic exists, ask if the user wants to overwrite it $overwrite = overwritePic($targetfile, $dpic, (scalar(@sellist) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); } my $ok; if ($operation == MOVE) { $ok = move($dpic, $origsdir); } elsif ($operation == COPY) { $ok = copy($dpic, $origsdir); updateOneRow($dpic, $picLB) if ($ok and $lb == $picLB); } if ($ok) { $success++; # count nr of successfull moves # add the location info in the search database $searchDB{$targetfile} = $searchDB{$dpic}; copy_thumbnail($dpic, $origsdir, \$errors); if ($operation == MOVE) { delete $searchDB{$dpic}; rename_slideshow_pic($dpic, $targetfile); deleteCachedPics($dpic); delete_thumbnail($dpic); } } else { $errors .= $operation_text.": Could not process $dpic to $origsdir: $!\n"; } } } else { # no source folder: should never happen $errors .= "Warning: Could not move picture $dpic to $origs; Folder $dir does not exist!\n"; $errors .= lang("Operation cancelled\n"); last; # cancel function (don't copy or move any further files) } } # foreach end # clean up, user info, reselection if ($errors ne '') { $errors = $operation_text.". These errors occured while processing ".scalar @sellist." selected pictures:\n$errors"; showText($operation_text.'Errors during operation', $errors, NO_WAIT); } if ($success == 0) { # nothing happend, no update needed log_it($operation_text." - ".lang("Ready!")." (nothing changed)"); return; } if ($operation == MOVE) { my @pics = $lb->info('children'); if (($#pics > $#sellist) and ($success != 0)) { # if just some pictures were selected foreach my $dpic (@sellist) { $lb->delete('entry', $dpic) if ($lb->info('exists', $dpic)); reloadPic() if (($lb == $picLB) and ($dpic eq $actpic)); } } else { # all pictures were moved updateThumbsPlus() if ($lb == $picLB); } showNrOf() if ($lb == $picLB); # after deletion we select the picture after the last selected file select_next($lb, $select_after); } log_it($operation_text." - ".lang("Ready!")." ".langf("Processed %d/%d picture(s).", $success, scalar(@sellist))); } ############################################################## ############################################################## sub copy_thumbnail { my $dpic = shift; # path and name to the picture which thumbnail should be copies my $target_dir = shift; # target folder (without /.thumbs!) my $errors = shift; # reference to error string to concatenate my $target_thumb_dir = dirname(getThumbFileName("$target_dir/dummy.jpg")); # make target thumbdir if needed return if (not makeDir($target_thumb_dir, NO_ASK)); my $dpic_thumb = getThumbFileName($dpic); if ((-d dirname($target_thumb_dir)) and (-f $dpic_thumb)) { if (not copy($dpic_thumb, $target_thumb_dir)) { $$errors .= "Could not copy thumbnail $dpic_thumb to $target_thumb_dir: $!"; } } else { $$errors .= "No thumbnail $dpic_thumb or no folder $target_thumb_dir!"; } return } ############################################################## ############################################################## sub delete_thumbnail { my $dpic = shift; # path and name to the picture which thumbnail should be deleted my $errors = shift; # reference to error string to concatenate my $dpic_thumb = getThumbFileName($dpic); if (unlink($dpic_thumb) != 1) { # unlink returns the number of successfull removed files) $$errors .= "Could not remove thumbnail $dpic_thumb: $!"; } return } ############################################################## # movePicsDialog - move the selected pictures to a choosen dir ############################################################## sub movePicsDialog { my $lb = shift; # the reference to the active listbox widget if (ref($lb) eq 'Tk::Canvas') { warn "movePicsDialog: Move operation not supported in Canvas widget (light table)"; return; } my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $targetdir = getDirDialog("Move pictures to"); return if ($targetdir eq ''); movePics($targetdir, $lb, @sellist) } ############################################################## # movePics - move the selected pictures to a choosen dir ############################################################## sub movePics { my $targetdir = shift; my $lb = shift; # the reference to the active listbox widget if (ref($lb) eq 'Tk::Canvas') { warn "movePics: Move operation not supported in Canvas widget (light table)"; return; } my @sellist = @_; return unless (-d $targetdir); # check if target folder is writable # hint: -w File or directory is writable by this (effective) user or group # -W File or directory is writable by this real user or group if (not -w $targetdir) { showText('Error while moving', "Folder $targetdir is not writable!", NO_WAIT); return; } return if (@sellist < 1); # picture to select after move (has to be defined before we manipulate the listbox!) # we try to select the picture which is shown after the last picture of the current selection my $select_after = $lb->info('next', $sellist[-1]); makeDir(dirname(getThumbFileName("$targetdir/dummy.jpg")), ASK); my $i = 0; my $overwrite = OVERWRITE; my $changed = 0; my $errors = ''; my $pw = progressWinInit($lb, "Move pictures"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); my $pic = basename($dpic); next if ($targetdir eq dirname($dpic)); $i++; progressWinUpdate($pw, "moving ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $tpic = "$targetdir/$pic"; my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); $overwrite = overwritePic($tpic, $dpic, (scalar(@sellist) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); # move picture if (!move($dpic, $tpic)) { $errors .= "Could not move $dpic to $tpic: $!"; } else { $changed++; # count nr of successfull moves # only if move was successfull, we also move the thumbnail if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { if (!move($thumbpic, $thumbtpic)) { $errors .= "Could not move thumbnail $thumbpic to $thumbtpic: $!"; } } # move XMP, WAV, RAW files do_other_files($lb, MOVE, $dpic, $tpic, \$errors); rename_slideshow_pic($dpic, $tpic); $searchDB{$tpic} = $searchDB{$dpic}; # copy meta info in search database delete $searchDB{$dpic}; # delete meta info of moved pic in search database } } progressWinEnd($pw); if ($errors ne '') { $errors = "These errors occured while moving ".scalar @sellist." selected pictures:\n$errors"; showText('Error while moving', $errors, NO_WAIT); } if ($changed == 0) { # nothing happend, no update needed log_it("ready! (nothing moved)"); return; } my @pics = $lb->info('children'); if ($#pics > $#sellist) { # if not all pictures were selected foreach my $dpic (@sellist) { $lb->delete('entry', $dpic) if ($lb->info('exists', $dpic)); reloadPic() if (($lb == $picLB) and ($dpic eq $actpic)); } } else { # all pictures were moved updateThumbsPlus() if ($lb == $picLB); } showNrOf() if ($lb == $picLB); # select the picture after the last selected file select_next($lb, $select_after); log_it("ready! ($changed/".scalar @sellist." moved)"); } ############################################################## # overwritePic ############################################################## sub overwritePic { my $old = shift; # this will be overwritten ny $new my $new = shift; # this will overwrite $old my $nr = shift; # the number of all (left) files to check, if this nr is > 1 there will be two "for all" buttons return 1 if (!-f $old); # if $old does not exists, we don't need to ask ... my $rc = 3; # dummy value my $olddir = dirname($old); my $oldpic = basename($old); my $oldthumb = getThumbFileName($old); my $oldinfo = getAllFileInfo($old); my $newdir = dirname($new); my $newpic = basename($new); my $newthumb = getThumbFileName($new); my $newinfo = getAllFileInfo($new); # open window my $oww = $top->Toplevel(); $oww->title(lang("Overwrite").'?'); $oww->iconimage($mapiviicon) if $mapiviicon; $oww->Label(-anchor => 'w', -text => langf("\"%s\" exists. Do you want to overwrite it?",$oldpic), -bg => $conf{color_bg}{value})->pack; my $nF = $oww->Frame()->pack(-anchor => 'w', -padx => 3, -pady => 3); my $ca = $oww->Canvas(-bd => 0, -width => 100, -height => 50)->pack(-padx => 3, -pady => 3); my $oF = $oww->Frame()->pack(-anchor => 'w', -padx => 3, -pady => 3); # draw a red arrow $ca->createLine(50, 0,50,50, -width => 5, -fill => 'red'); $ca->createLine(50,50,70,20, -width => 5, -fill => 'red'); $ca->createLine(50,50,30,20, -width => 5, -fill => 'red'); my $newP; my $oldP; $newP = $oww->Photo(-file => $newthumb, -gamma => $config{Gamma}) if (-f $newthumb); $oldP = $oww->Photo(-file => $oldthumb, -gamma => $config{Gamma}) if (-f $oldthumb); $nF->Label(-image => $newP)->pack(-side => 'left') if $newP; $oF->Label(-image => $oldP)->pack(-side => 'left') if $oldP; $nF->Label(-justify => 'left', -text => lang("this file")."\n$newdir\n$newinfo", -bg => $conf{color_bg}{value})->pack(-padx => 3, -side => 'left'); $oF->Label(-justify => 'left', -text => lang("will overwrite this file")."\n$olddir\n$oldinfo", -bg => $conf{color_bg}{value})->pack(-padx => 3, -side => 'left'); $oww->Label(-anchor => 'w', -text => langf("%d files to go ...",$nr), -bg => $conf{color_bg}{value})->pack if ($nr > 1); my $bF = $oww->Frame()->pack(-padx => 3, -pady => 3, -fill => 'x', -expand => 1); $bF->Button(-text => lang("Overwrite"), -command => sub { $rc = OVERWRITE; })->pack(-side => 'left', -fill => 'x', -expand => 1); $bF->Button(-text => lang("Overwrite all"), -command => sub { $rc = OVERWRITEALL; })->pack(-side => 'left', -fill => 'x', -expand => 1) if ($nr > 1); my $Xbut = $bF->Button(-text => lang('Cancel'), -command => sub { $rc = CANCEL; })->pack(-side => 'left', -fill => 'x', -expand => 1); my $XbutAll = undef; $XbutAll = $bF->Button(-text => lang("Cancel all"), -command => sub { $rc = CANCELALL; })->pack(-side => 'left', -fill => 'x', -expand => 1) if ($nr > 1); $oww->bind('', sub { if (($nr > 1) and (Tk::Exists($XbutAll))) { $XbutAll->Invoke; } else { $Xbut->Invoke; } }); $oww->Popup; $oww->waitVariable(\$rc); $oww->withdraw(); $oww->destroy(); die "wrong rc value: $rc" if (($rc < -1) or ($rc > 2)); return $rc; } ############################################################## # sendTo - send all selected pics via email ############################################################## sub sendTo { my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); # check if some files are links return if (!checkLinks($lb, @sellist)); my $mail_tool = $conf{external_mail_tool}{value}; if ($mail_tool =~ m/thunderbird/i) { } elsif ($mail_tool =~ m/evolution/i) { } elsif ($mail_tool =~ m/icedove/i) { } elsif ($mail_tool =~ m/outlook/i) { if (@sellist > 1) { my $dpic = $sellist[0]; my $pic = basename($dpic); $lb->messageBox(-icon => 'warning', -message => "Sorry, but the commando line of Outlook supports only one attachment. Only the first picture ($pic) will be added to the email, the rest is ignored.", -title => 'Too many attachments', -type => 'OK'); # clear list and add only the first pic @sellist = (); push @sellist, $dpic; } } else { $lb->messageBox(-icon => 'warning', -message => "Sorry, the selected mail tool ($mail_tool) is not supported! Please try to find the command line syntax to send a mail with attachment and send this info to Martin-Herrmann\@gmx.de.", -title => 'External mail tool not yet supported', -type => 'OK'); return; } if (($mail_tool !~ m/outlook/i) and ((system "$mail_tool --version") != 0)) { $lb->messageBox(-icon => 'warning', -message => "Sorry, no mail tool ($mail_tool) found! Please use Ctrl-o (Options->$conf{external_mail_tool}{tab}->$conf{external_mail_tool}{long}) to select the right tool.", -title => 'External mail tool not available', -type => 'OK'); return; } # open dialog window my $myDiag = $top->Toplevel(); $myDiag->title("Change size/quality before sending"); $myDiag->iconimage($mapiviicon) if $mapiviicon; my $file_size = get_list_size(\@sellist); $myDiag->Label(-text =>"The selected picture(s) have a file size of $file_size.\nChange the size and/or quality of the ".scalar @sellist." selected picture(s) before sending via email?", -bg => $conf{color_bg}{value} )->pack(-anchor => 'w',-padx => 3,-pady => 3); $myDiag->Checkbutton(-variable => \$config{MailPicNoChange}, -text => "leave pictures untouched", -command => sub { foreach my $w (qw(sq sl)) {set_child_normal($myDiag->{$w}, !$config{MailPicNoChange});} })->pack(-anchor => 'w'); # quality scale $myDiag->{sq} = labeledScale($myDiag, 'top', 24, lang("Quality (%)"), \$config{MailPicQuality}, 10, 100, 1); qualityBalloon($myDiag->{sq}); # pixel size scale $myDiag->{sl} = labeledScale($myDiag, 'top', 24, "Maximum length (pixels)", \$config{MailPicMaxLength}, 10, 2000, 1); foreach my $w (qw(sq sl)) {set_child_normal($myDiag->{$w}, !$config{MailPicNoChange});} my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $myDiag->destroy(); log_it("sending ".scalar @sellist." pictures via email"); unless ($config{MailPicNoChange}) { # copy to trash log_it("send to: copy pictures to temp folder"); foreach my $dpic (@sellist) { mycopy($dpic, $trashdir, OVERWRITE); } # exchange the folder from original to trash foreach (@sellist) { $_ = "$trashdir/".basename($_); } # resize foreach my $dpic (@sellist) { log_it("send to: resizing pictures ".basename($dpic)); my $command = "mogrify"; $command .= " -geometry \"".$config{MailPicMaxLength}.'x'.$config{MailPicMaxLength}.">\""; $command .= " -quality ".$config{MailPicQuality}." \"$dpic\""; print "changeSizeQuality: com = $command\n" if $verbose; execute($command); } } # the email subject & caption my ($subject, $caption) = email_subject_caption("Pictures", "Text", \@sellist); my $attachments = email_attachments($mail_tool, \@sellist); # /usr/bin/evolution mailto:Martin-Herrmann@gmx.de?attach=file:///home/pic1.jpg\&attach=file:///home/pic2.jpg\&subject=My%20Pictures\&body=Text%20for%20description & # for Outlook under WinXP use: # "C:\Program Files\Microsoft Office\OFFICE11\OUTLOOK.EXE" /c ipm.note /a C:\path\to\picture.jpg log_it("send to: starting email client ..."); my $command = "\"$mail_tool\" "; if (($mail_tool =~ m/thunderbird/i) or ($mail_tool =~ m/icedove/i)) { $command .= "-compose \"subject=\'$subject\',attachment=\'$attachments\',body=\'$caption\'\""; } elsif ($mail_tool =~ m/evolution/i) { #$command .= "\"mailto:Receiver?attach=\'$attachments\'\\&subject=Pictures\\&body=Text\""; $command .= "\"mailto:Receiver?attach=$attachments\&subject=$subject\&body=\'$caption\'\"" ; } elsif ($mail_tool =~ m/outlook/i) { if (ProcBackgroundAvail) { $command =~ s!\/!\\!g; # replace UNIX path delimiter with Windows style / -> \ my $dpic = $sellist[0]; $dpic =~ s!\/!\\!g; # replace UNIX path delimiter with Windows style / -> \ # no quotes around mail tool and pic when using Proc::Background! Proc::Background->new($mail_tool, "/c", "ipm.note", "/a", $dpic); log_it(lang('Ready!')); } else { log_it("Perl module Proc::Background not available: Can't start email client"); print "Perl module Proc::Background not available: Can't start email client\n"; } return; } else { # this case is already handled adove. } $command .= " &" unless ($EvilOS); print "command = $command\n";# if $verbose; #execute($command); # starting Outlook with system is a real nightmare (system "$command") == 0 or warn "$command failed: $!"; log_it(lang('Ready!')); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang("Default"), -command => sub { $config{MailPicNoChange} = 0; $config{MailPicQuality} = 80; $config{MailPicMaxLength} = 1000; foreach my $w (qw(sq sl)) {set_child_normal($myDiag->{$w}, !$config{MailPicNoChange});} })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $OKB->bind('', sub { $OKB->Invoke; } ); $ButF->Button(-text => lang('Cancel'), -command => sub { $myDiag->destroy(); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $myDiag->Popup; if ($EvilOS) { # sometimes to dialog disappears when clicked on, so we need a short grab $myDiag->grab; $myDiag->after(50, sub{$myDiag->grabRelease}); } $OKB->focus; $myDiag->waitWindow(); $myDiag->destroy() if Tk::Exists($myDiag); } ############################################################## # try to find a subject and text for the email by searching # IPTC headline, object name and caption of the pictures ############################################################## sub email_subject_caption { my ($subject, $caption, $piclist) = @_; foreach my $dpic (@{$piclist}) { # use the headline or the ObjectName of the first picture as email subject my $sub = getIPTCHeadline($dpic); my $obj = getIPTCObjectName($dpic); # scip next if ($sub eq '' and $obj eq ''); if ($sub ne '') { $subject = $sub; } else { $subject = $obj; } # use the caption of this picture for the email text # we shouldn't mix it with captions from other pictures my $cap = getIPTCCaption($dpic); $caption = $cap if ($cap ne ''); last; # finished } return ($subject, $caption); } ############################################################## # /usr/bin/evolution mailto:Martin-Herrmann@gmx.de?attach=file:///home/pic1.jpg\&attach=file:///home/pic2.jpg\&subject=My%20Pictures\&body=Text%20for%20description & ############################################################## sub email_attachments { my ($mail_tool, $piclist) = @_; my $attach = ''; # outlook is only able to handle one attachment if ($mail_tool =~ m/outlook/i) { $$piclist[0] =~ s!\/!\\!g; # replace UNIX path delimiter with Windows style / -> \ $attach = " /a \"$$piclist[0]\" "; } else { # all other mail tools foreach my $dpic (@{$piclist}) { if ($attach eq '') { # the first attachment $attach = "file://$dpic"; } else { # additional attachments if ($mail_tool =~ m/evolution/i) { $attach .= "\&attach=file://$dpic"; } else { # thunderbird, icedove etc. $attach .= ",file://$dpic"; } } } } return $attach; } ############################################################## # convertPics - convert selected pics to another format ############################################################## sub convertPics { my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); return if (!checkExternProgs("convertPics", "convert")); # check if some files are links return if (!checkLinks($lb, @sellist)); # open dialog window my $win = $top->Toplevel(); $win->title("Convert to other picture formats"); $win->iconimage($mapiviicon) if $mapiviicon; $win->Label(-text =>"Convert the ".scalar @sellist." selected pictures to another picture format.\nThe orininal files will be left untouched.\nThe converted pictures are stored in the actual diretory.", -bg => $conf{color_bg}{value} )->pack(-anchor => 'w',-padx => 3,-pady => 3); my $notebook = $win->NoteBook(-width => 500, -background => $conf{color_bg}{value}, # background of active page (including its tab) -inactivebackground => $conf{color_entry}{value}, # tabs of inactive pages -backpagecolor => $conf{color_bg}{value}, # background behind notebook )->pack(-expand => "yes", -fill => 'both', -padx => 5, -pady => 5); my $format = "gif"; my $gifF = $notebook->add("gif", -label => "GIF", -raisecmd => sub { $format = "gif"; }); my $pngF = $notebook->add("png", -label => "PNG", -raisecmd => sub { $format = "png"; }); my $tifF = $notebook->add("tiff", -label => "TIFF", -raisecmd => sub { $format = "tiff"; }); $win->{PicQuality} = 95; $pngF->{sq} = labeledScale($pngF, 'top', 24, lang("Quality (%)"), \$win->{PicQuality}, 0, 100, 1); $balloon->attach($pngF->{sq}, -msg => 'Quality range from 0% (fastest compression) to 100% (best but slowest). For 0%, the Huffman-only strategy is used, which is fastest but not necessarily the worst compression. The default is 75%, which means nearly the best compression with adaptive filtering. If the image is a natural image (a photo), then use "adaptive" filtering with quality 95%. The quality setting has no effect on the appearance of PNG images, since the compression is always lossless. For PNG images, quality is regarded as two decimal figures. The first (tens) is the zlib compression level, 1-9. The second (ones digit) is the PNG filtering type: 0 is none, 1 is "sub", 2 is "up", 3 is "average", 4 is "Paeth", and 5 is "adaptive".'); my $ButF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $win->destroy(); #my $format = $notebook->raised(); print "format = $format\n"; log_it("converting ".scalar @sellist." $format pictures"); my $i = 0; my $pw = progressWinInit($top, "Convert pictures"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); progressWinUpdate($pw, "convert picture ($i/".scalar @sellist.") ...", $i, scalar @sellist); $i++; my $ndpic = $dpic; $ndpic =~ s/(.*)\.jp(g|eg)$/$1.$format/i; if (-f $ndpic) { my $rc = $top->messageBox(-icon => 'question', -message => "$ndpic exists already.\nShould I really overwrite it?", -title => "Overwrite?", -type => 'OKCancel'); next if ($rc !~ m/Ok/i); } log_it("convert picture ".basename($dpic)); my $command = "convert"; $command .= " -quality ".$win->{PicQuality} if ($format eq "png"); $command .= " \"$dpic\" \"$ndpic\""; print "convertPics:: com = $command\n"; # if $verbose; execute($command); progressWinUpdate($pw, "convert picture ($i/".scalar @sellist.") ...", $i, scalar @sellist); } progressWinEnd($pw); log_it(lang('Ready!')); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $OKB->bind('', sub { $OKB->Invoke; } ); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $win->destroy(); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $xBut); $win->Popup; if ($EvilOS) { # sometimes to dialog disappears when clicked on, so we need a short grab $win->grab; $win->after(50, sub{$win->grabRelease}); } $OKB->focus; $win->waitWindow(); $win->destroy() if Tk::Exists($win); } ############################################################## # renamePic - let the user rename the seleced pictures ############################################################## sub renamePic { my $lb = shift; if (ref($lb) eq 'Tk::Canvas') { warn "renamePic: Rename operation not supported in Canvas widget (light table)"; return; } my @sellist = getSelection($lb); my @resellist = @sellist; return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $i = 0; my $errors = ''; my $pw = progressWinInit($lb, "Rename pictures"); foreach my $dpic (@sellist){ last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "renaming picture ($i/".scalar @sellist.") ...", $i, scalar @sellist); my $pic = basename($dpic); my $dir = dirname($dpic); my $thumb = getThumbFileName($dpic); my $newname = $pic; next if (!checkWriteable($dpic)); my $rc = myEntryDialog("Rename picture", "Please enter a new name for file\n$pic\n(in $dir)", \$newname, getThumbFileName($dpic)); next if (($rc ne 'OK') or ($newname eq '') or ($newname eq $pic)); # check for correct JPEG suffix if (is_a_JPEG($dpic) and ($newname !~ /(.*)(\.jp(g|eg))/i)) { $newname =~ /(.*)\.(.*)/; my $correct = "$1.jpg"; my $rc = $lb->messageBox(-icon => 'question', -message => "$newname has not a correct JPEG suffix.\nShould I change it to $correct?", -title => "Change suffix?", -type => 'OKCancel'); if ($rc eq 'Ok') { $newname = "$correct"; } } my $ndpic = "$dir/$newname"; # check if new file name already exists if (-f $ndpic) { my $rc = $lb->Dialog( -title => "File exists", -text => "$newname already exists!", -buttons => ['Overwrite', 'Cancel'])->Show(); next if ($rc ne 'Overwrite'); # skip this file } if (!rename ($dpic, $ndpic)) { $errors .= "Could not rename $pic to $newname: $!"; next; } # correct the searchDB $searchDB{$ndpic} = $searchDB{$dpic}; # copy meta info in search database delete $searchDB{$dpic}; # delete meta info of renamed pic in search database renameCachedPic($dpic, $ndpic); rename_slideshow_pic($dpic, $ndpic); foreach (@resellist) { $_ = $ndpic if ($_ eq $dpic); } if ($dpic eq $actpic) { $actpic = $ndpic; } hlistEntryRename($lb, $dpic, $ndpic); # change the displayed name $lb->itemConfigure($ndpic, $lb->{thumbcol}, -text => getThumbCaption($newname)); my $rating_size = get_rating_and_size($ndpic, $lb); $lb->itemConfigure($ndpic, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); # rename thumbnail if (-f $thumb) { if (!rename ($thumb, dirname($thumb)."/$newname")) { $errors .= "Could not rename thumbnail $pic to $newname: $!"; } } # rename XMP, WAV, RAW files do_other_files($lb, RENAME, $dpic, $ndpic, \$errors); # rename exif info file, if any if (-f "$dir/$exifdirname/$pic") { if (!rename ("$actdir/$exifdirname/$pic", "$actdir/$exifdirname/$newname")) { $errors .= "Could not rename exif info file $pic to $newname: $!"; } } # rename backup file, if any renameBackup($lb, $dpic, $newname, ASK); } if ($errors ne '') { $errors = "These errors occured while renaming ".scalar @sellist." selected pictures:\n$errors"; showText('Error while renaming', $errors, NO_WAIT); } progressWinEnd($pw); reselect($lb, @resellist); if ($lb == $picLB) { setTitle(); log_it("ready! ($i/".scalar @sellist." renamed)"); } } ############################################################## # renameNonJPEG - check if there are any non-JPEG files # and rename them ############################################################## # todo enhance this to cope with other formats sub renameNonJPEG { my $dpic = shift; my $newname = shift; foreach my $suf (split /\|/, $nonJPEGsuffixes) { my $njpic = $dpic; $njpic =~ s/(.*)\.jp(g|eg)$/$1.$suf/i; if (-f $njpic) { my $nnjpic = "$actdir/$newname"; $nnjpic =~ s/(.*)\.jp(g|eg)$/$1\.$suf/i; my $rc = $top->messageBox(-icon => 'question', -message => "There is a non-JPEG file\n\"".basename($njpic)."\"\nOk to rename it to:\n\"".basename($nnjpic)."\"?", -title => "Rename non-JPEG?", -type => 'OKCancel'); return 0 if ($rc !~ m/Ok/i); if (!rename ("$njpic", "$nnjpic")) { $top->messageBox(-icon => 'warning', -message => "Could not rename non-JPEG picture $njpic to $nnjpic: $!", -title => 'Error', -type => 'OK'); } } } return 1; } ############################################################## # showBackup ############################################################## sub showBackup { my @sellist = $picLB->info('selection'); if (@sellist != 1) { $top->messageBox(-icon => 'info', -message => "Please select exacty one picture for this function.", -title => "Wrong selection", -type => 'OK'); return; } my $bpic = buildBackupName($sellist[0]); my ($yes, $opic) = has_orig_file(fileparse($sellist[0], '\.[^.]*')); if (-f $bpic) { showPicInOwnWin($bpic); } elsif ($yes) { showPicInOwnWin($opic); } else { log_it('Sorry, no backup and no original found.'); } } ############################################################## # renameBackup - check if there is a backup file # and rename it ############################################################## sub renameBackup { my $lb = shift; my $dpic = shift; my $newname = shift; my $ask = shift; return unless $config{RenameBackup}; my $bpic = buildBackupName($dpic); return unless (-f $bpic); # no backup - no rename my $dir = dirname($dpic); my $pic = basename($dpic); my $nbpic = basename(buildBackupName("$dir/$newname")); my $rc = $nbpic; if ((defined $ask) and ($ask == ASK)) { $rc = myButtonDialog("Rename backup?", "Should I also rename the backup file ".basename($bpic)."?\nRename to:", undef, $nbpic, $pic, 'Cancel'); return if ($rc =~ m/Cancel/i); } my $new_bak_name = "$dir/$rc"; if (-f $new_bak_name) { # should not happen $lb->messageBox(-icon => 'warning', -message => "Backup picture $bpic should be renamed to $new_bak_name. But $new_bak_name exists! Skipping rename action.", -title => 'Error', -type => 'OK'); return; } if (rename ($bpic, $new_bak_name)) { hlistEntryRename($lb, $bpic, $new_bak_name); # correct the searchDB - copy meta info in search database $searchDB{$new_bak_name} = $searchDB{$bpic}; delete $searchDB{$bpic}; # change the displayed name if ($lb->info("exists", $new_bak_name)) { $lb->itemConfigure($new_bak_name, $lb->{thumbcol}, -text => getThumbCaption($new_bak_name)); my $rating_size = get_rating_and_size($new_bak_name, $lb); $lb->itemConfigure($new_bak_name, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); } # rename thumbnail my $thumb = getThumbFileName($bpic); if (-f $thumb) { my $nthumb = getThumbFileName($new_bak_name); if (!rename ($thumb, $nthumb)) { $lb->messageBox(-icon => 'warning', -message => "Could not rename thumbnail $thumb to $nthumb: $!", -title => 'Error', -type => 'OK'); } } } else { $lb->messageBox(-icon => 'warning', -message => "Could not rename backup picture $bpic to $new_bak_name: $!", -title => 'Error', -type => 'OK'); } } ############################################################## # getRenameFormat ############################################################## sub getRenameFormat { my $format = $config{FileNameFormat}; # copy to tmp variable my $rc = myEntryDialog(lang('Enter file name format'), 'Please enter the file name format %f = file name (without suffix) %F = file name substring (%Fn-m use old file name from char n to m) %y = year (yyyy) %m = month (mm) %d = day (dd) %h = hour (hh) %M = Minute (MM) %s = second (ss) %pO = IPTC Object name %po = IPTC Object name with spaces replaced by underscore ("_") %pH = IPTC Headline %ph = IPTC Headline with spaces replaced by underscore ("_") %xa = EXIF aperture %xe = EXIF exposure time %xm = EXIF camera model %xr = EXIF artist %iw = image width %ih = image height Examples: "%y%m%d-%h%M%s" will rename all pictures to their internal EXIF date e.g. 20121231-155959 (the file date will be used, if there is no EXIF date). "%F4-7" will rename PIC0001.jpg to file name substring from 4th char up to 7th char e.g 0001.jpg If you select 3 pictures and enter "flower" as file name format, the pics will be renamed to "flower-001.jpg", "flower-002.jpg" and "flower-003.jpg". The file suffix will always be added. Leave the format line below empty to use the default format ('.$config{FileNameFormatDef}.').', \$format); return 'Cancel' if ($rc ne 'OK'); if ($format eq '') { $format = $config{FileNameFormatDef}; } if ($format =~ m/.*\/.*/) { $top->messageBox(-icon => 'warning', -message => "Sorry, but a / is not allowed in a file name.", -title => 'Error', -type => 'OK'); return 'Cancel'; } $config{FileNameFormat} = $format; # save back to the config return $rc; } ############################################################## # renameSmart - rename the selected pictures using e.g. the EXIF date ############################################################## sub renameSmart { my $lb = shift; if (ref($lb) eq 'Tk::Canvas') { warn "renameSmart: Rename operation not supported in Canvas widget (light table)"; return; } my @sellist = getSelection($lb); my @resellist = @sellist; return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $doForAll = 0; my $errors = ''; my $useFileDate = undef; my @renamed; my $rc = getRenameFormat(); return if ($rc ne 'OK'); my $format = $config{FileNameFormat}; my $i = 0; my $pw = progressWinInit($lb, "smart rename"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; my $pic = basename($dpic); my $dir = dirname($dpic); progressWinUpdate($pw, "renaming ($i/".scalar @sellist.") ...", $i, scalar @sellist); unless (-f $dpic) { # may happen when renaming backups $errors .= "$pic: not found, seems to be an already renamed backup? - skipping\n"; next; } my $newname = ''; my $rc = applyRenameFormat($dpic, $format, \$newname, \$doForAll); next if ($rc eq "Skip this picture"); last if ($rc eq "Cancel all"); $newname = findNewName("$dir/$newname"); # todo: handle backup pics it should be possible to preserve the "-bak" part my $ndpic = "$dir/$newname"; if (-f $ndpic) { # just a safety check $errors .= "$pic: new name $newname already exists - skipping\n"; next; } # rename the picture if (renamePicInt($lb, $dpic, $ndpic, \$errors)) { push @renamed, $ndpic; # rename the hlist entry hlistEntryRename($lb, $dpic, $ndpic); # display the new file name $lb->itemConfigure($ndpic, $lb->{thumbcol}, -text => getThumbCaption($ndpic)); my $rating_size = get_rating_and_size($ndpic, $lb); $lb->itemConfigure($ndpic, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); foreach (@resellist) { $_ = $ndpic if ($_ eq $dpic); } } } # fix the renaming of the first pic of a set (pic.jpg -> pic-00.jpg) my $renamed = renameSmartFix(\$errors, @renamed); foreach my $dpic (keys %{$renamed}) { my $ndpic = $$renamed{$dpic}; # rename the hlist entry hlistEntryRename($lb, $dpic, $ndpic); # display the new file name $lb->itemConfigure($ndpic, $lb->{thumbcol}, -text => getThumbCaption($ndpic)); my $rating_size = get_rating_and_size($ndpic, $lb); $lb->itemConfigure($ndpic, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); foreach (@resellist) { $_ = $ndpic if ($_ eq $dpic); } } progressWinEnd($pw); reselect($lb, @resellist); if ($lb == $picLB) { log_it("ready! (renamed $i/".scalar @sellist.")"); setTitle(); } if ($errors ne '') { $errors = "These errors occured while renaming ".scalar @sellist." selected pictures:\n$errors"; showText("Error while renaming", $errors, NO_WAIT); } $lb->focusForce; } ############################################################## # renamePicInt - rename a pic, the thumb, backup, exif, nonjpeg # searchDB and cached pic ############################################################## sub renamePicInt { my $lb = shift; # listbox reference my $dpic = shift; my $ndpic = shift; my $errors = shift; # ref to error string my $pic = basename($dpic); my $dir = dirname($dpic); my $npic = basename($ndpic); my $rc = 0; if (!rename ($dpic, $ndpic)) { # rename failed $$errors .= "Could not rename $pic to $npic: $!\n"; $rc = 0; } else { # rename worked # rename the thumbnail my $thumbdir = dirname(getThumbFileName($dpic)); if (!rename ("$thumbdir/$pic", "$thumbdir/$npic")) { $$errors .= "Could not rename thumbnail $pic to $npic: $!\n"; } # rename exif info file, if any if (-f "$dir/$exifdirname/$pic") { if (!rename ("$dir/$exifdirname/$pic", "$dir/$exifdirname/$npic")) { $$errors .= "Could not rename exif info file $pic to $npic: $!\n"; } } # rename the XMP, WAV, RAW sidecar files, if any do_other_files($lb, RENAME, $dpic, $ndpic, \$errors); # rename backup file, if any renameBackup($picLB, $dpic, $npic); # rename non-JPEG file, if any renameNonJPEG($dpic, $npic); # correct the searchDB $searchDB{$ndpic} = $searchDB{$dpic}; # copy meta info in search database delete $searchDB{$dpic}; # delete meta info of renamed pic in search database renameCachedPic($dpic, $ndpic); rename_slideshow_pic($dpic, $ndpic); $actpic = $ndpic if (($dpic eq $actpic) and (-f $ndpic)); $rc = 1; } return $rc; } ############################################################## # renameSmartFix - fix the renaming of renameSmart by adding # "-001" to the first pic of a set # e.g. pic1.jpg and pic1-002.jpg will be renamed to # pic1-001.jpg and pic1-002.jpg # see also: findNewName() # todo: this really is an ugly solution - fix it ############################################################## sub renameSmartFix { my $errors = shift; # ref to scalar, errors will be added my @piclist = @_; return unless (@piclist); my %hash; $hash{$_} = 1 foreach (@piclist); my %renamed; # hash of the renamed files (key: old name, value: new name) # search the list for files matching file-002.jpg foreach my $dpic (@piclist) { if ($dpic =~ m/(.*)-002\.(.*)$/i) { # e.g. file-002.jpg my $pic = "$1.$2"; my $npic = "$1-001.$2"; # if there is a file named file.jpg if (defined $hash{$pic}) { # and no file named file-001.jpg unless (defined $hash{$npic}) { print "renameSmartFix: rename $pic to $npic\n" if $verbose; # we rename file.jpg to file-001.jpg if (renamePicInt(undef, $pic, $npic, $errors)) { $renamed{$pic} = $npic; } } } } } return \%renamed; } ############################################################## # applyRenameFormat ############################################################## sub applyRenameFormat { my $dpic = shift; my $format = shift; # e.g. %y%m%d-%h%M%s my $newname = shift; # reference to string my $doForAll = shift; # reference to bool my $pic = basename($dpic); $$newname = $format; # replace %f with the file name if (($format =~ m/\%f/) and ($pic =~ /(.*)\.(.*)/)) { my $name = $1; # $1 makes some problems in s/// $$newname =~ s/%f/$name/g; } # idea from Thierry Daucourt # replace %F with the file name substring if ($format =~ m/\%F(\d+)\-(\d+)/) { my $begin = $1 - 1; # we start with index 1, not 0 my $end = $2 - 1; if ($pic =~ /(.*)\.(.*)/) { my $name = $1; #print "begin: $begin end: $end length ($name): ",length($name),"\n"; # some safety checks if (($begin <= $end) and ($end < length($name)) and ($begin >= 0)) { $name = substr($name, $begin, $end - $begin + 1); } $$newname =~ s/\%F(\d+)\-(\d+)/$name/g; } } # get the date and replace it, only when needed (it is needed when one of the placeholders %y %m %d %h %M or %s is used) if ($format =~ m/(\%y|\%m|\%d|\%h|\%M|\%s)/) { my $datestr = ''; $datestr = getEXIFDate($dpic); if ($datestr eq '') { $datestr = getFileDate($dpic, NO_FORMAT); $datestr = getDateTimeEXIFString($datestr); unless ($$doForAll) { my $rc = checkDialog("Use file date?", "$pic has no EXIF date, shall I use the file date ($datestr) instead?", $doForAll, "don't ask again", getThumbFileName($dpic), 'OK', "Skip this picture", "Cancel all"); return $rc if (($rc eq "Skip this picture") or ($rc eq "Cancel all")); } } my @datetime = split / /, $datestr; my @times = split /:/, $datetime[1]; my @dates = split /:/, $datetime[0]; $$newname =~ s/%y/$dates[0]/g; $$newname =~ s/%m/$dates[1]/g; $$newname =~ s/%d/$dates[2]/g; $$newname =~ s/%h/$times[0]/g; $$newname =~ s/%M/$times[1]/g; $$newname =~ s/%s/$times[2]/g; } # get EXIF data and replace it, only when needed if ($format =~ m/(\%xa|\%xe|%xm|%xr)/) { my $aperture = sprintf("%02.1f", getEXIFAperture($dpic, NUMERIC)); $$newname =~ s/%xa/$aperture/g; my $exposure = sprintf("%.3f", getEXIFExposureTime($dpic, NUMERIC)); $$newname =~ s/%xe/$exposure/g; my $model = getEXIFModel($dpic); $model =~ tr/\000/ /; # remove null termination (\000) chars $model =~ s/( )+/ /g; # replace more than one space with one $model =~ s/\s+$//; # cut trailing whitespace $$newname =~ s/%xm/$model/g; my $artist = getEXIFArtist($dpic); $$newname =~ s/%xr/$artist/g; } # get image data and replace it, only when needed if ($format =~ m/(\%iw|\%ih)/) { my ($w, $h) = getSize($dpic); $$newname =~ s/%iw/$w/g; $$newname =~ s/%ih/$h/g; } # use iptc data if required # thanks to Alexander Zangerl for the patch if ($format =~ m/\%p(h|o|H|O)/) { # retrieve headline or objectname my $what=$1; my $attr=($what =~ /h/i? getIPTCHeadline($dpic): getIPTCObjectName($dpic)); $attr =~ s/ /_/g if ($what =~ /(h|o)/); # get rid of the spaces if asked $$newname =~ s/%p(h|o|H|O)/$attr/g; } print "applyRenameFormat: $pic -> -$$newname- (format: $format)\n" if $verbose; return 'OK'; } ############################################################## # findNewName - find a unused name by adding a number # e.g. name-002.jpg # input: filename with dir! with or without suffix # output: new filename - no dir!!! # hint: in file sets which would end in the same name this # function will rename only the second and folling files # see also: renameSmartFix() - set set the name of the first file ############################################################## sub findNewName { my $dpic = shift; my $dir = dirname($dpic); my $pic = basename($dpic); if ($pic !~ /(.*)(\.jp(g|eg))/i) { $pic .= ".jpg"; # pic does not have a jpeg suffix - adding .jpg } $pic =~ /(.*)(\.jp(g|eg))/i; # now split again (we need $1 and $2) my $base = $1; my $new = $base; my $suffix = $2; # if a file with this name already exists, we add a number for ( 2 .. 999 ) { # three digits if (-f "$dir/$new$suffix") { $new = sprintf "%s-%03d", $base, $_; # three digits } else { last; } } print "findNewName: $pic -> $new$suffix\n" if $verbose; return "$new$suffix"; } ############################################################## # check_new_keywords - check if new keywords were found in the pictures and ask to add them to the catalog ############################################################## sub check_new_keywords { return unless ($config{CheckNewKeywords}); return if (keys %new_keywords <= 0); return unless (get_new_keywords()); # open window my $win = $top->Toplevel(); $win->title(lang('New IPTC keywords')); $win->iconimage($mapiviicon) if $mapiviicon; my $text = '...'; $win->Label(-textvariable => \$text)->pack(-expand => 0, -fill => 'x'); my $tlb = $win->Scrolled("HList", -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 0, -columns => 2, -scrollbars => 'osoe', -selectmode => 'extended', -width => 80, -height => 30, )->pack(-expand => 1, -fill => 'both'); $tlb->header('create', 0, -text => lang('Keyword'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 1, -text => lang('Quantity'), -headerbackground => $conf{color_entry}{value}); my $butF1 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $butF2 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $butF3 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $butF1->Button(-text => lang('Select all'), -command => sub { selectAll($tlb); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF1->Button(-text => lang('Select none'), -command => sub { $tlb->selectionClear(); })->pack(-side => 'left', -padx => 3, -pady => 3); my $ab = $butF2->Button(-text => lang('Add'), -command => sub { my @sellist = getSelection($tlb); return unless checkSelection($win, 1, 0, \@sellist, lang("keyword(s)")); add_new_keywords(\@sellist); my $nr = show_new_keywords($tlb); $win->destroy() if ($nr < 1); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($ab, -msg => lang('Add selected keywords to keyword catalog')); my $ib = $butF2->Button(-text => lang('Ignore'), -command => sub { my @sellist = getSelection($tlb); return unless checkSelection($win, 1, 0, \@sellist, lang("keyword(s)")); foreach (@sellist) { $ignore_keywords{$_} = 1; delete $new_keywords{$_} if (defined $new_keywords{$_}); } my $nr = show_new_keywords($tlb); $win->destroy() if ($nr < 1); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($ib, -msg => lang('Ignore selected keywords')); $butF3->Checkbutton(-variable => \$config{CheckNewKeywords}, -text => lang("Check for new keywords"))->pack(-side => 'left', -anchor => 'w'); my $Xbut = $butF3->Button(-text => lang('Ask later'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { selectAll($tlb); } ); $win->Popup(-popover => 'cursor'); repositionWindow($win); my $nr = show_new_keywords($tlb); $text = langf("Found %d new IPTC keywords, please choose how to proceed.", $nr); $win->waitWindow; } ############################################################## # show_new_keywords - show a list of keywords in a hlist ############################################################## sub show_new_keywords { my $lb = shift; # the hlist widget my @list = get_new_keywords(); $lb->delete('all'); foreach my $key (sort @list) { $lb->add($key); $lb->itemCreate($key, 0, -text => $key);#, -style => $comS); $lb->itemCreate($key, 1, -text => $new_keywords{$key});#, -style => $iptcS); } return (scalar @list); } ############################################################## # get_new_keywords - get new keywords from global hash, return list with new keywords (e.g. nature.animal.dog) ############################################################## sub get_new_keywords { my @new_keywords; foreach my $key (keys %new_keywords) { # skip if keyword is in the ignore list next if (defined $ignore_keywords{$key}); # replace dot "." with slash "/" - that's the way they are stored in the prekeys list my $keyS = $key; $keyS =~ s|\.|\/|g; # check if this is a new key (not in @prekeys list) if (!isInList($keyS, \@prekeys)) { # add new keyword to list push @new_keywords, $key; } } return @new_keywords; } ############################################################## # add_new_keywords - add new keywords to my keyword catalog (e.g. nature.animal.dog) ############################################################## sub add_new_keywords { my $new_keys_ref = shift; foreach my $key (@{$new_keys_ref}) { my $new_key = ''; # convert / separator to . $key =~ s|\/|\.|g; # add hierarchical (joined) keywords e.g. nature.animal.dog as nature, nature.dog and nature.animal.dog foreach (split /\./, $key) { $new_key .= $_; push @prekeys, $new_key unless (isInList($new_key, \@prekeys)); $new_key .= '/'; } # remove from global hash delete $new_keywords{$key}; } # show in keyword window (if open) if (Exists($nav_F->{key_frame})) { insertTreeList($nav_F->{key_frame}->{tree}, @prekeys); } } ############################################################## # check if we accidentially remove pictures with a high rating ############################################################## sub preserve_high_rated_pics { my $preserve = 0; my $sellist = shift; my %high_rated_pics; foreach my $dpic (@$sellist) { if (defined $searchDB{$dpic}{URG}) { if (($searchDB{$dpic}{URG} > 0) and ($searchDB{$dpic}{URG} <= $config{AskDeleteHighRatingLevel})) { $high_rated_pics{$dpic} = $searchDB{$dpic}{URG}; } } } if (keys(%high_rated_pics) > 0) { $preserve = 1; # open window my $win = $top->Toplevel(); $win->title(lang('Delete high rated pictures?')); $win->iconimage($mapiviicon) if $mapiviicon; my $w = int($top->screenwidth * 0.5); my $h = int($top->screenheight * 0.5); $win->geometry("${w}x${h}+0+0"); my $text = lang("loading ..."); $win->Label(-textvariable => \$text)->pack(-expand => 0, -fill => 'x'); my $tlb = $win->Scrolled("HList", -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 0, -columns => 4, -scrollbars => 'osoe', -selectmode => 'none', #-background => $conf{color_bg}{value}, #8fa8bf -width => 80, #-height => 30, )->pack(-expand => 1, -fill => 'both'); $tlb->header('create', 0, -text => lang('Thumbnail'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 1, -text => lang('Name'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 2, -text => lang('Rating'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 3, -text => lang('Folder'), -headerbackground => $conf{color_entry}{value}); my $butF = $win->Frame()->pack(-expand => 0, -fill =>'x', -padx => 3, -pady => 3); $butF->Button(-text => lang('Delete'), -command => sub { $preserve = 0; $win->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $butF->Button(-text => lang('Cancel'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { return if (!$tlb->info('children')); showPicInOwnWin(getNearestItem($tlb)); }); $win->bind('', sub { return if (!$tlb->info('children')); showPicInOwnWin(getNearestItem($tlb)); }); $win->Popup(); my %thumbs; # add all pictures with thumbnails, name and rating ... foreach my $dpic (sort keys(%high_rated_pics)) { my $name = basename($dpic); my $dir = dirname($dpic); my $thumb = getThumbFileName($dpic); my $urg = iptc_rating_stars($searchDB{$dpic}{URG})." ($high_rated_pics{$dpic})"; $tlb->add($dpic); if (-f $thumb) { $thumbs{$thumb} = $top->Photo(-file => $thumb, -gamma => $config{Gamma}); if (defined $thumbs{$thumb}) { $tlb->itemCreate($dpic, 0, -image => $thumbs{$thumb}, -itemtype => 'imagetext', -style => $thumbS); } } $tlb->itemCreate($dpic, 1, -text => $name, -style => $comS); $tlb->itemCreate($dpic, 2, -text => $urg, -style => $iptcS); $tlb->itemCreate($dpic, 3, -text => $dir, -style => $comS); } # display the dialog text $text = langf("%d of the %d selected pictures have a rating higher or equal to %d.",scalar(keys(%high_rated_pics)), scalar(@$sellist), $config{AskDeleteHighRatingLevel})."\n".langf("Please confirm to delete %d pictures.",scalar(@$sellist))."\n".lang("Use or middle mouse button to view a picture."); $win->waitWindow; foreach (keys %thumbs) { $thumbs{$_}->delete if (defined $thumbs{$_}); } # free memory } return $preserve; } ############################################################## # reverse slideshow hash for fast checks if a picture is in a (or several) slideshows # key1 = dpic, key2 = folder, key3 = slideshows name, value = index (starts with 0) # base: %slideshow hash # if performance issues occure, the %slideshow_pics hash could be stored and updated only when needed ############################################################## sub build_slideshow_pic_hash { my %slideshow_pics; foreach my $folder (keys %slideshows) { foreach my $collection (keys %{$slideshows{$folder}}) { my $index = 0; foreach my $dpic (@{$slideshows{$folder}{$collection}{pics}}) { #print "build_slideshow_pic_hash: $index: $dpic \n"; $slideshow_pics{$dpic}{$folder}{$collection} = $index; $index++; } } } return \%slideshow_pics; } ############################################################## # check if a picture rename or move action affects any collections # and renames the picture in the colletions # todo: this approach is not able to handle several instances # of a picture in one slideshow ############################################################## sub rename_slideshow_pic { my $old_name = shift; my $new_name = shift; my $slideshow_pics = build_slideshow_pic_hash(); if (defined $$slideshow_pics{$old_name}) { print "$old_name is used in a slideshow:\n"; foreach my $folder (keys %{$slideshow_pics->{$old_name}}) { foreach my $collection (keys %{$slideshow_pics->{$old_name}->{$folder}}) { my $index = $slideshow_pics->{$old_name}->{$folder}->{$collection}; print " in collection: $folder $collection with index $index\n"; if ($slideshows{$folder}{$collection}{pics}[$index] eq $old_name) { print " found pic as index $index - replacing it with $new_name\n"; log_it(basename($old_name)." is used in collection $folder $collection. -> Fixed."); $slideshows{$folder}{$collection}{pics}[$index] = $new_name; } else { print " Error: pic $old_name with index $index not found!!!\n"; } } } } } ############################################################## # check if we accidentially remove pictures which are used in # slideshows # function derived from preserve_high_rated_pics() ############################################################## sub preserve_slideshow_pics { my $preserve = 0; my $sellist = shift; my %used_pics; my $slideshow_pics = build_slideshow_pic_hash(); foreach my $dpic (@$sellist) { #print "preserve_slideshow_pics: checking for $dpic\n"; if (defined $$slideshow_pics{$dpic}) { #print "$dpic is used in a slideshow!\n"; $used_pics{$dpic} = 1; } } if (keys(%used_pics) > 0) { $preserve = 1; # open window my $win = $top->Toplevel(); $win->title(lang('Delete pictures used in collections?')); $win->iconimage($mapiviicon) if $mapiviicon; my $w = int($top->screenwidth * 0.5); my $h = int($top->screenheight * 0.5); $win->geometry("${w}x${h}+0+0"); my $text = lang("loading ..."); $win->Label(-textvariable => \$text)->pack(-expand => 0, -fill => 'x'); my $tlb = $win->Scrolled('HList', -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 0, -columns => 6, -scrollbars => 'osoe', -selectmode => 'none', #-background => $conf{color_bg}{value}, #8fa8bf -width => 80, #-height => 30, )->pack(-expand => 1, -fill => 'both'); $tlb->header('create', 0, -text => lang('Thumbnail'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 1, -text => lang('Name'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 2, -text => lang('Collection folder'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 3, -text => lang('Collection'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 4, -text => lang('#'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 5, -text => lang('File folder'), -headerbackground => $conf{color_entry}{value}); my $butF = $win->Frame()->pack(-expand => 0, -fill =>'x', -padx => 3, -pady => 3); $butF->Button(-text => lang('Delete'), -command => sub { $preserve = 0; $win->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $butF->Button(-text => lang('Cancel'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { return if (!$tlb->info('children')); showPicInOwnWin(getNearestItem($tlb)); }); $win->bind('', sub { return if (!$tlb->info('children')); showPicInOwnWin(getNearestItem($tlb)); }); # key l to open corresponding picture collection $win->bind('', sub { return if (!$tlb->info('children')); my $dpic = getNearestItem($tlb); # warning: pic may be part of several collections # thus collection folder and name may be multiline strings my $cfolders = $tlb->itemCget($dpic, 2, -text); my $collections = $tlb->itemCget($dpic, 3, -text); my @folders = split(/\n/, $cfolders); my @names = split(/\n/, $collections); my $folder = $folders[0]; my $collection = $names[0]; if (@folders > 1) { my @sel_list; return if (!mySelListBoxDialog(lang("Select collection to open"), langf("Picture is used in %d collections.\nPlease select which one to open.",scalar @names), SINGLE, lang("Open collection"), \@sel_list, @names)); return if (not @sel_list); # return if nothing is selected foreach (@sel_list) { $folder = $folders[$_]; $collection = $names[$_]; } } if (exists $slideshows{$folder}{$collection}) { my $pics = $slideshows{$folder}{$collection}{pics}; light_table_edit($pics, $folder, $collection); } else { warn "Warning: collection $folder $collection does not exist! Should not happen!"; } #print "key l pressed: $dpic $cfolder $collection\n"; }); $win->Popup(); my %thumbs; # add all pictures with thumbnails, name and rating ... foreach my $dpic (sort keys(%used_pics)) { my $name = basename($dpic); my $dir = dirname($dpic); my $thumb = getThumbFileName($dpic); my @collectionfolders; my @collectionnames; my @collectionindex; # list all collections using this picture foreach my $folder (keys %{$slideshow_pics->{$dpic}}) { foreach my $collection (keys %{$slideshow_pics->{$dpic}->{$folder}}) { # add 1 to make the index readable my $index = $$slideshow_pics{$dpic}{$folder}{$collection} + 1; push @collectionfolders, $folder; push @collectionnames, $collection; push @collectionindex, $index; } } my $cfolders = join "\n", @collectionfolders; my $cnames = join "\n", @collectionnames; my $cindex = join "\n", @collectionindex; $tlb->add($dpic); if (-f $thumb) { $thumbs{$thumb} = $top->Photo(-file => $thumb, -gamma => $config{Gamma}); if (defined $thumbs{$thumb}) { $tlb->itemCreate($dpic, 0, -image => $thumbs{$thumb}, -itemtype => 'imagetext', -style => $thumbS); } } $tlb->itemCreate($dpic, 1, -text => $name, -style => $comS); $tlb->itemCreate($dpic, 2, -text => $cfolders, -style => $iptcS); $tlb->itemCreate($dpic, 3, -text => $cnames, -style => $comS); $tlb->itemCreate($dpic, 4, -text => $cindex, -style => $comS); $tlb->itemCreate($dpic, 5, -text => $dir, -style => $comS); } # display the dialog text $text = langf("%d of the %d selected pictures are used in a collection.", scalar(keys(%used_pics)), scalar(@$sellist))."\n".langf("Please confirm to delete %d pictures.",scalar(@$sellist))."\n".lang("Use or middle mouse button to view a picture.")." ".lang("Use to open collection."); $win->waitWindow; foreach (keys %thumbs) { $thumbs{$_}->delete if (defined $thumbs{$_}); } # free memory } return $preserve; } ############################################################## # deletePics - deletes selected pictures # mode: TRASH or REMOVE # TRASH = move to $trashdir # REMOVE = remove ############################################################## sub deletePics { my $lb = shift; # the reference to the active listbox widget my $mode = shift; # constant TRASH or REMOVE if (ref($lb) eq 'Tk::Canvas') { warn "deletePics: Delete operation not supported in Canvas widget (light table)"; return; } my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my @childs = $lb->info('children'); my $all = 0; $all = 1 if (@childs == @sellist); # all pics are selected my $str = ''; my @dummylist = (); my $changed = 0; my $update = 0; # build the show and the delete list foreach my $dpic (@sellist) { my $pic = basename($dpic); my $size = getFileSize($dpic, FORMAT); $str .= sprintf "%-40s %10s\n", $pic, $size; } # picture to select after deletion (has to be defined before we manipulate the listbox!) # we try to select the picture which is shown after the last picture of the current selection my $select_after = $lb->info('next', $sellist[-1]); if ($mode == REMOVE) { # remove mode my $rc = myButtonDialog("Really delete?", "Please press Ok to delete these ".scalar @sellist." files.\nThere is no undelete!\n\nPath: $actdir\n\n$str", undef, 'OK', 'Cancel'); return unless ($rc eq 'OK'); } elsif ($mode == TRASH) { # remove to trash mode # check if the trash dir is available if (!-d $trashdir) { $lb->messageBox(-icon => 'warning', -message => "Trashdir $trashdir not found!\nPlease create this dir (shell: mkdir $trashdir) and retry.\n\nAborting.", -title => "Delete pictures", -type => 'OK'); return; } # check if we are in the trash dir if ($actdir eq $trashdir) { $lb->messageBox(-icon => 'warning', -message => "Please use to really remove files from the trash!", -title => "Delete pictures", -type => 'OK'); return; } makeDir("$trashdir/$thumbdirname", NO_ASK); } else { warn "deletePics called without or with a wrong mode ($mode). Aborting"; return; } # check if we accidentially remove pictures with a high rating if ($config{AskDeleteHighRating}) { return if preserve_high_rated_pics(\@sellist); } # check if some pics are used in slideshows return if preserve_slideshow_pics(\@sellist); my $errors = ''; my $i = 0; my $pw; $pw = progressWinInit($lb, "Delete pictures") if (@sellist > 1); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; my $pic = basename($dpic); my $bakpic = $dpic; $bakpic =~ s/(.*)(\.jp(g|eg))/$1-bak$2/i; my $thumb = getThumbFileName($dpic); my $bakthumb = $thumb; $bakthumb =~ s/(.*)(\.jp(g|eg))/$1-bak$2/i; progressWinUpdate($pw, "deleting ($i/".scalar @sellist.") ...", $i, scalar @sellist); if ($mode == REMOVE) { if ( removeFile($dpic) ) { $changed++; #delete $searchDB{$dpic}; # line is moved to removeFile() deleteCachedPics($dpic); delete_XMP_file($dpic); $lb->delete('entry', $dpic) unless $all; } } else { # $mode == TRASH - move picture to trash if (move($dpic, $trashdir)) { $changed++; # count nr of successfull moves my $tpic = "$trashdir/$pic"; # change the location info in the search database $searchDB{$tpic} = $searchDB{$dpic}; $searchDB{$tpic}{odir} = dirname($dpic); delete $searchDB{$dpic}; deleteCachedPics($dpic); $lb->delete('entry', $dpic) unless $all; # only if move was successfull, we also move the thumbnail if ((-d "$trashdir/$thumbdirname") and (-f $thumb)) { if (!move("$thumb", "$trashdir/$thumbdirname")) { $errors .= "Could not move thumbnail \"$thumb\" to $trashdir/$thumbdirname: $!\n"; } } do_other_files($lb, MOVE, $dpic, $tpic, \$errors); } else { $errors .= "Could not move picture \"$pic\" to $trashdir: $!\n"; } } # if file is removed and a backup file exists and is not in the delete list, # we offer to rename the backup to the original name # todo this should be done in one dialog for all files at the end if ((!-f $dpic) and (-f $bakpic) and !isInList($bakpic, \@sellist)) { my $age = getAgeOfFile($bakpic); $age = " (which is $age old)" unless ($age eq ''); my $bakname = basename($bakpic); my $rc = myButtonDialog(lang('Restore backup?'), langf("Picture \"%s\" has a backup file%s.\nShould I rename the backup \"%s\" to \"%s\"?", $pic, $age, $bakname, $pic), $bakthumb, lang('Rename'), lang('Cancel'), lang('Cancel all')); last if ($rc eq lang('Cancel all')); if ($rc eq lang('Rename')) { if (!rename ("$bakpic", "$dpic")) { $errors .= "Could not rename $bakpic to $pic: $!\n"; } else { $searchDB{$dpic} = $searchDB{$bakpic}; delete $searchDB{$bakpic}; # rename thumbnail rename ("$bakthumb", "$thumb"); if ($lb->info("exists", $bakpic)) { unless (hlistEntryRename($lb, $bakpic, $dpic)) { warn "error renaming hlist entry $bakpic to $dpic"; } } # if the backup is already visible we don't need an update if ($lb->info("exists", $dpic)) { # change the displayed name $lb->itemConfigure($dpic, $lb->{thumbcol}, -text => getThumbCaption($pic)); my $rating_size = get_rating_and_size($dpic, $lb); $lb->itemConfigure($dpic, $lb->{filecol}, -itemtype => 'image', -image => $rating_size, -style => $fileS); } else { $update++; } } } } if (!-f $dpic) { # ask to delete non-JPEG file, if any foreach my $suf (split /\|/, $nonJPEGsuffixes) { my $njpic = $dpic; $njpic =~ s/(.*)\.jp(g|eg)$/$1.$suf/i; if (-f $njpic) { my $rc = $lb->messageBox(-icon => 'question', -message => "There is a non-JPEG file\n\"".basename($njpic)."\"\nOk to delete it too?", -title => "Delete non-JPEG?", -type => 'OKCancel'); last if ($rc !~ m/Ok/i); if ($mode == REMOVE) { if ( removeFile($njpic) ) { } } elsif ($mode == TRASH) { if (!move("$njpic", "$trashdir")) { $errors .="Could not move \"".basename($njpic)."\" to $trashdir: $!\n"; } } } } } } # foreach progressWinEnd($pw); log_it(langf("Deleted %d of %d pictures", $changed, scalar(@sellist))); if ($errors ne '') { $errors = "These errors occured while deleting the ".scalar @sellist." selected pictures:\n$errors"; showText("Error while deleting", $errors, NO_WAIT); } checkTrash() if ($changed > 0); $update++ if $all; if ($update > 0) { if ($lb == $picLB) { updateThumbs(); } else { $lb->delete("all"); } } # select the picture after the last selected file select_next($lb, $select_after); $lb->focus; } ############################################################## # getAgeOfFile - returns a string representing the age of the # given file (with max two of the units: # day, hour, minute, second) ############################################################## sub getAgeOfFile { my $file = shift; return '' unless (-f $file); my $diff = abs(time() - (lstat $file)[9]); my @secs = qw/86400 3600 60/; my @unit = (lang("days"), lang("hours"), lang("minutes")); my $str = ''; my $count = 0; for my $t ( 0 .. $#secs) { my $i = int($diff/$secs[$t]); if ($i > 0) { $str = "$str $i $unit[$t]"; $count++; last if ($count >= 2); # two numbers are enough } $diff %= $secs[$t]; } $str = "$str $diff ".lang("seconds") if ($count < 2); return $str; } ############################################################## # findValidIndex - try to find a index to show e.g. after a # delete ############################################################## sub findValidIndex { my $lb = shift; my $i = shift; # startindex my @pics = $lb->info('children'); if ((defined $i) and ($i > $#pics)) { $i = $#pics; } # if possible show the pic following the last deleted one while ((!$lb->info("exists", $i)) and ($i < $#pics)) { $i++; } if ($i > $#pics) { $i = 0; } return $i; } sub centerWindow { #################################################### # Args: (0) window to center # (1) [optional] desired width # (2) [optional] desired height # # Returns: *nothing* #################################################### my($window, $width, $height) = @_; $window->idletasks; $width = $window->reqwidth unless $width; $height = $window->reqheight unless $height; my $x = int(($window->screenwidth / 2) - ($width / 2)); my $y = int(($window->screenheight / 2) - ($height / 2)); $window->geometry($width . 'x' . $height . "+" . $x . "+" . $y); } ############################################################## # repositionWindow - reposition window to fit in the desktop ############################################################## sub repositionWindow { my $win = shift; my $xoffset = shift; # optional x offset (1 or 0) reposition window by half the width my $border = 40; my $reposition = 0; my $geo = $win->geometry; my ($w, $h, $x, $y) = splitGeometry($geo); print "geo $w $h $x $y\n" if $verbose; $h = $win->screenheight if ($h > $win->screenheight); $w = $win->screenwidth if ($w > $win->screenwidth); if ( ($y+$h+$border) > $win->screenheight) { $y = $y - ( ($y+$h+$border) - $win->screenheight ); $reposition = 1; } if ( ($x+$w+$border) > $win->screenwidth) { $x = $x - ( ($x+$w+$border) - $win->screenwidth ); $reposition = 1; } if ($x < 0) { $x = 0; $reposition = 1; } if ($y < 0) { $y = 0; $reposition = 1; } if ($xoffset) { if ($x > 400) { $x -= int($w/2+10); } else { $x += int($w/2+10); } $reposition = 1; } if ($reposition) { print "reposing to $w $h $x $y\n" if $verbose; $win->geometry($w . 'x' . $h . "+" . $x . "+" . $y); $win->update; } } ############################################################## # printlist ############################################################## sub printlist { print "---\n"; foreach (@_) {print "$_\n";} print "---\n"; } ############################################################## # printhash ############################################################## sub printhash { my $hash = shift; foreach (sort keys %{$hash}) { print "$$hash->{$_} = $_ \n"; } } ############################################################## # bindItem - binds the motion event to the picture ############################################################## sub bindItem { my $id = shift; $c->bind($id, '' => sub { ($idx,$idy)=($Tk::event->x,$Tk::event->y); }); # change the mouse pointer $c->bind($id, '' => sub { # Color picker # get mouse coordinates my $x = $c->canvasx($Tk::event->x); my $y = $c->canvasy($Tk::event->y); # get and apply offset (because pic may be centered in canvas) my ($x1, $y1, $x2, $y2) = $c->bbox($id); # todo: test this change! if ($x1) { $x -= $x1; $y -= $y1; $x = 1 if ($x < 1); $y = 1 if ($y < 1); $x = $x2-$x1-2 if ($x > $x2-$x1-2); $y = $y2-$y1-2 if ($y > $y2-$y1-2); # get the color information from the picture my($r,$g,$b) = $c->itemcget($id, -image)->get($x, $y); #convert to hex from decimal $config{ColorPicker} = sprintf "#%.2x%.2x%.2x", $r, $g, $b; log_it("Color picker: $config{ColorPicker}"); $colorPickerInfo->configure(-background => $config{ColorPicker}); $c->configure(-cursor => "crosshair"); } }); $c->bind($id, '' => sub { $c->configure(-cursor => "crosshair"); }); $c->bind($id, '' => sub { $c->configure(-cursor => "arrow"); }); # enable panning in the canvas (autoscroll) $c->bind($id, '' => sub { # actual mouse coordinates $c->configure(-cursor => "fleur"); my ($mx,$my)=($Tk::event->x,$Tk::event->y); my ($x1,$x2) = $c->xview; my ($y1,$y2) = $c->yview; return if ($x1 == 0 and $x2 == 1 and $y1 == 0 and $y2 == 1); my $dx = 0; $dx = ($mx-$idx)/$width if ($width >= 1); # avoid division by zero my $dy = 0; $dy = ($my-$idy)/$height if ($height >= 1); # avoid division by zero $c->xviewMoveto($x1-$dx) unless ($x1 == 0 and $x2 == 1); $c->yviewMoveto($y1-$dy) unless ($y1 == 0 and $y2 == 1); ($idx,$idy)=($mx,$my); }); # show picture coordinates $c->bind($id, '' => sub { return unless $conf{show_coordinates}{value}; my $zf = 1; # get mouse coordinates my $x = $c->canvasx($Tk::event->x); my $y = $c->canvasy($Tk::event->y); # get the actual zoom factor from the global variable $zoomFactorStr if ($zoomFactorStr =~ m/(.*)%$/) { # cut off the % sign return if ($1 eq "?"); $zf = $1; # get the zoom factor in % (e.g. 80%) $zf /= 100; # the zoom factor as float (e.g. 0.8) } else { warn "zoomStep: zoomFactorStr not matching *% - returning!" if $verbose; return; } return if ($zf <= 0); # get and apply offset (because pic may be centered in canvas) my ($x1, $y1, $x2, $y2) = $c->bbox($id); $x -= $x1; $y -= $y1; # apply zoom factor $x = int($x/$zf); $y = int($y/$zf); # set borders $x = 0 if ($x < 0); $y = 0 if ($y < 0); $x = $width if ($x > $width); $y = $height if ($y > $height); my $right_border = $width - $x; my $lower_border = $height - $y; log_it("coordinates: $x, $y (-$right_border, -$lower_border)"); }); } ############################################################## # changeDir ############################################################## sub changeDir { my $newDir = shift; return 0 unless defined $newDir; if ( !chdir $newDir ) { my $dialog = $top->Dialog(-title => "Changing to $newDir folder failed", -text => "Can't change to $newDir folder: $!", -buttons => ['OK']); $dialog->Show(); warn "Can't change to $newDir folder: $!"; return 0; } return 1; } ############################################################## # getCorners - get the visible corners of an canvas ############################################################## sub getCorners { my $c = shift; my(@xview) = $c->xview; my(@yview) = $c->yview; my(@scrollregion) = @{$c->cget(-scrollregion)}; ($xview[0] * ($scrollregion[2]-$scrollregion[0]) + $scrollregion[0], $yview[0] * ($scrollregion[3]-$scrollregion[1]) + $scrollregion[1], $xview[1] * ($scrollregion[2]-$scrollregion[0]) + $scrollregion[0], $yview[1] * ($scrollregion[3]-$scrollregion[1]) + $scrollregion[1], ); } ############################################################## # find best zoom and subsample value to fit or fill # a picture into an available pixel area (e.g. canvas) ############################################################## sub zoom_fit { my $dpic = shift; # the picture including dir (e.g. /home/herrmann/pic001.jpg) my $w = shift; # the available canvas width my $h = shift; # the available canvas height my $zoom = 0; my $subsample = 0; my $string = '100%'; if ((-f $dpic) and (defined $w) and (defined $h) and ($w > 1) and ($h > 1)) { my ($pic_w, $pic_h) = getSize($dpic); my $w_factor = $pic_w/($w - 6); # the offset (6) is needed, maybe because of the border? my $h_factor = $pic_h/($h - 6); my $max; if ( $conf{zoom_fit_fill}{value} == FIT) { $max = max($w_factor, $h_factor); # find the biggest zoom factor } else { # or FILL $max = min($w_factor, $h_factor); } if ($max > 1) { # search for a zoom/subsample pair which will zoom the pic at least to the needed factor 1/$max my $i; for ($i = 0; $i < (@frac - 2); $i += 2) { if (($frac[$i]/$frac[$i+1]) < (1/$max)) { last; } } $zoom = $frac[$i]; $subsample = $frac[$i+1]; $string = int(1/($subsample/$zoom) * 100).'%'; } } return ($zoom, $subsample, $string); } ############################################################## ############################################################## sub zoom_photo_object { my $photo = shift; # reference to a photo object my $zoom = shift; my $subsample = shift; if ($zoom != $subsample) { # if the values are the same there is nothing to do # open new photo object my $zoomed = $top->Photo; $zoomed->blank; # if $zoom is 1 we can skip the zoom step, else we should first zoom and then subsample. # This is slower, but delivers much better quality. # Tk::Photo seems to subsample first and zoom afterwards if -zoom and -subsamle is used # in one command which will produce ugly block artefacts if ($zoom == 1) { # this will speed up the zooming of some zoom facors (e.g. 16%, 20%, 25%, 50%) up to 10 times!!! $zoomed->copy($$photo, -subsample => $subsample); $$photo->delete; $$photo = undef; $$photo = $top->Photo; $$photo->copy($zoomed); # then subsample it } else { # slower, but better quality $zoomed->copy($$photo, -zoom => $zoom); # first zoom pciture ... $$photo->delete; $$photo = undef; $$photo = $top->Photo; $$photo->copy($zoomed, -subsample => $subsample); # ... then subsample it } $$photo->configure(-gamma => $config{Gamma}); $zoomed->delete; $zoomed = undef; } return 1; } ############################################################## # autozoom - zooms the given photo object to fit into the # available pixel area ############################################################## sub autoZoom { if (!$config{AutoZoom}) { return '100%'; } my $photo = shift; # reference to a photo object my $dpic = shift; # the file including dir (e.g. /home/herrmann/pic001.jpg) my $cw = shift; # the available width my $ch = shift; # the available height my ($zoom, $subsample, $zoom_string) = zoom_fit($dpic, $cw, $ch); log_it(langf("zooming to %s ...", $zoom_string)); zoom_photo_object($photo, $zoom, $subsample); return $zoom_string; } ############################################################## # getZoomAndSub - build a appropriate fraction for zoom and # subsample from a zoomfactor (float) ############################################################## sub getZoomAndSub { my $targetfactor = shift; # the target zoom factor e.g. 0.66 my $step = shift; # -1 = stay beyond $targetfactor; +1 = return a bigger value than $targetfactor my $i = 0; my $dif = 1000; # difference to the targetfactor my $diflast = $dif + 1; # last difference # search the @frac array for the right fraction for ($i = 0; $i < (@frac - 2); $i += 2) { $dif = abs(($frac[$i]/$frac[$i+1]) - $targetfactor); # how far are we away? #$dif *= -1 if ($dif < 0); # the difference must allways be positive #printf " up %1.3f %2d %1.3f %2d/%-2d %1.3f\n", $targetfactor, $i, ($frac[$i]/$frac[$i+1]), $frac[$i], $frac[$i+1], $dif; last if ( $dif > $diflast); # if the difference starts to grow we jump out $diflast = $dif; } $i -= 2; # the last fraction had the lowest difference to the targetfactor $i -= $step*2; # go to the next or previous fraction # boundary checks (stay in the array) $i = 0 if ($i < 0); $i = @frac - 1 if ($i > @frac - 1); return ($frac[$i], $frac[$i+1]); } ############################################################## # zoomStep - increase/decrease the actual zoom factor ############################################################## sub zoomStep { my $step = shift; # +1 or -1 my $zoom = 1; # fallback value my $subsample = 5; # fallback value # get the actual zoom factor from the global variable $zoomFactorStr if ($zoomFactorStr =~ m/(.*)%$/) { # cut off the % sign print "matching *% $1\n" if $verbose; my $zf = $1; # get the zoom factor in % $zf /= 100; # the zoom factor as float # find the next / previous zoom level ($zoom, $subsample) = getZoomAndSub($zf, $step); print "z = $zoom s = $subsample for $zf\n" if $verbose; } else { warn "zoomStep: zoomFactorStr not matching *% - returning!" if $verbose; return; } # zoom the picture zoom($zoom, $subsample); } ############################################################## # zoom - zooms the actual displayed picture to the given # zoom and subsample values ############################################################## sub zoom { my ($zoom, $subsample) = @_; print "zoom: $zoom $subsample\n" if $verbose; my $dpic = $actpic; # zoom the actual picture return unless (defined $photos{$dpic}); $top->Busy; log_it(langf("zooming to %s ...", int($zoom/$subsample*100)."%")); $photos{$dpic}->delete; delete $photos{$dpic}; print "reloading $actpic\n" if $verbose; $photos{$dpic} = $top->Photo(-file => $dpic, -gamma => $config{Gamma}); my $zoomed = $top->Photo; $zoomed->blank; $zoomed->copy($photos{$dpic}, -zoom => $zoom); # delete item from canvas $c->delete('withtag', $dpic); # remove it from the canvas #deleteCachedPics($dpic); $photos{$dpic}->delete; $photos{$dpic} = undef; $photos{$dpic} = $top->Photo; #$photos{$dpic}->blank if $photos{$dpic}; $photos{$dpic}->copy($zoomed, -subsample => $subsample); $photos{$dpic}->configure(-gamma => $config{Gamma}); $zoomed->delete; $zoomed = undef; # center pic in canvas, only when it's smaller my $xoffset = 0; my $yoffset = 0; $xoffset = int(($c->width - $photos{$dpic}->width) /2) if ($c->width > $photos{$dpic}->width); $yoffset = int(($c->height - $photos{$dpic}->height)/2) if ($c->height > $photos{$dpic}->height); # insert pic to the canvas, (state=hidden it will be shown in showPic() later) my $id = $c->createImage($xoffset, $yoffset, -image => $photos{$dpic}, -anchor => 'nw', -tag => ['pic', $dpic], -state => 'hidden'); bindItem($id); addToCachedPics($dpic); $top->Unbusy; showPic($dpic); } ############################################################## # zoom100 - zoom the actual pic to 100% ############################################################## sub zoom100 { return if (!$actpic); log_it("loading ".basename($actpic)." ..."); deleteCachedPics($actpic); # we need to reread the picture, so we should clear the cachedPics list first my $t = $config{AutoZoom}; # save auto zoom value $config{AutoZoom} = 0; # stop auto zoom showPic($actpic); # display the picture without auto zoom $config{AutoZoom} = $t; # reset autozoom to the saved value return; } ############################################################## # fitPicture - (re)zoom the actual picture to fit into the canvas ############################################################## sub fitPicture { return unless (-f $actpic); deleteCachedPics($actpic); my $autoZoomSave = $config{AutoZoom}; # save actual autoZoom value $config{AutoZoom} = 1; # enable auto zoom showPic($actpic); $config{AutoZoom} = $autoZoomSave; # restore old autoZoom value return; } ############################################################## # layout - change the layout of mapivi main window ############################################################## sub layout { my $withAdjuster = shift; saveAdjusterPos() if $withAdjuster; $config{Layout} = 0 if (($config{Layout} > 5) or ($config{Layout} < 0)); my $info = ''; if ($config{Layout} == 0) { $info = lang("3 columns: Navigation Thumbnails Picture"); $config{ShowNavFrame} = 1; $config{ShowThumbFrame} = 1; $config{ShowPicFrame} = 1; } elsif ($config{Layout} == 1) { $info = lang("2 columns: Navigation Thumbnails"); $config{ShowNavFrame} = 1; $config{ShowThumbFrame} = 1; $config{ShowPicFrame} = 0; } elsif ($config{Layout} == 2) { $info = lang("1 column: Thumbnails"); $config{ShowNavFrame} = 0; $config{ShowThumbFrame} = 1; $config{ShowPicFrame} = 0; } elsif ($config{Layout} == 3) { $info = lang("2 columns: Thumbnails Picture"); $config{ShowNavFrame} = 0; $config{ShowThumbFrame} = 1; $config{ShowPicFrame} = 1; } elsif ($config{Layout} == 4) { $info = lang("1 column: Picture"); $config{ShowNavFrame} = 0; $config{ShowThumbFrame} = 0; $config{ShowPicFrame} = 1; } elsif ($config{Layout} == 5) { $info = lang("2 columns: Navigation Picture"); $config{ShowNavFrame} = 1; $config{ShowThumbFrame} = 0; $config{ShowPicFrame} = 1; } else { warn "error: toggle = ".$config{Layout}.", this should never happen!"; $config{Layout} = 0; return; } if ($info ne '') { log_it(lang("Window layout")." ".$info); } showHideFrames(); $top->update; setAdjusterPos() if $withAdjuster; $layoutOld = $config{Layout}; # save the actual Layout } ############################################################## # setAdjusterPos - set the position of the Adjusters according # to the global hash values ############################################################## sub setAdjusterPos { my $x = $subF->width; # width of the surrounding frame my $dirS = $dirA->slave; my $thuS = $thumbA->slave; my $min = 40; # min distance for safety my $dirXnew = $min; # width of adjuser $dirA my $thumbXnew = $min; # width of adjuser $thumbA $x = $top->width if ($x == 1); # $x = 1 at startup, so we use the window width if ($config{Layout} == 0) { # dirs thumbs picture $dirXnew = int($config{Layout0dirX}*$x/100); $thumbXnew = int($config{Layout0thumbX}*$x/100); } elsif ($config{Layout} == 1) { # dirs thumbs $dirXnew = int($config{Layout1dirX}*$x/100); $thumbXnew = int($x - $dirXnew); } elsif ($config{Layout} == 2) { } elsif ($config{Layout} == 3) { # thumbs picture $thumbXnew = int($config{Layout3thumbX}*$x/100); } elsif ($config{Layout} == 4) { } elsif ($config{Layout} == 5) { # dirs picture $dirXnew = int($config{Layout5dirX}*$x/100); } else { warn "error: toggle = ".$config{Layout}.", this should never happen!"; $dirXnew = 1, $thumbXnew = 1; $config{Layout} = 0; return; } print "layoutNew=".$config{Layout}." dirXnew=$dirXnew (".int($dirXnew/$x*100)."%) thumbXnew=$thumbXnew (".int($thumbXnew/$x*100)."%) x=$x nav=".$config{ShowNavFrame}." thumb=".$config{ShowThumbFrame}." pic=".$config{ShowPicFrame}."\n" if $verbose; $dirS->configure(-width => $dirXnew) if ($dirS->ismapped()); $thuS->configure(-width => $thumbXnew) if ($thuS->ismapped()); $top->update; } ############################################################## # saveAdjusterPos - save the actual position of the Adjusters # to the global hash ############################################################## sub saveAdjusterPos { my $x = $subF->width; # width of the surrounding frame my $dirS = $dirA->slave; my $thuS = $thumbA->slave; return if ($x < 1); my $dirX = 0; my $thumbX = 0; if ($dirS->ismapped()) { # get the actual width of the dir frame $dirX = $dirS->width; # convert it to a percentual value $dirX = $dirX / $x * 100; # not too small not to wide (between 5% and 95%) $dirX = 95 if ($dirX > 95); $dirX = 5 if ($dirX < 5); } if ($thuS->ismapped()) { # get the actual width of the thumb frame $thumbX = $thuS->width; # convert it to a percentual value $thumbX = $thumbX / $x * 100; # not too small not to wide (between 5% and 95%) $thumbX = 95 if ($thumbX > 95); $thumbX = 5 if ($thumbX < 5); } if ($layoutOld == 0) { $config{Layout0dirX} = $dirX if ($dirS->ismapped()); $config{Layout0thumbX} = $thumbX if ($thuS->ismapped()); } elsif ($layoutOld == 1) { $config{Layout1dirX} = $dirX if ($dirS->ismapped()); } elsif ($layoutOld == 3) { $config{Layout3thumbX} = $thumbX if ($thuS->ismapped()); } elsif ($layoutOld == 5) { $config{Layout5dirX} = $dirX if ($dirS->ismapped()); } print "layoutOld=$layoutOld dirX=$dirX% thumbX=$thumbX% x=$x\n" if $verbose; } ############################################################## # readConfig - read the configuration from file to hash ############################################################## sub readConfig { my $rcfile = shift; my $configRef = shift; print "readConfig: reading $rcfile\n" if $verbose; if (!$rcfile) { warn "readConfig: no file!"; return; } if (ref($configRef) ne 'HASH') { warn "readConfig: $configRef is no hash ref!"; return; } return 0 if (!-f $rcfile); my $file; if (!open($file, '<', $rcfile)) { warn "readConfig: Couldn't open $rcfile: $!"; return 0; } my $errors = 0; while (<$file>) { chomp; # no newline s/^#.*//; # no comments (lines starting with #) s/^\s+//; # no leading white s/\s+$//; # no trailing white next unless length; # anything left? my ($key, $value) = split(/\s*=\s*/, $_, 2); # split around the equal sign $value =~ s/
/\n/g; # replace "
" by newline if (!defined $configRef->{$key}) { warn "readConfig: key $key (value: $value) should not belong to the config hash - removing\n" ; $errors++; next; } # save in global config hash, overwrite default value $configRef->{$key} = $value; } close $file; if (($errors > 0) and (-d $trashdir)) { my $datetime = getDateTimeShortString(time()); # save a copy of the old config in the trash # todo: remove very old backups warn "saving a backup of the config in the trash ($trashdir)\n"; mycopy($rcfile, $trashdir."/".basename($rcfile)."-$datetime", OVERWRITE); } return 1; } ############################################################## # saveConfig - save the configuration from hash to file ############################################################## sub saveConfig { my $rcfile = shift; my $config = shift; my $value; print "saveConfig: writing $rcfile\n" if $verbose; my $file; if (!open($file, '>', $rcfile)) { warn "saveConfig: Couldn't open $rcfile: $!"; return 0; } print $file "\n# Configuration file for mapivi $version\n\n"; print $file "# last update: ", scalar localtime, "\n\n"; print $file "# This file will be overwritten each time you quit mapivi\n"; #print $file "# or call the \"Save config\" menu item.\n\n"; foreach (sort keys %{$config}) { $value = $$config{$_}; $value =~ s/\n/
/g; # replace newline by "
" print $file $_," = ", $value,"\n"; } close $file; return 1; } ############################################################## # readArrayFromFile - read an array from a file ############################################################## sub readArrayFromFile { my $file = shift; my @list; if (!$file) { warn "readArrayFromFile: no file!"; return; } return () if (!-f $file); my $fileH; if (!open($fileH, '<', $file)) { warn "readArrayFromFile:: Couldn't open $file: $!"; return (); } while (<$fileH>) { chomp; # no newline s/^#.*//; # no comments (lines starting with #) s/^\s+//; # no leading white s/\s+$//; # no trailing white next unless length; # anything left? push @list, $_; } close $fileH; return @list; } ############################################################## # saveArrayToFile - save a array to a file ############################################################## sub saveArrayToFile { my $file = shift; my $listref = shift; my $value; my $fileH; if (!open($fileH, '>', $file)) { warn "saveArrayToFile: Couldn't open $file: $!"; return 0; } foreach (@$listref) { print $fileH "$_\n"; } close $fileH; return 1; } ############################################################## # showPicInOwnWin - displays a picture in a separate window # a mouse click on the picture will close # the window ############################################################## sub showPicInOwnWin { my $dpic = shift; #if ((!defined $dpic) or ($dpic eq '') or (!-f $dpic)) { # no picture given, take selection from main window # my @sellist = $picLB->info('selection'); #return unless checkSelection($top, 1, 0, \@sellist); #$dpic = $sellist[0]; # simply take the first if there are more selected #$lb = $picLB; #} return unless -f $dpic; my @list; push @list, $dpic; show_multiple_pics(\@list, 0); } ############################################################## # show_multiple_pics - displays several pictures in a separate # window a mouse click on the picture will close the window ############################################################## sub show_multiple_pics { my $pic_list = shift; # reference to a picture list, each with full path my $index = shift; # start index number, first pic is index = 0 my $start_fullscreen = shift; # optional, NORMAL or FULLSCREEN my $start_slideshow = shift; # optional, SHOW or NO_SHOW unless (defined $pic_list) { warn "pic list undef"; return; } unless (ref($pic_list) eq 'ARRAY') {warn "pic list is no array reference"; return; } if (@{$pic_list} < 1) {warn "pic list is empty"; return; } my $balloon_addon = "\n\n(Click on picture to close window; use PgUp and PgDown for next/previous picture)"; my $dpic = @{$pic_list}[$index]; my $pic = basename($dpic); my ($photo, $zoomFactor); my $canvasw = 0.8*$top->screenwidth; my $canvash = 0.8*$top->screenheight; my $rc = load_zoom_pic($dpic, \$photo, \$zoomFactor, $canvasw, $canvash); return unless ($rc); # open window my $win = $top->Toplevel(-bg => 'black'); $win->{pic_list} = $pic_list; $win->{index} = $index; $win->{photo} = $photo; $win->{slideshow} = 0; $win->title(sprintf "(%d/%d) %s %s", ($index+1), scalar @{$pic_list}, $pic, $zoomFactor); $win->iconname($pic); # use the picture thumbnail as window icon my $iconfile = getThumbFileName($dpic); $win->{iconPhoto} = $win->Photo(-file => $iconfile) if (-f $iconfile); $win->idletasks if $EvilOS; # this line is crucial (at least on windows) $win->iconimage($win->{iconPhoto}) if $win->{iconPhoto}; $win->{canvas} = $win->Canvas(-width => $canvasw, -height => $canvash, -relief => 'sunken', -bd => $config{Borderwidth})->pack(-expand => 1, -side => 'top', -padx => 0, -pady => 0, -fill => 'both'); # remove all default binding for the canvas (e.g. scroll with cursor and page up/down keys) $win->{canvas}->bindtags(undef); fullscreen($win) if (defined $start_fullscreen and $start_fullscreen == FULLSCREEN); # insert pic in canvas and center it my $xoffset = 0; my $yoffset = 0; $xoffset = int(($canvasw - $win->{photo}->width) /2); $yoffset = int(($canvash - $win->{photo}->height)/2); $win->{canvas}->createImage($xoffset, $yoffset, -image => $win->{photo}, -tag => ['pic',$dpic], -anchor => 'nw'); #canvas_center($win->{canvas}, $win->{photo}->width, $win->{photo}->height); $win->{balloonmsg} = makeBalloonMsg($dpic).$balloon_addon; if ($config{PicWinBalloon}) { $balloon->attach($win->{canvas}, -balloonposition => "mouse", -msg => \$win->{balloonmsg}); } # show text and enlarge font size to fit window show_text_on_canvas($win->{canvas}, get_meta_micro($dpic)); # the context menu my $menu = $win->Menu(-title => "Menu"); $win->{menu} = $menu; $menu->checkbutton(-label => "Balloon popup info", -variable => \$config{PicWinBalloon}, -command => sub { if ($config{PicWinBalloon}) { $balloon->attach($win->{canvas}, -balloonposition => "mouse", -msg => \$win->{balloonmsg}); } else { $balloon->detach($win->{canvas}); } }); if (scalar(@{$pic_list}) > 1) { $menu->separator; $menu->command(-label => lang("Next picture"), -command => sub { next_prev_pic($win, 1); }, -accelerator => ""); $menu->command(-label => lang("Previous picture"), -command => sub { next_prev_pic($win, -1); }, -accelerator => ""); $menu->separator; $menu->checkbutton(-label => lang('Slideshow'), -variable => \$win->{slideshow}, -command => sub { slideshow_pic($win); }, -accelerator => ""); $menu->checkbutton(-label => lang('Slideshow animation'), -variable => \$conf{animation}{value}); } $menu->command(-label => lang('Fullscreen'), -command => sub { fullscreen($win); }, -accelerator => ""); $menu->separator; $menu->command(-label => lang('Add to collection'), -command => sub { my $dpic = @{$win->{pic_list}}[$win->{index}]; my @list; push @list, $dpic; light_table_add(\@list); }); $menu->command(-label => lang('Open this folder'), -command => sub { my $dpic = @{$win->{pic_list}}[$win->{index}]; my $dir = dirname($dpic); if (-d $dir) { openDirPost($dir) if ($dir ne $actdir); showPic($dpic); } }); $menu->separator; # Warning: The Close menu entry must always be the last item to be called by $menu->invoke('last'); $menu->command(-label => lang('Close'), -command => sub { $win->{menu}->unpost(); # close menu $win->{slideshow} = 0; # stop slideshow $win->grabRelease(); $win->withdraw(); # close window $win->{photo}->delete; # free photo memory $win->{iconPhoto}->delete if $win->{iconPhoto}; $win->destroy(); }, -accelerator => ''); # mouse and button bindings $win->bind('', sub { $menu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); $win->bind('', sub { $menu->invoke('last'); }); $win->bind('', sub { $menu->invoke('last'); }); $win->bind('', sub { $menu->invoke('last'); }); # invoke $win->{but} when the window is closed by the window manager (x-button) $win->protocol('WM_DELETE_WINDOW' => sub { $menu->invoke('last'); }); # key-desc,F03,toggle overlay information (EXIF, IPTC, ...) $win->bind('', sub { toggle(\$conf{show_micro_meta}{value}); show_text_on_canvas($win->{canvas}, get_meta_micro(@{$win->{pic_list}}[$win->{index}]));} ); # next picture keys foreach my $key ('', '', '', '') { $win->bind($key, sub { $win->{slideshow} = 0; next_prev_pic($win, 1); }); } # previous picture keys foreach my $key ('', '', '', '') { $win->bind($key, sub { $win->{slideshow} = 0; next_prev_pic($win, -1); }); } # 10 pictures for/back $win->bind('', sub { $win->{slideshow} = 0; next_prev_pic($win, 10); }); $win->bind('', sub { $win->{slideshow} = 0; next_prev_pic($win, -10); }); # keys End and Pos1 to jump to first and last pic $win->bind('', sub { $win->{slideshow} = 0; next_prev_pic($win, (scalar(@{$win->{pic_list}})-$win->{index}-1)); }); $win->bind('', sub { $win->{slideshow} = 0; next_prev_pic($win, -1*$win->{index}); }); # s - start / stop slideshow $win->bind('', sub { return if (scalar(@{$pic_list}) <= 1); toggle(\$win->{slideshow}); slideshow_pic($win); }); # faster / slower slideshow $win->bind('', sub { if ($win->{slideshow}) { $config{SlideShowTime}-- if ($config{SlideShowTime} >= 1); info_window($win, $config{SlideShowTime}." ".lang("second(s)")); log_it("slideshow time: ".$config{SlideShowTime}." sec"); } } ); # key-desc,+,zoom in or slideshow slower $win->bind('', sub { if ($win->{slideshow}) { $config{SlideShowTime}++ if ($config{SlideShowTime} < 300); info_window($win, $config{SlideShowTime}." ".lang("second(s)")); log_it("slideshow time: ".$config{SlideShowTime}." sec"); } }); # bind mousewheel if ($Tk::VERSION >= 804) { if ($^O eq 'MSWin32') { $win->bind('' => [ sub { if ($_[1] < 0) {next_prev_pic($win, 1);} else {next_prev_pic($win, -1);} }, Ev('D') ]); } else { $win->bind('<4>' => sub { next_prev_pic($win, -1) unless $Tk::strictMotif; }); $win->bind('<5>' => sub { next_prev_pic($win, 1) unless $Tk::strictMotif; }); } } # key-desc,F11,toggle fullscreen mode when displaying picture in own window $win->bind('', sub { fullscreen($win);}); $win->{canvas}->focusForce if (Exists($win->{canvas})); if (defined $start_slideshow and $start_slideshow == SHOW) { $win->{slideshow} = 1; slideshow_pic($win); } log_it(lang('Ready!')); } ############################################################## # slideshow_pic: slideshow all pics, to be used in sub show_multiple_pics() ############################################################## sub slideshow_pic { my $win = shift; return unless $win; my $last_time; info_window($win, lang("Slideshow Start")) if ($win->{slideshow}); until ($win->{slideshow} == 0) { return unless $win; if (!defined $last_time || Tk::timeofday()-$last_time > $config{SlideShowTime}) { next_prev_pic($win, 1); $last_time = Tk::timeofday(); } DoOneEvent(); # stay responsive last if (!$win->{slideshow}); } info_window($win, lang("Slideshow End")) if (not $win->{slideshow}); } ############################################################## # next_prev_pic: switch to next or previous pic in list, to be used in sub show_multiple_pics() ############################################################## sub next_prev_pic { my $win = shift; my $step = shift; # int: e.g. 1 for next pic or -1 for previous pic, but 10 or 100 are also valid return if ($step == 0); # no need to do anything my $total_pics = scalar @{$win->{pic_list}}; return if ($total_pics <= 1); return unless $win; return unless $win->{canvas}; $win->{canvas}->Busy; # we can't use $win here else the cursor won't change $win->{index} += $step; # underflow - go back to last pic $win->{index} = $total_pics - 1 if ($win->{index} < 0); # overflow - go back to first pic $win->{index} = 0 if ($win->{index} > $total_pics - 1); my $dpic = @{$win->{pic_list}}[$win->{index}]; my $pic = basename($dpic); $win->title(sprintf "(%d/%d) loading %s ...", ($win->{index}+1), $total_pics, $pic); $win->update(); my $zoomFactor; # store actual photo object to release memory later my $photo_last = $win->{photo}; my @photo_last_ids = $win->{canvas}->find('withtag', 'pic'); my $photo_last_id = $photo_last_ids[0]; my $rc = load_zoom_pic($dpic, \$win->{photo}, \$zoomFactor, $win->{canvas}->width, $win->{canvas}->height); # close window on error $win->{menu}->Invoke('last') unless ($rc); $win->title(sprintf "(%d/%d) %s %s", ($win->{index}+1), $total_pics, $pic, $zoomFactor);#, $slideshow); # update icon $win->iconname($pic); # insert new pic in canvas and center it my $x_to = 0; my $y_to = 0; $x_to = int(($win->{canvas}->width - $win->{photo}->width) /2); $y_to = int(($win->{canvas}->height - $win->{photo}->height)/2); my $x_from = $x_to; # forward animation from bottom to center my $y_from = $win->{canvas}->height; # backward animation from top to center $y_from = 0 - $win->{photo}->height if ($step < 0); # no animation -> place picture in target position $y_from = $y_to if (not $conf{animation}{value}); # load picture on canvas my $photo_id = $win->{canvas}->createImage($x_from, $y_from, -image => $win->{photo}, -tag => ['pic',$dpic], -anchor => 'nw'); # move picture if animation is selected if ($conf{animation}{value}) { move_items_on_canvas($win->{canvas}, $photo_id, $photo_last_id, $x_from, $y_from, $x_to, $y_to); } # remove last picture $win->{canvas}->delete($photo_last_id); # free memory of last photo object $photo_last->delete if (defined $photo_last); # show picture meta info (EXIF, IPTC, ...) show_text_on_canvas($win->{canvas}, get_meta_micro($dpic)); $win->{balloonmsg} = makeBalloonMsg($dpic); $win->{canvas}->Unbusy; return; } ############################################################## # move two items (works at least for images) synchonous # on a canvas from a start to an end position in some steps # the coordinates belong to the first item ############################################################## sub move_items_on_canvas { my $canvas = shift; my $item = shift; # ID of canvas item my $item2 = shift; # ID of canvas item my ($x_from, $y_from, $x_to, $y_to) = @_; my $steps = $conf{animation_steps}{value}; # animation in n steps my $x_step = ($x_from - $x_to)/$steps; my $y_step = ($y_from - $y_to)/$steps; $canvas->Busy; my $starttime = Tk::timeofday(); for my $step (1 .. $steps) { my ($x1, $y1) = $canvas->coords($item); $canvas->coords($item, $x1-$x_step, $y1-$y_step); my ($x2, $y2) = $canvas->coords($item2); $canvas->coords($item2, $x2-$x_step, $y2-$y_step); # hint: $canvas->move does not work here $canvas->update; } $canvas->Unbusy; my $duration = Tk::timeofday() - $starttime; print "duration = $duration\n" if $verbose; if ($duration < 0.7*$conf{animation_duration}{value} or $duration > 1.3*$conf{animation_duration}{value}) { print "adapting steps from $steps " if $verbose; $steps = round($conf{animation_duration}{value}/$duration*$steps) if ($duration > 0); # some safety borders $steps = 1 if ($steps < 1); $steps = 200 if ($steps > 200); print "to $steps\n" if $verbose; $conf{animation_steps}{value} = $steps; } return; } ############################################################## # load_zoom_pic - load and zoom a picture # returns 1 on success and 0 on failure ############################################################## sub load_zoom_pic { my $dpic = shift; my $photo = shift; # reference to photo object my $zoomFactor = shift; # reference to zoom factor (string) my $w = shift; # picture target width my $h = shift; # picture target height if (!-f $dpic) { #$top->messageBox(-icon => 'warning', -message => "load_zoom_pic: Error no file $dpic", # -title => 'Error', -type => 'OK'); log_it("Error no file: $dpic!"); return 0; } log_it("opening $dpic in new window ..."); $$photo = $top->Photo(-file => $dpic, -gamma => $config{Gamma}); if (!$$photo) { #$top->messageBox(-icon => 'warning', -message => "load_zoom_pic: Error no photo $dpic!", # -title => 'Error', -type => 'OK'); log_it("Error no photo: $dpic!"); return 0; } increasePicPopularity($dpic); if ($config{trackPopularity}) { updateOneRow($dpic, $picLB); # update popularity (viewed x times) info $picLB->update; } $$zoomFactor = autoZoom(\$$photo, $dpic, $w, $h); return 1; } ############################################################## # showThumbList - displays a list of thumbs on a scrollable pane ############################################################## sub showThumbList { my $thumbs = shift; # reference on an array containing pictures my $title = shift; # optinal window title unless (@$thumbs) { log_it("$title: no pictures"); return; } my $nr = @$thumbs; # total number log_it("displaying $nr thumbs in new window ..."); #stopWatchStart(); # open window my $win = $top->Toplevel(-bg => "black"); $win->withdraw; $win->title("$title - $nr pictures"); # set the icon $win->iconname("Pictures"); $win->iconimage($mapiviicon) if $mapiviicon; my $topFrame = $win->Frame()->pack(-fill => 'both'); my %tphotos; # local hash to store the thumbnail photo objects $topFrame->Button(-text => "Close", -command => sub { cleanUpAndClose($win, \%tphotos); })->pack(-side => 'left'); $win->{label} = "$nr pictures, 0 selected"; $topFrame->Label(-textvariable => \$win->{label})->pack(-side => 'left'); my $cols = 6; $cols = $nr if ($nr < $cols); my $maxrows = int($win->screenheight/($config{ThumbSize} + 20)); # todo for 10 pics there should be 2 rows but the window is not high enough my $rows = int($nr/$cols) + 1; $rows = $maxrows if ($rows > $maxrows); print "tiler: nr:$nr col:$cols row:$rows maxrows:$maxrows\n" if $verbose; my $tiler = $win->Scrolled("Tiler", -columns => $cols, -rows => $rows, -scrollbars => 'oe', )->pack(-fill => 'both', -expand => 1); # list of all the window objects of $tiler # special values are $a[$i]->{selected} a boolean value 1=selected 0=not selected # and $a[$i]->{dpic} the path and the name of the displayed picture my @a; # the context menu my $menu = $win->Menu(-title => "Menu"); ############# selection menu my $sel_menu = $menu->cascade(-label => "select ..."); $sel_menu->cget(-menu)->configure(-title => "Selection menu"); ############# select all $sel_menu->command(-label => "select all", -command => sub { foreach (@a) { $_->{selected} = 1; } my $sel = 0; foreach (@a) { $sel++ if $_->{selected}; } $win->{label} = "$nr pictures, $sel selected"; }); ############# select none $sel_menu->command(-label => "select none", -command => sub { foreach (@a) { $_->{selected} = 0; } my $sel = 0; foreach (@a) { $sel++ if $_->{selected}; } $win->{label} = "$nr pictures, $sel selected"; }); ############# invert selection $sel_menu->command(-label => "invert selection", -command => sub { foreach (@a) { toggle(\$_->{selected}); } my $sel = 0; foreach (@a) { $sel++ if $_->{selected}; } $win->{label} = "$nr pictures, $sel selected"; }); ############# list selection $sel_menu->command(-label => "list selection", -command => sub { my @sel = (); # get the selection foreach (@a) { push @sel, $_->{dpic} if $_->{selected}; } my $text = scalar @sel." pictures are selected:\n"; foreach (@sel) { $text .= "$_\n"; } showText("selected pictures", $text, NO_WAIT); }); $menu->separator; ############# open picture in main window # todo: check if open_pic_in_main() could be used here too $menu->command(-label => "open picture in main window", -accelerator => '', -command => sub { my @sel; # get the selection foreach (@a) { push @sel, $_->{dpic} if $_->{selected}; } return unless checkSelection($win, 1, 1, \@sel); my $dpic = $sel[0]; my $dir = dirname($dpic); my $pic = basename($dpic); if (!-d $dir) { $win->messageBox(-icon => 'warning', -message => "Sorry, but the folder\n$dir\nis not availabale at the moment.\nMaybe you should clean your database\nor insert a removable media.", -title => 'folder not found', -type => 'OK'); return; } $top->deiconify; $top->raise; $top->focus; openDirPost($dir) if ($dir ne $actdir); showPic($dpic); }); ############# add to collection (light table) $menu->command(-label => lang("Add to collection"), -command => sub { my @sel; # get the selection foreach (@a) { push @sel, $_->{dpic} if $_->{selected}; } return unless (@sel); light_table_add(\@sel); }); ############# copy selected $menu->command(-label => "copy selected ...", -command => sub { my @sel; # get the selection foreach (@a) { push @sel, $_->{dpic} if $_->{selected}; } return unless (@sel); my $targetdir = getDirDialog("Copy pictures to"); return unless (-d $targetdir); makeDir(dirname(getThumbFileName("$targetdir/dummy.jpg")), ASK); my $pw = progressWinInit($win, "copy pictures"); my $i = 0; my $overwrite = OVERWRITE; my $n = 0; # count successfull copied pictures foreach my $dpic (@sel) { last if progressWinCheck($pw); my $pic = basename($dpic); $i++; progressWinUpdate($pw, "copy picture ($i/".scalar @sel.") ...", $i, scalar @sel); my $tpic = "$targetdir/$pic"; # if the pic exists, ask if the user wants to overwrite it $overwrite = overwritePic($tpic, $dpic, (scalar(@sel) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); if (mycopy($dpic, $tpic, OVERWRITE)) { $n++; my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { mycopy($thumbpic, $thumbtpic, OVERWRITE) } } } # foreach - end progressWinEnd($pw); }); ############# show infos $menu->command(-label => "show picture info", -command => sub { my @sel; foreach (@a) { push @sel, $_->{dpic} if $_->{selected}; } return unless (@sel); return unless askSelection(\@sel, 10, "picture info"); foreach my $dpic (@sel) { my $info = makeBalloonMsg($dpic); showText($dpic, $info, NO_WAIT, getThumbFileName($dpic)); } }); ############# delete $menu->command(-label => "delete selected pictures to trash", -command => sub { delPicsToTrash($win, \@a, $thumbs, $title, \%tphotos); }, -accelerator => ''); $win->bind('', sub { delPicsToTrash($win, \@a, $thumbs, $title, \%tphotos); } ); # mouse and button bindings $win->bind('', sub { $menu->Popup(-popover => "cursor", -popanchor => "nw"); } ); my $i = 0; my $frame; my $pw = progressWinInit($picLB, "Show thumbnails"); foreach my $dpic (@$thumbs) { last if progressWinCheck($pw); progressWinUpdate($pw, "loading thumbnail (".($i+1)."/$nr) ...", ($i+1), $nr); #if ( $i % $cols == 1 or $cols == 1 ) { # start new table row (modulo) # $frame = $tiler->Frame()->pack(); #} my $thumbFile = getThumbFileName($dpic); $tphotos{$dpic} = $win->Photo(-file => $thumbFile, -gamma => $config{Gamma}) if (-f $thumbFile); if (! $tphotos{$dpic}) { #$top->messageBox(-icon => 'warning', -message => "showThumbList: Error no thumb for photo $dpic!", # -title => 'Error', -type => 'OK'); $tphotos{$dpic} = $mapivi_icons{'EmptyThumb'}; next unless $tphotos{$dpic}; } my $j = $i; # we need a local copy here $a[$i] = $tiler->Frame(-border => 1, -relief => "raised"); $a[$i]->{selected} = 0; $a[$i]->{dpic} = $dpic; my $check = $a[$i]->Checkbutton(-variable => \$a[$i]->{selected}, -border => 1, -padx => 0, -pady => 0, -command => sub { my $sel = 0; foreach (@a) { $sel++ if $_->{selected}; } $win->{label} = "$nr pictures, $sel selected"; },)->pack(-side => 'left', -expand => 0, -fill => "none", -anchor => "s", -padx => 0, -pady => 0); my $but = $a[$i]->Button(-image => $tphotos{$dpic}, -border => 0, -relief => 'flat', -padx => 0, -pady => 0, -command => sub { $check->Invoke if (Exists($check)); },)->pack(-side => 'left', -expand => 0, -fill => "none", -padx => 0, -pady => 0); $but->bind('', sub { showPicInOwnWin($dpic); }); my $msg; # the balloon message is generated on demand later, to speed up the loading of the thumbs $balloon->attach($but, -postcommand => sub { $msg = makeBalloonMsg($dpic); $msg .= "\n\nRight mouse button for context menu, middle mouse button to open picture";}, -balloonposition => "mouse", -msg => \$msg); $tiler->Manage($a[$i]); $i++; } progressWinEnd($pw); $win->bind('', sub { cleanUpAndClose($win, \%tphotos); }); $win->bind('', sub { cleanUpAndClose($win, \%tphotos); }); $win->deiconify; $win->raise; #stopWatchStop("showThumbList"); log_it(lang('Ready!')); } ############################################################## # cleanUpAndClose - for showThumbList ############################################################## sub cleanUpAndClose { my $win = shift; my $hashref = shift; $win->withdraw; foreach (keys %{$hashref}) { delete_photo_object($$hashref{$_}); } Tk->break; } ############################################################## # delPicsToTrash ############################################################## sub delPicsToTrash { my ($win, $a, $thumbs, $title, $tphotos) = @_; unless (defined $a) { warn "a undef"; return; } unless (ref($a) eq 'ARRAY') {warn "a is no array"; return; } unless (defined $thumbs) { warn "thumbs undef"; return; } unless (ref($thumbs) eq 'ARRAY') {warn "thumbs is no array"; return; } my @sel; my $deleted = 0; my $errors = ''; if (!-d $trashdir) { $win->messageBox(-icon => 'warning', -message => "Trashdir $trashdir not found!\nPlease create this dir (shell: mkdir $trashdir) and retry.\n\nAborting.", -title => "Delete pictures", -type => 'OK'); return; } # check if we are in the trash dir if ($actdir eq $trashdir) { $win->messageBox(-icon => 'warning', -message => "Please use to really remove files from the trash!", -title => "Delete pictures", -type => 'OK'); return; } makeDir("$trashdir/$thumbdirname", NO_ASK); foreach my $i (reverse 0 .. $#{$a}) { if ($$a[$i]->{selected}) { my $dpic = $$a[$i]->{dpic}; my $pic = basename($dpic); if (move($dpic, $trashdir)) { $deleted++; # count nr of successfull moves my $tpic = "$trashdir/$pic"; $searchDB{$tpic} = $searchDB{$dpic}; $searchDB{$tpic}{odir} = dirname($dpic); delete $searchDB{$dpic}; deleteCachedPics($dpic); my $thumb = getThumbFileName($dpic); if ((-d "$trashdir/$thumbdirname") and (-f $thumb)) { if (!move($thumb, "$trashdir/$thumbdirname")) { $errors .= "Could not move thumbnail \"$thumb\" to $trashdir/$thumbdirname: $!\n"; } } splice @$thumbs, $i, 1; # remove picture from list } else { $errors .= "Could not move picture \"$dpic\" to $trashdir: $!\n"; } } } # clean up and close window if ($errors ne '') { $errors = "These errors occured while deleting the selected pictures:\n$errors"; showText("Error while deleting", $errors, NO_WAIT); } log_it("deleted $deleted pictures"); # while it's not possible to remove objects from Tk::Tiler we need to close the # window and reload the function with the rest of the pictures cleanUpAndClose($win, $tphotos); # recursive call of this function showThumbList($thumbs, $title); } ############################################################## # makeBalloonMsg ############################################################## sub makeBalloonMsg { my $dpic = shift; return "$dpic\nis currently not available" if (!-f $dpic); my $linktarget = ''; my $pic = basename($dpic); my $dir = dirname($dpic); my $fsize = getFileSize($dpic, FORMAT); my $fdate = getFileDate($dpic, FORMAT); my ($w, $h) = getSize($dpic); my $exif = date_iso_to_relative(getShortEXIF($dpic, NO_WRAP)); if ($exif ne '') { $exif = formatString($exif, 80, -1); $exif = "\nEXIF: ".$exif; } my $iptc = getIPTC($dpic, SHORT); $iptc = formatString($iptc, 80, -1); # needed for many joined keywords if ($iptc ne '') { $iptc = "\n\n".$iptc; # if IPTC is not empty, add a little distance } my $comment = getComment($dpic, LONG); # show only the first 800 chars of the comment, else the balloon box is too full $comment = cutString($comment, 797, "..."); $comment = formatString($comment, 80, -1); if ($comment ne '') { $comment = "\n\n".$comment; # if comment is not empty, add a little distance } if (-l $dpic) { $linktarget = "\nLink: links to: ".readlink($dpic); } return "File: $pic\nDir: $dir\nSize: $fsize (${w}x$h)\nDate: $fdate $linktarget$exif$iptc$comment"; } ############################################################## ############################################################## sub options_edit { my @add_colors; push @add_colors, $config{ColorPicker}; configuration_edit($top, \%conf, \@conf_tab_order, undef, # apply-button callback sub{configuration_set_default()}, # reset options button callback \@add_colors, $conf{color_entry}{value}, $mapiviicon); } ############################################################## # options ############################################################## sub options { if (Exists($ow)) { $ow->deiconify; $ow->raise; return; } $ow = $top->Toplevel(); $ow->withdraw; $ow->title("Mapivi options"); $ow->iconname("Options"); $ow->iconimage($mapiviicon) if $mapiviicon; my $notebook = $ow->NoteBook(-width => 500, -background => $conf{color_bg}{value}, # background of active page (including its tab) -inactivebackground => $conf{color_entry}{value}, # tabs of inactive pages -backpagecolor => $conf{color_bg}{value}, # background behind notebook )->pack(-expand => 1, -fill => 'both', -padx => 5, -pady => 5); my $aF = $notebook->add('gen', -label => 'General'); my $bF = $notebook->add('thumbs', -label => 'Thumbnails'); my $cF = $notebook->add('view', -label => 'Window'); my $eF = $notebook->add('col', -label => 'Colors'); my $dF = $notebook->add('adv', -label => 'Advanced'); $notebook->raise($config{OptionsLastPad}); my %tmpconf = %{ dclone(\%config) }; my $w = 37; labeledEntry($aF,'top',20,"Copyright notice",\$tmpconf{Copyright}); my $langF = $aF->Frame()->pack(-anchor => 'w', -fill => 'x'); $langF->Label(-text => 'Language (needs restart) ')->pack(-side => 'left', -anchor => 'w'); my @lang = languages_find($lang_path); # add english to the top of the list unshift @lang, 'en'; $langF->Optionmenu(-variable => \$tmpconf{Language}, -options => \@lang)->pack(-side => 'left', -anchor => 'w'); my $sdbB = $aF->Checkbutton(-variable => \$tmpconf{SaveDatabase}, -text => "Store the search database to a file")->pack(-anchor => 'w'); $balloon->attach($sdbB, -msg => "If this is enabled all image meta information (Comments, EXIF, IPTC, file name) of all images visited will be stored into a database. The database can be used to search pictures. It is highly recommended to enable this option."); $aF->Checkbutton(-variable => \$tmpconf{ShowHiddenDirs}, -text => 'Show hidden folders (starting with a dot ".")')->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{AskGenerateThumb}, -text => "Ask before generating thumbnails")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{AskDeleteThumb}, -text => "Ask before deleting thumbnails")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{AskMakeDir}, -text => "Ask before making a folder (e.g. $thumbdirname)")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{WarnBeforeResize}, -text => "Warn me before using change size/quality")->pack(-anchor => 'w'); my $cfnjB = $aF->Checkbutton(-variable => \$tmpconf{CheckForNonJPEGs}, -text => "Check for non-JPEG pictures")->pack(-anchor => 'w'); $balloon->attach($cfnjB, -msg => "If this is enabled and there are some non-JPEGs Mapivi will ask the user if they should be converted to JPEGs. After the conversion the images can be displayed by Mapivi. The originals (non-JPEGs) may be left untouched or deleted."); $aF->Checkbutton(-variable => \$tmpconf{BitsPixel}, -text => "Calculate and show picture compression in bit per pixel")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{AspectRatio}, -text => "Calculate and show image aspect ratio (e.g. 4:3 or 3:2)")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{ShowFileDate}, -text => "Show file date in the size column")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{RenameBackup}, -text => "Rename backup file, if the file is renamed")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{WAV_file_operations}, -text => "WAV audio files follow picture file operations (copy, move, rename, delete *.wav file)")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{XMP_file_operations}, -text => "XMP sidecar files follow picture file operations (copy, move, rename, delete *.xmp file)")->pack(-anchor => 'w'); $aF->Checkbutton(-variable => \$tmpconf{RAW_file_operations}, -text => "RAW (nef, crw, cr2) files follow picture file operations (copy, move, rename, delete *.nef, *.crw or *.cr2 file)")->pack(-anchor => 'w'); my $trb = $aF->Checkbutton(-variable => \$tmpconf{jpegtranTrim}, -text => "use the -trim switch when doing lossless rotation")->pack(-anchor => 'w'); $balloon->attach($trb, -msg => "The rotation operates rather oddly if the image dimensions are not a multiple of the iMCU size (usually 8 or 16 pixels), because they can only transform complete blocks in the desired way. jpegtran's default behavior when transforming an odd-size image is designed to preserve exact reversibility and mathematical consistency of the transformation set. For practical use, you may prefer to discard any untransformable edge pixels using the -trim switch rather than having a strange-looking strip along the right and/or bottom edges of a transformed image."); $aF->Checkbutton(-variable => \$tmpconf{AskDeleteHighRating}, -text => "Ask before deleting pictures with a high rating (see also below)")->pack(-anchor => 'w'); my $aFadhr = labeledScale($aF, 'top', $w, "Ask before deleting with this rating", \$tmpconf{AskDeleteHighRatingLevel}, 1, 7, 1); $balloon->attach($aFadhr, -msg => 'Mapivi will ask before pictures with a high rating are deleted. The function may be activated using the checkbutton above. Please adjust the rating interval using this slider. Example: With a value of 3 Mapivi will ask for a confirmation before deleting pictures with a rating of 1, 2 or 3.'); my $aFcp = labeledScale($aF, 'top', $w, "Max number of cached pictures", \$tmpconf{MaxCachedPics}, 2, 10, 1); $balloon->attach($aFcp, -msg => "Mapivi is able to cache some pictures.\nCached pictures can be displayed very fast, but eat up memory."); my $aFtp = labeledScale($aF, 'top', $w, "Number of displayed thumbnails", \$tmpconf{ThumbMaxLimit}, 10, 10000, 10); $balloon->attach($aFtp, -msg => "If more pictures than this limit have to be displayed\nMapivi will ask how to proceed."); my $aFst = labeledScale($aF, 'top', $w, "Maximum size of trash (MB)", \$tmpconf{MaxTrashSize}, 1, 1000, 100); $balloon->attach($aFst, -msg => "The trash size is not really limited, but there will be a warning, when this limit is reached."); labeledScale($aF, 'top', $w, "Slideshow pause time (sec)", \$tmpconf{SlideShowTime}, 1, 300, 1); # ############### Thumbnail notepad ######################## my $abF = $bF->Frame()->pack(-fill => 'x', -expand => 0); my $a1bF = $abF->Frame()->pack(-side => 'left', -fill => 'x', -expand => 0); my $a2bF = $abF->Frame()->pack(-side => 'left', -fill => 'x', -expand => 0); my $bFuet = $a1bF->Checkbutton(-variable => \$tmpconf{UseEXIFThumb}, -text => "Use EXIF thumbnails where available")->pack(-anchor => 'w'); $balloon->attach($bFuet, -msg => "Use the EXIF thumbnails where availabe,\nif not available a thumbnail is generated from the picture\n(very fast, but may not reflect a post processed picture)."); $a1bF->Checkbutton(-variable => \$tmpconf{RotateThumb}, -text => "Rotate EXIF thumbnail when rotating picture")->pack(-anchor => 'w'); my $example; my $previewB; if (-f $thumbExample) { $example = $top->Photo(-file => $thumbExample, -gamma => $config{Gamma}); if ($example) { $a2bF->Label(-text => 'Click here for a preview')->pack(); $previewB = $a2bF->Button(-image => $example, -bd => $config{Borderwidth}, -command => sub { my $thumb = "$trashdir/thumbExample.jpg"; my $prefix = thumbnail_prefix(\%tmpconf); my $com = thumbnail_postfix($prefix, $thumbExample, $thumb); execute($com); if (-f $thumb) { my $prev = $top->Photo(-file => $thumb, -gamma => $config{Gamma}); $previewB->configure(-image => $prev) if $prev; } })->pack(); $balloon->attach($previewB, -msg => "Press here to update the thumbnail\nwith the choosen options"); } } $previewB->Invoke if (Exists($previewB)); my $bFstp = labeledScale($bF, 'top', $w, "Size (pixel)", \$tmpconf{ThumbSize}, 10, 200, 1); $balloon->attach($bFstp, -msg => "This is the length and the heigt of the thumbnail.\nWith a value of e.g. 100 you will get a 100x100 thumbnail."); my $bFqt = labeledScale($bF, 'top', $w, lang("Quality (%)"), \$tmpconf{ThumbQuality}, 30, 100, 5); qualityBalloon($bFqt); #my $zF = $bF->Frame()->pack(-fill => 'x', -expand => 1); my $zshS = labeledScale($bF, 'top', $w, "Sharpness (radius)", \$tmpconf{ThumbSharpen}, 0, 40, 0.1); $balloon->attach($zshS, -msg => "The higher the value, the slower the conversion\n(suggestion: between 0 and 4)"); my $bFbs = labeledScale($bF, 'top', $w, "Frame size (pixel)", \$tmpconf{ThumbBorder}, 0, 50, 1); $balloon->attach($bFbs, -msg => "Set the thumbnail frame size."); $bF->Checkbutton(-variable => \$tmpconf{UseThumbShadow}, -text => "Add a shadow")->pack(-anchor => 'w'); my $bFbgc = labeledEntryColor($bF,'top',$w,"Thumbnail frame color",'Set',\$tmpconf{ColorThumbBG}); $balloon->attach($bFbgc, -msg => "Set the thumbnail frame color."); my $bFnob = labeledScale($bF, 'top', 42, "Number of processes generating thumbnails", \$tmpconf{MaxProcs}, 1, 10, 1); $balloon->attach($bFnob, -msg => "Mapivi will generate the thumbnails in the background.\nChoose the maximum number of parallel executed processes.\nNumbers greater than one or two may only be appropriate on a muliprocessor plattform."); # ############### window notepad ######################## #$cF->Checkbutton(-variable => \$tmpconf{ShowClock}, # -text => "Display a clock in the status bar")->pack(-anchor => 'w'); $cF->Checkbutton(-variable => \$tmpconf{ShowMenu}, -text => "Show menu bar")->pack(-anchor => 'w'); $cF->Checkbutton(-variable => \$tmpconf{ShowInfoFrame}, -text => "Show status bar on the upper side")->pack(-anchor => 'w'); $cF->Checkbutton(-variable => \$tmpconf{ShowNavFrame}, -text => "Show navigation frame on the left side")->pack(-anchor => 'w'); $cF->Checkbutton(-variable => \$tmpconf{ShowThumbFrame}, -text => "Show thumbnail list")->pack(-anchor => 'w'); $cF->Checkbutton(-variable => \$tmpconf{ShowPicFrame}, -text => "Show picture frame on the right side")->pack(-anchor => 'w'); my $aFc = $cF->Checkbutton(-variable => \$tmpconf{ShowCommentField}, -text => "Display comment info in picture view")->pack(-anchor => 'w'); $balloon->attach($aFc, -msg => "show/hide the textfield containing the picture comments\nand the buttons to add, edit and remove a comment.\nThis field is usually located above the actual picture"); my $aFic = $cF->Checkbutton(-variable => \$tmpconf{ShowIPTCFrame}, -text => "Display IPTC box in picture view")->pack(-anchor => 'w'); $balloon->attach($aFic, -msg => "show/hide the box containing the picture IPTC headline and caption\nand a button to store it.\nThis field is usually located above the actual picture"); my $aFp = $cF->Checkbutton(-variable => \$tmpconf{ShowPicInfo}, -text => "Show picture info as a balloon on the actual picture")->pack(-anchor => 'w'); $balloon->attach($aFp, -msg => "if this is enabled and you move and hold your mouse pointer\nover the actual picture (right frame of the main window)\na balloon info box (with EXIF, comment, size, ...) will appear"); my $aIc = $cF->Checkbutton(-variable => \$tmpconf{ShowInfoInCanvas}, -text => "Overlay picture with picture info (EXIF, IPTC, ...)")->pack(-anchor => 'w'); $balloon->attach($aIc, -msg => "show/hide picture infos on the picture itself"); #$cF->Checkbutton(-variable => \$tmpconf{ShowCoordinates}, # -text => "Display the coordinates of the mouse cursor in the status bar")->pack(-anchor => 'w'); my $fontF = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($fontF, -msg => "Font for the main window and nearly all dialogs.\nIt's recommeded to choose a fixed font."); my $fontL = $fontF->Label(-text => "Font family: ", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $fontF->Label(-textvariable => \$tmpconf{FontFamily}, -bg => $conf{color_bg}{value})->pack(-side => 'left'); $fontF->Button(-text => 'Set', -command => sub { my $font = $tmpconf{FontFamily}; my $rc = myFontDialog($ow, 'Select font family', \$font, $tmpconf{FontSize}); return unless $rc; $tmpconf{FontFamily} = $font; $ow->Busy; my $font2 = $top->Font(-family => $tmpconf{FontFamily}, -size => $tmpconf{FontSize}); $fontL->configure(-font => $font2); $fontL->update(); $ow->Unbusy; })->pack(-side => 'left'); $fontF->Label(-text => " Font size: ", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $fontF->Scale( -variable => \$tmpconf{FontSize}, -from => 5, -to => 20, -resolution => 1, -sliderlength => 30, -orient => 'horizontal', -showvalue => 0, -width => 15, -bd => $config{Borderwidth}, -command => sub { $ow->Busy; my $font = $top->Font(-family => $tmpconf{FontFamily}, -size => $tmpconf{FontSize}); $fontL->configure(-font => $font); $fontL->update(); $ow->Unbusy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $fontF->Label(-textvariable => \$tmpconf{FontSize})->pack(-side => 'left'); my $propFontF = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($propFontF, -msg => "Please choose a propotional font here which is available in different sizes.\nIt will be used in the keyword browser (tag cloud)."); my $propFontL = $propFontF->Label(-text => "Proportional font family: ", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $propFontF->Label(-textvariable => \$tmpconf{PropFontFamily}, -bg => $conf{color_bg}{value})->pack(-side => 'left'); $propFontF->Button(-text => 'Set', -command => sub { my $font = $tmpconf{PropFontFamily}; my $rc = myFontDialog($ow, 'Select font family', \$font, $tmpconf{PropFontSize}); return unless $rc; $tmpconf{PropFontFamily} = $font; $ow->Busy; my $font2 = $top->Font(-family => $tmpconf{PropFontFamily}, -size => $tmpconf{PropFontSize}); $propFontL->configure(-font => $font2); $propFontL->update(); $ow->Unbusy; })->pack(-side => 'left'); $propFontF->Label(-text => " Font size: ", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $propFontF->Scale( -variable => \$tmpconf{PropFontSize}, -from => 5, -to => 30, -resolution => 1, -sliderlength => 30, -orient => 'horizontal', -showvalue => 0, -width => 15, -bd => $config{Borderwidth}, -command => sub { $ow->Busy; my $font = $top->Font(-family => $tmpconf{PropFontFamily}, -size => $tmpconf{PropFontSize}); $propFontL->configure(-font => $font); $propFontL->update(); $ow->Unbusy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $propFontF->Label(-textvariable => \$tmpconf{PropFontSize})->pack(-side => 'left'); my $tfontF = $cF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $tfontL = $tfontF->Label(-text => "Thumbnail font size:", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $tfontF->Scale( -variable => \$tmpconf{ThumbCaptFontSize}, -from => 5, -to => 20, -resolution => 1, -sliderlength => 30, -orient => 'horizontal', -showvalue => 0, -width => 15, -bd => $config{Borderwidth}, -command => sub { $ow->Busy; my $font = $top->Font(-family => $tmpconf{FontFamily}, -size => $tmpconf{ThumbCaptFontSize}); $tfontL->configure(-font => $font); $tfontL->update(); $ow->Unbusy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $tfontF->Label(-textvariable => \$tmpconf{ThumbCaptFontSize})->pack(-side => 'left'); # ############### color notepad ######################## $w = 36; $eF->Label(-text => 'Please restart Mapivi to see all color changes')->pack(); my $presets = $eF->Frame()->pack(-fill =>'x', -padx => 0, -pady => 0); $presets->Label(-text => 'Presets')->pack(-side => 'left', -anchor => 'w'); $presets->Button(-text => 'bright', -command => sub { $tmpconf{ColorBG} = "#efefef"; $tmpconf{ColorFG} = "black"; $tmpconf{ColorMenuBG} = "LightGoldenrod2"; $tmpconf{ColorMenuFG} = "black"; $tmpconf{ColorBG2} = "#e5e5e5"; $tmpconf{ColorBGCanvas} = "#efefef"; $tmpconf{ColorHlBG} = "#eeeeee"; $tmpconf{ColorActBG} = "LightGoldenrod1"; $tmpconf{ColorEntry} = "gray90"; $tmpconf{ColorSel} = "LightGoldenrod2"; $tmpconf{ColorSelBut} = "red3"; $tmpconf{ColorSelFG} = "black"; $tmpconf{ColorName} = "black"; $tmpconf{ColorComm} = "black"; $tmpconf{ColorIPTC} = "black"; $tmpconf{ColorEXIF} = "black"; $tmpconf{ColorFile} = "black"; $tmpconf{ColorDir} = "black"; $tmpconf{ColorThumbBG} = "azure3"; })->pack(-side => 'left'); $presets->Button(-text => 'white/yellow', -command => sub { $tmpconf{ColorBG} = "white"; $tmpconf{ColorFG} = "black"; $tmpconf{ColorMenuBG} = "LightGoldenrod3"; $tmpconf{ColorMenuFG} = "black"; $tmpconf{ColorBG2} = "#fff9d8"; $tmpconf{ColorBGCanvas} = "white"; $tmpconf{ColorHlBG} = "white"; $tmpconf{ColorActBG} = "LightGoldenrod1"; $tmpconf{ColorEntry} = "gray90"; $tmpconf{ColorSel} = "LightGoldenrod2"; $tmpconf{ColorSelBut} = "red3"; $tmpconf{ColorSelFG} = "black"; $tmpconf{ColorName} = "black"; $tmpconf{ColorComm} = "black"; $tmpconf{ColorIPTC} = "black"; $tmpconf{ColorEXIF} = "black"; $tmpconf{ColorFile} = "black"; $tmpconf{ColorDir} = "black"; $tmpconf{ColorThumbBG} = "LightGoldenrod1"; })->pack(-side => 'left'); $presets->Button(-text => 'blue', -command => sub { $tmpconf{ColorBG} = "SlateGray1"; $tmpconf{ColorFG} = "black"; $tmpconf{ColorMenuBG} = "SlateGray3"; $tmpconf{ColorMenuFG} = "black"; $tmpconf{ColorBG2} = "SlateGray2"; $tmpconf{ColorBGCanvas} = "SlateGray1"; $tmpconf{ColorHlBG} = "#e3f6ff"; $tmpconf{ColorActBG} = "DeepSkyBlue1"; $tmpconf{ColorEntry} = "SlateGray1"; $tmpconf{ColorSel} = "DeepSkyBlue1"; $tmpconf{ColorSelBut} = "red3"; $tmpconf{ColorSelFG} = "black"; $tmpconf{ColorName} = "black"; $tmpconf{ColorComm} = "black"; $tmpconf{ColorIPTC} = "black"; $tmpconf{ColorEXIF} = "black"; $tmpconf{ColorFile} = "black"; $tmpconf{ColorDir} = "black"; $tmpconf{ColorThumbBG} = "SlateGray3"; })->pack(-side => 'left'); $presets->Button(-text => 'bright/blue', -command => sub { $tmpconf{ColorBG} = "#efefef"; $tmpconf{ColorFG} = "black"; $tmpconf{ColorMenuBG} = "gray40"; $tmpconf{ColorMenuFG} = "white"; $tmpconf{ColorBG2} = "#e5e5e5"; $tmpconf{ColorBGCanvas} = "#efefef"; $tmpconf{ColorHlBG} = "#eeeeee"; $tmpconf{ColorActBG} = "#9fb6cd"; $tmpconf{ColorEntry} = "gray90"; $tmpconf{ColorSel} = "#9fb6cd"; $tmpconf{ColorSelBut} = "red3"; $tmpconf{ColorSelFG} = "black"; $tmpconf{ColorName} = "black"; $tmpconf{ColorComm} = "black"; $tmpconf{ColorIPTC} = "black"; $tmpconf{ColorEXIF} = "black"; $tmpconf{ColorSize} = "black"; $tmpconf{ColorDir} = "black"; $tmpconf{ColorThumbBG} = "gray85"; })->pack(-side => 'left'); $presets->Button(-text => 'gray', -command => sub { $tmpconf{ColorBG} = "#aeaeae"; $tmpconf{ColorFG} = "black"; $tmpconf{ColorMenuBG} = "#aaa"; $tmpconf{ColorMenuFG} = "black"; $tmpconf{ColorBG2} = "#c8c8c8"; $tmpconf{ColorBGCanvas} = "#222"; $tmpconf{ColorHlBG} = "#a1a1a1"; $tmpconf{ColorActBG} = "#ae6666"; $tmpconf{ColorEntry} = "#ccc"; $tmpconf{ColorSel} = "#9fb6cd"; $tmpconf{ColorSelBut} = "red3"; $tmpconf{ColorSelFG} = "#000"; $tmpconf{ColorName} = "#000060"; $tmpconf{ColorComm} = "#600000"; $tmpconf{ColorIPTC} = "#404000"; $tmpconf{ColorEXIF} = "#006000"; $tmpconf{ColorFile} = "#004040"; $tmpconf{ColorDir} = "#000060"; $tmpconf{ColorThumbBG} = "#ccc"; })->pack(-side => 'left'); $presets->Button(-text => 'dark room', -command => sub { $tmpconf{ColorBG} = "gray30"; $tmpconf{ColorFG} = "gray85"; $tmpconf{ColorMenuBG} = "gray40"; $tmpconf{ColorMenuFG} = "gray90"; $tmpconf{ColorBG2} = "gray30"; $tmpconf{ColorBGCanvas} = "gray30"; $tmpconf{ColorHlBG} = "gray60"; $tmpconf{ColorActBG} = "gray60"; $tmpconf{ColorEntry} = "gray60"; $tmpconf{ColorSel} = "gray40"; $tmpconf{ColorSelBut} = "red4"; $tmpconf{ColorSelFG} = "gray85"; $tmpconf{ColorName} = "gray85"; $tmpconf{ColorComm} = "gray85"; $tmpconf{ColorIPTC} = "gray85"; $tmpconf{ColorEXIF} = "gray85"; $tmpconf{ColorFile} = "gray85"; $tmpconf{ColorDir} = "gray85"; $tmpconf{ColorThumbBG} = "gray60"; })->pack(-side => 'left'); #labeledEntryColor($eF,'top',$w,"Background color: window",'Set',\$tmpconf{ColorBG}); #labeledEntryColor($eF,'top',$w,"Background color: menu",'Set',\$tmpconf{ColorMenuBG}); #labeledEntryColor($eF,'top',$w,"Background color: thumbnail table",'Set',\$tmpconf{ColorBG2}); #labeledEntryColor($eF,'top',$w,"Background color: picture",'Set',\$tmpconf{ColorBGCanvas}); #labeledEntryColor($eF,'top',$w,"Background color: highlight",'Set',\$tmpconf{ColorHlBG}); #labeledEntryColor($eF,'top',$w,"Background color: active",'Set',\$tmpconf{ColorActBG}); #labeledEntryColor($eF,'top',$w,"Background color: entry fields",'Set',\$tmpconf{ColorEntry}); labeledEntryColor($eF,'top',$w,"Background color: selections",'Set',\$tmpconf{ColorSel}); labeledEntryColor($eF,'top',$w,"Background color: selected button",'Set',\$tmpconf{ColorSelBut}); labeledEntryColor($eF,'top',$w,"Foreground color: selections",'Set',\$tmpconf{ColorSelFG}); labeledEntryColor($eF,'top',$w,"Foreground color: progress bar",'Set',\$tmpconf{ColorProgress}); labeledEntryColor($eF,'top',$w,"Font color: keyword cloud",'Set',\$tmpconf{ColorCloud}); #labeledEntryColor($eF,'top',$w,"Font color",'Set',\$tmpconf{ColorFG}); #labeledEntryColor($eF,'top',$w,"Font color: menu",'Set',\$tmpconf{ColorMenuFG}); labeledEntryColor($eF,'top',$w,"Font color: name",'Set',\$tmpconf{ColorName}); labeledEntryColor($eF,'top',$w,"Font color: comment",'Set',\$tmpconf{ColorComm}); labeledEntryColor($eF,'top',$w,"Font color: IPTC",'Set',\$tmpconf{ColorIPTC}); labeledEntryColor($eF,'top',$w,"Font color: EXIF",'Set',\$tmpconf{ColorEXIF}); labeledEntryColor($eF,'top',$w,"Font color: size",'Set',\$tmpconf{ColorFile}); labeledEntryColor($eF,'top',$w,"Font color: folder",'Set',\$tmpconf{ColorDir}); # ############### advanced notepad ######################## $w = 37; $dF->Checkbutton(-variable => \$verbose, -text => "verbose: print some debug info to STDOUT")->pack(-anchor => 'w'); my $trackB = $dF->Checkbutton(-variable => \$tmpconf{trackPopularity}, -text => "Track popularity of pictures (how often viewed in Mapivi)")->pack(-anchor => 'w'); $balloon->attach($trackB, -msg => "If this is enabled Mapivi will increase a counter\neverytime a picture is viewed with Mapivi.\nThe counter value is not saved in the picture\njust in the Mapivi database."); $dF->Checkbutton(-variable => \$tmpconf{CheckForLinks}, -text => "Check if a file is a link before processing it")->pack(-anchor => 'w'); #my $addMapB = #$dF->Checkbutton(-variable => \$tmpconf{AddMapiviComment}, # -text => "add a comment to pictures created/processed by mapivi")->pack(-anchor => 'w'); #$balloon->attach($addMapB, -msg => "If this is enabled Mapivi will add a JPEG comment\nto each created or processed picture."); $dF->Checkbutton(-variable => \$tmpconf{EXIFshowApp}, -text => "show App*-Info and MakerNotes and ColorComponents in EXIF info")->pack(-anchor => 'w'); my $ctcb = $dF->Checkbutton(-variable => \$tmpconf{CenterThumb}, -text => "center selected thumbnail")->pack(-anchor => 'w'); $balloon->attach($ctcb, -msg => "center the selected thumbnail,\nto show at least the next\nand the previous thumbnail"); $dF->Checkbutton(-variable => \$tmpconf{BeepWhenLooping}, -text => "play a beep sound when jumping to the first e.g. last picture")->pack(-anchor => 'w'); my $ctdb = $dF->Checkbutton(-variable => \$tmpconf{CentralThumbDB}, -text => "Store all thumbnails in a central place")->pack(-anchor => 'w'); $balloon->attach($ctdb, -msg => "If this is enabled all thumbnails will be\nstored in a central place ($thumbDB),\nif disabled thumbnails will be stored\ndecentral in sub folders (.thumbs)."); my $tbb = $dF->Checkbutton(-variable => \$tmpconf{ToggleBorder}, -text => "Remove the window border in fullscreen mode (experimental)")->pack(-anchor => 'w'); $balloon->attach($tbb, -msg => "Enable a real fullscreen mode,\nbut may not work as expected on all\noperating systems and window managers.\nTry it, switch to fullscreen (key: F11),\nif it works it's fine, if not just disable it again."); my $fblfb = $dF->Checkbutton(-variable => \$tmpconf{SlowButMoreFeatures}, -text => "enable some time intensive features (needs restart)")->pack(-anchor => 'w'); $balloon->attach($fblfb, -msg => "If this is selected, you will get e.g. some\nmore zoom levels.\nThis may slow down Mapivi a bit, so this option\nis only recommended for faster computers."); $dF->Checkbutton(-variable => \$tmpconf{CheckNewKeywords}, -text => "Check for new keywords and ask to add them to my catalog")->pack(-anchor => 'w'); $dF->Checkbutton(-variable => \$tmpconf{UrgencyChangeWarning}, -text => "Show a warning when a rating/urgency has been changed")->pack(-anchor => 'w'); $dF->Checkbutton(-variable => \$tmpconf{AutoImport}, -text => "Start import wizard at Mapivi startup if source folder is available")->pack(-anchor => 'w'); $dF->Checkbutton(-variable => \$tmpconf{SelectLastPic}, -text => "Select last shown picture after Mapivi startup")->pack(-anchor => 'w'); my $opfb = $dF->Checkbutton(-variable => \$tmpconf{supportOtherPictureFormats}, -text => "Show also other picture formats than just JPEG. Danger! (experimental feature)")->pack(-anchor => 'w'); $balloon->attach($opfb, -msg => "If this is selected, Mapivi will also show other picture formats (e.g. GIF and PNG),\nat least as thumbnails. But adding IPTC and other meta information is not possible.\nThis feature is not tested, so use it on your own risk."); my $aspS = labeledScale($dF, 'top', $w, "Delta factor for aspect ratio (%)", \$tmpconf{AspectSloppyFactor}, 0, 5, 0.1); $balloon->attach($aspS, -msg => "Adjust the accuracy of the aspect ratio display (rightmost column size).\nThis is the delta factor in percent when calculating the aspect ratio.\nFor example a picture with size 304x200 will still be displayed as a 3:2 picture,\nif the factor is equal or bigger than 1.4%.\nUse 0.0% if you need really exact values.\n3.0% is acceptable for me."); labeledScale($dF, 'top', $w, "preview size in filter dialog (pixel)", \$tmpconf{FilterPrevSize}, 50, 500, 5); labeledScale($dF, 'top', $w, "Comment text box height (lines)", \$tmpconf{CommentHeight}, 1, 50, 1); labeledScale($dF, 'top', $w, "Gamma value, when displaying pictures", \$tmpconf{Gamma}, 0.1, 10.0, 0.01); labeledScale($dF, 'top', $w, "Maximum number of lines of a IPTC info/comment", \$tmpconf{LineLimit}, 1, 20, 1); labeledScale($dF, 'top', $w, "Maximum length of a comment line", \$tmpconf{LineLength}, 5, 80, 1); # ############### button frame ######################## my $butF = $ow->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $butF->Button(-text => lang('OK'), -command => sub { %config = %{ dclone(\%tmpconf) }; applyConfig(); $example->delete if $example; $config{OptionsLastPad} = $notebook->raised(); $ow->destroy(); } )->pack(-side=>'left', -expand => 1, -fill =>'x'); # bind ctrl-x to OK button $ow->bind('', sub { $OKB->Invoke; }); $butF->Button(-text => lang('Apply'), -command => sub { %config = %{ dclone(\%tmpconf) }; $previewB->Invoke() if (Exists($previewB)); applyConfig(); } )->pack(-side=>'left', -expand => 1, -fill =>'x'); my $Xbut = $butF->Button(-text => lang('Cancel'), -command => sub { $example->delete if $example; $config{OptionsLastPad} = $notebook->raised(); $ow->destroy(); } )->pack(-side=>'left', -expand => 1, -fill =>'x'); bind_exit_keys_to_button($ow, $Xbut); $ow->Popup; } ############################################################## # applyConfig ############################################################## sub applyConfig { language_load($config{Language}); $progressBar->configure(-blocks => $config{MaxProcs}, -to => $config{MaxProcs}); $dirtree->configure(-showhidden => $config{ShowHiddenDirs}); $comS->configure( -foreground=>$config{ColorComm}, -background=>$conf{color_bg2}{value}); $iptcS->configure(-foreground=>$config{ColorIPTC}, -background=>$conf{color_bg}{value}); $exifS->configure(-foreground=>$config{ColorEXIF}, -background=>$conf{color_bg2}{value}); $fileS->configure(-foreground=>$config{ColorFile}, -background=>$conf{color_bg}{value}); $dirS->configure( -foreground=>$config{ColorDir}, -background=>$conf{color_bg2}{value}); toggleHeaders(); $top->optionAdd('*selectBackground', $config{ColorSel}, 'userDefault'); $picLB->configure(-selectbackground => $config{ColorSel}); # undocumented feature, but does not work (it stops the execution of the sub) # $top->RecolorTree(-background => $conf{color_bg}{value}); # we don't try to color everything, just a few widgets to give a visual feedback $top->configure (-bg => $conf{color_bg}{value}); $dirtree->configure(-bg => $conf{color_bg}{value}, -selectbackground => $config{ColorSel}); $c->configure (-bg => $conf{color_bg_canvas}{value}); $menubar->configure(-bg => $conf{color_bg}{value}); my @wlist = $top->children; foreach my $widget (@wlist) { my $ref = ref($widget); if ($ref eq "Tk::Frame" or $ref eq "Tk::Menu") { $widget->configure(-bg => $conf{color_bg}{value}); } } # don't know if this is very appropriate $top->optionAdd('*selectBackground', $config{ColorSel}, 'userDefault'); $top->optionAdd("*highlightColor", $config{ColorSel}, 'userDefault'); $top->optionAdd("*highlightBackground", $conf{color_hl_bg}{value}, 'userDefault'); $top->optionAdd("*background", $conf{color_bg}{value}, 'userDefault'); $top->optionAdd("*activeBackground", $conf{color_act_bg}{value}, 'userDefault'); # change font my $font = $top->Font(-family => $config{FontFamily}, -size => $config{FontSize}, ); $top->optionAdd("*font", $font, 'userDefault'); $top->Walk( sub { print "changing widget font ",ref($_[0])," to $font\n" if $verbose; eval { $_[0]->configure(-font => $font); } }); showHideFrames(); $top->update; setAdjusterPos(); startStopClock(); } ############################################################## # showHideFrames - pack or packForget the EXIF and Comment # frame ############################################################## sub showHideFrames { # the pack command seems only to work, if we packforget all # following widgets # so we always remove them all - from the inner to the outer ones # and pack them again according to the actual settings foreach ($c, $iptcF, $comF, $mainF, $thumbA, $thumbF, $dirA, $nav_F, $subF, $infoF) { $_->packForget if ($_->ismapped); } if ($config{ShowMenu}) { $top->configure(-menu => $menubar); } else { $top->configure(-menu => ''); } if ($config{ShowInfoFrame}) { $infoF->pack(-side => 'top', -anchor=>'w', -padx => 0, -pady => 0, -fill => 'x', -expand => 0); } $subF->pack(-side => 'top', -anchor=>'w', -padx => 0, -pady => 0, -fill => 'both', -expand => 1); if ($config{ShowNavFrame}) { $nav_F->pack(-side => 'left', -anchor=>'w', -padx => 0, -pady => 0, -expand => 1, -fill => 'both'); $dirA->packAfter($nav_F, -side => 'left', -padx => 3) if (($config{ShowThumbFrame}) or ($config{ShowPicFrame})); } if ($config{ShowThumbFrame}) { $thumbF->pack(-side => 'left', -anchor=>'w', -padx => 0, -pady => 0, -expand => 1, -fill => 'both'); } if ($config{ShowPicFrame}) { $thumbA->packAfter($thumbF, -side => 'left', -padx => 3) if ($config{ShowThumbFrame}) ; $mainF->pack(-side => 'left', -anchor=>'w', -padx => 0, -pady => 0, -ipadx => 0, -ipady => 0); } if ($config{ShowCommentField}) { $comF->pack(-fill => 'x',-expand => 1, -anchor=>'w', -padx => 0, -pady => 0) ; } if ($config{ShowIPTCFrame}) { $iptcF->pack(-fill => 'x',-expand => 1, -anchor=>'w', -padx => 0, -pady => 0) ; update_IPTC_frame_content($actpic); } $c->pack(-expand => 1, -fill => 'both', -padx => 0, -pady => 0, -ipadx => 0, -ipady => 0); } ############################################################## # buttonComment ############################################################## sub buttonComment { my $widget = shift; my $side = shift; my $but = $widget->Checkbutton(-variable => \$conf{add_tool_info}{value}, -anchor => 'w', -text => lang('Add comment') )->pack(-side => $side, -anchor => 'w', -padx => 3, -pady => 3); #$balloon->attach($but, -msg => lang("Add a comment to pictures created or processed with Mapivi")); $balloon->attach($but, -msg => $conf{add_tool_info}{info}); } ############################################################## # buttonBackup ############################################################## sub buttonBackup { my $widget = shift; my $side = shift; my $but = $widget->Checkbutton(-variable => \$config{MakeBackup}, -anchor => 'w', -text => lang('Create backup') )->pack(-side => $side, -anchor => 'w', -padx => 3, -pady => 3); $balloon->attach($but, -msg => lang("Create a backup of the original picture in the same folder named name-bak.jpg")); } ############################################################## # labeledEntryButton - build a frame containing a labeled entry # and a button with a file selector ############################################################## sub labeledEntryButton { # input values my ($parentWidget, $position, $width, $label, $buttext, $varRef, $dir) = @_; my $frame = labeledEntry($parentWidget, $position, $width, $label, $varRef); setFileButton($frame,'right',$buttext,$label,$varRef, $dir); return $frame; } ############################################################## # labeledEntryColor - build a frame containing a labeled entry # and a button with a color selector ############################################################## sub labeledEntryColor { # input values my ($parentWidget, $position, $width, $label, $buttext, $varRef) = @_; my $frame = labeledEntry($parentWidget, $position, $width, $label, $varRef); $frame->{button} = setColorButton($frame,'right',$buttext,$varRef); return $frame; } ############################################################## # labeledEntry - build a frame containing a labeled entry # for backward compability ############################################################## sub labeledEntry { # input values my ($parentWidget, $position, $width, $label, $varRef, $width2) = @_; labeledEntryFlex($parentWidget, $position, $width, $label, $varRef, 'left', $width2); } ############################################################## # labeledEntryFlex - build a frame containing a labeled entry ############################################################## sub labeledEntryFlex { # input values my ($parentWidget, $position, $width, $label, $varRef, $int_pos, $width2) = @_; # $width2 is optional and the width of the entry field, defaults to the first width $width2 = $width unless defined $width2; my $frame = $parentWidget->Frame()->pack(-side => $position, -expand => 0, -fill => 'x', -padx => 0, -pady => 3); $frame->Label(-text => $label, -width => $width, -anchor => 'w', )->pack(-side => $int_pos, -padx => 3, -fill => 'x'); if (MatchEntryAvail) { # set the choice list to an empty list, if it's undefined $entryHistory{$label} = [] unless (defined $entryHistory{$label}); $frame->{entry} = $frame->MatchEntry(-textvariable => $varRef, -choices => $entryHistory{$label}, -ignorecase => 0, -maxheight => 20, # add the new value to the list when enter or tab is pressed -entercmd => sub { addItemToList($frame->{entry}, $entryHistory{$label}, $varRef); }, -tabcmd => sub { addItemToList($frame->{entry}, $entryHistory{$label}, $varRef); }, -width => $width2, )->pack(-side => $int_pos, -expand => 1, -fill => 'x', -padx => 0); } else { $frame->{entry} = $frame->Entry(-textvariable => $varRef, -width => $width2, )->pack(-side => $int_pos, -expand => 1, -fill => 'x', -padx => 0); } $frame->{entry}->xview('end'); $frame->{entry}->icursor('end'); return $frame; } ############################################################## # addItemToList - add a new value to the list and remove double entries ############################################################## sub addItemToList { my $widget = shift; my $listref = shift; my $varref = shift; return if (!defined $$varref); return if ($$varref eq ''); # todo: remove double values and remove old values push @{$listref}, $$varref; my %d; # build a hash foreach (@{$listref}) { $d{$_} = 1; } @{$listref} = (sort { uc($a) cmp uc($b); } keys %d); $widget->configure(-choices => $listref); } ############################################################## # labeledEntry2 - build a frame containing two labeled entrys ############################################################## sub labeledEntry2 { # input values my ($parentWidget, $position, $width1, $width2, $label1, $varRef1, $label2, $varRef2) = @_; my $frame = $parentWidget->Frame()->pack(-side => $position, -expand => 0, -fill => 'x', -padx => 3, -pady => 3); $frame->Label(-text => $label1, -width => $width1, -anchor => 'w', -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -padx => 3); my $entry1 = $frame->Entry(-textvariable => $varRef1, -width => $width2, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); $entry1->xview('end'); $entry1->icursor('end'); $frame->Label(-text => $label2, -width => $width1, -anchor => 'w', -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -padx => 3); my $entry2 = $frame->Entry(-textvariable => $varRef2, -width => $width2, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); $entry2->xview('end'); $entry2->icursor('end'); return $frame; } ############################################################## # labeledDoubleEntry - build a frame containing two labeled entrys ############################################################## sub labeledDoubleEntry { # input values my ($parentWidget, $position, $width, $label, $label2, $dVarRef, $dBalloon, $tVarRef, $tBalloon) = @_; my $fullframe = $parentWidget->Frame()->pack(-side => $position, -fill => 'x', -expand => 0, -padx => 0, -pady => 0); my $frame = labeledEntry($fullframe, 'left', $width, $label, $dVarRef, ($width+5)); $balloon->attach($frame, -msg => $dBalloon); $frame = labeledEntry($fullframe, 'left', $width, $label2, $tVarRef, ($width+5)); $balloon->attach($frame, -msg => $tBalloon); return $fullframe; } ############################################################## # labeledScale - build a frame containing a labeled scale ############################################################## sub labeledScale { # input values my ($parentWidget, $position, $width, $label, $varRef, $from, $to, $res, $callback) = @_; my $frame = $parentWidget->Frame(-bd => 0)->pack(-side => $position, -fill => 'x', -padx => 3, -pady => 3); $frame->Label(-text => $label, -width => $width, -anchor => 'w', -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -padx => 3); $frame->{scale} = $frame->Scale(-variable => $varRef, #-length => $width, -from => $from, -to => $to, -resolution => $res, -sliderlength => 30, -orient => 'horizontal', -width => 15, -showvalue => 0, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); if ($callback) { $frame->{scale}->configure(-command => sub { &$callback; }); } $frame->Label(-textvariable => $varRef, -width => 5, -anchor => "e", -bd => $config{Borderwidth}, -relief => 'sunken', -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -padx => 1); return ($frame); } ############################################################## # select folder: use Gtk2::FileChooserDialog if available ############################################################## sub folder_dialog { my ($w, $title, $initdir) = @_; my $dir; if ($gtk2_avail) { # 3.argument is action: open, save, select-folder or create-folder my $chooser = Gtk2::FileChooserDialog->new ($title, undef, "select-folder", 'gtk-cancel' => 'cancel', 'gtk-open' => 'ok'); $chooser->set_default_response ('ok'); $chooser->set_current_folder($initdir) if -d $initdir; if ('ok' eq $chooser->run ()) { print "chose ".$chooser->get_filename()."\n"; $dir = $chooser->get_filename(); } $chooser->destroy; } else { my $choosendir = $w->chooseDirectory(-title => $title, -initialdir => $initdir); if ((defined $choosendir) and ( -d $choosendir )) { $dir = $choosendir; } } return $dir; } ############################################################## # setFileButton - open a file selector and set file or dir name ############################################################## sub setFileButton { # input values my ($parentWidget, $position, $butlabel, $fileselLabel, $varRef, $dir) = @_; # $dir is optional, if defined and true a dir will be selected instead of a file $parentWidget->Button(-text => $butlabel, -command => sub { if ((defined $dir) and ($dir == 1)) { #my $dir = $parentWidget->chooseDirectory(-title => $fileselLabel, -initialdir => $$varRef); my $dir = folder_dialog($parentWidget, $fileselLabel, $$varRef); if ((defined $dir) and ( -d $dir )) { $$varRef = $dir; } } else { my $file = $parentWidget->getOpenFile(-title => $fileselLabel, -initialdir => dirname($$varRef)); if ((defined $file) and (-f $file)) { $$varRef = $file; } } }, )->pack(-side => $position); } ############################################################## # setColorButton - open a color selector and set the color ############################################################## sub setColorButton { # input values my ($parentWidget, $position, $butlabel, $varRef) = @_; my $ccbut; $ccbut = $parentWidget->Button(-text => $butlabel, -pady => 0, -bg => $$varRef, -command => sub { my $rc = color_chooser(); if (defined $rc) { $ccbut->configure(-bg => $rc); $$varRef = $rc; # this is needed when updating the button if ($$varRef eq 'black') { $ccbut->configure(-fg => 'white'); } else { $ccbut->configure(-fg => 'black'); } } })->pack(-side => $position, -pady => 0, -padx => 1); # this is needed when drawing the button if ($$varRef eq 'black') { $ccbut->configure(-fg => 'white'); } else { $ccbut->configure(-fg => 'black'); } return $ccbut; } ############################################################## # color_chooser - open a window and offer some colors to select ############################################################## sub color_chooser { my $title = 'Please select a color'; # open window my $win = $top->Toplevel(); $win->withdraw; $win->title($title); $win->iconimage($mapiviicon) if $mapiviicon; $win->iconname($title); my $frame; my $return_color = 0; my $colP = $win->Button(-text => 'Color picker', -height => 0, -width => 0, -padx => 0, -pady => 0, -relief => "groove", -background => $config{ColorPicker}, -command => sub { $return_color = $config{ColorPicker}; } )->pack(-padx => 0, -pady => 0); $balloon->attach($colP, -msg => $config{ColorPicker}); my $colorF = $win->Frame()->pack(-fill => 'both', -expand => 1); my $i = 0; foreach (@allcolors) { $i++; if ($i == 1 or $i % 12 == 1) { # a frame for the first and every 12th button (modulo) $frame = $colorF->Frame()->pack(-side => 'left', -anchor => 'n'); } my $but; $but = $frame->Button(#-bitmap => "cbut", -text => " ", -height => 0, -width => 0, -padx => 0, -pady => 0, -relief => "groove", -background => $_, -command => sub { my $col = $but->cget(-bg); $return_color = $col; } )->pack(-padx => 0, -pady => 0); $balloon->attach($but, -msg => $_); } my $xBut = $win->Button(-text => "Close", -command => sub { print "returning: undef\n"; $return_color = undef; }, )->pack(-fill => 'x'); # 50 ways to leave your window ;) bind_exit_keys_to_button($win, $xBut); $xBut->focus; $win->Popup; $win->waitVariable(\$return_color); $win->withdraw; $win->destroy; return $return_color; } ############################################################## # makeNewDir - get a new dir name from the user and create this # new dir in the actual dir ############################################################## sub makeNewDir { my $path = shift; my $tree = shift; my $newDir = lang("newfolder"); my $rc = myEntryDialog(lang("New folder ..."), langf("Enter name of new folder in %s",$path),\$newDir); return if ($rc ne 'OK' or $newDir eq ''); if (-d "$path/$newDir") { $top->messageBox(-icon => 'warning', -message => "$newDir already exists!", -title => lang('Error'), -type => 'OK'); return 0; } if (!mkdir "$path/$newDir", oct(750)) { $top->messageBox(-icon => 'warning', -message => langf("Error making folder %s/%s: %s", $path, $newDir, $!), -title => lang('Error'), -type => 'OK'); return 0; } dirSave("$path/$newDir"); exists &Tk::DirTree::chdir ? $tree->chdir("$path/$newDir") : $tree->set_dir("$path/$newDir"); exists &Tk::DirTree::chdir ? $dirtree->chdir("$path/$newDir") : $dirtree->set_dir("$path/$newDir"); return "$path/$newDir"; } ############################################################## # getSelectedDir - get the selected folder # return value is either undefined, empty sting or a folder ############################################################## sub getSelectedDir { my $dir = ''; # if the dir tree is visible, try to get the selected dir if ($dirtree->ismapped()) { $dir = ($dirtree->selectionGet())[0]; # normalize the path if (defined $dir) { $dir =~ s/\\/\//g; # replace Windows path delimiter with UNIX style \ -> / $dir =~ s/\/+/\//g; # replace multiple slashes with one // -> / } } return $dir; } ############################################################## # getRightDir - get the selected or the actual dir ############################################################## sub getRightDir { my $dir = getSelectedDir(); # this is the fall back solution $dir = $actdir if ((!defined $dir) or ($dir eq '') or (!-d $dir)); return $dir; } ############################################################## # cleanOneDir - remove the .thumbs and .exif subdir ############################################################## sub cleanOneDir { my $dir = shift; my @subdirs = ("$dir/$thumbdirname", "$dir/$exifdirname"); foreach my $subdir (@subdirs) { if (-d $subdir) { my $rc = rmtree($subdir, 0, 1); # dir, 0 = no message for each file, 1 = skip write protected files print "removed $rc elements in $subdir\n" if $verbose; } } } ############################################################## # deleteDir ############################################################## sub deleteDir { my $dir = getRightDir(); if (!-d $dir) { $top->messageBox(-icon => 'warning', -message => langf("Folder %s does not exists!",$dir), -title => lang('Error'), -type => lang('OK')); return; } my $dirname = basename($dir); my $dirs = 0; my $files = 0; # rmdir will only remove empty folders, only if this fails we ask if (not rmdir $dir) { # get some infos about the dir my $size = 0; my $timeout = ''; my $start_time = Tk::timeofday(); log_it(lang("scanning folder ...")); $top->Busy; find(sub { # jump out after 5 seconds if (Tk::timeofday()-$start_time > 5) { $timeout = lang(" at least (scanning stopped by timeout)"); $File::Find::prune = 1; return; } $dirs++ if (-d $File::Find::name); if (-f $File::Find::name) { $files++; $size += getFileSize("$File::Find::name", NO_FORMAT); } }, $dir); $top->Unbusy; log_it(lang("folder scanned!")); $size = computeUnit($size); # ask only if there are still files and more than 2 folders (. and ..) if (($files > 0) or ($dirs > 2)) { my $question = langf("Found%s\n%8d folders and\n%8d files with a total size of\n%8s in \"%s\".\nReally delete?\n", $timeout, $dirs, $files, $size, $dirname); $question .= lang("Warning: There is no undelete!"); my $rc = $top->messageBox(-icon => 'question', -message => $question, -title => lang("Delete folder?"), -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } } print "rmtree: dir = $dir\n" if $verbose; rmtree($dir, 0, 1); # dir, 0 = no message for each file, 1 = skip write protected files # remove the deleted pics from the search database cleanDatabaseFolder($dir); my $dirid = $dir; $dirid =~ s/\//\\/g if $EvilOS; # windows needs backslashes # update dir tree $dirtree->delete('entry', $dirid) if ($dirtree->info('exists', $dirid)); # get parent folder my $path = dirname($dir); while (!-d $path) { $path = dirname($dir); last if ($path eq ''); } # open parent dir if we've deleted the actual dir openDirPost($path) unless (-d $dir); log_it(langf("Ready! Removed folder %s with %d files.",$dirname,$files)); } ############################################################## # renameDir ############################################################## sub renameDir { my $dir = getRightDir(); if (!-d $dir) { warn "dir $dir is no dir"; return; } my $path = dirname($dir); my $newDir = basename($dir); my $rc = myEntryDialog("Rename folder","Enter new name for folder $dir",\$newDir); return if ($rc ne 'OK' or $newDir eq ''); my $newDir_withpath = "$path/$newDir"; if (-d $newDir_withpath) { $top->messageBox(-icon => 'warning', -message => "$newDir already exists!", -title => lang('Error'), -type => 'OK'); return; } if (!rename $dir, "$path/$newDir") { $top->messageBox(-icon => 'warning', -message => "error renaming folder $dir to $newDir_withpath: $!", -title => lang('Error'), -type => 'OK'); return; } # move the moved pics also in the search database renameDatabaseFolder($dir, $newDir_withpath); # refresh the dir tree display #$path =~ s/\//\\/g if $EvilOS; # windows needs backslashes $newDir_withpath =~ s/\//\\/g if $EvilOS; # windows needs backslashes exists &Tk::DirTree::chdir ? $dirtree->chdir($newDir_withpath) : $dirtree->set_dir($newDir_withpath); $dirtree->Subwidget('scrolled')->configure(-directory => $newDir_withpath); #$dirtree->close($path); #$dirtree->open($path); my $dirid = $dir; $dirid =~ s/\//\\/g if $EvilOS; # windows needs backslashes $dirtree->delete('entry', $dirid) if ($dirtree->info('exists', $dirid)); if ($dirtree->info('exists', $newDir_withpath)) { $dirtree->see($newDir_withpath); # select the new dir $dirtree->selectionSet($newDir_withpath); } $actdir = $newDir_withpath if (!-d $actdir); } ############################################################## # calcSize - calc new picture size # considering the aspect ratio and landscape/portait # mode ############################################################## sub calcSize { my ($w, $ow, $oh) = @_; my $aspect = $ow/$oh; my ($nw, $nh); if ($ow >= $oh) { # landscape $nw = $w; $nh = round($nw/$aspect); } else { # portrait $nh = $w; $nw = round($aspect*$nh); } return ($nw, $nh); } ############################################################## # qualityBalloon ############################################################## sub qualityBalloon { $balloon->attach(shift, -msg => lang("Quality of picture\nAppropriate settings are between 50% and 95%,\n80% is often a good tradeoff between size and quality for web and email\nfor further processing and best quality 95% is recommended\nValues over 95% just increase file size, not quality")); } ############################################################## # changeSizeQuality - change the size and quality of all # selected JPEG pictures # based on code from Hans-Peter Rangol 10/13/2002. # Needs mogrify from ImageMagick, preserves Exif-Data, # depending on the version of mogrify (at least 5.1.1 does not!) ############################################################## sub changeSizeQuality { return if (!checkExternProgs('changeSizeQuality', 'mogrify')); my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $rc = 0; if ($config{WarnBeforeResize}) { my $rc = checkDialog("Change size quality", "This function will change the size and/or quality\ of $selected selected pictures to a choosable value.\ The EXIF/IPTC and JPEG comment may be preserved,\ depending on your version of the program mogrify.\ So please make a test with a backup picture first.\ It's possible to save and restore the EXIF info with\ menu: \"EXIF info\"->\"save\".\n", \$config{WarnBeforeResize}, "ask every time", '', 'OK', 'Cancel'); return if ($rc ne 'OK'); } # get the size of the first picture my ($width, $height) = getSize($sellist[0]); my $origW = $width; my $origH = $height; my $widthP = 100; my $heightP = 100; if ($height == 0) { # avoid division by zero $top->messageBox(-message => "Sorry, but the size of ".basename($sellist[0])." is not available - Aborting.", -icon => 'warning', -title => "No size info", -type => 'OK'); return; } my $aspect = $width/$height; my $PixPro = "pro"; # open dialog window my $myDiag = $top->Toplevel(); $myDiag->title(lang('Resize')); $myDiag->iconimage($mapiviicon) if $mapiviicon; $myDiag->Label(-text => langf("Change the size and/or quality of %d selected pictures",$selected), -bg => $conf{color_bg}{value} )->pack(-anchor => 'w',-padx => 3,-pady => 3); my $qS = labeledScale($myDiag, 'top', 18, lang("Quality (%)"), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); # check if the Imagemagick version supports the strip command my $strip = 0; $strip = 1 if (`mogrify` =~ m/.*-strip.*/); # check, if the ImageMagick version supports the unsharp command my $unsharp = 0; $unsharp = 1 if (`mogrify` =~ m/.*-unsharp.*/); my $keepaspect = 1; my $csf1 = $myDiag->Frame()->pack(-fill =>'x',-padx => 3,-pady => 3); # $csf1->Button(-text => "100%", # -width => 12, # -command => sub { # $height = $origH; # $width = $origW; # $widthP = round($width/$origW * 100); # $heightP = round($height/$origH * 100); # })->pack(-side => 'left', -fill =>'x', -padx => 1); { # add some relative size buttons my @list = ( 100, 50, 33, 25, 10); foreach my $size (@list) { $csf1->Button(-text => "${size}%", -width => 9, -command => sub { $PixPro = 'pro'; $keepaspect = 1; if ($size == 100) { $height = $origH; $width = $origW; $widthP = round($width/$origW * 100); $heightP = round($height/$origH * 100); } else { $widthP = $size; $heightP = $size; $width = round($origW * $widthP/100); $height = round($origH * $heightP/100); } })->pack(-side => 'left',-fill =>'x', -expand => 1, -padx => 1); } } my $csf2 = $myDiag->Frame()->pack(-fill =>'x',-padx => 3,-pady => 3); { # add some absolut size buttons my @list = ( 2000, 1500, 1000, 800, 600 ); foreach my $size (@list) { $csf2->Button(-text => "${size}px", -width => 9, -command => sub { $PixPro = 'pix'; $keepaspect = 1; ($width, $height) = calcSize($size, $origW, $origH); $widthP = round($width/$origW * 100); $heightP = round($height/$origH * 100); })->pack(-side => 'left',-fill =>'x', -expand => 1, -padx => 1); } } my $w = 20; $myDiag->Checkbutton(-variable => \$keepaspect, -anchor => 'w', -text => langf("Keep aspect ratio (original size %dx%d)",$origW, $origH))->pack(-anchor => 'w', -padx => 3,-pady => 3); my $absoluteF = $myDiag->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill =>'x',-padx => 3,-pady => 3); $absoluteF->Radiobutton(-text => lang("Absolute size in pixel"), -variable => \$PixPro, -value => 'pix')->pack(-anchor => 'w', -padx => 3,-pady => 3); my $labFw = labeledEntry($absoluteF, 'top', $w, lang("Width"), \$width); my $labFh = labeledEntry($absoluteF, 'top', $w, lang("Height"), \$height); my $relativeF = $myDiag->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill =>'x',-padx => 3,-pady => 3); $relativeF->Radiobutton(-text => lang("Relative size in %"), -variable => \$PixPro, -value => 'pro')->pack(-anchor => 'w', -padx => 3,-pady => 3); my $labFwp = labeledEntry($relativeF, 'top', $w, lang("Width"), \$widthP); my $labFhp = labeledEntry($relativeF, 'top', $w, lang("Height"), \$heightP); my $labEw = ($labFw->children)[1]; my $labEh = ($labFh->children)[1]; my $labEwp = ($labFwp->children)[1]; my $labEhp = ($labFhp->children)[1]; $labEw->bind('', sub { if ($keepaspect) { $height = round($width/$aspect); # int() does not round! } $widthP = round($width/$origW * 100); $heightP = round($height/$origH * 100); $PixPro = "pix"; }); $labEh->bind('', sub { if ($keepaspect) { $width = sprintf("%.0f",($aspect*$height)); } $widthP = round($width/$origW * 100); $heightP = round($height/$origH * 100); $PixPro = "pix"; }); $labEwp->bind('', sub { if ($keepaspect) { $heightP = $widthP; # int() does not round! } $width = round($origW * $widthP/100); $height = sprintf("%.0f",($origH * $heightP/100)); $PixPro = "pro"; }); $labEhp->bind('', sub { if ($keepaspect) { $widthP = $heightP; } $width = round($origW * $widthP/100); $height = sprintf("%.0f",($origH * $heightP/100)); $PixPro = "pro"; }); my $filf = $myDiag->Frame()->pack(-fill =>'x',-padx => 3,-pady => 3); $filf->Label(-text => lang("Resize method"), -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); my $resfilt = $filf->Optionmenu(-options => [qw(Point Box Triangle Hermite Hanning Hamming Blackman Gaussian Quadratic Cubic Catrom Mitchell Lanczos Bessel Sinc)], -variable => \$config{ResizeFilter}, -textvariable => \$config{ResizeFilter})->pack(-side => 'left', -anchor => 'w'); $balloon->attach($resfilt, -msg => lang("Recommendation: Lanczos filter for high quality pictures.")); if ($strip) { $myDiag->Checkbutton(-variable => \$config{PicStrip}, -anchor => 'w', -text => lang("Strip all meta information (EXIF, IPTC, ...)"))->pack(-anchor => 'w',-padx => 3,-pady => 3); } # option to sharpen the image with an unsharp mask operator if ($unsharp) { my $umF = $myDiag->Frame()->pack(-fill =>'x', -padx => 0); my $umcB = $umF->Checkbutton(-variable => \$config{Unsharp}, -anchor => 'w', -text => lang("Unsharp mask"))->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1,-padx => 3,-pady => 3); $balloon->attach($umcB, -msg => "The unsharp option sharpens an image. We convolve the image with a Gaussian operator of the given radius and standard deviation (sigma). For reasonable results, radius should be larger than sigma. Use a radius of 0 to have the method select a suitable radius."); $umF->Button(-text => lang("Options"), -anchor => 'w', -command => sub { unsharpDialog(); })->pack(-side => 'left', -anchor => 'w', -padx => 3); } my $sS = labeledScale($myDiag, 'top', 18, lang("Sharpness (radius)"), \$config{PicSharpen}, 0, 10, 0.1); $balloon->attach($sS, -msg => "Resizing a picture to a smaller size usually causes some blurring\nuse this function to sharpen the picture and reduce the blurring\nHowever if the unsharp mask option is available I recommend using it instead of sharpen\nThis function is deactivated when set to 0"); my $blS = labeledScale($myDiag, 'top', 18, lang("Blur (radius)"), \$config{PicBlur}, 0, 10, 0.1); $balloon->attach($blS, -msg => "Maybe used in conjunction with Sharpness"); buttonBackup($myDiag, 'top'); buttonComment($myDiag, 'top'); my $ButF = $myDiag->Frame()->pack(-fill =>'x',-padx => 3,-pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $rc = 1; $myDiag->withdraw(); $myDiag->destroy(); })->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $myDiag->withdraw(); $myDiag->destroy(); } )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); bind_exit_keys_to_button($myDiag, $xBut); $OKB->focus; $myDiag->Popup; $myDiag->waitWindow; return if ($rc != 1); # check if some files are links return if (!checkLinks($picLB, @sellist)); return if (checkWriteableMulti(@sellist) eq 'Cancel all'); log_it("changing the size/quality of $selected pictures ..."); my $pw = progressWinInit($top, "changing size/quality"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; my $pic = basename($dpic); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); my ($w, $h) = getSize($dpic); if ($PixPro eq "pro") { if (($w == 0) or ($h == 0)) { # avoid division by zero $top->messageBox(-message => "Sorry, but the size of $pic is not available - skipping picture.", -icon => 'warning', -title => "No size info", -type => 'OK'); next; } $width = sprintf("%.0f",($w * $widthP/100)); $height = sprintf("%.0f",($h * $heightP/100)); print "resizing to procent $w $h -> $width $height ($widthP $heightP)\n" if $verbose; } # call external command mogrify # the comment option of mogrify overwrites all existing comments! my $command = "mogrify"; $command .= " -blur ".$config{PicBlur} if ($config{PicBlur} > 0); $command .= " -size ${width}x${height}"; $command .= " -geometry ${width}x${height}"; $command .= "\\\!" if (!$keepaspect); $command .= " -filter ".$config{ResizeFilter}; $command .= " -strip ".$config{PicStrip} if ($config{PicStrip} and $strip); $command .= " -sharpen ".$config{PicSharpen} if ($config{PicSharpen} > 0); $command .= " -unsharp ".$config{UnsharpRadius}.'x'.$config{UnsharpSigma}."+".$config{UnsharpAmount}."+".$config{UnsharpThreshold}." " if ($config{Unsharp} and $unsharp); $command .= " -quality ".$config{PicQuality}." \"$dpic\""; print "changeSizeQuality: com = $command\n" if $verbose; execute($command); progressWinUpdate($pw, "changing size/quality ($i/$selected) ...", $i, $selected); # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time print "new $width x $height old: $w x $h\n" if $verbose; touch(getThumbFileName($dpic)) if (($width == $w) and ($height == $h)); # only when the size changed addProcessInfoToPicComment($command, $dpic); updateOneRow($dpic, $picLB); showImageInfo($dpic) if ($dpic eq $actpic); } # foreach end progressWinEnd($pw); log_it("ready! ($i of $selected changed)"); generateThumbs(ASK, SHOW); } ############################################################## # dragPic - enable panning of an object in a canvas # needs $c->{picWidth} and $c->{picHeight} to be # set to the object (picture) width and height ############################################################## sub dragPic { my $c = shift; # the canvas my $i = shift; # the item to drag $c->bind($i, '' => sub { ($c->{idx}, $c->{idy})=($Tk::event->x,$Tk::event->y); }); $c->bind($i, '' => sub { # actual mouse coordinates $c->configure(-cursor => "fleur"); my ($mx,$my) = ($Tk::event->x,$Tk::event->y); my ($x1,$x2) = $c->xview; my ($y1,$y2) = $c->yview; return if ($x1 == 0 and $x2 == 1 and $y1 == 0 and $y2 == 1); my $dx = 0; $dx = ($mx-$c->{idx})/$c->{picWidth} if ($c->{picWidth} >= 1); # avoid division by zero my $dy = 0; $dy = ($my-$c->{idy})/$c->{picHeight} if ($c->{picHeight} >= 1); # avoid division by zero $c->xviewMoveto($x1-$dx) unless ($x1 == 0 and $x2 == 1); $c->yviewMoveto($y1-$dy) unless ($y1 == 0 and $y2 == 1); ($c->{idx},$c->{idy}) = ($mx,$my); }); } ############################################################## # filterPic - apply a image filter to the picture ############################################################## sub filterPic { if (Exists($filterW)) { $filterW->deiconify; $filterW->raise; return; } my $fdir = $actdir; return if (!checkExternProgs("filterPic", "mogrify")); # check, if a new version of ImageMagick's mogrify with the unsharp and level option is available my $unsharp = 0; my $level = 0; my $usage = `mogrify`; $unsharp = 1 if ($usage =~ m/.*-unsharp.*/); $level = 1 if ($usage =~ m/.*-level.*/); my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); # check if some files are links return if (!checkLinks($picLB, @sellist)); my ($pic, $dpic, $dirtpic, $i); log_it("image processing: preparing preview ..."); # take the first picture as preview picture $dpic = $sellist[0]; $pic = basename($dpic); # open dialog window $filterW = $top->Toplevel(); $filterW->withdraw(); # hide window while populating $filterW->title("Image processing $pic"); $filterW->iconimage($mapiviicon) if $mapiviicon; my $p = $filterW; my $lF = $p->Frame()->pack(-anchor => 'n', -side => 'left'); my $rF = $p->Frame()->pack(-anchor => 'n', -side => 'left'); my $leftF = $lF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-expand => 1, -fill => 'both', -side => 'left'); my $rightF = $lF->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-expand => 1, -fill => 'both', -side => 'right'); $leftF->Label (-text => "Original")->pack(-fill => 'x'); $rightF->Label(-text => "Processed")->pack(-fill => 'x'); my %filters = ( "equalize" => 0, "normalize" => 0, "despeckle" => 0, "grayscale" => 0, "enhance" => 0, "negate" => 0, "antialias" => 0, "contrast" => 0, ); # try to get the saved filter settings if (-f "$user_data_path/filters") { my $hashRef = retrieve("$user_data_path/filters"); warn "could not retrieve filter settings" unless defined $hashRef; %filters = %{$hashRef}; } # layout infos: # leftF rightF # original processed # $icon($thumb) $thumbicon($thumbnew) # $photo($actdir/pic) $previewP($prevpic) my @xy = (0, 0); my $pc; my $icon; my $thumbicon; my $previewP; # the preview thumb my $thumb = "$trashdir/$thumbdirname/$pic.jpg"; my $thumbnew = "$trashdir/$thumbdirname/$pic"; my $thumbPreviewB; return if (!mycopy ("$fdir/$pic", "$thumb", OVERWRITE)); return if (!resizePic("$thumb", $config{FilterPrevSize}, $config{FilterPrevSize}, $config{PicQuality})); # the cropped preview pic my $prevpic = "$trashdir/$pic"; my $previewB; return if (!mycopy("$fdir/$pic", $prevpic, OVERWRITE)); return if (!cropPic($prevpic, $config{FilterPrevSize}, $config{FilterPrevSize},0,0, $config{PicQuality})); if ((defined $thumb) and (-f $thumb)) { $icon = $top->Photo(-file => "$thumb", -gamma => $config{Gamma}); if ($icon) { $leftF->Label(-image => $icon )->pack(-padx => 3, -pady => 3,-anchor => "e"); $thumbPreviewB = $rightF->Button(-image => $icon, -command => sub { return if !mycopy("$thumb" , "$thumbnew", OVERWRITE); return if !mycopy("$fdir/$pic", "$prevpic" , OVERWRITE); # we need to recrop everytime, because the crop sector may be changed by the user @xy = getCorners($pc); # get the crop offset return if !cropPic($prevpic, $config{FilterPrevSize},$config{FilterPrevSize},$xy[0],$xy[1], $config{PicQuality}); $filterW->Busy; applyFilter("$thumbnew", \%filters, PREVIEW); if ($thumbicon) { # if the photo object is already defined we just need to configure it $thumbicon->configure(-file => "$thumbnew", -gamma => $config{Gamma}); } else { # else we define it $thumbicon = $top->Photo(-file => "$thumbnew", -gamma => $config{Gamma}); $thumbPreviewB->configure(-image => $thumbicon); } applyFilter("$prevpic", \%filters, PREVIEW); if ($previewP) { # if the photo object is already defined we just need to configure it $previewP->configure(-file => "$prevpic", -gamma => $config{Gamma}); } else { # else we define it $previewP = $top->Photo(-file => "$prevpic", -gamma => $config{Gamma}); $previewB->configure(-image => $previewP); } $filterW->Unbusy; })->pack(-padx => 3, -pady => 3,-anchor => 'w'); $balloon->attach($thumbPreviewB, -msg => "Press on the thumbnail or the Preview-button\nto see how the settings affect the picture"); } } # load the original picture in original size into a scrollable canvas # to set the crop frame $pc = $leftF->Scrolled("Canvas", -scrollbars => 'osoe', -width => $config{FilterPrevSize}, -height => $config{FilterPrevSize}, -relief => 'sunken', #-cursor => "fleur", -bd => $config{Borderwidth})->pack(-expand => 1, -fill => 'both'); # this is needed for dragPic() ($pc->{picWidth}, $pc->{picHeight}) = getSize("$fdir/$pic"); $top->Busy; my $photo = $top->Photo(-file => "$fdir/$pic", -gamma => $config{Gamma}); my $id = $pc->createImage(0, 0, -image => $photo, -anchor => "nw"); dragPic($pc, $id); # enable panning of the pic in the canvas my ($x1, $y1, $x2, $y2) = $pc->bbox($id); $pc->configure(-scrollregion => [0, 0, $x2-$x1, $y2-$y1]); # load the croped preview picture $previewP = $top->Photo(-file => "$prevpic", -gamma => $config{Gamma}); if ($previewP) { $previewB = $rightF->Button(-image => $previewP, -command => sub {$thumbPreviewB->Invoke();}, )->pack(-expand => 1, -fill => 'both', -padx => 0, -pady => 0, -anchor => "nw"); $balloon->attach($previewB, -msg => "Press on the picture or the Preview-button\nto see how the settings affect the picture"); } $top->Unbusy; my $mF = $rF->Frame()->pack(-expand => 1, -fill => 'both'); my $lbf = $mF->Frame()->pack(-expand => 1, -fill => 'both', -side => 'left'); my $rbf = $mF->Frame()->pack(-expand => 1, -fill => 'both', -side => 'right'); foreach (sort keys %filters) { $lbf->Checkbutton(-variable => \$filters{$_}, -anchor => 'w', -text => "$_")->pack(-anchor => 'w'); } #my $scF = $rF->Frame()->pack(-fill =>'x', -expand => 1); my $qS = labeledScale($rF, 'top', 12, lang("Quality (%)"), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); my $sS = labeledScale($rF, 'top', 12, "Sharpness", \$config{PicSharpen}, 0, 10, 0.1); $balloon->attach($sS, -msg => "appropriate settings are between 0 (no sharpen) and 4,\nthe higher the value the slower the conversion"); my $colF = $rF->Frame()->pack(-fill =>'x'); my $colcB = $colF->Checkbutton(-variable => \$config{ColorAdj}, -anchor => 'w', -text => "Color adjustment")->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1); $balloon->attach($colcB, -msg => "Adjust brightness, hue,\nsaturation and gamma"); $colF->Button(-text => lang('Options'), -anchor => 'w', -command => sub { colorDialog(); })->pack(-side => 'left', -anchor => 'w', -padx => 3); # sharpen the image with an unsharp mask operator if ($unsharp) { my $umF = $rF->Frame()->pack(-fill =>'x'); my $umcB = $umF->Checkbutton(-variable => \$config{Unsharp}, -anchor => 'w', -text => "Unsharp mask")->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1); $balloon->attach($umcB, -msg => "The -unsharp option sharpens an image. We convolve the image with a Gaussian operator of the given radius and standard deviation (sigma). For reasonable results, radius should be larger than sigma. Use a radius of 0 to have the method select a suitable radius."); $umF->Button(-text => lang('Options'), -anchor => 'w', -command => sub { unsharpDialog(); })->pack(-side => 'left', -anchor => 'w', -padx => 3); } if ($level) { my $lvF = $rF->Frame()->pack(-fill =>'x'); my $lvB = $lvF->Checkbutton(-variable => \$config{Level}, -anchor => 'w', -text => "Level")->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1); $balloon->attach($lvB, -msg => "Level adjusts the levels of an image by scaling the colors falling between specified white and black points to the full available quantum range."); $lvF->Button(-text => lang('Options'), -anchor => 'w', -command => sub { levelDialog(); })->pack(-side => 'left', -anchor => 'w', -padx => 3); } my $decoF = $rF->Frame()->pack(-fill =>'x'); $decoF->Checkbutton(-variable => \$config{FilterDeco}, -anchor => 'w', -text => "Add border or text")->pack(-side => 'left', -anchor => 'w', -fill => 'x', -expand => 1); $decoF->Button(-text => lang('Options'), -anchor => 'w', -command => sub {decorationDialog(scalar @sellist,0);})->pack(-side => 'left', -anchor => 'w', -padx => 3); buttonBackup($rF, 'top'); buttonComment($rF, 'top'); my $ButF = $rF->Frame()->pack(-fill =>'x'); $ButF->Button(-text => lang('Preview'), -command => sub {$thumbPreviewB->Invoke();} )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { # save the filter settings nstore(\%filters, "$user_data_path/filters") or warn "could not store filter settings in file"; $uw->withdraw if (Exists($uw)); $lw->withdraw if (Exists($lw)); $colw->withdraw if (Exists($colw)); $decoW->withdraw if (Exists($decoW)); $filterW->withdraw(); # close window my $pw = progressWinInit($top, "Process pictures"); my $nr = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $pic = basename($dpic); next if (!checkWriteable($dpic)); last if (!makeBackup($dpic)); $nr++; progressWinUpdate($pw, "processing ($nr/".scalar @sellist.") ...", $nr, scalar @sellist); # we need to reread the picture to show the effect, # so we should clear the cachedPics list first deleteCachedPics($dpic); applyFilter($dpic, \%filters, NO_PREVIEW, "processing ($nr/".scalar @sellist.") ..."); updateOneRow($dpic, $picLB); # redisplay the processed picture if it is the actual picture showPic($dpic) if ($dpic eq $actpic); } progressWinEnd($pw); reselect($picLB, @sellist); log_it("ready! ($nr of ".scalar @sellist." processed)"); generateThumbs(ASK, SHOW); $filterW->destroy; })->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $filterW->destroy if (Exists($filterW)); $uw->destroy if (Exists($uw)); $lw->destroy if (Exists($lw)); $colw->destroy if (Exists($colw)); $decoW->destroy if (Exists($decoW)); } )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); bind_exit_keys_to_button($filterW, $Xbut); $OKB->focus; $filterW->Popup; log_it("image processing: preview ready!"); $filterW->waitWindow; log_it("image processing: cleaning up ..."); $icon->delete if $icon; $photo->delete if $photo; $thumbicon->delete if $thumbicon; $previewP->delete if $previewP; $uw->destroy if (Exists($uw)); $lw->destroy if (Exists($lw)); $colw->destroy if (Exists($colw)); $decoW->destroy if (Exists($decoW)); removeFile($prevpic); removeFile($thumb); removeFile($thumbnew); log_it("image processing ready!"); } ############################################################## # applyFilter ############################################################## sub applyFilter { my $dpic = shift; my $filters = shift; my $preview = shift; # PREVIEW = preview mode, NO_PREVIEW = real conversion my $info = shift; # optional, user info text $info = "processing ".basename($dpic)." ..." if (!defined $info); log_it($info); # check if file is a link and get the real target return if (!getRealFile(\$dpic)); # call external command mogrify my $command = "mogrify "; foreach (keys %{$filters}) { if ($_ eq "grayscale") { $command .= "-colorspace GRAY -colors 256 " if $$filters{$_}; } else { $command .= "-$_ " if $$filters{$_}; } } $command .= "-sharpen ".$config{PicSharpen}." " if ($config{PicSharpen} > 0); $command .= "-gamma ".$config{PicGamma}." " if (($config{PicGamma} != 1.0) and ($config{ColorAdj})); $command .= "-modulate ".$config{PicBrightness}.",".$config{PicSaturation}.",".$config{PicHue}." " if ($config{ColorAdj}); $command .= makeDrawOptions($dpic) if ((!$preview) and ($config{FilterDeco})); # do not add a border or a text in the preview $command .= "-unsharp ".$config{UnsharpRadius}.'x'.$config{UnsharpSigma}."+".$config{UnsharpAmount}."+".$config{UnsharpThreshold}." " if $config{Unsharp}; $command .= "-level \"".$config{LevelBlack}."%/".$config{LevelWhite}."%/".$config{LevelGamma}."\" " if $config{Level}; $command .= "-quality ".$config{PicQuality}; execute($command." \"$dpic\" "); addDropShadow($dpic) if ($config{FilterDeco}); addProcessInfoToPicComment($command, $dpic); log_it("image processing ready!"); } ############################################################## # removeFile - delete a file ############################################################## sub removeFile { my $file = shift; return 1 if (!-f $file); if ( unlink($file) != 1) { # unlink returns the number of successful removed files $top->messageBox(-icon => 'warning', -message => "Could not delete file \"$file\": $!", -title => 'Error', -type => 'OK'); return 0; } else { # remove file from search database, if it exists delete $searchDB{$file}; } return 1; } ############################################################## # resizePic ############################################################## sub resizePic { my ($dpic, $x, $y, $quality) = @_; unless (-f $dpic) { warn "no picture $dpic found!"; return 0; } my $command = "mogrify -size ${x}x${y} -geometry ${x}x${y} -quality $quality \"$dpic\" "; execute($command); return 1; } ############################################################## # crop - crop pictures in a lossless way ############################################################## sub crop { if (!checkExternProgs("crop", "jpegtran")) { $top->messageBox(-icon => 'warning', -message => "Could not find jpegtran, so there is no support for lossless JPEG cropping!\nYou will get jpegtran here: http://jpegclub.org\nNote: Download and install the jpegtran version with crop patch.\nNormal cropping is however possible.", -title => "No jpegtran available", -type => 'OK'); } else { # check if jpegtran supports lossless cropping my $usage = `jpegtran -? 2>&1`; if ($usage !~ m/.*-crop.*/) { $top->messageBox(-icon => 'warning', -message => "Sorry, but your version of jpegtran does not support lossless cropping!\nTry to get the lossless crop patch from http://jpegclub.org.\nNormal cropping is however possible.", -title => "Wrong jpegtran version", -type => 'OK'); } } my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my ($w, $h, $x, $y); my $i = 0; my $doforall = 0; my $askDifSize = 1; my $first = $sellist[0]; my ($wm, $hm) = getSize($first); my $pw; $pw = progressWinInit($lb, 'Crop pictures') if (@sellist > 1); foreach my $dpic (@sellist) { if ($pw) {last if progressWinCheck($pw)}; $i++; progressWinUpdate($pw, "cropping picture ($i/".scalar @sellist.") ...", $i, scalar @sellist) if ($pw); my $pic = basename($dpic); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); next if (!checkWriteable($dpic)); my ($wo, $ho) = getSize($dpic); if ($wo == 0 or $ho == 0) { $top->messageBox(-icon => 'warning', -message => "Sorry, picture $pic has no correct size (${wo}x$ho)!", -title => "Crop file", -type => 'OK'); next; } if ($doforall and $askDifSize and (($wo != $wm) or ($ho != $hm))) { my $rc = $top->messageBox(-icon => 'question', -message => "Picture $pic has not the same size as the preview picture.\nShould I continue and adjust the crop range if necessary?\nNote:\nThis will be done for all following pictures too!", -title => "Question", -type => 'OKCancel'); if ($rc !~ m/Ok/i) { $i--; last; } else { $askDifSize = 0; } } if (!$doforall) { # adjust size according to aspect ratio ($w, $h) = calcAspectSize($wo, $ho, $config{CropAspect}); $x = 0; $y = 0; last if (!cropDialog($dpic, \$x, \$y, \$w, \$h, $wo, $ho, \$doforall, scalar @sellist)); print "cropDialog returned $pic x:$x y:$y w:$w h:$h" if $verbose; } # save crop frame offset before adjusting too small pics my $xsave = $x; my $ysave = $y; if (($x + $w) > $wo) { # crop frame outside the picture $x = $wo - $w; if ($x < 0) { $top->messageBox(-icon => 'warning', -message => "Skipping picture $pic!\nThe width ($wo) is too small for the crop frame ($w).", -title => "Picture too small", -type => 'OK'); # restore crop frame offset after adjusting to small pics $x = $xsave; $y = $ysave; next; } } if (($y + $h) > $ho) { # crop frame outside the picture $y = $ho - $h; if ($y < 0) { $top->messageBox(-icon => 'warning', -message => "Skipping picture $pic!\nThe height ($ho) is too small for the crop frame ($h).", -title => "Picture too small", -type => 'OK'); # restore crop frame offset after adjusting to small pics $x = $xsave; $y = $ysave; next; } } printf "cropping $pic %4dx%4d+%4d+%4d\n", $w, $h, $x, $y if $verbose; next if (!makeBackup($dpic)); # crop the picture $top->Busy; cropPic($dpic,$w,$h,$x,$y,95); $top->Unbusy; # check if crop has the right size # due to the 8 pixel blocks, sometimes the size is too big (a few pixels) my ($nw, $nh) = getSize($dpic); if (($nw > $w) or ($nh > $h)) { # but a recrop will help ... $top->Busy; cropPic($dpic,$w,$h,0,0,95); $top->Unbusy; print "recropping $pic w:$nw > $w h: $nh > $h n" if $verbose; } # restore crop frame offset after adjusting to small pics $x = $xsave; $y = $ysave; addCommentToPic("Picture lossless cropped by Mapivi $version ($mapiviURL)", $dpic, NO_TOUCH) if ($conf{add_tool_info}{value}); updateOneRow($dpic, $lb); deleteCachedPics($dpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one } # foreach end progressWinEnd($pw) if ($pw); reselect($lb, @sellist); log_it("ready! ($i of ".scalar @sellist." cropped)"); generateThumbs(ASK, SHOW); } ############################################################## # calcAspectSize # return new picture width and height according to the given # aspect ratio and master direction ############################################################## sub calcAspectSize { my $w = shift; # width my $h = shift; # height my $aspect = shift; # aspect ratio e.g. 3/2 or 4/3; use 0 for no aspect ratio my $m = shift; # (optional) master ('w' if the width is the master or "h" for height) # calculate new size if ($aspect != 0) { # if there is no aspect ratio there is nothing to do if (defined $m) { # master defined if ($m eq 'w') { # width is master if ($w >= $h) { # landscape image $h = sprintf "%.0f", ($w / $aspect); # int() does not round! } else { # portait image $h = sprintf "%.0f", ($w * $aspect); } } else { # height is master if ($w >= $h) { # landscape image $w = sprintf "%.0f", ($h * $aspect); } else { # portait image $w = sprintf "%.0f", ($h / $aspect); # round } } } else { # no master defined if ($w >= $h) { # landscape image if (($h != 0) and ($w/$h >= $aspect)) { # too wide $w = sprintf "%.0f", ($h * $aspect); # round } else { # too high $h = sprintf "%.0f", ($w / $aspect); # round } } else { # portait image if (($h != 0) and ($w/$h >= 1/$aspect)) { # too wide $w = sprintf "%.0f", ($h / $aspect); # round } else { # too high $h = sprintf "%.0f", ($w * $aspect); # round } } } } return ($w, $h); } ############################################################## # setNewAspect ############################################################## sub setNewAspect { my $c = shift; my $aspect = shift; my $w = $c->{m_x2} - $c->{m_x1}; my $h = $c->{m_y2} - $c->{m_y1}; ($w, $h) = calcAspectSize($w, $h, $aspect); $c->{m_x2} = $c->{m_x1} + $w; $c->{m_y2} = $c->{m_y1} + $h; $c->{m_aspect} = getAspectRatio($w, $h); drawFrame($c); } ############################################################## # bindForResize # based on code from Jason Tiller and Ala Qumsieh posted in the Perl/TK (ptk; comp.lang.perl.tk) list in 2003 ############################################################## sub bindForResize { my $canvas = shift; # Drag requests: # 0 = No drag requested in this direction. # 1 = Drag top (for y) or left (for x) edge of rectangle. # -1 = Drag bottom (for y) or right (for x) edge of rectangle. my ( $dx, $dy ) = ( 0, 0 ); # Drag mode: NO_ACTIVE_MODE, MOVE_MODE, or RESIZE_MODE. use constant M_NO_ACTIVE_MODE => 0; use constant M_MOVE_MODE => 1; use constant M_RESIZE_MODE => 2; my $mode = M_NO_ACTIVE_MODE; # How close to the edge we have to be to initiate a resize (instead # of a move) drag. Expressed in percentage of overall # height/width. my $resize_within = 0.05; # Within 5% of edge to resize. # Initial location of mouse pointer. my ($oldx, $oldy) = (0) x 2; # ID of rectangle that we're resizing. my $rect; # Bind left-mouse clicks (<1>) over any widget with a 'RECT' tag to # do... $canvas->CanvasBind('<1>' => sub { my ( $x, $y ) = ( $Tk::event->x, $Tk::event->y ); my ( $x0, $y0, $x1, $y1 ) = $canvas->coords( 'RECT' ); return if ((not defined $x0) or (not defined $y0) or (not defined $x1) or (not defined $y1) or ($x < $x0) or ($x > $x1) or ($y < $y0) or ($y > $y1)); #my ( $x0, $y0, $x1, $y1 ) = $canvas->coords( 'RECT' ); my ( $width, $height ) = ( $x1 - $x0, $y1 - $y0 ); # Determine if the user wants to size in the x direction. If # the user clicks within $resize_within of the edge, then he # wants to resize. $dx = 0; if( $x < ( $x0 + $resize_within * $width ) ) { $dx = 1; } elsif( $x > ( $x1 - $resize_within * $width ) ) { $dx = -1; } # Do the same for the y direction. $dy = 0; if( $y < ( $y0 + $resize_within * $width ) ) { $dy = 1; } elsif( $y > ( $y1 - $resize_within * $width ) ) { $dy = -1; } # If resizing in either direction, set resize mode. $mode = ( $dx || $dy ) ? M_RESIZE_MODE : M_MOVE_MODE; my $id = $canvas->find( qw|withtag RECT| ); ( $oldx, $oldy, $rect ) = ( $x, $y, $id ); return; } ); # Bind motion with the left mouse button down () over a # widget with a 'RECT' tag to do... $canvas->CanvasBind('' => sub { my ( $x, $y ) = ( $Tk::event->x, $Tk::event->y ); #print "B1 Motion: $x $y\n"; if( $mode == M_RESIZE_MODE ) { #print "M_RESIZE_MODE\n"; # Get coordinates of resizing rectangle. my ( $x0, $y0, $x1, $y1 ) = $canvas->coords( 'RECT' ); # Resize logic. If we're moving the left border, then # change the coordinates of the left edge ($x0) to be the # current mouse position's x position ($x), else set the # rectangle's right edge. if ( $dx == 1 ) { $x0 = $x; } elsif ( $dx == -1 ) { $x1 = $x; } if ( $dy == 1 ) { $y0 = $y; } elsif ( $dy == -1 ) { $y1 = $y; } $x0 = 0 if ($x0 < 0); $x1 = $canvas->width if ($x1 > $canvas->width); $y0 = 0 if ($y0 < 0); $y1 = $canvas->height if ($y1 > $canvas->height); # Set the coordinates of the resizing rectangle. $canvas->coords( 'RECT', $x0, $y0, $x1, $y1 ); draw_grid($canvas, $x0, $y0, $x1, $y1); } else { #print "M_MOVE_MODE\n"; my ( $x0, $y0, $x1, $y1 ) = $canvas->coords( 'RECT' ); return if ((not defined $x0) or (not defined $y0) or (not defined $x1) or (not defined $y1) or ($x < $x0) or ($x > $x1) or ($y < $y0) or ($y > $y1)); # Move the rectangle under mouse pointer relative to its # old position. $canvas->move( $canvas->find( 'withtag', 'RECT' ), $x - $oldx, $y - $oldy ); draw_grid($canvas, $canvas->coords( 'RECT' )); # Update "old" coordinates. ( $oldx, $oldy ) = ( $x, $y ); } } ); # Set to false when we've changed the cursor. Tells us we want to # reset the cursor when we leave a rectangle. my $cursor_is_normal = 1; # Maps cursor position to cursor shape. # 0 = middle of shape, 1 = left/top edge, 2 = right/bottom edge. # [$x][$y] my @cursors = ( # [ (0,0), (0,1), (0,2) ] [ 'fleur', 'top_side', 'bottom_side' ], # [ (1,0), (1,1), (1,2) ] [ 'left_side', 'top_left_corner', 'bottom_left_corner' ], # [ (2,0), (2,1), (2,2) ] [ 'right_side', 'top_right_corner', 'bottom_right_corner' ] ); my @old_cursors = ( 3, 3 ); # ( x, y ) $canvas->CanvasBind( '' => sub { my @coords = $canvas->coords( 'RECT' ); $mode = M_NO_ACTIVE_MODE; $canvas->configure( -cursor => 'left_ptr' ); @old_cursors = ( 3, 3 ); $cursor_is_normal = 1; drawFrame($canvas, @coords); $canvas->raise($rect); } ); # Update the mouse cursor based on where the pointer is on the # canvas. If it's not over a rectangle, set it to the default # ('left_ptr'). If it's over a rectangle, set to a target cursor # if the pointer is in the drag region (center) else to a resize # cursor. $canvas->CanvasBind( '' => sub { #print "CanvasBind Motion\n"; #my $id = $canvas->find( qw|withtag current| ); #my @tags = $canvas->gettags($id); #for (0 .. $#tags) { print "$_ $tags[$_]\n"; } # Bail if we're not over a rectangle. my ( $x, $y ) = ( $Tk::event->x, $Tk::event->y ); my ( $x0, $y0, $x1, $y1 ) = $canvas->coords( 'RECT' ); if ( (not defined $x0) or (not defined $y0) or (not defined $x1) or (not defined $y1) or ($x < $x0) or ($x > $x1) or ($y < $y0) or ($y > $y1)) { unless( $cursor_is_normal ) { $canvas->configure( -cursor => 'left_ptr' ); @old_cursors = ( 3, 3 ); $cursor_is_normal = 1; } return; } # Don't update the cursor once we've started a drag or resize # operation. return unless $mode == M_NO_ACTIVE_MODE; my( $width, $height ) = ( $x1 - $x0, $y1 - $y0 ); # Now figure out where we are in the widget. my ( $px, $py ) = ( 0, 0 ); # Determine if the user wants to size in the x direction. If # the user clicks within $resize_within of the edge, then he # wants to resize. if( $x > ( $x1 - $resize_within * $width ) ) { $px = 2; } elsif( $x < ( $x0 + $resize_within * $width ) ) { $px = 1; } # Do the same for the y direction. if( $y > ( $y1 - $resize_within * $width ) ) { $py = 2; } if( $y < ( $y0 + $resize_within * $width ) ) { $py = 1; } # Don't update cursor unless it's changed. return if ( $px == $old_cursors[0] and $py == $old_cursors[1] ); $canvas->configure( -cursor => $cursors[$px][$py] ); @old_cursors = ( $px, $py ); $cursor_is_normal = 0; } ); return; } ############################################################## # generate a zommed preview picture e.g. for cropDialog ############################################################## sub make_preview_pic { my ($dpic, $zpicP, $zpicx, $zpicy) = @_; # References to preview picture: photo object, x-size, y-size $$zpicP = undef; my $sc_w = $top->screenwidth; my $sc_h = $top->screenheight; # if the picture is already available and zoomed to a usefull size we use it to save some time if (exists $photos{$dpic}) { $$zpicx = $photos{$dpic}->width; $$zpicy = $photos{$dpic}->height; if (($$zpicx > 0.25*$sc_w) and ($$zpicx < 0.9*$sc_w) and ($$zpicy > 0.25*$sc_h) and ($$zpicy < 0.9*$sc_h)) { log_it("using preview picture ..."); $$zpicP = $photos{$dpic}; #print "using preview picture ...\n"; } } # if this didn't work we zoom a new preview picture if (!defined $$zpicP) { log_it("creating preview picture ..."); my $zpic = "$trashdir/".basename($dpic); if (!mycopy($dpic, $zpic, OVERWRITE)) { warn "copy error"; return 0; } my $per = 0.75; # preview pic should be 75% of the min screen size my $cropPreviewSize = int($per * $sc_w); $cropPreviewSize = int($per * $sc_h) if ($sc_h < $sc_w); # just shrink big pictures, do not blow up small ones my $command = 'mogrify -geometry "'.$cropPreviewSize.'x'.$cropPreviewSize.'>" -quality 80 "'.$zpic.'"'; print "croppreview: $command\n" if $verbose; $top->Busy; (system $command) == 0 or warn "$command failed: $!"; $top->Unbusy; if (!-f $zpic) { $top->messageBox(-icon => 'warning', -message => "Sorry, error zooming preview picture $dpic!", -title => "Generating preview picture", -type => 'OK'); return 0; } ($$zpicx, $$zpicy) = getSize($zpic); $$zpicP = $top->Photo(-file => $zpic, -gamma => $config{Gamma}) if (-f $zpic); if (!$zpicP) { $top->messageBox(-icon => 'warning', -message => "Error displaying zoomed preview picture $zpic!", -title => "Generating preview picture", -type => 'OK'); return 0; } } log_it(lang('Ready!')); return 1; } ############################################################## # cropDialog - let the user set the crop offset ############################################################## sub cropDialog { my ($dpic, $xr, $yr, $wr, $hr, $wo, $ho, $doforallr, $nr) = @_; # $xr, $yr, $wr $hr x,y-offset and width and height of crop frame (type: reference on scalar) # $wo, $ho width and height of original picture (type: scalar) # $doforallr bool (type: reference on scalar) # $nr number of pics to crop my $rc; my $pc; # the canvas widget my $x2 = $$xr + $$wr; my $y2 = $$yr + $$hr; my ($zpicP, $zpicx, $zpicy); # preview picture: photo object, x-size, y-size return unless make_preview_pic($dpic, \$zpicP, \$zpicx, \$zpicy); # open window my $cropW = $top->Toplevel(); $cropW->title(lang('Crop picture (lossless)')); $cropW->iconimage($mapiviicon) if $mapiviicon; my $cropFL = $cropW->Frame()->pack(-side => 'left', -anchor => 'w'); my $cropFR = $cropW->Frame()->pack(-side => 'left', -anchor => 'n'); my $fc = $cropFL->Frame()->pack(); $pc = $fc->Canvas(-width => $zpicx, -height => $zpicy, -relief => 'sunken', -bd => $config{Borderwidth})->pack(-side => 'left', -padx => 3); # store some values in the canvas hash $pc->{m_aspect} = "[x:y]"; $pc->{m_wo} = $wo; $pc->{m_ho} = $ho; my $fF = $cropFR->Frame(-bd => $config{Borderwidth}, -relief => 'groove' )->pack(-anchor => 'w', -padx => 3, -pady => 3, -expand => 0, -fill => 'x'); $fF->Label(-text => lang('Help'))->pack(-expand => 0, -fill => 'x'); my $rotext = $fF->ROText(-wrap => "word", -bg => $conf{color_bg}{value}, -bd => 0, -width => 26, -height => 5)->pack(-expand => 0, -fill => 'x', -anchor => 'w'); $rotext->insert('end', lang('Use left mouse button to move and adjust the crop frame')); my $iF = $cropFR->Frame(-bd => $config{Borderwidth}, -relief => 'groove' )->pack(-anchor => 'w', -padx => 3, -pady => 3, -expand => 0, -fill => 'x'); $iF->Label(-text => "Info")->pack(-expand => 0, -fill => 'x'); $iF->Label(-text => "File: ".basename($dpic), -bg => $conf{color_bg}{value})->pack(-anchor => 'w'); $iF->Label(-text => "old size: ${wo} x ${ho}", -bg => $conf{color_bg}{value})->pack(-anchor => 'w'); my $lf = $iF->Frame()->pack(-anchor => 'w'); $lf->Label(-text => "new size:", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); $lf->Label(-textvariable => \$pc->{m_w}, -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); $lf->Label(-text => 'x', -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); $lf->Label(-textvariable => \$pc->{m_h}, -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); my $caF = $iF->Frame()->pack(-anchor => 'w'); $caF->Label(-text => "crop area:", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); $caF->Label(-textvariable => \$pc->{m_xyxy}, -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); my $cropRect; my @cropRectCoords; #$pc->bind('' => sub { $pc->Tk::focus }); bindForResize($pc); # insert pic my $id; $id = $pc->createImage(0, 0, -image => $zpicP, -anchor => 'nw', -tags =>'PIC') if $zpicP; my ($px1, $py1, $px2, $py2) = $pc->bbox($id); print "cropDialog: x1 $px1 x2 $px2 y1 $py1 y2 $py2 $wo $ho\n" if $verbose; if (($px1 == $px2) or ($py1 == $py2)) { $top->messageBox(-icon => 'warning', -message => "Error displaying zoomed preview of $dpic!", -title => "Crop file", -type => 'OK'); return 0; } # calculate the x and y zoom factor my $xz = $wo/($px2-$px1); my $yz = $ho/($py2-$py1); # store info in canvas widget $pc->{m_xzoom} = $xz; $pc->{m_yzoom} = $yz; $pc->{m_step} = 16; # resolution/step width for lossless crop must be 16 or 8, depends on picture encoding plusMinusEntry($iF, \$pc->{m_y1}, \$pc->{m_step}, 0, $ho, \&drawFrame, $pc, 'h'); my $iF1 = $iF->Frame()->pack(); my $iF11 = $iF1->Frame()->pack(-side => 'left', -padx => 5, -pady => 5); my $iF12 = $iF1->Frame()->pack(-side => 'left', -padx => 5, -pady => 5); plusMinusEntry($iF11, \$pc->{m_x1}, \$pc->{m_step}, 0, $wo, \&drawFrame, $pc, 'w'); plusMinusEntry($iF12, \$pc->{m_x2}, \$pc->{m_step}, 0, $wo, \&drawFrame, $pc, 'w'); plusMinusEntry($iF, \$pc->{m_y2}, \$pc->{m_step}, 0, $ho, \&drawFrame, $pc, 'h'); my $stepF = $iF->Frame()->pack(-anchor => 'w'); $stepF->Label(-text => lang("Step width"))->pack(-side => 'left', -anchor => 'w'); $stepF->Radiobutton(-variable => \$pc->{m_step}, -anchor => 'w', -text => "1", -value => 1, )->pack(-side => 'left', -anchor => 'w'); $stepF->Radiobutton(-variable => \$pc->{m_step}, -anchor => 'w', -text => "8", -value => 8, )->pack(-side => 'left', -anchor => 'w'); $stepF->Radiobutton(-variable => \$pc->{m_step}, -anchor => 'w', -text => "16", -value => 16, )->pack(-side => 'left', -anchor => 'w'); my $aF = $cropFR->Frame(-bd => $config{Borderwidth}, -relief => 'groove' )->pack(-anchor => 'w', -padx => 3, -pady => 3, -expand => 0, -fill => 'x'); $aF->Label(-text => lang("Aspect ratio"))->pack(-expand => 0, -fill => 'x'); my $aspF = $aF->Frame()->pack(-anchor => 'w'); $aspF->Label(-text => lang("Actual aspect ratio:"), -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); $aspF->Label(-textvariable => \$pc->{m_aspect}, -width => 8, -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); #my $dummy; my $aspectm = $aF->Optionmenu(-options => [ ['X:Y (any aspect ratio)' => 0], ['3:2 (e.g. 10x15)' => 3/2], ['4:3' => 4/3], ['5:4 (PAL)' => 5/4], ['7:5 (e.g. 13x18)' => 7/5], ['9:7 (e.g. 4,5x3,5)' => 9/7], ['16:9' => 16/9], ['5:2' => 5/2], ['1:1' => 1/1], ], -textvariable => \$config{CropAspect}, -command => sub { setNewAspect($pc, $config{CropAspect}); } )->pack(-side => 'top', -anchor => 'w'); $aspectm->configure(-variable => \$config{CropAspect}); #['1:1' => 1/1], ], -textvariable => \$dummy)->pack(-side => 'top', -anchor => 'w'); $cropFR->Checkbutton(-variable => \$config{CropGrid}, -anchor => 'w', -text => lang('Display 1/3 crop grid'), -command => sub { drawFrame($pc); }, )->pack(-anchor => 'w', -padx => 5, -pady => 3); buttonBackup($cropFR, 'top'); buttonComment($cropFR, 'top'); if ($nr > 1) { $cropFR->Checkbutton(-variable => \$$doforallr, -anchor => 'w', -text => lang("Use setting for all pictures") )->pack(-anchor => 'w'); } my $ButF = $cropFR->Frame()->pack(-fill =>'x', -expand => 1, -padx => 0, -pady => 2); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $$xr = $pc->{m_x1}; $$yr = $pc->{m_y1}; $$wr = $pc->{m_x2} - $pc->{m_x1}; $$hr = $pc->{m_y2} - $pc->{m_y1}; $cropW->withdraw(); $rc = 1; $cropW->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $cropW->withdraw(); $cropW->destroy(); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($cropW, $Xbut); # first popup the window then draw the frame! $cropW->Popup; $cropW->update; my $distx = int($zpicx/10); my $disty = int($zpicy/10); drawFrame($pc, $distx, $disty, ($zpicx-$distx), ($zpicy-$disty)); $cropW->waitWindow; # clean up (unless it is a existing pic used in showPhoto()) $zpicP->delete unless (exists $photos{$dpic}); #removeFile($zpic); return $rc; } ############################################################## # plusMinusEntry ############################################################## sub plusMinusEntry { my ($widget, $value, $step, $min, $max, $callback, $cb_para1, $cb_para2) = @_; $$value = 0 unless (defined $$value); my $frame = $widget->Frame(-relief => 'sunken')->pack(); $frame->Label(-textvariable => $value, -bg => $conf{color_bg}{value}, -width => 6)->pack(-side => 'left', -anchor => 'w'); $frame->Button(-image => $mapivi_icons{Minus}, -padx => 0, -pady => 0, -command => sub { $$value -= $$step; $$value = $min if ($$value < $min); $$value = $max if ($$value > $max); $callback->($cb_para1, $cb_para2); })->pack(-side => 'left', -anchor => 'w', -padx => 0, -pady => 0); $frame->Button(-image => $mapivi_icons{Plus}, -padx => 0, -pady => 0, -command => sub { $$value += $$step; $$value = $min if ($$value < $min); $$value = $max if ($$value > $max); $callback->($cb_para1, $cb_para2); })->pack(-side => 'left', -anchor => 'w', -padx => 0, -pady => 0); } ############################################################## # normalizeCoords - assign coordinates to allowed values (stepwidth) ############################################################## sub normalizeCoords { my $canvas = shift; foreach my $coord (qw(m_x1 m_x2 m_y1 m_y2)) { # assign it to the step width $canvas->{$coord} = sprintf "%.0f", ($canvas->{$coord}/$canvas->{m_step}); $canvas->{$coord} *= $canvas->{m_step}; # check lower bound $canvas->{$coord} = 0 if ($canvas->{$coord} < 0); } # check upper bound foreach my $coord (qw(m_x1 m_x2)) { $canvas->{$coord} = $canvas->{m_wo} if ($canvas->{$coord} > $canvas->{m_wo}); } foreach my $coord (qw(m_y1 m_y2)) { $canvas->{$coord} = $canvas->{m_ho} if ($canvas->{$coord} > $canvas->{m_ho}); } } ############################################################## # drawFrame ############################################################## sub drawFrame { my $canvas = shift; my @coords; my $direction = 'h'; if (@_ == 4) { # canvas coordinates are given @coords = @_; $canvas->{m_x1} = int($coords[0] * $canvas->{m_xzoom}); $canvas->{m_y1} = int($coords[1] * $canvas->{m_yzoom}); $canvas->{m_x2} = int($coords[2] * $canvas->{m_xzoom}); $canvas->{m_y2} = int($coords[3] * $canvas->{m_yzoom}); normalizeCoords($canvas); } elsif (@_ == 0) { # use the real coordinates normalizeCoords($canvas); $coords[0] = int($canvas->{m_x1} / $canvas->{m_xzoom}); $coords[1] = int($canvas->{m_y1} / $canvas->{m_yzoom}); $coords[2] = int($canvas->{m_x2} / $canvas->{m_xzoom}); $coords[3] = int($canvas->{m_y2} / $canvas->{m_yzoom}); } elsif (@_ == 1) { # optional direction h or w $direction = shift; normalizeCoords($canvas); } else { warn "drawFrame:: error wrong number of args ".scalar @_."\n"; return; } my $w = $canvas->{m_x2} - $canvas->{m_x1}; my $h = $canvas->{m_y2} - $canvas->{m_y1}; # adjust size according to aspect ratio ($w, $h) = calcAspectSize($w, $h, $config{CropAspect}, $direction); #($w, $h) = calcAspectSize($w, $h); $canvas->{m_x2} = $canvas->{m_x1} + $w; $canvas->{m_y2} = $canvas->{m_y1} + $h; $canvas->{m_xyxy} = $canvas->{m_x1}.",".$canvas->{m_y1}." - ".$canvas->{m_x2}.",".$canvas->{m_y2}; $canvas->{m_w} = $w; $canvas->{m_h} = $h; $canvas->{m_aspect} = getAspectRatio($w, $h); $coords[0] = int($canvas->{m_x1} / $canvas->{m_xzoom}); $coords[1] = int($canvas->{m_y1} / $canvas->{m_yzoom}); $coords[2] = int($canvas->{m_x2} / $canvas->{m_xzoom}); $coords[3] = int($canvas->{m_y2} / $canvas->{m_yzoom}); $canvas->delete('withtag', 'RECT'); $canvas->createRectangle(@coords, -tags => ['RECT'], -outline => 'red'); # draw 1/3 grid - divide the crop frame in nine rectangles draw_grid($canvas, @coords); $canvas->raise('RECT'); } ############################################################## ############################################################## sub draw_grid { my $canvas = shift; my @coords = @_; # draw 1/3 grid - divide the crop frame in nine rectangles $canvas->delete('withtag', 'GRID'); if ($config{CropGrid}) { my $grid_dist_h = round(($coords[3] - $coords[1])/3); my $grid_dist_w = round(($coords[2] - $coords[0])/3); $canvas->createLine($coords[0],$coords[1] + $grid_dist_h, $coords[2],$coords[1] + $grid_dist_h, -dash => [6,4,2,4],-tags => ['GRID'], -width => 1, -fill => '#ccc'); $canvas->createLine($coords[0],$coords[1] + 2*$grid_dist_h,$coords[2],$coords[1] + 2*$grid_dist_h, -dash => [6,4,2,4],-tags => ['GRID'], -fill => '#ccc'); $canvas->createLine($coords[0] + $grid_dist_w, $coords[1],$coords[0] + $grid_dist_w,$coords[3], -dash => [6,4,2,4],-tags => ['GRID'], -fill => '#ccc'); $canvas->createLine($coords[0] + 2*$grid_dist_w,$coords[1],$coords[0] + 2*$grid_dist_w,$coords[3], -dash => [6,4,2,4],-tags => ['GRID'], -fill => '#ccc'); } $canvas->delete('withtag', 'FRAME'); # draw a pseudo transparent box around the crop frame $canvas->createRectangle( 1, 1, $coords[0], $canvas->height-1, -tags => ['FRAME'], -outline => undef, -fill => 'black', -stipple => 'transp', ); $canvas->createRectangle( $coords[0], 1, $canvas->width-1, $coords[1], -tags => ['FRAME'], -outline => undef, -fill => 'black', -stipple => 'transp', ); $canvas->createRectangle( $coords[2], $coords[1], $canvas->width-1, $canvas->height-1, -tags => ['FRAME'], -outline => undef, -fill => 'black', -stipple => 'transp', ); $canvas->createRectangle( $coords[0], $coords[3], $coords[2], $canvas->height-1, -tags => ['FRAME'], -outline => undef, -fill => 'black', -stipple => 'transp', ); } ############################################################## # cropPic - cut a rect out of the pic # needs a geometry (e.g. 200x200+33+66) # overwrites the given file!!! # returns true if it worked ############################################################## sub cropPic { my $dpic = shift; return 0 if (!-f $dpic); # pic will be overwritten!!! my $w = shift; # width my $h = shift; # height my $x = shift; # x offset my $y = shift; # y offset my $qua = shift; # quality my ($pw, $ph) = getSize($dpic); #return 1 if (($pw <= $w) and ($ph <= $h)); # if the requested size is bigger than the pic we adapt to the real pic size $w = $pw if ($w > $pw); $h = $ph if ($h > $ph); my $geo = "${w}x${h}+${x}+${y}"; my $command = ''; # try to use fast lossless cropping for JPEGs if available if (is_a_JPEG($dpic) and checkExternProgs('crop', 'jpegtran')) { # check if jpegtran supports lossless cropping my $usage = `jpegtran -? 2>&1`; if ($usage =~ m/.*-crop.*/) { $command = "jpegtran -copy all -crop $geo -outfile \"$dpic\" \"$dpic\""; print "$dpic: cropping lossless using jpegtran\n" if $verbose; } } # the fallback solution if ($command eq '') { $command = "mogrify -crop $geo -quality $qua \"$dpic\""; print "$dpic: cropping lossy using mogrify (reason: not a JPEG or wrong jpegtran version)\n"; # if $verbose; } if ((system $command) != 0) { warn "$command failed: $!"; return 0; } else { return 1; } } ############################################################## # mycopy ############################################################## sub mycopy { my $from = shift; my $to = shift; my $overwrite = shift; # OVERWRITE = overwrite without asking ASK_OVERWRITE = ask before overwrite if (!-f $from) { $top->messageBox(-icon => 'warning', -message => "file $from not found!", -title => "Copy file", -type => 'OK'); return 0; } return 1 if ($from eq $to); # no need to copy a file on itself # if target exists and ask overwrite modus on if ((-f $to) and ($overwrite == ASK_OVERWRITE)) { my $rc = $top->messageBox(-icon => 'warning', -message => "file $to exist. Ok to overwrite?", -title => 'Copy file', -type => 'OKCancel'); return 0 if ($rc !~ m/Ok/i); } if (!copy($from, $to)) { $top->messageBox(-icon => 'warning', -message => "Could not copy $from to $to: $!", -title => 'Copy file', -type => 'OK'); return 0; } return 1; } ############################################################## # mylink ############################################################## sub mylink { my $old = shift; my $new = shift; my $overwrite = shift; # 1 = overwrite without asking 0 = ask before overwrite return 0 if $EvilOS; # sorry, no links on non-UNIX system, use Linux instead ;) if (!-f $old) { $top->messageBox(-icon => 'warning', -message => "file $old not found!", -title => "Link file", -type => 'OK'); return 0; } if ((-f $new) and !$overwrite) { my $rc = $top->messageBox(-icon => 'warning', -message => "file $new exist. Ok to overwrite?", -title => "Link file", -type => 'OKCancel'); return 0 if ($rc !~ m/Ok/i); } if (!symlink ("$old", "$new")) { $top->messageBox(-icon => 'warning', -message => "Could not link $old to $new: $!", -title => "Link file", -type => 'OK'); return 0; } return 1; } ############################################################## # checkLinks - check if there are links, count them and ask # whether to proceed ############################################################## sub checkLinks { my $lb = shift; # listbox ref my @list = @_; my $selected = @list; return 1 unless ($config{CheckForLinks}); if (@list < 1) { warn "checkLinks: uops, list is empty. Aborting!"; return 0; } my $links = 0; foreach my $dpic (@list) { if (-l $dpic) { $links++; } } if ($links > 0) { my $rc = $top->messageBox(-message => "$links of $selected selected pictures are links.\nDo you really want to change them?", -icon => 'question', -title => "Work on linked files?", -type => 'OKCancel'); if ($rc eq 'Ok') { return 1; } else { return 0; } } return 1; # no links, Ok to continue ... } ############################################################## # getBitPix - calculate picture compression in bit per pixel ############################################################## sub getBitPix { my $dpic = shift; return $quickSortHashBitsPixel{$dpic} if ($quickSortSwitch and defined $quickSortHashBitsPixel{$dpic}); my $b = 0; if (defined $searchDB{$dpic}{SIZE}) { $b = $searchDB{$dpic}{SIZE}; } else { $b = getFileSize($dpic, NO_FORMAT); # in Bytes } $b *= 8; # Bytes * 8 = bits my $p = getPixels($dpic); # avoid division by zero if ($p == 0) { $p = 1; $b = 0; } $quickSortHashBitsPixel{$dpic} = ($b/$p) if $quickSortSwitch; return ($b/$p); } ############################################################## # getPixels - get the number of pixels of a picture ############################################################## sub getPixels { my $dpic = shift; return $quickSortHashPixel{$dpic} if ($quickSortSwitch and defined $quickSortHashPixel{$dpic}); my $x = 0; my $y = 0; $x = $searchDB{$dpic}{PIXX} if $searchDB{$dpic}{PIXX}; $y = $searchDB{$dpic}{PIXY} if $searchDB{$dpic}{PIXY}; $quickSortHashPixel{$dpic} = int($x*$y) if $quickSortSwitch; return int($x*$y); } ############################################################## # getSize - get the image width and height # returns 0,0 if no file or size available ############################################################## sub getSize { my $dpic = shift; my $meta = shift; # optional, the Image::MetaData::JPEG object of $dpic if available if ((!defined $dpic) or ($dpic eq '')) { warn "getSize: Sorry, but there is no file!"; return (0, 0); } if (!-f $dpic) { warn "Sorry, but \"$dpic\" is no file!"; return (0, 0); } my $w = 0; my $h = 0; if (is_a_JPEG($dpic)) { $meta = getMetaData($dpic, "SOF", 'FASTREADONLY') unless (defined($meta)); ($w, $h) = $meta->get_dimensions() if $meta; } else { my $info = image_info($dpic); if (my $error = $info->{error}) { warn "getSize: Can't parse image info: $error\n"; } ($w, $h) = dim($info); } # remove non-digit chars, sometimes sizes like "48.000px" are returned if (defined $w) { $w =~ s|[a-z]||g; } else { $w = 0; } if (defined $h) { $h =~ s|[a-z]||g; } else { $h = 0; } #$w =~ s|[a-z]||g; #$h =~ s|[a-z]||g; #$w = 0 unless (defined $w); #$h = 0 unless (defined $h); return (int($w), int($h)); } ############################################################## # is_a_JPEG - returns true (1) if the given file is a JPEG/JFIF ############################################################## sub is_a_JPEG { my $dpic = shift; return 0 if (not defined $dpic); return 0 if (not -f $dpic); my @c; my $fh; # open file and read the first 3 bytes return 0 unless (open $fh,'<', $dpic); for my $i (0 .. 2) { read($fh, $c[$i], 1); } close $fh; # JPEG JFIF files start with 0xFF 0xD8 0xFF # todo: this check is necessary but not sufficent if ( (ord($c[0]) == 0xFF) && (ord($c[1]) == 0xD8) && (ord($c[2]) == 0xFF) ) { return 1; } else { return 0; } } ############################################################## ############################################################## sub is_a_slideshow_file { my $file = shift; return 0 if (not defined $file); return 0 if (not -f $file); # match *.sld file name pattern return 1 if ($file =~ m/.*\.sld/); return 1 if ($file =~ m/.*\.gqv/); return 0; } ############################################################## # argument needs no path! ############################################################## sub is_a_video { my $file = shift; return 0 if (not defined $file); return 0 if ($file eq ''); return 1 if ($file =~ m/.*\.mov/i); return 1 if ($file =~ m/.*\.mp4/i); return 1 if ($file =~ m/.*\.mpg/i); return 1 if ($file =~ m/.*\.mkv/i); return 1 if ($file =~ m/.*\.avi/i); return 1 if ($file =~ m/.*\.ts/i); return 0; } ############################################################## # make_mapivi_folders ############################################################## sub make_mapivi_folders { if (!-d $user_data_path) { # ask the user for permission to create a configdir my $rc = $top->messageBox(-icon => 'question', -message => "Mapivi would like to create the folder \"$user_data_path\" to store user specific data, like the search database, keyword tree, configuration, trash, etc. Please press Ok to create folder.", -title => "Mapivi installation", -type => 'OKCancel'); if ($rc =~ m/Ok/i) { if ( !mkdir $user_data_path, oct(700) ) { # 0700 = only for the user $top->messageBox(-icon => 'warning', -message => "Error making folder $user_data_path: $!", -title => "Mapivi installation", -type => 'OK'); return; } } else { return; } } # make trash folder if (!-d $trashdir) { if ( !mkdir $trashdir, oct(755) ) { $top->messageBox(-icon => 'warning', -message => "Error making trashdir $trashdir: $!", -title => "Mapivi installation", -type => 'OK'); return; } else { if (!-d "$trashdir/$thumbdirname") { if ( !mkdir "$trashdir/$thumbdirname", oct(755) ) { $top->messageBox(-icon => 'warning', -message => "Error making trashthumbdir $trashdir/$thumbdirname: $!", -title => "Mapivi installation", -type => 'OK'); return; } } } } } ############################################################## # checkGeometry ############################################################## sub checkGeometry { my $geoRef = shift; my ($w, $h, $x, $y) = splitGeometry($$geoRef); my $screenx = $top->screenwidth; my $screeny = $top->screenheight; my $tw = $top->reqwidth; my $th = $top->reqheight; print "checkGeometry: geo = $w ($tw) x $h ($th) + $x + $y ($screenx x $screeny)\n" if $verbose; if ((($w + $x) > $screenx) or (($h + $y) > $screeny)) { warn "Mapivi: window is out of screen, resizing!\n"; $screenx -= 20; $screeny -= 80; $$geoRef = "${screenx}x${screeny}+0+0"; } else { print "checkGeometry: window geometry ok\n" if $verbose; } } ############################################################## # splitGeometry - returns width, height, x, y of the geomtry ############################################################## sub splitGeometry { my $geo = shift; my @tmp = split /x/, $geo; my $w = $tmp[0]; @tmp = split /\+/, $tmp[1]; return ($w, $tmp[0], $tmp[1], $tmp[2]); } ############################################################## # checkAdjusterGeometry ############################################################## sub checkAdjusterGeometry { my $geoRef = shift; my $adj1Ref = shift; my $adj2Ref = shift; my $letterWidth = $top->fontMeasure($nrofL->cget(-font), "0"); if ($letterWidth < 8) {warn "letterWidth $letterWidth < 8!!!\n"; $letterWidth = 8; } my $x1 = $$adj1Ref * $letterWidth; my $x2 = $$adj2Ref * $letterWidth; my $wx; ($wx, undef, undef, undef) = splitGeometry($$geoRef); print "$x1 + $x2 letter: $letterWidth windowW: $wx?\n" if $verbose; if (($x1 + $x2 + 120) > $wx) { # add x for scrollbars and safety warn "Adjuster need to much place, changing back to minimum!"; $$adj1Ref = 10; $$adj2Ref = 10; } else { warn "Adjuster ok" if $verbose; } } ############################################################## # checkSystem ############################################################## sub checkSystem { # UNIX and Windows have different PATH separators und suffixes my $sep = ':'; $sep = ';' if $EvilOS; my $suffix = ''; $suffix = '.exe' if $EvilOS; # check if the external programs listet in the global hash %exprogs are available my @path = split /$sep/, $ENV{PATH}; foreach my $dir (@path) { foreach my $prog (keys %exprogs) { next if ($exprogs{$prog} > 0); # already found it somewhere else if (-x "$dir/$prog$suffix") { $exprogs{$prog}++; #print " $prog in $dir found!\n"; } # Windows may also have a .bat suffix elsif ($EvilOS) { if (-x "$dir/$prog.bat") { $exprogs{$prog}++; } } } } } ############################################################## # checkExternProgs - checks if the external programs needed # for a certain function exist ############################################################## sub checkExternProgs { my $sub = shift; # name of the calling sub my @neededProgs = @_; # list of needed external programs my @missingProgs = missingProgs($sub, @neededProgs); if (@missingProgs > 0) { my $msg = ''; $msg .= explainMissingProg($sub, $_) foreach (@missingProgs); $top->messageBox(-icon => 'warning', -message => $msg, -title => "Extern program(s) not available", -type => 'OK'); return 0; # if just one prog is missing we better abort } return 1; # everything seems to be there } ############################################################## # missingProgs - given a list of required external programs, # return a list of those that are missing ############################################################## sub missingProgs { my $sub = shift; # name of the calling sub my @neededProgs = @_; # list of needed external programs my @missingProgs; if (@neededProgs <= 0) { warn "missingProgs called from sub $sub with no progs to check!"; } else { foreach (@neededProgs) { if (!defined $exprogs{$_}) { warn "missingProgs called from sub $sub with program $_, which is not in the exprogs hash!"; push @missingProgs, $_; } elsif ($exprogs{$_} < 1) { push @missingProgs, $_; } } } return @missingProgs } ############################################################## # explainMissingProg - returns info about a missing program ############################################################## sub explainMissingProg { my $sub = shift; my $missingProg = shift; my $com = ''; my $res = ''; if (defined $exprogscom{$missingProg}) { $com = "$missingProg is needed to ".$exprogscom{$missingProg}."\n"; } if (defined $exprogsres{$missingProg}) { $res = "$missingProg resource: ".$exprogsres{$missingProg}."\n"; } return "Sorry, but to run $sub you need the external program $missingProg. I could not find $missingProg in your PATH.\n${com}${res}Aborting."; } ############################################################## # hlistEntryRename - rename the entrypath of an hlist entry # -> after this the entry is accessable via the new path # e.g. old: /home/user/pic1.jpg -> new: /home/user/myPic.jpg # Hint: this function does not change the displayed name! ############################################################## sub hlistEntryRename { my ($hlist, $old, $new ) = @_; return 0 unless ($hlist->info('exists', $old)); return 0 if ($hlist->info('exists', $new)); hlistCopy($hlist, $old, $new); $hlist->delete('entry', $old) if ($hlist->info('exists', $new)); return 1; } ############################################################## # hlistCopy - copy an item of a hlist to another position ############################################################## sub hlistCopy { my($hl, $from_entry, $to_entry) = @_; if ($hl->info('exists', $to_entry)) { return; } my @entry_args; foreach ($hl->entryconfigure($from_entry)) { push @entry_args, $_->[0] => $_->[4] if defined $_->[4]; } my $next = $hl->info('next', $from_entry); if ($next) {$hl->add($to_entry, @entry_args, -before => $next);} else {$hl->add($to_entry, @entry_args);} foreach my $col (1 .. $hl->cget(-columns)-1) { my @item_args; foreach ($hl->itemConfigure($from_entry, $col)) { push @item_args, $_->[0] => $_->[4] if defined $_->[4]; } $hl->itemCreate($to_entry, $col, @item_args); } } ############################################################## # startStopClock - starts and stops the clock, display # and remove the clock label ############################################################## sub startStopClock { if ($conf{show_clock}{value}) { # 1000ms = 1 second $clocktimer = $top->repeat(1000, \&showTimeOrMemory) if !$clocktimer; $clockL->pack(-side => 'left'); showTimeOrMemory(); } else { $clocktimer->cancel if $clocktimer; $time = ''; $date = ''; $clockL->packForget() if (Exists($clockL)); } } ############################################################## # showTimeOrMemory - calculate actual time or memory usage and # display it in clockL label ############################################################## sub showTimeOrMemory { return unless (Exists($clockL)); if ($conf{clock_or_memory}{value}) { my $procTabAvail = (eval {require Proc::ProcessTable}) ? 1 : 0 ; $time = 'n.a.'; $time = int(getMemoryUsage()/(1024*1024))."MB" if $procTabAvail; $date = "Memory usage of Mapivi.\nClick to show clock."; } else { my (undef,$m,$h,$d,$M,$y,$wd,undef, undef,undef) = localtime(time()); my @workday = qw/Sun Mon Tue Wed Thu Fri Sat/; $y += 1900; $M++; $time = sprintf "%02d:%02d", $h, $m; $date = sprintf "%3s, %02d.%02d.%04d\nClick to show memory.", $workday[$wd], $d, $M, $y; } $clockL->update; } my $htmlW; # global make-html window widget my $htmlInfo; ############################################################## # makeHTML - build HTML web pages from the selected pictures ############################################################## sub makeHTML { if (Exists($htmlW)) { $htmlW->deiconify; $htmlW->raise; return; } my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my ($pic); # open make html window $htmlW = $lb->Toplevel(); $htmlW->title("Build web pages"); $htmlW->iconimage($mapiviicon) if $mapiviicon; $htmlInfo = "Build HTML web pages of $selected selected pictures"; $htmlW->Label(-textvariable =>\$htmlInfo,-bg => $conf{color_bg}{value} )->pack(-side => 'top',-fill => 'x',-padx => 3,-pady => 3); my $w = 30; my $l1 = labeledEntry($htmlW, 'top', $w, "Title of Gallery", \$config{HTMLGalleryTitle}); my $l2 = labeledEntry($htmlW, 'top', $w, "Link to gallery index page", \$config{HTMLGalleryIndex}); my $l3 = labeledEntry($htmlW, 'top', $w, "Link to homepage", \$config{HTMLHomepage}); my $l4 = labeledEntry($htmlW, 'top', $w, "HTML footer", \$config{HTMLFooter}); my $l5 = labeledEntryButton($htmlW,'top',$w,"HTML target folder",'Set',\$config{HTMLTargetDir},1); my $l6 = labeledEntryButton($htmlW,'top',$w,"HTML template file",'Set', \$config{HTMLTemplate}); $balloon->attach($l1, -msg => "The content of this entry will be\ninserted in the field."); $balloon->attach($l2, -msg => "The content of this entry will be\ninserted in the field."); $balloon->attach($l3, -msg => "The content of this entry will be\ninserted in the field."); $balloon->attach($l4, -msg => "The content of this entry will be\ninserted in the field.\nIt may contain a link to your homepage\nand your email address."); $balloon->attach($l5, -msg => "Mapivi will save the generated\npages and pictures in this folder."); $balloon->attach($l6, -msg => "This is the used HTML template.\nThere are some example templates\nin the Mapivi package."); #labeledEntry($htmlW, 'top', $w, "Background of picture", \$config{HTMLBGcolor}); my $picF; $htmlW->Checkbutton(-variable => \$config{HTMLnoPicChange}, -anchor => 'w', -text => "Leave pictures untouched (just copy them)", -command => sub { my $state = 'normal'; $state = 'disabled' if ($config{HTMLnoPicChange}); setChildState($picF, $state); })->pack(-anchor => 'w'); $picF = $htmlW->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $picF->Label(-text =>"HTML pictures",-bg => $conf{color_bg}{value}, -anchor => 'w' )->pack(-side => 'top',-fill => 'x',-padx => 3,-pady => 3); #my $picF2 = $picF->Frame ()->pack(-expand => 1, -fill => 'x', -padx => 0, -pady => 0); my $sS = labeledScale($picF, 'top', $w, "Size (pixel)", \$config{HTMLPicSize}, 100, 2000, 1); $balloon->attach($sS, -msg => "This is the length of the longest side.\nWith a value of 500 a 1000x800 picture will be resized to 500x400."); my $qS = labeledScale($picF, 'top', $w, lang("Quality (%)"), \$config{HTMLPicQuality}, 10, 100, 1); qualityBalloon($qS); my $shS = labeledScale($picF, 'top', $w, "Sharpness (radius)", \$config{HTMLPicSharpen}, 0, 10, 0.1); $balloon->attach($shS, -msg => "The higher the value, the slower the conversion\n0 means no sharping.\n(suggestion: between 0 and 4)"); my $cof = $picF->Frame()->pack(-anchor => 'w'); $cof->Checkbutton(-variable => \$config{HTMLPicCopyright}, -anchor => 'w', -text => "Add some decorations (border, copyright)")->pack(-side => 'left', -anchor => 'w'); $cof->Button(-text => "Options", -anchor => 'w', -command => sub {decorationDialog($selected,0);})->pack(-side => 'left', -anchor => 'w'); $picF->Checkbutton(-variable => \$config{HTMLPicEXIF}, -anchor => 'w', -text => "Leave EXIF info in HTML pictures")->pack(-anchor => 'w'); labeledScale($htmlW, 'top', $w, "Number of thumbnail columns", \$config{HTMLcols}, 1, 10, 1); $htmlW->Checkbutton(-variable => \$config{HTMLaddComment}, -anchor => 'w', -text => "Show JPEG comments")->pack(-anchor => 'w'); $htmlW->Checkbutton(-variable => \$config{HTMLaddEXIF}, -anchor => 'w', -text => "Show EXIF infos")->pack(-anchor => 'w'); $htmlW->Checkbutton(-variable => \$config{HTMLaddIPTC}, -anchor => 'w', -text => "Show IPTC infos")->pack(-anchor => 'w'); my $ButF = $htmlW->Frame()->pack(-fill =>'x',-padx => 3,-pady => 3); my $OKB = $ButF->Button(-text => "Make HTML", -command => sub { return if ( !checkHTMLSettings() ); return if ( !makeHTMLSubdirs($config{HTMLTargetDir}) ); $lb->update; #my @pics ; #foreach (@sellist){ #push @pics, basename($_); #} # because the building of web galleries should also work # within the search dialog we can't throw away the path here cleanHTMLDirs($config{HTMLTargetDir}, @sellist); return if ( !makeHTMLPics(\%config, @sellist) ); $lb->update; return if ( !copyHTMLThumbs($config{HTMLTargetDir}, @sellist) ); my $table = makeHTMLIndex(\%config, @sellist); makeHTMLPages($table, \%config, @sellist); $htmlInfo = "make web pages - Ready!"; $htmlW->update; $htmlW->messageBox(-icon => 'info', -message => "Finished building web pages and pictures!", -title => "make HTML", -type => 'OK'); # bring the make html dialog window in front $htmlW->deiconify; $htmlW->raise; } )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); $ButF->Button(-text => "Close", -command => sub { $htmlW->withdraw(); $htmlW->destroy(); } )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); $htmlW->bind('', sub { $htmlW->destroy; } ); my $state = 'normal'; $state = 'disabled' if ($config{HTMLnoPicChange}); setChildState($picF, $state); $OKB->focus; $htmlW->Popup; $htmlW->waitWindow; } ############################################################## # checkHTMLSettings ############################################################## sub checkHTMLSettings { my $targetDir = $config{HTMLTargetDir}; print "checkHTMLSettings: $targetDir\n" if $verbose; if (!-d $targetDir) { my $rc = $htmlW->messageBox(-icon => 'question', -message => "$targetDir does not exists!\nShould I create it?!", -title => "check HTML settings", -type => 'OKCancel'); if ($rc !~ m/Ok/i) { return 0; } if ( !mkdir "$targetDir", oct(755) ) { $htmlW->messageBox(-icon => 'warning', -message => "can not create $targetDir: $!", -title => 'Error', -type => 'OK'); return 0; } } return 1; } ############################################################## # copyHTMLThumbs ############################################################## sub copyHTMLThumbs { my $targetDir = shift; my @pics = @_; my ($sthumb, $tthumb); # copy the pictures to the config dir foreach my $dpic (@pics) { my $pic = basename($dpic); $sthumb = getThumbFileName($dpic); $tthumb = "$targetDir/$HTMLThumbDir/$pic"; if (!-f $sthumb) { $htmlW->messageBox(-icon => 'warning', -message => "$sthumb not found! Stopping!", -title => "copy thumbs", -type => 'OK'); return 0; } if (!aNewerThanb($sthumb,$tthumb)) { print "skip thumb $pic (it is up to date)\n" if $verbose; next; } else { print "copy thumb $pic\n" if $verbose; } $htmlInfo = "copy thumb $pic for HTML page ..."; $htmlW->update; mycopy("$sthumb", "$tthumb", OVERWRITE); } return 1; } ############################################################## # makeHTMLSubdirs ############################################################## sub makeHTMLSubdirs { my $tdir = shift; # make pic and thumb dir foreach my $dir ($HTMLPicDir, $HTMLThumbDir) { my $sdir = "$tdir/$dir"; if (!-d $sdir) { if ( !mkdir $sdir, oct(755) ) { $htmlW->messageBox(-icon => 'warning', -message => "makeThumbSubdirs: can not create $sdir: $!", -title => 'Error', -type => 'OK'); return 0; } } } return 1; } ############################################################## # makeHTMLPics ############################################################## sub makeHTMLPics { my $tmpconfR = shift; my @pics = @_; my $targetDir = $tmpconfR->{'HTMLTargetDir'}; my $i = 0; my $nrpics = @pics; foreach my $dpic (@pics) { $i++; my $pic = basename($dpic); my $tpic = "$targetDir/$HTMLPicDir/$pic"; if (!-f $dpic) { warn "makeHTMLPics: $dpic does not exist!"; return 0; } if (!aNewerThanb($dpic,$tpic)) { warn "makeHTMLPics: $tpic is up to date - skipping\n" if $verbose; next; } else { warn "makeHTMLPics: converting $pic\n" if $verbose; } # just copy the pics ... if ($tmpconfR->{'HTMLnoPicChange'}) { $htmlInfo = "copy $pic ($i/$nrpics) for HTML page ..."; $htmlW->update; mycopy("$dpic", "$tpic", OVERWRITE); } # ... or convert them else { # adding -size XxY speeds up the convertion! (Dan Eble) my $command = " convert -size \"$tmpconfR->{'HTMLPicSize'}x$tmpconfR->{'HTMLPicSize'}\" -geometry \"$tmpconfR->{'HTMLPicSize'}x$tmpconfR->{'HTMLPicSize'}>\" -quality $tmpconfR->{'HTMLPicQuality'} "; if ($tmpconfR->{HTMLPicSharpen} > 0) { # ! Sharpen is the most time consuming option, when building thumbnails! $command .= "-sharpen $tmpconfR->{'HTMLPicSharpen'} " # the higher the value the slower the conversion } if ($tmpconfR->{HTMLPicCopyright} > 0) { $command .= makeDrawOptions($dpic); } $command .= " \"$dpic\" \"$tpic\" "; $htmlInfo = "converting $pic ($i/$nrpics) for HTML page ..."; $htmlW->update; #(system "$command") == 0 or warn "$command failed: $!"; execute($command); addDropShadow($tpic); if ($tmpconfR->{HTMLPicEXIF}) { # copy the EXIF header from the original pic to the html pic copyEXIF( $dpic, $tpic ); } else { # remove the EXIF header and thumb from the HTML pic my $errors = ''; removeEXIF($tpic, 'all', \$errors); } } } return 1; } ############################################################## # makeHTMLIndex ############################################################## sub makeHTMLIndex { my $tmpconfR = shift; my @pics = @_; my $targetDir = $tmpconfR->{'HTMLTargetDir'}; my $table = "\n"; my $i = 0; $htmlInfo = "building HTML thumbnail index ..."; $htmlW->update; foreach my $opic (@pics) { $i++; my $pic = basename($opic); if ( $i % $tmpconfR->{HTMLcols} == 1 or $tmpconfR->{HTMLcols} == 1 ) { # start new table row (modulo) $table .= "\n"; } #$lpic = "$HTMLPicDir/$pic"; my $dpic = "$targetDir/$HTMLPicDir/$pic"; my $lthumb = "$HTMLThumbDir/$pic"; my $size = getFileSize($dpic, FORMAT); my ($tx, $ty)= getSize("$targetDir/$lthumb"); my $picNoSuffix = $pic; # cut off trailing ".jpg" $picNoSuffix =~ s/\..*$//i; # this is the name of the picture without .jpg suffix my $title = getIPTCObjectName($opic); $title = "$picNoSuffix" if ($title eq ''); $title .= " ($size)"; # replace (german) umlaute by corresponding HTML-tags $title =~ s/([$umlauteHTML])/$umlauteHTML{$1}/g; my $htmlfile = ($i == 1) ? "index.html" : "$picNoSuffix.html"; $table .= "\n"; if ( $i % $tmpconfR->{HTMLcols} == 0 ) { # end table row (modulo) $table .= "\n"; } } $table .= "
\n"; $table .= "\n"; $table .= " \"$pic\"\n"; $table .= "\n"; $table .= "
\n"; return $table; } ############################################################## # createReplacementHashForPic ############################################################## sub createReplacementHashForPic { my $tmpconfR = shift; my $opic = shift; my $pic = basename($opic); my $targetDir = $tmpconfR->{'HTMLTargetDir'}; my $dpic = "$targetDir/$HTMLPicDir/$pic"; my $tpic = "$targetDir/$HTMLThumbDir/$pic"; my $picNoSuffix = $pic; $picNoSuffix =~ s/\..*$//i; my $size = getFileSize($dpic, FORMAT); my ($w, $h) = getSize($dpic); my ($thumbw, $thumbh)= getSize($tpic); my $title = getIPTCObjectName($opic); $title = $picNoSuffix if ($title eq ''); my $IPTCheadline = getIPTCHeadline($opic); my $headline = $IPTCheadline; $headline = $title if ($headline eq ''); my $com = ''; if ($tmpconfR->{'HTMLaddComment'}) { # only the first comment is copied by jhead, so we use the comment(s) of the original picture $com = getComment($opic, 3); # allows big comments (up to 1000 chars) $com =~ s/\n/
/g; # replace newline with the corresponding html tag } my $IPTCcaption = getIPTCCaption($opic); $IPTCcaption =~ s/\n/
/g; # replace newline with the corresponding html tag # caption comes from either the IPTC caption or the JPEG comment my $caption = $IPTCcaption; $caption = $com if ($caption eq ''); my $byline = getIPTCByLine($opic); my $bylinetitle = getIPTCByLineTitle($opic); $bylinetitle .= ": " if ($bylinetitle ne ''); $byline = $bylinetitle.$byline if ($byline ne ''); my $location = getIPTCSublocation($opic); my $city = ''; $city = getIPTCCity($opic); if ($city ne '') { $location .= ", " if ($location ne ''); $location .= $city; } my $province = ''; $province = getIPTCProvince($opic); my $country = ''; $country = getIPTCAttr($opic, "Country/PrimaryLocationName");#getIPTCCountryCode($opic); if ($country ne '') { $province .= ", " if ($province ne ''); $province .= $country; } if ($province ne '') { if ($location ne '') { $location .= " ($province)"; } else { $location = $province; } } my $exif = ''; $exif = getShortEXIF($opic, NO_WRAP) if ($tmpconfR->{'HTMLaddEXIF'}); $exif =~ s/\[t\]//g; # remove thumbnail indicator [t] $exif =~ s/\[s\]//g; # remove saved exif indicator [s] my $iptc = ''; $iptc = getShortIPTC($opic, LONG) if ($tmpconfR->{'HTMLaddIPTC'}); # Escape special HTML characters, except in file names # and in purely numeric values (e.g. width). (by Dan Eble) foreach ($pic, $byline, $caption, $com, $exif, $size, $headline, $iptc, $IPTCcaption, $IPTCheadline, $location, $time, $title) { $_ =~ s/([$htmlChars])/$htmlChars{$1}/g; } my %replace; $replace{''} = $pic; $replace{''} = $byline; $replace{''} = $caption; $replace{''} = $com; $replace{''} = $exif; $replace{''}= $picNoSuffix; $replace{''} = $size; $replace{''} = $headline; $replace{''} = $h; $replace{''} = $iptc; $replace{''} = $IPTCcaption; $replace{''} = $IPTCheadline; $replace{''} = $location; $replace{''} = "$HTMLPicDir/$pic"; $replace{''} = $thumbh; $replace{''} = "$HTMLThumbDir/$pic"; $replace{''} = $thumbw; $replace{''} = $time; $replace{''} = $title; $replace{''} = $w; return %replace; } ############################################################## # makeHTMLPages ############################################################## sub makeHTMLPages { my $table = shift; my $tmpconfR = shift; my @pics = @_; my $targetDir = $tmpconfR->{'HTMLTargetDir'}; my ($pic, $htmlpage, $page, $next, $prev, $galtitle, %bigrep, $maxwidth, $maxheight); my $sum = @pics; $maxwidth = 0; $maxheight = 0; $galtitle = $tmpconfR->{HTMLGalleryTitle}; $galtitle =~ s/ / /g; # replace space by html tag non-breakable space my $index = 0; foreach my $dpic (@pics) { $pic = basename($dpic); $htmlInfo = "extracting data from $pic ..."; $htmlW->update; my %replace = createReplacementHashForPic($tmpconfR, $dpic); if ($replace{''} > $maxheight) { $maxheight = $replace{''}; } if ($replace{''} > $maxwidth) { $maxwidth = $replace{''}; } # Next and previous pages wrap around from end to beginning. my $previndex = ($index - 1) % $sum; my $nextindex = ($index + 1) % $sum; # File names for previous, current, and next page. # The first is "index.html" to simplify the URL of the album. $prev = $previndex ? basename($pics[$previndex]) : "index.html"; $htmlpage = $index ? basename($pics[$index]) : "index.html"; $next = $nextindex ? basename($pics[$nextindex]) : "index.html"; # change extensions to ".html" foreach ($prev, $htmlpage, $next) { $_ =~ s/\..*$/\.html/i; } $replace{''} = $index+1; $replace{''} = $next; $replace{''} = $htmlpage; $replace{''} = $prev; $bigrep{$pic} = \%replace; $index++; } my ($s,$m,$ho,$d,$mo,$y) = getDateTime($time); # build up the date time string my $date = sprintf "%02d.%02d.%04d", $d, $mo, $y; my $time = sprintf "%02d:%02d", $ho, $m; my $datetime = sprintf "%02d.%02d.%04d %02d:%02d", $d, $mo, $y, $ho, $m; my %globalReplace; $globalReplace{''} = $date; $globalReplace{''} = $datetime; $globalReplace{''} = $tmpconfR->{HTMLFooter}; $globalReplace{''}= $tmpconfR->{HTMLGalleryIndex}; $globalReplace{''} = $galtitle; $globalReplace{''} = $tmpconfR->{HTMLHomepage}; $globalReplace{''} = $mapiviInfo; $globalReplace{''} = $maxheight; $globalReplace{''} = $sum; $globalReplace{''} = $maxwidth; $globalReplace{''} = $table; my $first_page; foreach my $dpic (@pics) { $pic = basename($dpic); $htmlpage = $bigrep{$pic}{''}; print "xxx pic=$pic html=$htmlpage ($dpic)\n" if $verbose; $htmlInfo = "building page $htmlpage ..."; $htmlW->update; $page = openTemplate($tmpconfR->{HTMLTemplate}); last if (!defined $page); # jump out # do global substitutions first so that they will not have # to be replaced for each expansion of $page = doSubstitutions($page, \%globalReplace); my $re; my @left = ('(',''); my @right = (')',''); $_ = $page; # find the text inside of sections ($re=$_)=~s/(()|(<\/mapivi:foreachpic>)|.)/$right[!$3]\Q$1\E$left[!$2]/gs; my @inside = (eval{/$re/},$@!~/unmatched/i); # find the text outside of sections ($re=$_)=~s/(()|(<\/mapivi:foreachpic>)|.)/$right[!$2]\Q$1\E$left[!$3]/gs; $re = "(" . $re . ")"; my @outside = (eval{/$re/},$@!~/unmatched/i); # if the sections were parsed without error, # process the templates inside the tags if ($inside[-1] && $outside[-1] && ($#inside+1 == $#outside)) { $page = ''; for (0..$#inside-1) { $page .= $outside[$_] . substituteForEachPic($tmpconfR, $inside[$_], \%bigrep, @pics); } $page .= $outside[-2]; } $page = doSubstitutions($page, $bigrep{$pic}); writePage("$targetDir/$htmlpage", $page); $first_page = "$targetDir/$htmlpage" if (!defined $first_page); $top->update; } if (defined $first_page and -f $first_page) { web_browser_open($first_page); } } ############################################################## # doSubstitutions # Input: the pageContent string (from template), followed by hash of # substitutions to make ############################################################## sub doSubstitutions { my ($pageContent, $replaceR )= @_; my($tag, $replacement); while (($tag, $replacement) = each(%$replaceR)) { warn "doSubstitutions: tag not defined" unless defined $tag; warn "doSubstitutions: $tag replacement not defined" unless defined $replacement; $pageContent =~ s/$tag/$replacement/g; } # replace (german) umlaute by corresponding html-tags $pageContent =~ s/([$umlauteHTML])/$umlauteHTML{$1}/g; return $pageContent; } ############################################################## # substituteForEachPic ############################################################## sub substituteForEachPic { my $tmpconfR = shift; my $template = shift; my $bigrepR = shift; my @pics = @_; my $result = ''; my $pic; foreach my $dpic (@pics) { $pic = basename($dpic); $result .= doSubstitutions($template, $$bigrepR{$pic}); } return $result; } ############################################################## # openTemplate - open, read and return template ############################################################## sub openTemplate { my $template = shift; my $file; if (!open($file, '<', $template)) { $htmlW->messageBox(-icon => 'error', -message => "cannot open template $template for reading: $!", -title => 'Error', -type => 'OK'); print "openTemplate: cannot open template $template for reading: ($!)\n"; return; } my $pageContent = (join '', <$file>); close ($file) || bail ("can't close template: ($!)"); return $pageContent; } ############################################################## # writePage - input path of page to render, not including $root ############################################################## sub writePage { # Spits out a page of HTML. my($file, $pageContent) = @_; my $outfile; # todo: Mapivi should not die here open($outfile, '>', $file) or die "Couldn't open $file: $!"; print $outfile $pageContent; close($outfile); } ############################################################## # cleanHTMLDirs - delete all files which are not needed anymore ############################################################## sub cleanHTMLDirs { my $targetDir = shift; my @dpics = @_; my @picsAct; my @toDelete; my $rc; my $pictures; # clean html files my @htmlfiles = grep {m/.*\.html$/i} getFiles($targetDir); if (@htmlfiles >= 1) { $rc = $htmlW->messageBox(-icon => 'question', -message => scalar @htmlfiles." HTML pages should be deleted in $targetDir.\nOk, to delete?", -title => "clean up HTML folders", -type => 'OKCancel'); if ($rc eq 'Ok') { foreach (@htmlfiles) { removeFile("$targetDir/$_"); } } } # clean pictures and thumbs foreach my $dir ("$targetDir/$HTMLPicDir", "$targetDir/$HTMLThumbDir") { @picsAct = getPics($dir, JUST_FILE, NO_CHECK_JPEG); # no sort needed my @pics; # now we need the pics list without path push @pics, basename($_) foreach (@dpics); @toDelete = diffList(\@picsAct, \@pics); next if (@toDelete < 1); # choose the right word depending on the dir $pictures = "pictures"; $pictures = "thumbnails" if ($dir =~ m/$HTMLThumbDir$/); $rc = $htmlW->messageBox(-icon => 'question', -message => scalar @toDelete." $pictures should be deleted in\n$dir\nOk, to delete?", -title => "clean up HTML folders", -type => 'OKCancel'); if ($rc !~ m/Ok/i) { next; } foreach (@toDelete) { removeFile ("$dir/$_"); } } } ############################################################## # function test for cut_list() ############################################################## #sub cut_list_test { # for (0 .. 10) { # my @list = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); # cut_list(\@list, $_); # if (@list != $_) { # print "Error test case $_: "; print "$_, " foreach @list; print "\n"; # } # else { # print "test $_ ok!\n"; # } # } #} ############################################################## # cut_list - operates directly on referenced list ############################################################## sub cut_list { my ($list, $nr) = @_; #print "nr = $nr list elements: ".scalar(@$list)."\n"; # nothing to do return if ($nr < 0); if ($nr == 0) { @$list = (); return; } return if ($nr > scalar @$list); # if there are less elements to remove than elements to preserve we pop from the end if ((scalar @$list - $nr) < $nr) { #print " poping ...\n"; pop @$list for (scalar $nr .. @$list-1); } else { # else we build a new list from the start #print " pushing ...\n"; my @new; push @new, $$list[$_] for (0 .. $nr-1); @$list = (); @$list = @new; } #print "nr = $nr list elements: ".scalar(@$list)."\n"; } ############################################################## # diffList - returns a list containing all elements of list1 # which are not in list2 (removes the elements of list2 from list1) ############################################################## sub diffList { my $list1Ref = shift; # reference to first list my $list2Ref = shift; # reference to second list return () unless (@{$list1Ref}); return (@{$list1Ref}) unless (@{$list2Ref}); # build a hash my %d; $d{$_}++ foreach (@{$list1Ref}); # delete all elements in hash, which are in list2 foreach (@{$list2Ref}) { delete $d{$_} if (exists $d{$_}); } return (keys %d); } ############################################################## # listIntersection - returns a list containing all elements # of list1 which are also in list2 ############################################################## sub listIntersection { my $list1Ref = shift; # reference to first list my $list2Ref = shift; # reference to second list my (@intersection, %count); foreach my $element (@{$list1Ref}, @{$list2Ref}) { $count{$element}++; } foreach my $element (keys %count) { push @intersection, $element if ($count{$element} > 1); } return @intersection; } ############################################################## # dirDiffWindow ############################################################## sub dirDiffWindow { if (Exists($ddw)) { $ddw->deiconify; $ddw->raise; $ddw->focus; return; } # open window $ddw = $top->Toplevel(); $ddw->withdraw; $ddw->title(lang("Compare folders")); $ddw->iconimage($mapiviicon) if $mapiviicon; my $f1 = $ddw->Frame()->pack(-fill => 'x', -padx => 3, -pady => 3); my $f1a = $f1->Frame()->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 0, -pady => 0); my $f1b = $f1->Frame()->pack(-side => 'left', -fill => 'y', -padx => 0, -pady => 0); my $f2 = $ddw->Frame()->pack(-fill => 'x', -padx => 2, -pady => 3); my $f2a = $f2->Frame()->pack(-side => 'top', -fill => 'y', -expand => 0, -padx => 3, -pady => 3, -anchor => 'w'); my $f2b = $f2->Frame()->pack(-side => 'top', -fill => 'both', -expand => 1, -padx => 3, -pady => 3, -anchor => 'w'); my $ddlb; $ddw->Label(-textvariable => \$ddw->{label}, -anchor => 'w', -bg => $conf{color_bg}{value})->pack(-padx => 3, -anchor => 'w'); $ddw->{label} = 'Choose folders to compare and press the "Compare" button.'; my $dir_a_entry = labeledEntryButton($f1a,'top',15,lang("Folder")." A",'Set',\$config{dirDiffDirA},1); my $dir_b_entry = labeledEntryButton($f1a,'top',15,lang("Folder")." B",'Set',\$config{dirDiffDirB},1); my $entry_menu = $ddw->Menu(-title => lang("Entry Menu")); $entry_menu->command(-label => "A: Use current folder", -command => sub { $config{dirDiffDirA} = $actdir; }); $entry_menu->command(-label => "A: Use B folder", -command => sub { $config{dirDiffDirA} = $config{dirDiffDirB}; }); $entry_menu->command(-label => "B: Use current folder", -command => sub { $config{dirDiffDirB} = $actdir; }); $entry_menu->command(-label => "B: Use A folder", -command => sub { $config{dirDiffDirB} = $config{dirDiffDirA}; }); $ddlb = $ddw->Scrolled("HList", -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 0, -columns => 12, -scrollbars => 'osoe', -selectmode => "extended", -background => $conf{color_bg}{value}, #8fa8bf -width => 40, -height => 20, )->pack(-expand => 1, -fill => 'both'); # key-i opens IPTC data of both pictures $ddlb->bind('', sub { return unless ($ddlb->info('children')); my @sellist = $ddlb->info('selection'); return unless (@sellist); foreach (@sellist) { my $pic = $ddlb->itemCget($_, $ddlb->{namecol}, -text); foreach my $dir (qw(dirDiffDirA dirDiffDirB)) { my $dpic = $config{$dir}."/$pic"; my $title = "IPTC/IIM info of $pic"; my $iptc = "File: $dpic\n".getIPTC($dpic, LONG); if ($iptc eq '') { $iptc = "Found no IPTC/IIM info in \"$dpic\"\n"; } showText($title, $iptc, NO_WAIT); } } } ); $balloon->attach($ddlb, -msg => "left click : select\nmiddle click: Open picture in new window\nright click : open context menu\nkey i : show IPTC info"); my $col = 0; $ddlb->header('create', $col, -text => 'Differences', -headerbackground => $conf{color_entry}{value}); $ddlb->{diffcol} = $col; $col++; $ddlb->header('create', $col, -text => 'Name', -headerbackground => $conf{color_entry}{value}); $ddlb->{namecol} = $col; $col++; $ddlb->header('create', $col, -text => 'Thumbnail A', -headerbackground => $conf{color_entry}{value}); $ddlb->{thumbAcol} = $col; $col++; $ddlb->header('create', $col, -text => 'Thumbnail B', -headerbackground => $conf{color_entry}{value}); $ddlb->{thumbBcol} = $col; $col++; $ddlb->header('create', $col, -text => 'Size A', -headerbackground => $conf{color_entry}{value}); $ddlb->{sizeAcol} = $col; $col++; $ddlb->header('create', $col, -text => 'Size B', -headerbackground => $conf{color_entry}{value}); $ddlb->{sizeBcol} = $col; $col++; $ddlb->header('create', $col, -text => 'IPTC A', -headerbackground => $conf{color_entry}{value}); $ddlb->{iptcAcol} = $col; $col++; $ddlb->header('create', $col, -text => 'IPTC B', -headerbackground => $conf{color_entry}{value}); $ddlb->{iptcBcol} = $col; $col++; $ddlb->header('create', $col, -text => 'EXIF A', -headerbackground => $conf{color_entry}{value}); $ddlb->{exifAcol} = $col; $col++; $ddlb->header('create', $col, -text => 'EXIF B', -headerbackground => $conf{color_entry}{value}); $ddlb->{exifBcol} = $col; $col++; $ddlb->header('create', $col, -text => 'Comments A', -headerbackground => $conf{color_entry}{value}); $ddlb->{comAcol} = $col; $col++; $ddlb->header('create', $col, -text => 'Comments B', -headerbackground => $conf{color_entry}{value}); $ddlb->{comBcol} = $col; $col++; my $progress = 0; $f1b->Button(-image => $mapivi_icons{Preferences}, #-text => "Set", -command => sub { $entry_menu->Popup(-popover => 'cursor', -popanchor => 'nw'); })->pack(-fill => 'y', -side => 'left'); $f1b->Button(-text => lang("Compare"), -command => sub { # check both dirs first foreach ($config{dirDiffDirA}, $config{dirDiffDirB}) { unless (-d $_) { $ddw->messageBox(-icon => 'warning', -message => langf("Folder \"%s\" is not valid!",$_), -title => lang('Error'), -type => 'OK'); return; } } if ($config{dirDiffDirA} eq $config{dirDiffDirB}) { $ddw->messageBox(-icon => 'warning', -message => lang("Please choose two different folders!"), -title => lang('Error'), -type => 'OK'); return; } $ddw->Busy; $ddlb->delete("all"); # clear listbox my (@onlyInDirA, @onlyInDirB, @intersec); dirDiff($config{dirDiffDirA}, $config{dirDiffDirB}, \@onlyInDirA, \@onlyInDirB, \@intersec); $ddw->{label} = langf("Found %d unique pictures in A, %d unique pictures in B and %d matching pictures",scalar @onlyInDirA,scalar @onlyInDirB,scalar @intersec); $ddw->update; my $pics = @onlyInDirA + @onlyInDirB + @intersec; my $last_time; my $i = 0; foreach my $pic (sort @onlyInDirA) { my $dpic = $config{dirDiffDirA}."/$pic"; ddInsertPic($ddlb, $dpic, '', 'only in dir A'); $i++; # show progress and found pics every 0.5 seconds - idea from Slaven if (!defined $last_time || Tk::timeofday()-$last_time > 0.5) { $progress = int($i/$pics*100); $ddw->update; $last_time = Tk::timeofday(); } } foreach my $pic (sort @onlyInDirB) { my $dpic = $config{dirDiffDirB}."/$pic"; ddInsertPic($ddlb, '', $dpic, 'only in dir B'); $i++; # show progress and found pics every 0.5 seconds - idea from Slaven if (!defined $last_time || Tk::timeofday()-$last_time > 0.5) { $progress = int($i/$pics*100); $ddw->update; $last_time = Tk::timeofday(); } } my $inter = 0; foreach my $pic (sort @intersec) { my $dpicA = $config{dirDiffDirA}."/$pic"; my $dpicB = $config{dirDiffDirB}."/$pic"; my $differences = ''; if (compareTwoPics($dpicA, $dpicB, \$differences)) { ddInsertPic($ddlb, $dpicA, $dpicB, $differences); $inter++; } $i++; # show progress and found pics every 0.5 seconds - idea from Slaven if (!defined $last_time || Tk::timeofday()-$last_time > 0.5) { $progress = int($i/$pics*100); $ddw->update; $last_time = Tk::timeofday(); } } $progress = 100; $ddw->{label} .= langf(" (%d of them differ)",$inter); $ddw->Unbusy; })->pack(-fill => 'y', -side => 'left'); $f1b->Button(-text => lang("Close"), -command => sub { $ddw->destroy; })->pack(-fill => 'y', -side => 'left'); $f2a->Label(-text => lang("Compare by "), -anchor => 'w', -bg => $conf{color_bg}{value})->pack(-side => 'left', -padx => 3); $f2a->Checkbutton(-variable => \$config{dirDiffSize}, -text => lang("File size"))->pack(-side => 'left'); $f2a->Checkbutton(-variable => \$config{dirDiffPixel}, -text => lang("Number of pixels"))->pack(-side => 'left'); $f2a->Checkbutton(-variable => \$config{dirDiffComment}, -text => lang("Comments"))->pack(-side => 'left'); $f2a->Checkbutton(-variable => \$config{dirDiffEXIF}, -text => "EXIF")->pack(-side => 'left'); $f2a->Checkbutton(-variable => \$config{dirDiffIPTC}, -text => "IPTC")->pack(-side => 'left'); $f2b->Button(-text => lang("Copy A->B"), -command => sub { return unless ($ddlb->info('children')); my @sellist = $ddlb->info('selection'); return unless (@sellist); my $i = 0; my $overwrite = OVERWRITE; my $n = 0; foreach (@sellist) { my $pic = $ddlb->itemCget($_, $ddlb->{namecol}, -text); $i++; $ddw->{label} = "copy ($i/".scalar @sellist.") ... "; $ddw->update; my $dpic = $config{dirDiffDirA}."/$pic"; next unless (-f $dpic); my $tpic = $config{dirDiffDirB}."/$pic"; # if the pic exists, ask if the user wants to overwrite it $overwrite = overwritePic($tpic, $dpic, (scalar(@sellist) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); if (mycopy($dpic, $tpic, OVERWRITE)) { # copy pic $n++; my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { mycopy($thumbpic,$thumbtpic, OVERWRITE) # copy thumbnail } $ddlb->delete("entry", $_); # remove entry from list box } } # foreach - end $ddw->{label} = "ready! ($n/".scalar @sellist." copied)"; $ddw->update; })->pack(-fill => 'x', -side => 'left', -padx => 2, -pady => 2); $f2b->Button(-text => lang("Copy A<-B"), -command => sub { return unless ($ddlb->info('children')); my @sellist = $ddlb->info('selection'); return unless (@sellist); my $i = 0; my $overwrite = OVERWRITE; my $n = 0; foreach (@sellist) { my $pic = $ddlb->itemCget($_, $ddlb->{namecol}, -text); $i++; $ddw->{label} = "copy ($i/".scalar @sellist.") ... "; $ddw->update; my $dpic = $config{dirDiffDirB}."/$pic"; next unless (-f $dpic); my $tpic = $config{dirDiffDirA}."/$pic"; # if the pic exists, ask if the user wants to overwrite it $overwrite = overwritePic($tpic, $dpic, (scalar(@sellist) - $i + 1)) if ($overwrite != OVERWRITEALL); next if ($overwrite == CANCEL); last if ($overwrite == CANCELALL); if (mycopy($dpic, $tpic, OVERWRITE)) { # copy pic $n++; my $thumbpic = getThumbFileName($dpic); my $thumbtpic = getThumbFileName($tpic); if ((-d dirname($thumbtpic)) and (-f $thumbpic)) { mycopy($thumbpic, $thumbtpic, OVERWRITE) # copy thumbnail } $ddlb->delete("entry", $_); # remove entry from list box } } # foreach - end $ddw->{label} = "ready! ($n/".scalar @sellist." copied)"; $ddw->update; })->pack(-fill => 'x', -side => 'left', -padx => 2, -pady => 2); $f2b->Button(-text => lang("Delete A"), -command => sub { return unless ($ddlb->info('children')); my @sellist = $ddlb->info('selection'); return unless (@sellist); my $rc = $ddw->messageBox(-message => "Really delete ".scalar @sellist." pictures in folder ".$config{dirDiffDirA}."?", -icon => 'question', -title => "Really delete?", -type => 'OKCancel'); return unless ($rc =~ m/Ok/i); my $i = 0; my $n = 0; foreach (@sellist) { my $pic = $ddlb->itemCget($_, $ddlb->{namecol}, -text); $i++; $ddw->{label} = "deleting ($i/".scalar @sellist.") ... "; $ddw->update; my $dpic = $config{dirDiffDirA}."/$pic"; unless (-f $dpic) { print "$dpic not found!\n"; next;} if (move($dpic, $trashdir)) { # move pic to trash $n++; my $tpic = "$trashdir/$pic"; # change the location info in the search database $searchDB{$tpic} = $searchDB{$dpic}; delete $searchDB{$dpic}; deleteCachedPics($dpic); # todo move thumbnail? # todo deleting the entry is wrong, if picture exists in both dirs $ddlb->delete("entry", $_); # remove entry from list box } } # foreach - end $ddw->{label} = "ready! ($n/".scalar @sellist." deleted)"; $ddw->update; })->pack(-fill => 'x', -side => 'left', -padx => 2, -pady => 2); $f2b->Button(-text => lang("Delete B"), -command => sub { return unless ($ddlb->info('children')); my @sellist = $ddlb->info('selection'); return unless (@sellist); my $rc = $ddw->messageBox(-message => "Really delete ".scalar @sellist." pictures in folder ".$config{dirDiffDirB}."?", -icon => 'question', -title => "Really delete?", -type => 'OKCancel'); return unless ($rc =~ m/Ok/i); my $i = 0; my $n = 0; foreach (@sellist) { my $pic = $ddlb->itemCget($_, $ddlb->{namecol}, -text); $i++; $ddw->{label} = "deleting ($i/".scalar @sellist.") ... "; $ddw->update; my $dpic = $config{dirDiffDirB}."/$pic"; unless (-f $dpic) { print "$dpic not found!\n"; next;} if (move($dpic, $trashdir)) { # move pic to trash $n++; my $tpic = "$trashdir/$pic"; # change the location info in the search database $searchDB{$tpic} = $searchDB{$dpic}; delete $searchDB{$dpic}; deleteCachedPics($dpic); # todo move thumbnail? # todo deleting the entry is wrong, if picture exists in both dirs $ddlb->delete("entry", $_); # remove entry from list box } } # foreach - end $ddw->{label} = "ready! ($n/".scalar @sellist." deleted)"; $ddw->update; })->pack(-fill => 'x', -side => 'left', -padx => 2, -pady => 2); my $progBar = $f2b->ProgressBar(-takefocus => 0, -borderwidth => 1, -relief => 'sunken', -length => 100, -height => 5, -padx => 0, -pady => 0, -variable => \$progress, -colors => [0 => $config{ColorProgress}], -resolution => 1, -blocks => 10, -anchor => 'w', -from => 0, -to => 100, )->pack(-side => 'left', -expand => 1, -fill => 'y', -padx => 3, -pady => 3, -anchor => 'w'); $balloon->attach($progBar, -msg => lang("Compare progress")); my $ws = 0.7; my $w = int($ws * $ddw->screenwidth); my $h = int($ws * $ddw->screenheight); my $x = int(((1 - $ws) * $ddw->screenwidth)/3); my $y = int(((1 - $ws) * $ddw->screenheight)/3); #print "geo==${w}x${h}+${x}+${y}\n"; $ddw->geometry("${w}x${h}+${x}+${y}"); $ddw->Popup; $ddw->waitWindow; } ############################################################## # compareTwoPics ############################################################## sub compareTwoPics { my $dpicA = shift; my $dpicB = shift; my $diff = shift; # Ref to differences my $rc = 0; # 0 = no difference 1 = pics are different if ($config{dirDiffSize} and (-s $dpicA != -s $dpicB)) { my $diff_bytes = getFileSize($dpicB, NO_FORMAT) - getFileSize($dpicA, NO_FORMAT); my $sign = '-'; $sign = '+' if ($diff_bytes > 0); if (abs($diff_bytes) > 1024) { $diff_bytes = computeUnit(abs($diff_bytes)); } else { $diff_bytes = abs($diff_bytes).'B'; } $$diff .= "file size ($sign$diff_bytes)\n"; $rc = 1; } if ($config{dirDiffComment} and (getComment($dpicA, LONG) ne getComment($dpicB, LONG))) { $$diff .= "comment\n"; $rc = 1; } if ($config{dirDiffEXIF} and (getShortEXIF($dpicA, NO_WRAP) ne getShortEXIF($dpicB, NO_WRAP))) { $$diff .= "EXIF\n"; $rc = 1; } if ($config{dirDiffIPTC} and (getIPTC($dpicA, SHORT) ne getIPTC($dpicB, SHORT))) { $$diff .= "IPTC\n"; $rc = 1; } if ($config{dirDiffPixel}) { my ($wa, $ha) = getSize($dpicA); my ($wb, $hb) = getSize($dpicB); if (($wa != $wb) or ($ha != $hb)) { $$diff .= "pixel size\n"; $rc = 1; } } return $rc; } ############################################################## # ddInsertPic - insert a row in the dir diff list ############################################################## sub ddInsertPic { my $lb = shift; my $dpicA = shift; # the dir A pic, empty string if non my $dpicB = shift; # the dir B pic, empty string if non my $reason = shift; # the difference if ((!-f $dpicA) and (!-f $dpicB)) { warn "both pics are missing!"; return; } my @childs = $lb->info('children'); my $count = 0; $count = @childs if (@childs); # create new row $lb->add($count); my (%ddthumbs, $rating_sizeA, $rating_sizeB, $comA, $comB, $exifA, $exifB, $iptcA, $iptcB); if (-f $dpicA) { $comA = getComment($dpicA, SHORT); $exifA = getShortEXIF($dpicA, WRAP); $iptcA = getShortIPTC($dpicA, SHORT); $rating_sizeA = get_rating_and_size($dpicA, $lb); my $thumbA = getThumbFileName($dpicA); if (-f $thumbA) { $ddthumbs{$thumbA} = $lb->Photo(-file => $thumbA, -gamma => $config{Gamma}); if (defined $ddthumbs{$thumbA}) { $lb->itemCreate($count, $lb->{thumbAcol}, -image => $ddthumbs{$thumbA}, -itemtype => "image"); } } } if (-f $dpicB) { $comB = getComment($dpicB, SHORT); $exifB = getShortEXIF($dpicB, WRAP); $iptcB = getShortIPTC($dpicB, SHORT); $rating_sizeB = get_rating_and_size($dpicB, $lb); my $thumbB = getThumbFileName($dpicB); if (-f $thumbB) { $ddthumbs{$thumbB} = $lb->Photo(-file => $thumbB, -gamma => $config{Gamma}); if (defined $ddthumbs{$thumbB}) { $lb->itemCreate($count, $lb->{thumbBcol}, -image => $ddthumbs{$thumbB}, -itemtype => "image"); } } } my $pic; if (-f $dpicA) { $pic = basename($dpicA); } else { $pic = basename($dpicB); } $lb->itemCreate($count, $lb->{diffcol}, -text => $reason, -style => $comS); $lb->itemCreate($count, $lb->{namecol}, -text => $pic, -style => $comS); $lb->itemCreate($count, $lb->{sizeAcol}, -itemtype => "image", -image => $rating_sizeA, -style => $fileS); $lb->itemCreate($count, $lb->{sizeBcol}, -itemtype => "image", -image => $rating_sizeB, -style => $fileS); $lb->itemCreate($count, $lb->{comAcol}, -text => $comA, -style => $comS); $lb->itemCreate($count, $lb->{comBcol}, -text => $comB, -style => $comS); $lb->itemCreate($count, $lb->{exifAcol}, -text => $exifA, -style => $exifS); $lb->itemCreate($count, $lb->{exifBcol}, -text => $exifB, -style => $exifS); $lb->itemCreate($count, $lb->{iptcAcol}, -text => $iptcA, -style => $iptcS); $lb->itemCreate($count, $lb->{iptcBcol}, -text => $iptcB, -style => $iptcS); } ############################################################## # dirDiff ############################################################## sub dirDiff { my $dir1 = shift; my $dir2 = shift; my $only1 = shift; # ref to array my $only2 = shift; # ref to array my $inter = shift; # ref to array return unless (-d $dir1); return unless (-d $dir2); my @pics1 = getPics($dir1, JUST_FILE, NO_CHECK_JPEG); # no sort needed my @pics2 = getPics($dir2, JUST_FILE, NO_CHECK_JPEG); # no sort needed @{$only1} = diffList(\@pics1, \@pics2); @{$only2} = diffList(\@pics2, \@pics1); @{$inter} = listIntersection(\@pics2, \@pics1); } ############################################################## # showkeys - show the key bindings ############################################################## sub showkeys { my $file; # open the file mapivi if (!open($file, '<', $0)) { warn "could not open $0 for read access!: $!"; return; } my @lines = <$file>; # read the complete file into the array lines close $file; my @keys; foreach my $line (@lines) { $line =~ s/\s+$//; # cut trailing whitespace $line =~ s/^\s+//; # cut leading whitespace # look for lines containing "key-desc" if ($line =~ m/.*key-desc.*/) { push @keys, $line; } } my $text; # sort the keys alphabetical foreach (sort { uc($a) cmp uc($b); } @keys) { my @a = split /,/, $_; if (@a != 3) { print "showKeys: suspicious line: $_\n"; next; } chomp($a[2]); $text .= sprintf "%-13s ... %s\n",$a[1], $a[2]; } my $title = lang("Key shortcuts"); showText($title, $text, NO_WAIT); } ############################################################## # buildDatabase - scans through all sub folders of # the actual dir an collects JPEG files # let the user select in which dirs # mapivi should build/refresh thumbnails ############################################################## sub buildDatabase { my $mydir = getRightDir(); my $rc = checkDialog( 'Add pictures to database in all sub folders', 'Mapivi will create a list of all sub folders of folder "'.basename($mydir).'" containing pictures. You are then able to select folders from the list.', \$config{SearchDBOnlyNew}, "add only new pictures", '', 'OK', 'Cancel'); return if ($rc ne 'OK'); log_it("searching sub folders ..."); my ($ok, $dirlist, $pic_count, $nr_of_pics_in_dir) = get_subdirs($mydir); log_it("Found ".scalar @{$dirlist}." folders with $pic_count pictures."); return if (not $ok); @{$dirlist} = sort @{$dirlist}; my @sellist; if (scalar @{$dirlist} == 1) { # no sub folder found -> no selection needed push @sellist, 0; # just add index number 0 to list } else { return if (!mySelListBoxDialog(lang("Select folders"), "Found ".scalar @{$dirlist}." folders with $pic_count JPEG pictures.\nThe database will be updated with the pictures of the selected folders.", MULTIPLE, "add to database", \@sellist, @{$dirlist})); } return if (not @sellist); # return if nothing is selected # copy the selected elements into the @sel_dirs list my @sel_dirs; my $sel_pic_count = 0; foreach (@sellist) { # sellist contains just the index numbers of the selected items push @sel_dirs, $$dirlist[$_]; # add number of pics in selected folders $sel_pic_count += $$nr_of_pics_in_dir{$$dirlist[$_]} } my $pw = progressWinInit($top, "add to database"); my $i = 0; my $new = 0; foreach my $dir (@sel_dirs) { last if progressWinCheck($pw); my $dirshort = cutString($dir, -40, "..."); print "build database recursive in $dir\n" if $verbose; my @dpics = getPics($dir, WITH_PATH, NO_CHECK_JPEG); # no sorting needed foreach (@dpics) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Adding metadata of picture ($i/$sel_pic_count) to database.\nProcessing folder $dirshort", $i, $sel_pic_count); next if ($config{SearchDBOnlyNew} and exists $searchDB{$_}); addToSearchDB($_); $new++; } } progressWinEnd($pw); log_it("database updated (scanned $i pictures, $new added)"); check_new_keywords(); } ############################################################## # cleanDatabase - remove all database entries of non existing # files ############################################################## sub cleanDatabase { my $count = 0; my $pics; my $ignoreText = ''; my $ignoreCount = 0; my $keys = keys %searchDB; my %ignorePaths = qw( /mnt/cdrom/ 1 ); # try to get the saved ignore paths if (-f "$user_data_path/ignorePaths") { my $hashRef = retrieve("$user_data_path/ignorePaths"); warn "could not retrieve ignorePaths" unless defined $hashRef; %ignorePaths = %{$hashRef}; } my $rc = editHashDialog(lang('Clean database'), 'This function will remove all invalid and outdated entries from the search database. When cleaning the database, all entries without an corresponding file will be removed. It is possible to exclude entries from cleaning depending on their path. This could be done e.g. for pictures on removable media like CDROMs or DVDs. Please add or remove paths from this list according to your file system. A typical entry for a linux system could be /mnt/cdrom', \%ignorePaths, lang('Start cleaning'), lang('Cancel'), 1 ); return if ($rc ne 'OK'); nstore(\%ignorePaths, "$user_data_path/ignorePaths") or warn "could not store ignorePaths"; log_it("cleaning database - please wait ..."); my $pw = progressWinInit($top, lang("Cleaning database")); my $i = 0; # loop through all database entries foreach my $pic (sort keys %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, lang("checking")." ($i/$keys) ...", $i, $keys); # if the pic path matches a path of @ignorePaths we skip the entry # this can be used to leave pictures in the database which are # located on removable media like CDs my $ignore = 0; foreach my $ipath (keys %ignorePaths) { if ($pic =~ m/^$ipath/) { $ignore = 1; $ignoreCount++; $ignoreText .= '('.lang('ignoring')." $pic)\n"; last; } } next if $ignore; # delete the picture from the database if it does not exists if (!-f $pic) { delete $searchDB{$pic}; $pics .= "$pic\n"; $count++; } } progressWinEnd($pw); log_it(lang("Cleaning database - ready")); my $text = "clean picture info database:\n\n"; if ($count > 0) { $text .= "Removed $count entries of non existing pictures:\n\n$pics"; } else { $text .= "Nothing to clean - database is up to date!\n\n"; } $keys = keys %searchDB; my $size = getFileSize($searchDBfile, FORMAT); $text .= "There are $keys entries in the database (file size: $size)\n\n"; $text .= "The following $ignoreCount entries have been ignored, because their path\nmatches a entry in the \%ignorePaths hash:\n\n$ignoreText" if ($ignoreText ne ''); showText(lang("Clean database"), $text, WAIT); } ############################################################## # cleanDatabaseFolder - clean the database in one folder ############################################################## sub cleanDatabaseFolder { my $directory = shift; log_it("updating database - please wait ..."); my $pw = progressWinInit($top, "updating search database"); my $i = 0; my $keys = keys %searchDB; # loop through all database entries foreach my $pic (keys %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "checking ($i/$keys) ...", $i, $keys); # if the pic path matches the given path # delete the picture from the database if it does not exists if (($pic =~ m/^$directory/) and (!-f $pic)) { delete $searchDB{$pic}; } } progressWinEnd($pw); log_it("database updated!"); } ############################################################## # renameDatabaseFolder - rename a folder in the database ############################################################## sub renameDatabaseFolder { my $olddir = shift; my $newdir = shift; log_it("updating database - please wait ..."); my $pw = progressWinInit($top, "updating search database"); my $i = 0; my $moved = 0; my $deleted = 0; my $keys = keys %searchDB; # loop through all database entries foreach my $dpic (keys %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "checking ($i/$keys) ...", $i, $keys); # if the pic path matches the old path if ($dpic =~ m/^$olddir/) { my $ndpic = $newdir.'/'.basename($dpic); if (-f $ndpic) { # add existing info to new hash entry $searchDB{$ndpic} = $searchDB{$dpic}; # and update the path info $searchDB{$ndpic}{odir} = $newdir; $moved++; } # delete the picture from the database delete $searchDB{$dpic}; $deleted++; } } progressWinEnd($pw); log_it("Database updated! Moved $moved pictures."); # safety check warn "renameDatabaseFolder: Moved $moved and deleted $deleted pictures. Both numbers should be the same!" if ($deleted > $moved); } ############################################################## # diff_database_statistic ############################################################## sub diff_database_statistic { log_it("generating database statistics ..."); my $statistic_file = "$user_data_path/database_statistic"; my $statistics_last; my $ok = 0; if (-f $statistic_file) { my $hashRef = retrieve($statistic_file); if (defined $hashRef) { $ok = 1; $statistics_last = $hashRef; } else { warn "could not retrieve $statistic_file"; } } my $statistics = database_statistic(keys(%statistic_data_longnames)); my $keys = keys %searchDB; if ($ok) { my $info = get_database_info().".\n\n"; foreach my $data_kind (sort keys %$statistics) { $info .= sprintf "%30s: %6d (%03.1f%%)\n", lang("pictures with ").$statistic_data_longnames{$data_kind}, $$statistics{$data_kind}, $$statistics{$data_kind}/$keys*100; } # retrieve the number of database entries at last check my $keys_last = $$statistics_last{'Nr_of_entries'}; if (defined $keys_last) { $info .= "\n".lang("Changes to last database statistic"); my $age = getAgeOfFile($statistic_file); $info .= langf(", which was %s ago", $age) if ($age ne ''); $info .= ":\n"; my $keys_diff = $keys-$keys_last; my $sign = ''; $sign = '+' if ($keys_diff > 0); my $diff_info = ''; $diff_info = sprintf "%30s: %s%d\n", lang("pictures"), $sign, $keys_diff if ($keys_diff != 0); foreach my $data_kind (sort keys %$statistics) { my $diff = $$statistics{$data_kind}-$$statistics_last{$data_kind}; if ($diff != 0) { my $sign = ''; $sign = '+' if ($diff > 0); $diff_info .= sprintf "%30s: %s%d\n", lang("pictures with ").$statistic_data_longnames{$data_kind}, $sign, $diff; } } if ($diff_info ne '') { $info .= $diff_info; } else { $info .= lang("No changes since last call."); } } showText(lang("Database Statistics"), $info, WAIT); } # store the actual number of database entries $$statistics{'Nr_of_entries'} = $keys; nstore($statistics, $statistic_file) or warn "could not store $statistic_file: $!"; log_it("database statistics finished!"); } ############################################################## ############################################################## sub database_statistic { my @keys = @_; # my $i = 0; my %statistics; # Warning! The keys names in @keys have to be same as in the %searchDB hash!!! my $pw = progressWinInit($top, "generating database statistics"); my $keys = keys %searchDB; # loop through all database entries and count pics with these kinds of metadata: foreach my $dpic (keys %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "analyzing pictures ($i/$keys) ...", $i, $keys); foreach my $data_kind (@keys) { my $data = $searchDB{$dpic}{$data_kind}; if (defined $data and ($data ne '')) { $statistics{$data_kind}++; } } } progressWinEnd($pw); return \%statistics; } ############################################################## # editEntryHistory ############################################################## sub editEntryHistory { my $buttext = "Remove"; my $text = "The left list shows all used entry fields, if you select one, the right listbox will show you all elements, that have been typed into this entry field. Select one or multiple element from the right listbox and press the $buttext button to delete them."; my $rc; # open window my $ew = $top->Toplevel(); $ew->title("Edit entry history"); $ew->iconimage($mapiviicon) if $mapiviicon; my $height = ($text =~ tr/\n//); $height += 2; $height = 10 if ($height > 10); # not to big, we have scrollbars my $rotext = $ew->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 110, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-expand => 0, -padx => 3, -pady => 3,-anchor => 'w'); $rotext->insert('end', $text); my $size = getFileSize($file_Entry_values, FORMAT); my $info = "File size of $file_Entry_values: $size"; my $lbf = $ew->Frame()->pack(-fill =>'x'); my $listBox = $lbf->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 30, -height => 25, )->pack(-side => 'left', -expand => 1, -fill =>'both', -padx => 3, -pady => 3); my @ekeys = sort keys %entryHistory; $listBox->insert('end', @ekeys); my $lbfr = $lbf->Frame()->pack(-side => 'left', -expand => 1, -fill =>'both'); my $listBox2 = $lbfr->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, #-width => 80, -height => 25, )->pack(-side => 'top', -expand => 1, -fill =>'both', -padx => 3, -pady => 3); $listBox->bind('', sub { my @sel = $listBox->curselection(); my $key = $ekeys[$sel[0]]; my @list = @{$entryHistory{$key}}; $listBox2->delete(0, 'end'); $listBox2->insert('end', @list); }); my $labF = $ew->Frame()->pack(-anchor => 'w', -padx => 3, -pady => 3); $labF->Label(-textvariable => \$info, -bg => $conf{color_bg}{value})->pack(-side => 'left'); $lbfr->Button(-text => $buttext, -command => sub { my @sel = $listBox->curselection(); my $key = $ekeys[$sel[0]]; foreach (reverse $listBox2->curselection()) { my $path = $listBox2->get($_); #print "deleting key $key element $_ ".${$entryHistory{$key}}[$_]."\n"; splice @{$entryHistory{$key}}, $_, 1; # remove it from list $listBox2->delete($_); } } )->pack(-expand => 1, -fill =>'x', -anchor => 'w', -padx => 3, -pady => 3); my $ButF = $ew->Frame()->pack(-fill =>'x'); my $OKB = $ButF->Button(-text => 'OK', -command => sub { $rc = 'OK'; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $OKB->bind('', sub { $OKB->Invoke; } ); $OKB->focus; $ew->Popup(-popover => 'cursor'); repositionWindow($ew); $ew->waitVariable(\$rc); $ew->withdraw; $ew->destroy; } ############################################################## # database_info - show infos and statistics about search database ############################################################## sub database_info { # first create a chronological statistic (number of pics for each month) my %chrono_hash; my $pic_count = 0; my $error_count = 0; my $i = 0; my $keys = keys %searchDB; my $pw = progressWinInit($top, "Calculating statistic (chronological distribution)"); foreach my $dpic (keys %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "adding picture ($i/$keys) ...", $i, $keys); if ($searchDB{$dpic}{TIME}) { my (undef,undef,undef,undef,$mo,$y) = getDateTime($searchDB{$dpic}{TIME}); my $key = sprintf "%04d%02d", $y, $mo; # key = yyyymm $chrono_hash{$key}++; $pic_count++; } else { $error_count++; } } progressWinEnd($pw); #print "found $error_count pictures without date info.\n" if ($error_count > 0); #print "found $pic_count pictures with date info.\n"; # fill up empty months in hash with zero my @chrono_list; foreach (sort keys %chrono_hash) { push @chrono_list, $_; } my $first_ymonth = $chrono_list[0]; my $last_ymonth = $chrono_list[-1]; my $first_month = substr($first_ymonth, 4 , 2); my $last_month = substr($last_ymonth, 4 , 2); my $first_year = substr($first_ymonth, 0 , 4); my $last_year = substr($last_ymonth, 0 , 4); for my $year ($first_year .. $last_year) { for my $month (1 .. 12) { next if (($year == $first_year) and ($month < $first_month)); last if (($year == $last_year) and ($month > $last_month)); my $yyyymm = sprintf "%04d%02d", $year, $month; if ($chrono_hash{$yyyymm}) { #print "$yyyymm is defined\n"; } else { #print "$yyyymm is not defined\n"; $chrono_hash{$yyyymm} = 0; } } } my $month_nr = keys %chrono_hash; #print "found $month_nr differnt month; max. pics $max_pics_per_month in month $max_month. first: $first_ymonth ($first_year $first_month) last: $last_ymonth ($last_year $last_month)\n"; # open window my $win = $top->Toplevel(); $win->title("Database Information - Timeline (Chronological Picture Distribution)"); $win->iconimage($mapiviicon) if $mapiviicon; # canvas size #my $h = int(0.3 * $win->screenheight); #my $w = int(0.9 * $win->screenwidth); my $w = 0; my $h = 0; my $h_scale_factor =1; my $month_w = $w/$month_nr; my $butF = $win->Frame()->pack(-expand => 0, -fill => 'y'); my $canvas = $win->Scrolled('Canvas', -scrollbars => 'osoe', #-width => $w, #-height => $h+26, -width => 10, -height => 10, -relief => 'sunken', )->pack(-side => 'top', -expand => 1, -fill => 'both', -padx => 3, -pady => 3); $canvas->configure(-scrollregion => [0, 0, 10, 10]); $butF->Button(-text => ' -- ', -command => sub { $w = $canvas->Subwidget("scrolled")->width - $ScW; # $ScW = scrollbar width $month_w = $w/$month_nr if ($month_w < 1); $month_w -= 5; $month_w = 1 if ($month_w < 1); database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => ' - ', -command => sub { $w = $canvas->Subwidget("scrolled")->width - $ScW; # $ScW = scrollbar width $month_w = $w/$month_nr if ($month_w < 1); $month_w--; $month_w = 1 if ($month_w < 1); database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => ' + ', -command => sub { $w = $canvas->Subwidget("scrolled")->width - $ScW; # $ScW = scrollbar width $month_w = $w/$month_nr if ($month_w < 1); $month_w++; database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => '++', -command => sub { $w = $canvas->Subwidget("scrolled")->width - $ScW; # $ScW = scrollbar width $month_w = $w/$month_nr if ($month_w < 1); $month_w += 5; database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => 'minimum', -command => sub { $month_w = 1; database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => 'medium', -command => sub { $month_w = 16; database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => 'large', -command => sub { $month_w = 36; database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => 'fit', -command => sub { $win->update; #$w = $canvas->Subwidget("scrolled")->width; #$h = $canvas->Subwidget("scrolled")->height; #$month_w = $w/$month_nr; $month_w = 0; database_info_update($canvas, \%chrono_hash, $month_w); })->pack(-side => 'left', -padx => 3, -pady => 3); $butF->Button(-text => 'Info', -command => sub { my $text = "Chronological distribution of pictures per month in the search database.\nThis chart uses the picture EXIF date when available.\n$pic_count pictures with and $error_count pictures without date info in database.\nIf you click on a box (or the number of pictures above the box or on the month below a box) the pictures of that month will be shown.\nSome information will appear, if mouse hovers above a box."; showText("Information", $text, NO_WAIT); })->pack(-side => 'left', -padx => 3, -pady => 3); my $msg = ''; $balloon->attach($canvas, -postcommand => sub { my ($current) = $canvas->find('withtag', 'current'); my @tags = $canvas->gettags($current); my $yyyymm = ''; foreach (@tags) { next if ($_ eq 'current'); $yyyymm = $_; } return if (length($yyyymm) != 6); my $act_month = substr($yyyymm, 4 , 2); my $act_year = substr($yyyymm, 0 , 4); $msg = "$act_month/$act_year: $chrono_hash{$yyyymm} pictures"; }, -balloonposition => "mouse", -msg => \$msg); $canvas->CanvasBind( '' => sub { my ($current) = $canvas->find('withtag', 'current'); my @tags = $canvas->gettags($current); my $yyyymm = ''; foreach (@tags) { next if ($_ eq 'current'); $yyyymm = $_; } return if (length($yyyymm) != 6); my $act_month = substr($yyyymm, 4 , 2); my $act_year = substr($yyyymm, 0 , 4); return if ($chrono_hash{$yyyymm} == 0); my $rc = $win->messageBox(-icon => 'question', -title => "Show $chrono_hash{$yyyymm} pictures from $act_month/$act_year?", -message => "Press OK to display $chrono_hash{$yyyymm} pictures from $act_month/$act_year.", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); my @list; my $start_time = buildUnixTime(sprintf "01.%02d.%04d", $act_month, $act_year); my $next_month = $act_month + 1; my $next_year= $act_year; if ($next_month > 12) { $next_month = 1; $next_year++; } my $end_time = buildUnixTime(sprintf "01.%02d.%04d", $next_month, $next_year) - 1; #print "xxx-start: $start_time .. end: $end_time act:$act_month, $act_year next: $next_month, $next_year\n"; my $i = 0; my $db_keys = keys %searchDB; my $pw = progressWinInit($win, "Searching pictures database"); foreach my $dpic (keys %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "searching ($i/$db_keys) ...", $i, $db_keys); my $time = $searchDB{$dpic}{TIME}; next unless (defined $time); next if ($time < $start_time); next if ($time > $end_time); push @list, $dpic; } progressWinEnd($pw); sortPics('exifdate', 1, \@list); showThumbList(\@list, "$act_month/$act_year"); }); $butF->Button(-text => "Close", -command => sub { $win->destroy(); } )->pack(-side => 'left',-expand => 0,-fill => 'x',-padx => 3,-pady => 3); $win->bind('', sub { $win->destroy; } ); $win->Popup; my $ww = int(0.8 * $top->screenwidth); my $wh = int(0.3 * $top->screenheight); $win->geometry("${ww}x${wh}+10+10"); $win->update; database_info_update($canvas, \%chrono_hash, $month_w); } ############################################################## # database_info_update - draw diagram ############################################################## sub database_info_update { my $canvas = shift; #my $w = shift; #my $h = shift; my $chrono_hash = shift; #my $pic_count = shift; #my $error_count = shift; my $month_w = shift; #my $month_nr = shift; #my $h_scale_factor = shift; my $month_nr = keys %{$chrono_hash}; my $w = $canvas->Subwidget("scrolled")->width - $ScW; # $ScW = scrollbar width my $h = $canvas->Subwidget("scrolled")->height - $ScW; # search the maximum number of pictures per month my $max_pics_per_month = 0; foreach (keys %{$chrono_hash}) { if ($chrono_hash->{$_} > $max_pics_per_month) { $max_pics_per_month = $chrono_hash->{$_}; } } my $axis_h = 30; # height for x axis and month and year numbers my $h_scale_factor = $max_pics_per_month/($h - $axis_h); $month_w = $w/$month_nr if ($month_w == 0); $canvas->delete('all'); #$canvas->configure(-scrollregion => [0, 0, $month_nr*$month_w-10, $h+26]); $canvas->configure(-scrollregion => [0, 0, $month_nr*$month_w, $h]); my $x = 2; my $step = 0; foreach my $yyyymm (sort keys %{$chrono_hash}) { my $act_month = substr($yyyymm, 4 , 2); my $act_year = substr($yyyymm, 0 , 4); # draw a box for each month my $id = $canvas->createRectangle( $x, $h-$axis_h, int($x+$month_w-1), $h-$axis_h-int($chrono_hash->{$yyyymm}/$h_scale_factor), -fill => $conf{color_act_bg}{value}, -outline => $config{ColorSel}, -tags => $yyyymm, -width => 1, ); # mark month border $canvas->createLine( $x, $h-$axis_h, $x, $h-int(0.5*$axis_h), -fill => $conf{color_fg}{value}); # mark year border if ($act_month eq '01') { $canvas->createLine( $x, $h-$axis_h, $x, $h, -fill => $conf{color_fg}{value}); } # write month if more then 16 pixel available if ($month_w >= 16) { $canvas->createText($x+int($month_w/2), $h-$axis_h+6, -font => $small_font, -text => $act_month, -anchor => 'n', -justify => 'center', -fill => $conf{color_fg}{value}, -tags => $yyyymm); } # write number of pics if enough space and number of pics bigger than 0 if (($month_w > length($chrono_hash->{$yyyymm})*8) and ($chrono_hash->{$yyyymm} > 0)) { my $h = $h-$axis_h-int($chrono_hash->{$yyyymm}/$h_scale_factor); $h = 14 if ($h < 14); $canvas->createText($x+int($month_w/2), $h, -font => $small_font, -text => $chrono_hash->{$yyyymm}, -anchor => 's', -justify => 'center', -fill => $conf{color_fg}{value}, -tags => $yyyymm); } # write year if ($act_month eq '07') { $canvas->createText($x, $h, -font => $small_font, -text => $act_year, -anchor => 's', -justify => 'center', -fill => $conf{color_fg}{value}); } $step++; $x = int($month_w * $step); } # draw x axis $canvas->createLine( 0, $h-$axis_h, $month_nr*$month_w, $h-$axis_h, -fill => $conf{color_fg}{value}); } ############################################################## # keyword_browse - browse picture collection by keywords (tagclouds) ############################################################## sub keyword_browse { # list of keywords to constraint the browsing/searching my @search_keys; # list of keywords to exclude from browsing/searching my @exclude_keys; # get stored values if ($config{KeywordExclude}) { @exclude_keys = split / /, $config{KeywordExclude}; } # open window my $win = $top->Toplevel(); $win->title('Keyword browser (tag cloud)'); $win->iconimage($mapiviicon) if $mapiviicon; my $cc; my $butF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $butF2 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $butF3 = $win->Frame(-relief => 'groove'); if ($config{KeywordMore}) { $butF3->pack(-after => $butF2, -expand => 0, -fill => 'x', -padx => 4, -pady => 3); } else { $butF3->packForget(); } my $add_mode = 1; my $label = ''; my $hb = $butF->Button(-text => 'home', -command => sub { # reset search_keys @search_keys = (); $label = ''; show_keywords($win, \@search_keys, \@exclude_keys); })->pack(-side => 'left', -padx => 3); $balloon->attach($hb, -msg => "Restart\nShow all keywords"); my $bb = $butF->Button(-text => 'back', -command => sub { return unless (@search_keys); # remove last element of array search_keys pop @search_keys; $label = ''; $label .= "$_ " foreach (@search_keys); show_keywords($win, \@search_keys, \@exclude_keys); })->pack(-side => 'left', -padx => 3); $balloon->attach($bb, -msg => "Go back\nRemove last keyword from list"); $butF->Label(-textvariable => \$label, )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); my $addB = $butF->Checkbutton(-text => 'add mode', -variable => \$add_mode)->pack(-side => 'left'); $balloon->attach($addB, -msg => 'If add mode is enabled, keywords will be added and the search is narrowed to pictures containing all displayed keywords. If add mode is disabled, each click on a keyword will start a new search for just this keyword.'); my $Xbut = $butF->Button(-text => lang('Close'), -command => sub { # store excluded keywords for next session $config{KeywordExclude} = ''; $config{KeywordExclude} .= "$_ " foreach (@exclude_keys); # clode window $win->destroy(); })->pack(-side => 'right', -padx => 3); $balloon->attach($Xbut, -msg => 'Close window (key: ESC)'); bind_exit_keys_to_button($win, $Xbut); $butF2->Button(-text => lang('Show'), -command => sub { my @list = get_pics_with_keywords(\@search_keys, \@exclude_keys); showThumbList(\@list, $label); })->pack(-side => 'left', -padx => 3); $butF2->Button(-text => 'show m', -command => sub { $act_modus = KEYWORDCLOUD; @act_keywords = @search_keys; @act_keywords_ex = @exclude_keys; showThumbs(); })->pack(-side => 'left', -padx => 3); my $lab2 = $butF2->Label(-textvariable => \$win->{label2}, -anchor => 'w' )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 1); $balloon->attach($lab2, -msg => "x pictures (y/z keywords)\nx = number of pictures with the selected keywords\ny = number of displayed keywords\nz = number of all matching keywords"); my $more_button; $more_button = $butF2->Checkbutton(-variable => \$config{KeywordMore}, -text => 'more options', -command => sub { if ($config{KeywordMore}) { $butF3->pack(-after => $butF2, -expand => 0, -fill => 'x', -padx => 4, -pady => 3); } else { $butF3->packForget(); } })->pack(-side => 'right', -padx => 5); $balloon->attach($more_button, -msg => 'Click here to see some more options'); my $label_ex = ''; $label_ex .= "$_ " foreach (@exclude_keys); my $butF3i = $butF3->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $ceb = $butF3i->Button(-text => 'clear', -command => sub { # reset exclude_keys @exclude_keys = (); $label_ex = ''; show_keywords($win, \@search_keys, \@exclude_keys); })->pack(-side => 'left', -padx => 3); $balloon->attach($ceb, -msg => "Clear all keywords from exclude list"); $butF3i->Label(-text => 'Excluded:', )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); $butF3i->Label(-textvariable => \$label_ex, )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); $butF3->Label(-text => 'Right click to select a keyword, left click to exclude it')->pack(-anchor => 'w', -padx => 3); my $lib = $butF3->Checkbutton(-variable => \$config{KeywordLimit}, -text => 'Limit to 100 keywords', -command => sub {show_keywords($win, \@search_keys, \@exclude_keys);} )->pack(-anchor => 'w', -padx => 3); $balloon->attach($lib, -msg => 'Limit to a maximum of the 100 most popular keywords.'); my $butF3j = $butF3->Frame()->pack(-fill =>'x', -padx => 0, -pady => 0); my $dab = $butF3j->Checkbutton(-variable => \$config{KeywordDate}, -text => 'Limit by date between ', -command => sub {show_keywords($win, \@search_keys, \@exclude_keys);} )->pack(-side => 'left', -anchor => 'sw', -pady => 0); $balloon->attach($dab, -msg => "Limit to a date range.\nThe first scale is the first day of the selected year\nthe second scale is the last day of the selected year.\nIf both scales show e.g. 2009 only keywords from pictures taken\nbetween 2009-01-01 and 2009-12-31 are shown.\nThe EXIF date is used for this function."); # get date limits (absolute limits) from searchDB my ($first, $last) = get_date_limits(); # get the actual selected limits from the configuration hash my (undef,undef,undef,undef,undef,$start) = getDateTime($config{KeywordStart}); my (undef,undef,undef,undef,undef,$end) = getDateTime($config{KeywordEnd}); $butF3j->Scale(-variable => \$start, -from => $first, -to => $last, -resolution => 1, -sliderlength => 30, -orient => 'horizontal', -showvalue => 1, -width => 15, -command => sub { $end = $start if ($end < $start); $win->{LAST_RESCALE_TIMER_MH}->cancel if ($win->{LAST_RESCALE_TIMER_MH}); # after 500 msec we recalculate the keywords this gives better responsiveness $win->{LAST_RESCALE_TIMER_MH} = $win->after(500, sub { # sec,min,hour,day,mon,year (day = 1-31 month = 0-11) $config{KeywordStart} = timelocal(0,0,0,1,0,$start); $config{KeywordEnd} = timelocal(0,0,0,31,11,$end); show_keywords($win, \@search_keys, \@exclude_keys) if $config{KeywordDate}; }); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 0); $butF3j->Scale(-variable => \$end, -from => $first, -to => $last, -resolution => 1, -sliderlength => 30, -orient => 'horizontal', -showvalue => 1, -width => 15, -command => sub { $start = $end if ($start > $end); $win->{LAST_RESCALE_TIMER_MH}->cancel if ($win->{LAST_RESCALE_TIMER_MH}); # after 500 msec we recalculate the keywords this gives better responsiveness $win->{LAST_RESCALE_TIMER_MH} = $win->after(500, sub { # sec,min,hour,day,mon,year (day = 1-31 month = 0-11) $config{KeywordStart} = timelocal(0,0,0,1,0,$start); $config{KeywordEnd} = timelocal(0,0,0,31,11,$end); show_keywords($win, \@search_keys, \@exclude_keys) if $config{KeywordDate}; }); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 0); my $butF3k = $butF3->Frame()->pack(-fill =>'x', -padx => 0, -pady => 0); my $rab = $butF3k->Checkbutton(-variable => \$conf{nav_rating_on}{value}, -text => 'Limit by rating between', -command => sub {show_keywords($win, \@search_keys, \@exclude_keys);} )->pack(-side => 'left', -anchor => 'sw', -pady => 3); $balloon->attach($rab, -msg => "Limit to a rating range.\nIf the first scale shows e.g. 2 and the second scale shows 4\nonly keywords from pictures with a rating of 2, 3 or 4 are shown.\nThe IPTC urgency is used for this function.\nNote: 1 is the highest (best) rating, 8 the lowest."); rating_button_min_max($butF3k, \$conf{search_rating_max}{value}, \$conf{search_rating_min}{value}, sub { $win->{LAST_RESCALE_TIMER_MH}->cancel if ($win->{LAST_RESCALE_TIMER_MH}); # after 500 msec we recalculate the keywords this gives better responsiveness $win->{LAST_RESCALE_TIMER_MH} = $win->after(500, sub { show_keywords($win, \@search_keys, \@exclude_keys) if $conf{nav_rating_on}{value}; }); }); $cc = $win->Scrolled('Canvas', -scrollbars => 'osoe', -width => 700, -height => 400, -relief => 'sunken' )->pack(-expand => 1, -fill => 'both', -padx => 1, -pady => 1); $cc->configure(-scrollregion => [0, 0, 700, 400]); $win->{canvas} = $cc; $win->Popup(-popover => 'cursor'); show_keywords($win, \@search_keys, \@exclude_keys); # reaction for clicking on a keyword (tag) $cc->CanvasBind('' => sub { my ($current) = $cc->find('withtag', 'current'); my @tags = $cc->gettags($current); foreach (@tags) { next if ($_ eq 'current'); if ($add_mode) { # add new keyword to list, if it is not already there push @search_keys, $_ unless (isInList($_, \@search_keys)); } else { # clear list and add just the new selected keyword @search_keys = (); push @search_keys, $_; } } $label = ''; $label .= "$_ " foreach (@search_keys); show_keywords($win, \@search_keys, \@exclude_keys); }); # reaction for right clicking on a keyword (tag) $cc->CanvasBind('' => sub { my ($current) = $cc->find('withtag', 'current'); my @tags = $cc->gettags($current); foreach (@tags) { next if ($_ eq 'current'); push @exclude_keys, $_ unless (isInList($_, \@exclude_keys)); } $label_ex = ''; $label_ex .= "$_ " foreach (@exclude_keys); show_keywords($win, \@search_keys, \@exclude_keys); }); # wait for the close button $win->waitWindow; } ############################################################## # get_date_limits - get the first and the last year from database ############################################################## sub get_date_limits { my $first = 99999999999; my $last = 0; # using each instead of keys (about 1.5 times faster) # http://stackoverflow.com/questions/22841830 # Pros: # This uses very little memory as every time each is called it only # returns a pair of (key, value) element. # Cons: # You can't order the output by key. # The iterator it uses belongs to %h. If the code inside the loop calls # something that does keys %h, values %h or each %h, then the loop won't # work properly, because %h only has 1 iterator while (my ($dpic, undef) = each %searchDB) { my $time = $searchDB{$dpic}{TIME}; if ($time) { $last = $time if ($time > $last); $first = $time if ($time < $first); } } # from UNIX time to calendar years (undef,undef,undef,undef,undef,$last) = getDateTime($last); (undef,undef,undef,undef,undef,$first) = getDateTime($first); return ($first, $last); } ############################################################## # show_keywords - add keyword cloud to a canvas ############################################################## sub show_keywords { my $win = shift; # canvas my $search_keys = shift; # list reference for keywords which must be contained my $exclude_keys = shift; # list reference for keywords which must not be contained $win->Busy; # get the keywords according to the search keyword list ($search_keys) my ($count, %keyword_hash) = get_keywords($search_keys, $exclude_keys); my $all_keys = keys %keyword_hash; # we expect, that the window widget has an element called canvas my $cc = $win->{canvas}; # clear canvas $cc->delete('all'); $win->update; my $cc_width = $cc->width; my $win_width = $win->width; # limit the number of keywords to the 100 most popular keywords # todo 100 should not be a fixed value my $max_keys = 100; my $key_count = 0; if (($config{KeywordLimit}) and ((keys %keyword_hash) > $max_keys)) { my %new_hash; # sort hash by size of value (number of pictures with this keyword) foreach my $key (sort {$keyword_hash{$b} <=> $keyword_hash{$a}} keys %keyword_hash) { # copy the first 100 to a new hash $new_hash{$key} = $keyword_hash{$key}; $key_count++; last if ($key_count >= $max_keys); } # empty the original hash undef %keyword_hash; # copy the shortened hash back %keyword_hash = %new_hash; } if ($config{KeywordLimit}) { $win->{label2} = langf("%d pictures, %d/%d keywords", $count, scalar(keys(%keyword_hash)),$all_keys); } else { $win->{label2} = langf("%d pictures, %d keywords", $count, scalar(keys(%keyword_hash))); } if (keys %keyword_hash > 0) { # find max an min numbers my $min = 9999999; my $max = 0; foreach (keys %keyword_hash) { $min = $keyword_hash{$_} if ($keyword_hash{$_} < $min); $max = $keyword_hash{$_} if ($keyword_hash{$_} > $max); } # to have a nice size distribution we need the log function my $diff = 1; $diff = log($max - $min) if ($max != $min); # log(1) = 0! log(0) = -infinite #print "max $max min $min diff $diff\n"; $diff = 0.1 if ($diff == 0); # prevent division by zero # maximum and minimum font size for tag cloud my $font_min = 7; my $font_max = 18; my $font_middle = int(($font_max-$font_min)/2 + $font_min); # h and v space between tags/keywords my $x_space = 5; my $y_space = 3; my $x_max = 0; my $x = $x_space; my $y = $y_space + int($font_max/2); # sort keywords alphabetical foreach my $key (sort keys %keyword_hash) { my $size = $font_middle; # to have a nice size distribution we need the log function $size = int(log($keyword_hash{$key} - $min + 1)/$diff * ($font_max-$font_min) + $font_min) if ($max != $min); #printf "%-20s %5d %2.2f size: %2d", $key, $keyword_hash{$key}, log($keyword_hash{$key})/$diff, $size; # safety check $size = $font_max if ($size > $font_max); $size = $font_min if ($size < $font_min); #print " $size\n"; # bold style for the bigger fonts my $style = 'normal'; $style = 'bold' if ($size >= $font_middle); my $font = $top->Font(-family => $config{PropFontFamily}, -size => $size, -weight => $style); # the more often a keyword is used there brighter it is displayed my $color_percent = 100; $color_percent = int(log($keyword_hash{$key} - $min + 1)/$diff * 100) if ($max != $min); my $color = $win->Darken($config{ColorCloud}, $color_percent); # '#5D7298' # add the keyword (tag) to the canvas my $id = $cc->createText($x, $y, -text => $key, -fill => $color, -font => $font, -anchor => 'w', -tags => [$key]); # get the used canvas space my ($x1, $y1, $x2, $y2) = $cc->bbox($id); # calculate next coordinates $x += ($x2 - $x1) + $x_space; # if we are over the right border we move the last keyword to the next line if ($x > $cc_width) { $x = $x_space; $y += ($font_max + $y_space); # move text $cc->coords($id, $x, $y); # get the used canvas space again my ($x1, $y1, $x2, $y2) = $cc->bbox($id); # calculate next coordinates $x += ($x2 - $x1) + $x_space; # if we are now over the right border we have to increase the scrollregion $x_max = $x if ($x > $x_max); } } # adjust the canvas scrollbars to the used space $cc->configure(-scrollregion => [0, 0, $x_max, ($y + int($font_max/2) + $y_space)]); } else { # adjust the canvas scrollbars to the used space $cc->configure(-scrollregion => [0, 0, 0, 0]); } $win->Unbusy; return; } ############################################################## # returns true is the urgency urg is between min and max # function considers special cases like undefined urgency and # max and min values of 0 ############################################################## sub rating_valid { my ($urg, $max, $min) = @_; # all in IPTC scale (1 = best, 8 = lowest, 0 = no rating) my $ok = 0; if (not defined $urg) { if ($min == 0) { $ok = 1; # valid if urgency is undefined and min = 0 } } else { # urgency is defined if (($max > 0) and ($urg >= $max)) { if (($min == 0) or (($min > 0) and ($urg <= $min))) { $ok = 1; # normal case: urgency is between min and max if min is defined, else below max is enough } } } return $ok; } ############################################################## # get_keywords - get all keywords from the searchDB (may be restriced by a keyword list ($search_keys)) ############################################################## sub get_keywords { my $search_keys = shift; # list reference for included keywords my $exclude_keys = shift; # list reference for excluded keywords my %keyword_hash; my $count = 0; # build keyword/tag hash # loop through all pictures in the DB foreach my $dpic (keys %searchDB) { # skip if no keywords info in picture next unless (defined $searchDB{$dpic}{KEYS}); if ($config{KeywordDate}) { next if (defined $searchDB{$dpic}{TIME} and ($searchDB{$dpic}{TIME} < $config{KeywordStart})); next if (defined $searchDB{$dpic}{TIME} and ($searchDB{$dpic}{TIME} > $config{KeywordEnd})); } if ($conf{nav_rating_on}{value}) { next if (not rating_valid($searchDB{$dpic}{URG}, $conf{search_rating_max}{value}, $conf{search_rating_min}{value})); } # check if any items of the exclude_keys list are contained in this keyword string next if (string_contains($searchDB{$dpic}{KEYS}, $exclude_keys)); # check if all items of the search_keys list are contained in this keyword string next if (string_contains_not($searchDB{$dpic}{KEYS}, $search_keys)); #count number of pictures matching all keywords of the search keyword list $count++; # the keywords are stored as a space separated string so we need to split up my @keys = split / /, $searchDB{$dpic}{KEYS}; foreach my $key (@keys) { # hierarchical keywords are joined by an period "." todo this may cause problems ("Mr. X, "Louis XIV.", "Dr. Miller") my @subkeys = split /\./, $key; foreach (@subkeys) { # add keyword to hash and count how often it was found if (defined $keyword_hash{$_}) { $keyword_hash{$_}++; } else { $keyword_hash{$_} = 1; } } } } return ($count, %keyword_hash); } ############################################################## # search_by_location ############################################################## sub search_by_location { if (Exists($locw)) { $locw->deiconify; $locw->raise; $locw->focus; return; } my $lb = shift; # thumbnail widget e.g. $picLB # open window $locw = $top->Toplevel(); $locw->withdraw; $locw->title('Locations'); $locw->iconimage($mapiviicon) if $mapiviicon; my $locXBut = $locw->Button(-text => "Close", -command => sub { $config{LocGeometry} = $locw->geometry; $locw->destroy; })->pack(-fill => 'x'); add_location_tree($locw, $lb); # get all location info from the database (IPTC tags: country, state, city and sublocation) $top->Busy; my %loc_hash = get_locations(UPDATE); $top->Unbusy; insert_in_tree(LOCATION, $locw->{tree}, \%loc_hash); bind_exit_keys_to_button($locw, $locXBut); $locw->Popup; checkGeometry(\$config{LocGeometry}); $locw->geometry($config{LocGeometry}); $locw->waitWindow; return; } ############################################################## ############################################################## sub add_date_tree { my $w = shift; my $lb = shift; my $tree; my $af = $w->Frame()->pack(-fill =>'x', -padx => 2, -pady => 1); $tree = $w->Scrolled('Tree', -separator => '%', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 25, -height => 5, )->pack(-expand => 1, -fill =>'both', -padx => 1, -pady => 1); $w->{tree} = $tree; my $aaf = $w->Frame()->pack(-fill =>'x', -padx => 2, -pady => 1); my $update_but = $aaf->Button(-image => $mapivi_icons{'Update'}, -command => sub { # get all date/time info from the database (EXIF tag: creation date/time) $top->Busy; my %date_hash = get_dates(UPDATE); insert_in_tree(DATE, $tree, \%date_hash); $top->Unbusy; })->pack(-side => 'left'); $balloon->attach($update_but, -msg => "Update date/time info from database."); $aaf->Button(-image => $mapivi_icons{Help}, #-text => '?', -command => sub { showText("Help for date/time navigation", "Double click on any date/time in the tree to see pictures of that date/time in the main window. Smallest time frame is one hour.\nUse the middle mouse button or the key to see a preview in a new window.\nThe date/time information is gathered either from the EXIF creation date (when available) or the file creation date of all pictures in the database.\nThe number in square bracket represents the number of pictures for that date/time. If a rating constraint is selected it is applied when the pictures are shown. The number in square brackets are not affected by the rating constraint.\n", NO_WAIT); })->pack(-side => 'left'); focus_on_enter($tree->Subwidget("scrolled")); $tree->bind("", sub { my @date = $tree->info('selection'); return unless checkSelection($w, 1, 0, \@date, lang("date")); @act_date = split(/%/, $date[0]); # switch display modus to date navigation ... $act_modus = DATE; # ... and show pictures of the selected date updateThumbs(); }); $tree->bind("", sub { $tree->selectionClear(); $tree->selectionSet(getNearestItem($tree)); showThumbsByDate($w, $tree); }); $tree->bind('', sub { showThumbsByDate($w, $tree); }); } ############################################################## # add a collection (slideshow) tree to the given widget ############################################################## sub add_collection_tree { my $w = shift; my $lb = shift; my $tree; my $af = $w->Frame()->pack(-fill =>'x', -padx => 2, -pady => 1); $tree = $w->Scrolled('Tree', -separator => '%', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 25, -height => 5, )->pack(-expand => 1, -fill =>'both', -padx => 1, -pady => 1); $w->{tree} = $tree; # add some buttons my $aaf = $w->Frame()->pack(-fill =>'x', -padx => 2, -pady => 1); my $update_but = $aaf->Button(-image => $mapivi_icons{'Update'}, -command => sub { # save open/close info of tree (one one level deep needed!) my %mode; foreach ($tree->info('children')) { $mode{$_} = $tree->getmode($_); } $top->Busy; # get all collections insert_collections_in_tree($tree, \%slideshows); # reset mode to the the old setting on first level foreach ($tree->info('children')) { $tree->close($_) if ((defined $mode{$_}) and ($mode{$_} eq 'open')); $tree->open($_) if ((defined $mode{$_}) and ($mode{$_} eq 'close')); } log_it("Collections are up-to-date!"); $top->Unbusy; })->pack(-side => 'left'); $balloon->attach($update_but, -msg => lang('Update collections')); my $addB = $aaf->Button(-image => $mapivi_icons{PlusBig}, -command => sub { my ($ok, $folder, $collection) = get_selected_collection($tree, 0); if ($ok == 0) { # nothing selected -> create folder my $newfolder = 'new folder'; my $rc = myEntryDialog("New folder", "Please enter a name for a new collection folder", \$newfolder); return if (($rc ne 'OK') or ($newfolder eq '')); if (exists $slideshows{$newfolder}) { log_it("Folder $newfolder exists, please retry with another name."); return; } $slideshows{$newfolder} = { }; log_it("Created new folder $newfolder."); } elsif ($ok >= 1) { # folder or collection selected my $collection = 'new collection'; my $rc = myEntryDialog("New collection", "Please enter a name for a new picture collection in $folder", \$collection); return if (($rc ne 'OK') or ($collection eq '')); if (exists $slideshows{$folder}{$collection}) { log_it("Collection $folder $collection exists, please retry with another name."); return; } $slideshows{$folder}{$collection}{pics} = []; $slideshows{$folder}{$collection}{file} = ''; log_it("Created new collection $folder $collection."); } else { warn "add_collection_tree: should never happen: ok = $ok"; } $top->Busy; insert_collections_in_tree($tree, \%slideshows); $top->Unbusy; })->pack(-side => 'left'); $balloon->attach($addB, -msg => lang('Add folder or collection')); my $editB = $aaf->Button(-image => $mapivi_icons{Editor}, -command => sub { my ($ok, $folder, $collection) = get_selected_collection($tree, 1); if ($ok == 2) { my $pics = $slideshows{$folder}{$collection}{pics}; my $file = $slideshows{$folder}{$collection}{file}; light_table_edit($pics, $folder, $collection); log_it("Edit collection: $folder $collection"); } else { log_it("Please select a collection in the tree to edit first."); } })->pack(-side => 'left'); $balloon->attach($editB, -msg => lang('Edit collection')); my $saveB = $aaf->Button(-image => $mapivi_icons{Save}, -command => sub { if (save_slideshows()) { log_it(lang('Collections saved successfully!')); } else { log_it(lang('Error saving collections! (see console for further information)')); } })->pack(-side => 'left'); $balloon->attach($saveB, -msg => lang('Save all collections')); my $delB = $aaf->Button(-image => $mapivi_icons{Trash}, -command => sub { my ($ok, $folder, $collection) = get_selected_collection($tree, 1); if (not $ok) { log_it("Please select a collection in the tree first."); } if ($ok == 1) { # delete folder my $folder_collections = scalar(keys(%{$slideshows{$folder}})); my $rc = $w->messageBox(-message => "Delete folder $folder containing $folder_collections collections?", -icon => 'question', -title => lang('Delete folder').'?', -type => 'OKCancel'); if ($rc =~ m/Ok/i) { # delete hash entry delete $slideshows{$folder}; log_it("Deleted folder $folder."); } } elsif ($ok == 2) { # delete collection my $rc = $w->messageBox(-message => "Delete collection $folder $collection?", -icon => 'question', -title => lang('Delete collection').'?', -type => 'OKCancel'); if ($rc =~ m/Ok/i) { # delete hash entry delete $slideshows{$folder}{$collection}; log_it("Deleted collection: $folder $collection."); } } $top->Busy; insert_collections_in_tree($tree, \%slideshows); $top->Unbusy; })->pack(-side => 'left'); $balloon->attach($delB, -msg => lang('Delete collection')); my $helpB = $aaf->Button(-image => $mapivi_icons{Help}, -command => sub { showText("Help for collection navigation", "Double click on any collection in the tree to see pictures of that collection in the main window.", NO_WAIT); })->pack(-side => 'right'); $balloon->attach($helpB, -msg => lang('Help')); focus_on_enter($tree->Subwidget("scrolled")); $tree->bind("", sub { my ($ok, $folder, $collection) = get_selected_collection($tree, 1); return if (not $ok); # user clicked on folder if ($ok == 1) { # open/close folder (toggle) my $mode = $tree->getmode($folder); if ($mode eq 'open') { $tree->open($folder); } elsif ($mode eq 'close') { $tree->close($folder); } else {} # mode may also return "none" (for empty folders) } else { # user clicked on collection -> open it # switch display modus to collection navigation ... $act_modus = COLLECTION; @act_collection = ($folder, $collection); # ... and show pictures of the selected date updateThumbs(); } }); $tree->bind("", sub { $tree->selectionClear(); $tree->selectionSet(getNearestItem($tree)); print "add_collection_tree: not yet\n"; #showThumbsByDate($w, $tree); }); $tree->bind('', sub { print "add_collection_tree: not yet\n"; #showThumbsByDate($w, $tree); }); # add a context popup menu my $menu = $tree->Menu(-title => lang("Collection menu")); $tree->bind('', sub { $menu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); $menu->command(-label => lang('Rename ...'), -command => sub { my ($ok, $folder, $collection) = get_selected_collection($tree, 1); if ($ok == 1) { # rename folder my $newfolder = $folder; my $rc = myEntryDialog("Rename folder", "Please enter new name for collection folder $folder", \$newfolder); return if (($rc ne 'OK') or ($newfolder eq '')); if (exists $slideshows{$newfolder}) { log_it("Folder $newfolder exists, please retry with another name."); return; } $slideshows{$newfolder} = delete $slideshows{$folder}; log_it("Renamed folder $folder to $newfolder."); } elsif ($ok == 2) { # rename collection my $newcollection = $collection; my $rc = myEntryDialog("Rename collection", "Please enter new name for collection $collection", \$newcollection); return if (($rc ne 'OK') or ($newcollection eq '')); if (exists $slideshows{$folder}{$newcollection}) { log_it("Collection $folder $newcollection exists, please retry with another name."); return; } $slideshows{$folder}{$newcollection} = delete $slideshows{$folder}{$collection}; log_it("Renamed collection $collection to $newcollection."); } else { warn "add_collection_tree: should never happen: ok = $ok"; } $top->Busy; insert_collections_in_tree($tree, \%slideshows); $top->Unbusy; }); } ############################################################## ############################################################## sub get_selected_collection { my $tree = shift; my $min_select = shift; # minimum number of selected elements my @sel = $tree->info('selection'); my $ok = 0; # 0 = no selection, 1 = folder selection, 2 = collection selection my $folder = undef; # collection folder my $collection = undef; # collection name return ($ok, $folder, $collection) if (not @sel); if (checkSelection($tree, $min_select, 0, \@sel, lang('collection'))) { my @path = split(/%/, $sel[0]); # user selected a folder if (scalar(@path) == 1) { $ok = 1; $folder = $path[0]; } # user selected a collection, not a folder elsif (scalar(@path) == 2) { $ok = 2; $folder = $path[0]; $collection = $path[1]; } else { print "get_selected_collection: unclear selection: ".scalar(@path)."\n"; } } return ($ok, $folder, $collection); } ############################################################## # get selection from date tree and display thumbnails in new window ############################################################## sub showThumbsByDate { my $w = shift; my $tree = shift; my @dates = $tree->info('selection'); return unless checkSelection($w, 1, 0, \@dates, lang("date")); my @date = split(/%/, $dates[0]); my @list = get_pics_by(DATE, \@date); my $title = 'Date: '; $title .= "$_ " foreach (@date); showThumbList(\@list, $title); } ############################################################## # get selection from location tree and display thumbnails in new window ############################################################## sub showThumbsByLocation { my $w = shift; my @locs = $w->{tree}->info('selection'); return unless checkSelection($w, 1, 0, \@locs, lang("location(s)")); my @loc = split(/%/, $locs[0]); my @list = get_pics_by(LOCATION, \@loc); my $title = 'Location: '; $title .= "$_ " foreach (@loc); showThumbList(\@list, $title); } ############################################################## ############################################################## sub add_location_tree { my $locw = shift; my $lb = shift; my $af = $locw->Frame(-bd => 1, -relief => 'raised')->pack(-fill =>'x', -padx => 2, -pady => 1); my $update_replace_but; # search or add location button $locw->{SearchMode} = 1; #$af->Label(-text => 'Mode:')->pack(-expand => 0, -side => 'left', -fill => 'x'); $af->Radiobutton(-image => compound_menu($top, 'Search', 'system-search.png'), -variable => \$locw->{SearchMode}, -value => 1, -indicatoron => 0, -command => sub { cursor_search_add($locw->{SearchMode}, $locw->{tree}); return unless Exists($update_replace_but); my $state = 'normal'; $state = 'disabled' if ($locw->{SearchMode}); $update_replace_but->configure(-state => $state); })->pack(-expand => 1, -side => 'left', -fill => 'x', -padx => 2, -pady => 2); # $af->Radiobutton(-image => compound_menu($top, lang('Add'), 'list-add.png'), -variable => \$locw->{SearchMode}, -value => 0, -indicatoron => 0, -command => sub { cursor_search_add($locw->{SearchMode}, $locw->{tree}); return unless Exists($update_replace_but); my $state = 'normal'; $state = 'disabled' if ($locw->{SearchMode}); $update_replace_but->configure(-state => $state); })->pack(-expand => 1, -side => 'left', -fill => 'x', -padx => 2, -pady => 2); $balloon->attach($af, -msg => "Search or Add mode\nChoose if a double click on a location will search\nfor the location or add it to the selected pictures"); # update or replace location button $update_replace_but = $af->Optionmenu(-variable => \$config{LocationMode}, -textvariable => \$config{LocationMode}, -options => [ ['Update' => 'UPDATE'],['Replace' => 'REPLACE'], ],)->pack(-expand => 0, -side => 'left', -fill => 'x', -padx => 2, -pady => 2); $balloon->attach($update_replace_but, -msg => "Location Add Mode:\nIf Replace is selected all four locations (Country/State/City/Sublocation)\nwill be overwritten.\nIf Update is selected only the selected location will be updated.\nExample: If you select just a country (USA) and add this\nto a picture with existing location (e.g. City = New York)\nIn Update mode the City information will be preserved\nwhile in Replace mode New York will be deleted"); $locw->{tree} = $locw->Scrolled('Tree', -separator => '%', -scrollbars => 'osoe', -selectmode => 'single', -exportselection => 0, -width => 25, -height => 5, )->pack(-expand => 1, -fill =>'both', -padx => 1, -pady => 1); # add a filter entry to show only selected locations in the tree add_tree_filter($locw, $locw->{tree}, LOCATION, undef); my $aaf = $locw->Frame()->pack(-fill =>'x', -padx => 2, -pady => 1); my $update_loc_but = $aaf->Button(-image => $mapivi_icons{'Update'}, -foreground => $conf{color_fg}{value}, -command => sub { # get all location info from the database (IPTC tags: country, state, city and sublocation) $top->Busy; my %loc_hash = get_locations(UPDATE); # UPDATE = reread from database insert_in_tree(LOCATION, $locw->{tree}, \%loc_hash); $top->Unbusy; })->pack(-side => 'left'); $balloon->attach($update_loc_but, -msg => 'Update locations from database'); $aaf->Button(-image => $mapivi_icons{Help}, #-text => '?', -command => sub { showText("Help for location navigation", "The location tree may be used in two ways: Either to search for pictures from a selected location (Search mode) or to add location information to pictures (Add mode).\nIn Search mode the mouse pointer looks like a hand in Add mode the mouse pointer looks like a plus sign.\nSearch mode: Double click on any location in the tree to see pictures of that location in the main window.\nAdd mode: Double click on any location in the tree to add this location information to all selected pictures.\nMapivi will ask before overwriting existing location information.\nGeneral: Use the middle mouse button or the key to see a preview of all pictures from the selected location in a new window.\nThe location information is gathered from the IPTC tags Country, Province/State, City and SubLocation of all pictures in the database.\n", NO_WAIT); })->pack(-side => 'left'); # set cursor to either search or add to inform the user about the mode cursor_search_add($locw->{SearchMode}, $locw->{tree}); # enable/disable update/replace button { my $state = 'normal'; $state = 'disabled' if ($locw->{SearchMode}); $update_replace_but->configure(-state => $state); } focus_on_enter($locw->{tree}->Subwidget("scrolled")); #$balloon->attach($locw->{tree}, -msg => "Double click on a location to see pictures from there."); $locw->{tree}->bind("", sub { # search mode if ($locw->{SearchMode}) { my @locs = $locw->{tree}->info('selection'); return unless checkSelection($locw, 1, 0, \@locs, lang("location(s)")); @act_location = split(/%/, $locs[0]); # switch display modus to location $act_modus = LOCATION; updateThumbs(); } # add mode else { $lb = $picLB unless Exists($lb); my @locs = $locw->{tree}->info('selection'); return unless checkSelection($locw, 1, 1, \@locs, lang("location(s)")); my @loc = split(/%/, $locs[0]); my @sellist = getSelection($lb); return unless checkSelection($locw, 1, 0, \@sellist, lang("picture(s)")); # check before overwriting return if (not allow_location_overwrite($locw, \@sellist)); my $location; $location .= "$_ " foreach (@loc); my $nr = scalar @sellist; log_it("adding ${location}to $nr pictures ..."); my $errors = ''; my $count = 0; my $i = 0; my $pw = 0; $pw = progressWinInit($locw, "Adding location") if ($nr > 5); # add location info to selected pictures foreach my $dpic (@sellist) { last if ($pw and progressWinCheck($pw)); $i++; progressWinUpdate($pw, "Adding location information ($i/$nr) ...", $i, $nr) if $pw; my ($ok, $iptc, $meta) = get_IPTC_info($dpic); if (not $ok) { $errors .= "Could not open IPTC segment of $dpic\n";; next; } else { if (defined $loc[0] and $loc[0] ne $empty_str) { $iptc->{'Country/PrimaryLocationName'} = $loc[0]; } else { undef $iptc->{'Country/PrimaryLocationName'}; } if (defined $loc[1] and $loc[1] ne $empty_str) { $iptc->{'Province/State'} = $loc[1]; } else { undef $iptc->{'Province/State'}; } if (defined $loc[2] and $loc[2] ne $empty_str) { $iptc->{'City'} = $loc[2]; } else { undef $iptc->{'City'}; } if (defined $loc[3] and $loc[3] ne $empty_str) { $iptc->{'SubLocation'} = $loc[3]; } else { undef $iptc->{'SubLocation'}; } $meta->set_app13_data($iptc, $config{LocationMode}, 'IPTC'); if (!$meta->save()) { $errors .= "$dpic: writing of location failed!\n"; } else { # touch the thumbnail pic (set actual time stamp), to suppress rebuilding the next time touch(getThumbFileName($dpic)); updateOneRow($dpic, $lb); showImageInfoCanvas($dpic) if ($dpic eq $actpic); $count++; } } } progressWinEnd($pw) if $pw; log_it("added ${location}to $count of ".scalar @sellist." pictures."); if ($errors ne '') { $errors = "These errors occured while adding the location info to ".scalar @sellist." pictures.\n\n$errors"; showText("Errors while adding location", $errors, NO_WAIT); } } }); $locw->{tree}->bind("", sub { $locw->{tree}->selectionClear(); $locw->{tree}->selectionSet(getNearestItem($locw->{tree})); showThumbsByLocation($locw); }); $locw->{tree}->bind('', sub { showThumbsByLocation($locw); }); return; } ############################################################## ############################################################## sub filter_locations { my ($treew, $filter) = @_; my %loc_hash = get_locations(); if ($filter eq '') { # clear filter, show all insert_in_tree(LOCATION, $treew, \%loc_hash); } else { my $ac=0;my $bc=0;my $cc=0;my $dc=0; my %loc_filter; # filtered location hash foreach my $a (keys %loc_hash) { $ac++; # country match, add all entries below if ($a =~ m|$filter|i) { $loc_filter{$a} = $loc_hash{$a}; next; } foreach my $b (keys %{$loc_hash{$a}}) { $bc++; # state match, add all entries below if ($b =~ m|$filter|i) { $loc_filter{$a}{$b} = $loc_hash{$a}{$b}; next; } foreach my $c (keys %{$loc_hash{$a}{$b}}) { $cc++; # city match, add all entries below if ($c =~ m|$filter|i) { $loc_filter{$a}{$b}{$c} = $loc_hash{$a}{$b}{$c}; next; } foreach my $d (keys %{$loc_hash{$a}{$b}{$c}}) { $dc++; # sublocation match, add all entries below if ($d =~ m|$filter|i) { $loc_filter{$a}{$b}{$c}{$d} = $loc_hash{$a}{$b}{$c}{$d}; } } } } } insert_in_tree(LOCATION, $treew, \%loc_filter); # unfold tree in all levels (show all findings) tree_fold(OPEN, $treew); print "filter_locations: $ac $bc $cc $dc locations scanned\n"; } return; } ############################################################## # set the mouse cursor in widget either to add-mode (plus) or search-mode (hand) ############################################################## sub cursor_search_add { my $search = shift; my $w = shift; return unless Exists($w); my $cursor = 'plus'; $cursor = 'hand2' if $search; # 'target' $w->configure(-cursor => $cursor); } ############################################################## ############################################################## sub add_keywords_to_pics { my ($lb, $keys, $pics) = @_; if (scalar @{$keys} < 1) { print "add_keywords_to_pics: no keys\n"; return; } if (scalar @{$pics} < 1) { print "add_keywords_to_pics: no pics\n"; return; } my $warning = ''; # format given keyword list according to the configured format (all, joined, or last) my @keylist = keyword_format($keys, \$warning); if (@keylist) { my $iptc = { Keywords => \@keylist }; applyIPTC($lb, $iptc, $pics); } if ($warning ne '') { $warning = "IPTC keywords are limited to 64 characters. Please shorten keyword.\n$warning"; showText("Warnings while adding keywords", $warning, NO_WAIT); } return; } ############################################################## ############################################################## sub add_key_tree { my $keyw = shift; my $lb = shift; my $af = $keyw->Frame()->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 1); #my $mode_frame = $af->Frame(-bd => 1, -relief => 'raised')->pack(-expand => 1, -side => 'left', -fill => 'x'); $keyw->{SearchMode} = 1; #$mode_frame->Label(-text => 'Mode:')->pack(-expand => 1, -side => 'left', -fill => 'x'); $af->Radiobutton(-image => compound_menu($keyw, lang('Search'), 'system-search.png'), -variable => \$keyw->{SearchMode}, -value => 1, -indicatoron => 0, -command => sub { cursor_search_add($keyw->{SearchMode}, $keyw->{tree}); cursor_search_add($keyw->{SearchMode}, $keyw->{hot}); })->pack(-expand => 1, -side => 'left', -fill => 'x', -padx => 2, -pady => 2); $af->Radiobutton(-image => compound_menu($keyw, lang('Add'), 'list-add.png'), -variable => \$keyw->{SearchMode}, -value => 0, -indicatoron => 0, -command => sub { cursor_search_add($keyw->{SearchMode}, $keyw->{tree}); cursor_search_add($keyw->{SearchMode}, $keyw->{hot}); })->pack(-expand => 1, -side => 'left', -fill => 'x', -padx => 2, -pady => 2); $balloon->attach($af, -msg => lang('Search')." or ".lang('Add')." mode\nChoose if a double click on a keyword will search\nfor the keyword or add it to the selected pictures"); $af->Button(-image => $mapivi_icons{Help}, -pady => 0, -padx => 0, -command => sub { showText("Help for Navigate by Keywords", "The keyword tree below can be used in three ways:\n1. Search for pictures with a keyword (\"Search mode\", mouse cursor in hand shape)\n2. Add keywords to pictures (\"Add mode\", mouse cursor is a plus sign)\n3. Edit keyword tree\n\nDouble click on a keyword to either search for pictures or to add it to the selected pictures.\n\nTo edit the keywords, use the right mouse button and open the context menu.\n\nUse the middle mouse button (or key d) to open a new window containing all pictures with the selected keyword.", NO_WAIT); })->pack(-expand => 0, -side => 'left', -fill => 'x', -padx => 2, -pady => 2); my $add_frame = $keyw->Frame(-bd => 1, -relief => 'raised')->pack(-expand => 0, -fill =>'x', -padx => 2, -pady => 1); #my $add_frame2 = $add_frame->Frame()->pack(-expand => 0, -fill =>'x', -padx => 2, -pady => 0); my $addB = $add_frame->Button(-image => compound_menu($top, lang('Attach'), 'media-floppy.png', 0), -command => sub { $lb = $picLB unless Exists($lb); # get the selcted keywords from the tree my @keys = $keyw->{tree}->info('selection'); # and add the selected keywords from the hotlist foreach ($keyw->{hot}->curselection()) { push @keys, $keyw->{hot}->get($_); } #print "key to add: $_\n" foreach (@keys); return unless checkSelection($keyw, 1, 0, \@keys, lang("keyword(s)")); my @pics = $lb->info('selection'); return unless checkSelection($keyw, 1, 0, \@pics, lang("picture(s)")); add_keywords_to_pics($lb, \@keys, \@pics); } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 2); $balloon->attach($addB, -msg => lang('Add selected keywords to selected pictures')); my $rmB = $add_frame->Button(-image => compound_menu($top, lang('Detach'), 'edit-clear.png'), -command => sub { $lb = $picLB unless Exists($lb); # get the selected keywords from the tree my @keys = $keyw->{tree}->info('selection'); # and add the selected keywords from the hotlist foreach ($keyw->{hot}->curselection()) { push @keys, $keyw->{hot}->get($_); } return unless checkSelection($keyw, 1, 0, \@keys, lang("keyword(s)")); my @sellist = $lb->info('selection'); return unless checkSelection($keyw, 1, 0, \@sellist, lang("picture(s)")); my $pw = progressWinInit($keyw, "Remove keyword"); my $i = 0; my $sum = @sellist; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "removing keyword ($i/$sum) ...", $i, $sum); foreach my $key (@keys) { last if progressWinCheck($pw); progressWinUpdate($pw, "removing keyword $key ($i/$sum) ...", $i, $sum); my $item; if ($config{KeywordsAll} == 2) { # all, joined my @items = getAllItems($key); $item = join('.', @items); } else { # last $item = getLastItem($key); } print "remove key $item ($key) from $dpic\n" if $verbose; removeIPTCItem($dpic, 'Keywords', $item); updateOneRow($dpic, $lb); showImageInfoCanvas($dpic) if ($dpic eq $actpic); } } progressWinEnd($pw); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 2, -pady => 2); $balloon->attach($rmB, -msg => lang("Remove selected keywords from selected pictures")); my $add_mode = $add_frame->Optionmenu(-variable => \$config{KeywordsAll}, -textvariable => \$config{KeywordsAll}, -options => [ [lang('Join') => 2], [lang('All') => 1], [lang('Last') => 0], ], -pady => 0, -padx => 0)->pack(-side => 'left', -anchor => 'w', -fill => 'x', -padx => 2, -pady => 2); $balloon->attach($add_mode, -msg => "Keyword mode\nExample keyword: Person/Bundy/Kelly\n\"".lang('Join')."\" will add one keyword: \"Person.Bundy.Kelly\"\n\"".lang('All')."\" will add three keywords: \"Person\", \"Bundy\" and \"Kelly\"\n\"".lang('Last')."\" will add one keyword: \"Kelly\"\n\nDefault and recommended mode: \"".lang('Join')."\"\nIf you want to store and retrieve your keyword\nhierarchie from your pictures you should use \"".lang('Join')."\" mode.\nThe keyword mode is also used when removing keywords."); # keyword clipboard $keyw->{hot} = $keyw->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 25, -height => 5, )->pack(-expand => 0, -fill =>'both', -padx => 1, -pady => 1); # set cursor to either search or add to inform the user about the mode cursor_search_add($keyw->{SearchMode}, $keyw->{hot}); focus_on_enter($keyw->{hot}->Subwidget("scrolled")); $keyw->{hot}->insert('end', (sort keys %hot_keywords)); # if there is a selection in the clipboard we clear the selection in the tree and vice versa to avoid confusion $keyw->{hot}->bind('', sub { $keyw->{tree}->selectionClear(); $keyw->{tree}->anchorClear();}); my $hot_menu = $keyw->{hot}->Menu(-title => lang("Keyword clipboard menu")); $keyw->{hot}->bind('', sub { $hot_menu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); $hot_menu->command(-label => lang('Clear keyword clipboard'), -command => sub { undef %hot_keywords; $keyw->{hot}->delete(0, 'end'); }); $hot_menu->command(-label => lang('Remove selected keyword(s) from clipboard'), -command => sub { return unless checkSelection($keyw, 1, 0, \@{$keyw->{hot}->curselection()}, lang("keyword(s)")); # and add the sected keywords from the clipboard foreach ($keyw->{hot}->curselection()) { delete $hot_keywords{$keyw->{hot}->get($_)}; } $keyw->{hot}->delete(0, 'end'); $keyw->{hot}->insert('end', (sort keys %hot_keywords)); }); $keyw->{hot}->bind('', sub { double_click($keyw, $lb); }); $keyw->Adjuster->packAfter($keyw->{hot}, -pady => 3); # keyword tree $keyw->{tree} = $keyw->Scrolled('Tree', -separator => '/', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 25, -height => 15, )->pack(-expand => 1, -fill =>'both', -padx => 1, -pady => 1); # set cursor to either search or add to inform the user about the mode cursor_search_add($keyw->{SearchMode}, $keyw->{tree}); # add a filter entry to show only selected keywords in the tree add_tree_filter($keyw, $keyw->{tree}, KEYWORD, \@prekeys); # if there is a selection in the tree we clear the selection in the hotlist and vice versa to avoid confusion $keyw->{tree}->bind('', sub { $keyw->{hot}->selectionClear(0, 'end'); }); focus_on_enter($keyw->{tree}->Subwidget("scrolled")); # try to get and set the saved mode (opened and closed branches of the tree) my $modeRef; if (-f "$user_data_path/keywordMode") { $modeRef = retrieve("$user_data_path/keywordMode"); } $keyw->{tree}->{m_mode} = $modeRef if (defined $modeRef); addTreeMenu($keyw->{tree}, \@prekeys, $keyw->{hot}); insertTreeList($keyw->{tree}, @prekeys); $keyw->{tree}->bind("", sub { double_click($keyw, $lb); }); # middle mouse button on a keyword opens a new window containing all related pictures $keyw->{tree}->bind('', sub { $keyw->{tree}->selectionClear(); $keyw->{tree}->anchorClear(); $keyw->{tree}->selectionSet(getNearestItem($keyw->{tree})); showThumbsByKeyword($keyw); }); # key d on a keyword opens a new window containing all related pictures $keyw->{tree}->bind('', sub { showThumbsByKeyword($keyw); }); } ############################################################## # sub function for double clicking in the tree or the hotlist ############################################################## sub double_click { my $keyw = shift; my $lb = shift; #print "keywordtree double click\n"; # get the selected keywords from the tree my @keys = $keyw->{tree}->info('selection'); # and add the sected keywords from the hotlist foreach ($keyw->{hot}->curselection()) { push @keys, $keyw->{hot}->get($_); } return unless checkSelection($keyw, 1, 1, \@keys, lang("keyword(s)")); $lb = $picLB unless Exists($lb); # search mode if ($keyw->{SearchMode}) { # check if the user wants to add keywords and forgot to press "Search" my @sellist = getSelection($lb); if (@sellist > 1) { #$keyw->update; my $rc = $keyw->messageBox(-icon => 'question', -message => "You've selected several pictures.\nDo you want to switch to \"Add\" mode and add the keyword to the pictures?", -title => "Switch to Add-mode?", -type => 'YesNo'); $keyw->{SearchMode} = 0 if ($rc =~ m/Yes/i); } } # search-mode if ($keyw->{SearchMode}) { @act_keywords = split(/\//, $keys[0]); # do not exclude some keywords undef @act_keywords_ex; # switch display modus to location $act_modus = KEYWORD; updateThumbs(); } # add-mode else { my @pics = getSelection($lb); return unless checkSelection($keyw, 1, 0, \@pics, lang("picture(s)")); add_keywords_to_pics($lb, \@keys, \@pics); } } ############################################################## # get selection from keyword tree and display thumbnails in new window ############################################################## sub showThumbsByKeyword { my $keyw = shift; my @keys = $keyw->{tree}->info('selection'); return unless checkSelection($keyw, 1, 0, \@keys, lang("keyword(s)")); my @keywords = split(/\//, $keys[0]); my @keywords_ex; my @list = get_pics_with_keywords(\@keywords, \@keywords_ex); my $title = 'Keywords: '; $title .= "$_ " foreach (@keywords); showThumbList(\@list, $title); } ############################################################## # add a entry to show only items (keywords/locations) matching # a filter criteria in the tree ############################################################## sub add_tree_filter { # type = KEYWORD or LOCATION (both defined as constants) # @prekeys are only needed for type KEYWORD not for LOCATION my ($w, $treew, $type, $prekeys) = @_; my $filter = ''; my $frame = $w->Frame()->pack(-expand => 0, -fill => 'x'); $w->{entry} = $frame->Entry( -textvariable => \$filter, -validate => 'key', -validatecommand => sub { if ($type eq KEYWORD) { filter_tree($treew, $_[0], $prekeys); } elsif ($type eq LOCATION) { filter_locations($treew, $_[0]); } else { log_it("error: add_tree_filter called with wrong type: $type"); } return 1; }, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); $balloon->attach($w->{entry}, -msg => lang("Filter:\nEnter any word or part of it\nto filter tree.")); $w->{clear} = $frame->Button( -image => $mapivi_icons{'Clear'}, -command => sub { $filter = ''; tree_fold(CLOSE, $treew); $w->{entry}->focus; })->pack(-side => 'left', -expand => 0, -padx => 1); $balloon->attach($w->{clear}, -msg => lang("Clear filter and collapse tree")); $w->{entry}->focus; $w->{entry}->selectionRange(0,'end'); # select all } ############################################################## ############################################################## sub filter_tree { my ($treew, $filter, $prekeys) = @_; if ($filter eq '') { # reset tree insertTreeList($treew, @{$prekeys}); } else { my @list; foreach my $item (@{$prekeys}) { if ($item =~ m|$filter|i) { my @elements = split /\//, $item; my $string = ''; foreach my $element (@elements) { if ($string eq '') { $string = "$element"; } else { $string .= "/$element"; } if (not isInList($string, \@list)) { push @list, $string; } } } } insertTreeList($treew, @list); # open found keywords filter_tree_open($treew, '', $filter); } return; } ############################################################## # open all trees which match the filter criteria # recursive function ############################################################## sub filter_tree_open { my ($treew, $startkey, $filter) = @_; my $open = 0; foreach ($treew->info('children', $startkey)) { # if any of the childs matches we open the parent if (filter_tree_open($treew, $_, $filter)) { $treew->open($_); $open = 1; } else { $treew->close($_); } my @path = split (/\//,$_); # get the last item if ($path[-1]=~ m|$filter|i) { $open = 1; } } return $open; } ############################################################## ############################################################## sub add_key_cloud { my $w = shift; my $lb = shift; my @search_keys = (); my @exclude_keys = (); my $add_mode = 1; my $keyf = $w->Frame(-bd => 1, -relief => 'solid')->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 1); my $exkeyf = $w->Frame(-bd => 1, -relief => 'solid')->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 1); my $picf = $w->Frame(-bd => 1, -relief => 'flat')->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 1); # included keywords my $label = ''; my $af = $keyf->Frame()->pack(-expand => 0, -fill => 'x', -padx => 0, -pady => 0); $w->{Include} = $af->Label(-text => lang('Included'), -anchor => 'w')->pack(-side => 'left', -padx => 2, -pady => 0); $balloon->attach($w->{Include}, -msg => lang('Included keywords')."\n".lang('Click on a keyword below to add it to this list')); $w->{First} = $af->Button(-image => $mapivi_icons{'Clear'}, -command => sub { # reset search_keys @search_keys = (); $label = ''; show_keywords($w, \@search_keys, \@exclude_keys); })->pack(-side => 'right', -padx => 2, -pady => 2); $balloon->attach($w->{First}, -msg => lang('Clear list')); $w->{Back} = $af->Button(-image => $mapivi_icons{'Back'}, -command => sub { return unless (@search_keys); # remove last element of array search_keys pop @search_keys; $label = ''; $label .= "$_ " foreach (@search_keys); show_keywords($w, \@search_keys, \@exclude_keys); })->pack(-side => 'right', -padx => 2, -pady => 2); $balloon->attach($w->{Back}, -msg => lang('Remove last keyword from list')); $keyf->Label(-textvariable => \$label, -anchor => 'w', )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); # excluded keywords my $exlabel = ''; my $bf = $exkeyf->Frame()->pack(-expand => 0, -fill => 'x', -padx => 0, -pady => 0); $w->{Exclude} = $bf->Label(-text => lang('Excluded'), -anchor => 'w')->pack(-side => 'left', -padx => 2, -pady => 0); $balloon->attach($w->{Exclude}, -msg => lang('Excluded keywords')."\n".lang('Right click on a keyword below to add it to this list')); $w->{exFirst} = $bf->Button(-image => $mapivi_icons{'Clear'}, -command => sub { # reset search_keys @exclude_keys = (); $exlabel = ''; show_keywords($w, \@search_keys, \@exclude_keys); })->pack(-side => 'right', -padx => 2, -pady => 2); $balloon->attach($w->{exFirst}, -msg => lang('Clear list')); $w->{exBack} = $bf->Button(-image => $mapivi_icons{'Back'}, -command => sub { return unless (@exclude_keys); # remove last element of array search_keys pop @exclude_keys; $exlabel = ''; $exlabel .= "$_ " foreach (@exclude_keys); show_keywords($w, \@search_keys, \@exclude_keys); })->pack(-side => 'right', -padx => 2, -pady => 2); $balloon->attach($w->{exBack}, -msg => lang('Remove last keyword from list')); $exkeyf->Label(-textvariable => \$exlabel, -anchor => 'w', )->pack(-side => 'top', -expand => 0, -fill => 'x', -padx => 1, -pady => 1); # show button, label and refresh button $w->{Show} = $picf->Button(-text => lang('Show'),#-image => $mapivi_icons{'Show'}, -command => sub { $act_modus = KEYWORDCLOUD; @act_keywords = @search_keys; @act_keywords_ex = @exclude_keys; showThumbs(); })->pack(-side => 'left', -padx => 3, -pady => 2); $balloon->attach($w->{Show}, -msg => lang('Show these pictures')); # label my $lab2 = $picf->Label(-textvariable => \$w->{label2}, -anchor => 'w' )->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 1); $balloon->attach($lab2, -msg => "x pictures, y keywords\nx = number of pictures with the selected keywords\ny = number of displayed keywords"); # Help button $w->{Help} = $picf->Button(-image => $mapivi_icons{Help}, -command => sub { showText("Help for keyword cloud", "The box shows all used keywords. The bigger the keyword the more often it is used.\nA right click on any keyword will add it to the include, a left click to the exclude list.\nClick \"".lang('Show')."\" to show pictures containing the include keywords.\nTo restrict the search to pictures with a certain rating use the \"".lang('Rating')."\" button.", NO_WAIT); })->pack(-side => 'right', -padx => 3); $balloon->attach($w->{Help}, -msg => lang('Help')); # Refresh button $w->{Refresh} = $picf->Button(-image => $mapivi_icons{'UpdateS'}, -command => sub { show_keywords($w, \@search_keys, \@exclude_keys); })->pack(-side => 'right', -padx => 3); $balloon->attach($w->{Refresh}, -msg => lang('Reread keywords from database')); $w->{canvas} = $w->Scrolled('Canvas', -scrollbars => 'osoe', -width => 70, -height => 40, -relief => 'sunken', -bg => $conf{color_act_bg}{value}, -cursor => 'hand2', )->pack(-expand => 1, -fill => 'both', -padx => 1, -pady => 1); $w->{canvas}->configure(-scrollregion => [0, 0, 700, 400]); show_keywords($w, \@search_keys, \@exclude_keys); # todo: mouse wheel scrolling doesn't work, mh 2011-05 #focus_on_enter($w->{canvas}->Subwidget("scrolled")); # reaction for clicking on a keyword (tag) $w->{canvas}->CanvasBind('' => sub { my @curr = $w->{canvas}->find('withtag', 'current'); my @tags = $w->{canvas}->gettags($curr[0]); foreach (@tags) { next if ($_ eq 'current'); if ($add_mode) { # add new keyword to list, if it is not already there push @search_keys, $_ unless (isInList($_, \@search_keys)); } else { # clear list and add just the new selected keyword @search_keys = (); push @search_keys, $_; } } $label = ''; $label .= "$_ " foreach (@search_keys); show_keywords($w, \@search_keys, \@exclude_keys); }); # reaction for right clicking on a keyword (tag) $w->{canvas}->CanvasBind('' => sub { my @curr = $w->{canvas}->find('withtag', 'current'); my @tags = $w->{canvas}->gettags($curr[0]); foreach (@tags) { next if ($_ eq 'current'); push @exclude_keys, $_ unless (isInList($_, \@exclude_keys)); } $exlabel = ''; $exlabel .= "$_ " foreach (@exclude_keys); show_keywords($w, \@search_keys, \@exclude_keys); }); } ############################################################## ############################################################## sub add_search_frame { my $w = shift; $w->{labelw} = $w->Label(-text => '')->pack(-anchor => 'w'); # the search pattern my $pattern = $config{SearchPattern}; my $search_text = 'All IPTC infos, EXIF data, JPEG comments, the file names and paths of all pictures in the database will be searched. The search is an AND-search and case insensitive.'; # search box my $sf = $w->Frame(-bd => 1, -relief => 'raised')->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 10); $sf->Label(-text => lang('Search'))->pack(-side => 'top', -anchor => 'w', -padx => 2); my $af = $sf->Frame()->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 1); $w->{entry} = $af->Entry(-textvariable => \$pattern, -width => 10)->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); $balloon->attach($w->{entry}, -msg => 'Use * for zero or more chars, ? for exactly one char. Use \* to search for the star sign (*) and \? to search for a questionmark (?) itself. To search for a backslash (\) use two backslashes (\\\). Examples: "I * home" will match e.g. "I go home", "I run home" but also "I do not go home" "Tr?ck" will match "Trick" or "Track" "who\?" will match "who?" "\*\* Party \*\*" will match "** Party **"'); $w->{entry}->bind('', sub { $w->{search}->Invoke; } ); $w->{entry}->focus; $w->{entry}->selectionRange(0,'end'); # select all $w->{search} = $af->Button(-image => $mapivi_icons{Search}, -pady => 0, -command => sub { # clear incremental search labels $w->{incr_label_state} = ''; $w->{incr_label_pics} = ''; # store the patterns before we process them $config{SearchPattern} = $pattern; $act_modus = SEARCH; updateThumbs(); return; })->pack(-side => 'left', -anchor => 'w', -expand => 0,-fill => 'x',-padx => 1,-pady => 1); $balloon->attach($w->{search}, -msg => 'Press here to perform a search for the given search pattern.'."\n".$search_text); # search while typing - incremental search box my $if = $w->Frame(-bd => 1, -relief => 'raised')->pack(-expand => 0, -fill => 'x', -padx => 2, -pady => 10); my $pattern2 = ''; $w->{incr_pic_list_valid} = 0; $if->Label(-text => lang("Incremental search"))->pack(-side => 'top', -anchor => 'w', -padx => 2); my $incr_search_time_threshold = 500; # in ms $w->{entry2} = $if->Entry(-textvariable => \$pattern2, -validate => 'key', -validatecommand => sub { # stop running incremental search $w->{incr_search_running} = 0; # stop timer $w->{incr_timer}->cancel if ($w->{incr_timer}) ; my $pattern = $_[0]; # start new timer $w->{incr_timer} = $w->after($incr_search_time_threshold, sub { search_incremental($w, $pattern); }); return 1; }, -width => 10)->pack(-side => 'top', -anchor => 'w', -fill => 'x', -expand => 1, -padx => 2); $balloon->attach($w->{entry2}, -msg => "Enter any search pattern here.\nThe search will automatically start $incr_search_time_threshold ms after the last keypress.\n$search_text"); $w->{incr_label_state} = ''; $w->{incr_label_pics} = ''; $if->Label(-textvariable => \$w->{incr_label_state})->pack(-side => 'top', -anchor => 'w', -padx => 2); $if->Label(-textvariable => \$w->{incr_label_pics})->pack(-side => 'top', -anchor => 'w', -padx => 2); # end incremental search my $bf = $w->Frame()->pack(-anchor => 'sw', -expand => 1, -fill => 'x'); $bf->Button(-text => lang('Advanced Search'), -pady => 0, -command => sub { searchMetaInfo() })->pack(-side => 'left', -anchor => 'sw', -padx => 3, -pady => 3); $bf->Button(-image => $mapivi_icons{Help}, #-text => '?', -command => sub { showText("Help for search navigation", "Enter any search string in the entry and press the button to perform a search for the given search pattern in the Mapivi picture database. $search_text\nUse the \"".lang("Advanced Search")."\" button to perform an expert search.", NO_WAIT); })->pack(-side => 'left', -padx => 3, -pady => 3); } ############################################################## ############################################################## sub search_incremental { my $w = shift; my $pattern = shift; my $hit_count = 0; $w->{incr_label_pics} = ''; if ((not defined $pattern) or ($pattern eq '')) { print "search_incremental: no or empty pattern\n" if $verbose; return; } $w->{incr_search_running} = 1; $w->{incr_label_state} = lang('searching').' ...'; # clean the thumbnail table $picLB->delete('all'); $config{SearchPattern} = $pattern; $act_modus = SEARCH; set_act_nav_label(); #log_it("searching pictures ..."); my $search_hash; # if we've searched throu all pictures we can reuse the result list for the # next incremental search, if the new search pattern contains the old pattern # this will speed up searches if (($w->{incr_pic_list_valid}) and (index($pattern,$w->{incr_pic_list_pattern}) == 0)) { #print "using last result (".$w->{incr_pic_list_pattern}." is substr of $pattern)\n"; $search_hash = \%{$w->{incr_pic_list}}; } else { #print "using searchDB\n"; $search_hash = \%searchDB; } # replace (german) umlaute by corresponding letters $pattern =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); my $pat = makePattern($pattern);# support windows like search patterns # if pattern contains a whitespace we add the time-consuming look-ahead if ($pat =~ m/.*\s+.*/) { $pat = '(?=.*'.$pat; # and-function with look-ahead $pat =~ s/\s+/)(?=.*/g; # replace one or more whitespaces with )(?=.* $pat .= ')'; # "Tom Tim" is now: "(?=.*Tom)(?=.*Tim)" } my $last_time; my $i = 0; my $nr = keys %$search_hash; foreach my $dpic (keys %$search_hash) { last if ($w->{incr_search_running} == 0); $i++; # show progress every 300 ms if (!defined $last_time || Tk::timeofday()-$last_time > 0.3) { my $percent = int($i/$nr*100); $w->{incr_label_state} = lang('searching')." ($percent\%) ..."; $last_time = Tk::timeofday(); $w->update; } if ($conf{nav_rating_on}{value}) { next if (not rating_valid($searchDB{$dpic}{URG}, $conf{search_rating_max}{value}, $conf{search_rating_min}{value})); } my $meta = $dpic; $meta .= ' '.$searchDB{$dpic}{COM} if (defined $searchDB{$dpic}{COM}); $meta .= ' '.$searchDB{$dpic}{EXIF} if (defined $searchDB{$dpic}{EXIF}); $meta .= ' '.$searchDB{$dpic}{IPTC} if (defined $searchDB{$dpic}{IPTC}); $meta .= ' '.$searchDB{$dpic}{KEYS} if (defined $searchDB{$dpic}{KEYS}); if ((defined $meta) and ($meta ne '')) { # replace newlines with space $meta =~ s/\n/ /g; if ($meta =~ m/.*$pat.*/i) { # collect matching pics in a list $hit_count++; $w->{incr_pic_list}{$dpic}++; $w->{incr_label_pics} = langf("found %d pictures",$hit_count); addOneRow($picLB, $dpic, 1); } } } # foreach #print "incr search stop \"$pattern\" - searched $i of $nr pics - found ".scalar(@pic_list)." pictures\n"; $w->{incr_label_state} = lang('ready'); $w->{incr_label_pics} = langf("found %d pictures",$hit_count); # store actual search pattern $w->{incr_pic_list_pattern} = $pattern; # set a flag if the search finished, else delete search results if ($i == $nr) { $w->{incr_pic_list_valid} = 1; } else { if ($hit_count > 0) { $w->{incr_pic_list_valid} = 0; undef %{$w->{incr_pic_list}}; } } showNrOf(); return; } ############################################################## ############################################################## sub get_database_info { my $keys = keys %searchDB; my $size = getFileSize($searchDBfile, FORMAT); return langf("The database contains %d pictures, database file size is %s", $keys, $size); } ############################################################## ############################################################## sub add_rating_constraint { my $w = shift; my $nav_rating_frame = $w->Frame(-bd => 1, -relief => 'raised'); my $rab; $rab = $nav_rating_frame->Checkbutton(-variable => \$conf{nav_rating_on}{value}, -text => lang('Rating'), -indicatoron => 0, -command => sub { if ($conf{nav_rating_on}{value}) { $nav_rating_frame->{subframe}->pack(-after => $rab, -side => 'left', -expand => 1, -fill =>'x', -padx => 0, -pady => 0);# if (!ismapped($addF)); } else { $nav_rating_frame->{subframe}->packForget(); } # if (ismapped($addF)); #show_keywords($win, \@search_keys, \@exclude_keys); } )->pack(-side => 'left', -fill => 'both', -anchor => 'w', -padx => 1, -pady => 0); $balloon->attach($rab, -msg => "Limit pictures to a rating range.\nIf the first button shows e.g. 4 stars and the second button shows 2 stars\nonly pictures with a rating of 2, 3 or 4 stars are displayed.\nNote: The function is disabled in folder navigation mode."); $nav_rating_frame->{subframe} = $nav_rating_frame->Frame(); if ($conf{nav_rating_on}{value}) { $nav_rating_frame->{subframe}->pack(-after => $rab, -side => 'left', -expand => 1, -fill =>'x', -padx => 0, -pady => 0);# if (!ismapped($addF)); } else { $nav_rating_frame->{subframe}->packForget(); } rating_button_min_max($nav_rating_frame->{subframe}, \$conf{search_rating_max}{value}, \$conf{search_rating_min}{value}); return $nav_rating_frame; } ############################################################## ############################################################## sub add_dir_tree { my $dir_frame = shift; my $dirtree; $dirtree = $dir_frame->Scrolled('DirTree', -scrollbars => 'osoe', -width => 30, -height => 200, -showhidden => $config{ShowHiddenDirs}, -selectmode => 'browse', -exportselection => 1, -browsecmd => sub { # this function will show all subdirs when clicking on the + sign of a dir $dirtreedir = shift; $dirtreedir = Encode::encode('iso-8859-1', $dirtreedir); return if (@_ >= 1); if (!-d $dirtreedir) { log_it("$dirtreedir does not exists!"); print "dirtree: $dirtreedir does not exists!\n"; return; } $top->Busy; my @dirs = getDirs($dirtreedir); $top->Unbusy; return if (@dirs < 1); $top->Busy; my $lastdir = $dirtreedir.'/'.$dirs[-1]; if ($dirtree->info('exists', $lastdir)) { $dirtree->see($lastdir) if (-d $lastdir); } $top->Unbusy; }, -command => sub { openDirPost($dirtreedir); }, )->pack(-expand => 1, -fill => 'both'); return $dirtree; } ############################################################## ############################################################## sub get_pics_by { my $kind = shift; # either LOCATION or DATE my $data = shift; # list ref between one and four location names (country, state, city, sublocation) or dates (year, month, day, hour) my @list = (); my %hash; if ($kind == LOCATION) { %hash = get_locations(); } elsif ($kind == DATE) { %hash = get_dates(); } else { warn "Wrong kind: $kind"; } if (@$data == 1) { foreach my $a (sort keys %{$hash{$$data[0]}}) { foreach my $b (sort keys %{$hash{$$data[0]}{$a}}) { foreach my $c (sort keys %{$hash{$$data[0]}{$a}{$b}}) { push @list, sort keys %{$hash{$$data[0]}{$a}{$b}{$c}}; } } } } elsif (@$data == 2) { foreach my $b (sort keys %{$hash{$$data[0]}{$$data[1]}}) { foreach my $c (sort keys %{$hash{$$data[0]}{$$data[1]}{$b}}) { push @list, sort keys %{$hash{$$data[0]}{$$data[1]}{$b}{$c}}; } } } elsif (@$data == 3) { foreach my $c (sort keys %{$hash{$$data[0]}{$$data[1]}{$$data[2]}}) { push @list, sort keys %{$hash{$$data[0]}{$$data[1]}{$$data[2]}{$c}}; } } elsif (@$data == 4) { @list = sort keys %{$hash{$$data[0]}{$$data[1]}{$$data[2]}{$$data[3]}}; } else { warn "Wrong number of data: @$data"; } # remove pictures with wrong rating from list if ($conf{nav_rating_on}{value}) { my @list2 = (); foreach (@list) { next if (not rating_valid($searchDB{$_}{URG}, $conf{search_rating_max}{value}, $conf{search_rating_min}{value})); # add only pictures which fullfill the rating constraints push @list2, $_; } # reset original list @list = (); # copy new list to original list @list = @list2; } return @list; } ############################################################## ############################################################## sub insert_collections_in_tree { my $tree = shift; # tree widget ref my $hash = shift; # slideshow hash ref $tree->delete('all'); foreach my $folder (sort keys %$hash) { $tree->add($folder, -text => $folder); foreach my $collection (sort keys %{$hash->{$folder}}) { my $pics = scalar(@{$hash->{$folder}->{$collection}->{pics}}); $tree->add("$folder%$collection", -text => "$collection [$pics]"); } } # add plus/minus buttons to collapse tree $tree->autosetmode; # collapse tree in all levels tree_fold(CLOSE, $tree); return; } ############################################################## ############################################################## sub insert_in_tree { my $kind = shift; # either LOCATION or DATE my $tree = shift; # tree widget ref my $hash = shift; # location or date hash ref $tree->delete('all'); # insert the 4-level-deep hash in the tree foreach my $a (sort keys %$hash) { my $pics = 0; foreach my $b (sort keys %{$hash->{$a}}) { foreach my $c (sort keys %{$hash->{$a}->{$b}}) { foreach my $d (sort keys %{$hash->{$a}->{$b}->{$c}}) { $pics += keys %{$hash->{$a}->{$b}->{$c}->{$d}}; } } } $tree->add($a, -text => "$a [$pics]"); foreach my $b (sort keys %{$hash->{$a}}) { my $pics = 0; foreach my $c (sort keys %{$hash->{$a}->{$b}}) { foreach my $d (sort keys %{$hash->{$a}->{$b}->{$c}}) { $pics += keys %{$hash->{$a}->{$b}->{$c}->{$d}}; } } my $text = $b; $text = "$a-$b" if ($kind == DATE); $tree->add("$a%$b", -text => "$text [$pics]"); foreach my $c (sort keys %{$hash->{$a}->{$b}}) { my $pics = 0; foreach my $d (sort keys %{$hash->{$a}->{$b}->{$c}}) { $pics += keys %{$hash->{$a}->{$b}->{$c}->{$d}}; } my $text = $c; $text = "$a-$b-$c" if ($kind == DATE); $tree->add("$a%$b%$c", -text => "$text [$pics]"); foreach my $d (sort keys %{$hash->{$a}->{$b}->{$c}}) { my $pics = keys %{$hash->{$a}->{$b}->{$c}->{$d}}; my $text = $d; $text = "$d:00" if ($kind == DATE); $tree->add("$a%$b%$c%$d", -text => "$text [$pics]"); } } } } # add plus/minus buttons to collapse tree $tree->autosetmode; # collapse tree in all levels tree_fold(CLOSE, $tree); return; } ############################################################## # collapse or unfold a tree ############################################################## sub tree_fold { my $what = shift; # OPEN or CLOSE my $tree = shift; # tree widget my $item = shift; # optional item path e.g. "USA%West Virginia%Canaan", empty = root (all entries) foreach ($tree->info('children', $item)) { $what ? $tree->open($_) : $tree->close($_); if (scalar(@{$tree->info('children')}) >= 1) { # if there are still some children call function recursive tree_fold($what, $tree, $_); } } } ############################################################## # get_dates - get all date/time info from location hash or if not available from the searchDB ############################################################## sub get_dates { my $mode = shift; # optional: no argument or UPDATE log_it(lang('Getting date and time from database ...')); my $start = Tk::timeofday(); if ($dates_need_update or (defined $mode and $mode == UPDATE)) { %dates = get_dates_from_DB(); $dates_need_update = 0; } my $duration = sprintf "%.2f", (Tk::timeofday() - $start); log_it(lang('Ready!').' '.langf("Got date and time in %s seconds.",$duration)); return %dates; } ############################################################## # get_dates_from_DB - get all dates from the searchDB as hash ############################################################## sub get_dates_from_DB { my %date_hash; # build date/time hash # loop through all pictures in the DB #foreach my $dpic (keys %searchDB) { while (my ($dpic, undef) = each %searchDB) { #my $s = 0; my $m = 0; my $h = 0; my $d = 0; my $mo = 0; my $y = 0; if ($searchDB{$dpic}{TIME}) { (undef,undef,$h,$d,$mo,$y) = getDateTime($searchDB{$dpic}{TIME}); } $mo = sprintf "%02d", $mo; $d = sprintf "%02d", $d; $h = sprintf "%02d", $h; #my $key = sprintf "%04d%02d", $y, $mo; # key = yyyymm # four levels down to hour should be OK $date_hash{$y}{$mo}{$d}{$h}{$dpic}++; } return %date_hash; } ############################################################## # get_locations - get all locations from location hash or if not available from the searchDB ############################################################## sub get_locations { my $mode = shift; # optional: no argument or UPDATE log_it(lang('Getting locations from database ...')); my $start = Tk::timeofday(); if ($locations_need_update or (defined $mode and $mode == UPDATE)) { %locations = get_locations_from_DB(); $locations_need_update = 0; } my $duration = sprintf "%.2f", (Tk::timeofday() - $start); log_it(lang('Ready!').' '.langf("Got locations in %s seconds.",$duration)); return %locations; } ############################################################## # get_locations_from_DB - get all locations from the searchDB as hash ############################################################## sub get_locations_from_DB { my %location_hash; # build location hash # loop through all pictures in the DB #foreach my $dpic (keys %searchDB) { while (my ($dpic, undef) = each %searchDB) { my $country = $empty_str; my $state = $empty_str; my $city = $empty_str; my $subloc = $empty_str; if (defined $searchDB{$dpic}{IPTC}) { # add a newline, else the last match won't work my $iptc = $searchDB{$dpic}{IPTC}."\n"; # Country needs extra treatment, because in the short IPTC info # we can't distinguish between LocationName and LocationCode # Accoring to @IPTCAttributes Name comes before Code, so when Name is available # (and thus $country is no longer $empty_str) we ignore Code if (($iptc =~ m|Country\.: (.*)\n|) and ($country eq $empty_str)) { $country = $1; } if ($iptc =~ m|Provinc\.: (.*)\n|) { $state = $1; } if ($iptc =~ m|City\s*: (.*)\n|) { $city = $1; } if ($iptc =~ m|SubLoca\.: (.*)\n|) { $subloc = $1; } } $location_hash{$country}{$state}{$city}{$subloc}{$dpic}++; } return %location_hash; } ############################################################## # returns true if pics have no location info or if they have # location info and the user agrees to overwrite ############################################################## sub allow_location_overwrite { my $w = shift; # widget my $sellist = shift; # list ref my $ok = 1; my $pics_with_location = check_locations($sellist); if ($pics_with_location > 0) { $ok = 0; my $rc = $w->messageBox(-message => "$pics_with_location of the ".scalar(@$sellist)." selected pictures have a location info. This information will be overwritten. Please press Ok to continue.", -icon => 'question', -title => "Overwrite location?", -type => 'OKCancel'); $ok = 1 if ($rc =~ m/Ok/i); } return $ok; } ############################################################## # check_locations - check if the given list of pictures has any location info # returns the number of pictures with locations ############################################################## sub check_locations { my $pic_list = shift; # list reference my $count = 0; # loop through all pictures of the list foreach my $dpic (@$pic_list) { if (defined $searchDB{$dpic}{IPTC}) { my $iptc = $searchDB{$dpic}{IPTC}; if (($iptc =~ m|Country\.:.*\n|) or ($iptc =~ m|Provinc\.:.*\n|) or ($iptc =~ m|City\s*:.*\n|) or ($iptc =~ m|SubLoca\.:.*\n|)) { $count++; } } } return $count; } ############################################################## # filter the picture list (arg1) by IPTC keywords, # the pictures containing exclude keywords (arg2) are removed from the list # the number of excluded pics is returned. ############################################################## sub filter_pics { my $pics = shift; # list reference my $exclude_keys = shift; # string, space separated list for keywords which must not be contained my @exclude_list = split /\s+/, $exclude_keys; # split space separated list my @pic_list; # list of included pictures my $exclude_count = 0; # number of excluded pictures foreach my $dpic (@{$pics}) { if (defined $searchDB{$dpic}{KEYS}) { # check if any items of the exclude_list are contained in this keyword string # exclude pictures with certain keywords if (string_contains($searchDB{$dpic}{KEYS}, \@exclude_list)) { $exclude_count++; next; } } # collect matching pics in a list push @pic_list, $dpic; } @$pics = @pic_list; return $exclude_count; } ############################################################## # get_pics_with_keywords - returns a list of pictures with the # given keywords (source: searchDB) ############################################################## sub get_pics_with_keywords { my $search_keys = shift; # list reference my $exclude_keys = shift; # list reference for keywords which must not be contained my @pic_list; # build keyword/tag hash #foreach my $dpic (keys %searchDB) { while (my ($dpic,undef) = each %searchDB) { # skip if no keywords in picture next unless (defined $searchDB{$dpic}{KEYS}); if ($config{KeywordDate}) { my $time = $searchDB{$dpic}{TIME}; if (defined $time) { next if ($time < $config{KeywordStart}); next if ($time > $config{KeywordEnd}); } } if ($conf{nav_rating_on}{value}) { next if (not rating_valid($searchDB{$dpic}{URG}, $conf{search_rating_max}{value}, $conf{search_rating_min}{value})); } # check if any items of the exclude_keys list are contained in this keyword string next if (string_contains($searchDB{$dpic}{KEYS}, $exclude_keys)); # check if all items of the search_keys list are contained in this keyword string next if (string_contains_not($searchDB{$dpic}{KEYS}, $search_keys)); # collect matching pics in a list push @pic_list, $dpic; } return @pic_list; } ############################################################## # get_pics_by_searching - returns a list of pictures matching a search pattern (source: searchDB) ############################################################## sub get_pics_by_searching { my $pattern = shift; # search pattern my $exclude = shift; # explude pattern my @pic_list; return @pic_list if ((not defined $pattern) or ($pattern eq '')); log_it(lang('Searching pictures ...')); # replace (german) umlaute by corresponding letters $pattern =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $exclude =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); # cut the pattern to 20 chars to fit into the progressbar dialog my $pattern_show = cutString($pattern, 20, '..'); my $pat = makePattern($pattern);# support windows like search patterns my $exl = makePattern($exclude);# support windows like search patterns # if pattern contains a whitespace we add the time-consuming look-ahead if ($pat =~ m/.*\s+.*/) { $pat = '(?=.*'.$pat; # and-function with look-ahead $pat =~ s/\s+/)(?=.*/g; # replace one or more whitespaces with )(?=.* $pat .= ')'; # "Tom Tim" is now: "(?=.*Tom)(?=.*Tim)" } my $case = 'i'; $case = '' if $config{SearchCase}; my $i = 0; my $found = 0; my $nr = keys %searchDB; my $pw = progressWinInit($top, lang('Searching')); # using each instead of keys (for small mapivi DB (3460 pics, 1,3MB) 1.2-1.5 times faster) # http://stackoverflow.com/questions/22841830 # Pros: # This uses very little memory as every time each is called it only # returns a pair of (key, value) element. # Cons: # You can't order the output by key. # The iterator it uses belongs to %h. If the code inside the loop calls # something that does keys %h, values %h or each %h, then the loop won't # work properly, because %h only has 1 iterator while (my ($dpic, undef) = each %searchDB) { last if progressWinCheck($pw); $i++; $found = scalar @pic_list; progressWinUpdate($pw, langf("Searching for \"%s\" in database (%d/%d) ... Found %d picture(s)",$pattern_show,$i,$nr,$found), $i, $nr); if ($conf{nav_rating_on}{value}) { next if (not rating_valid($searchDB{$dpic}{URG}, $conf{search_rating_max}{value}, $conf{search_rating_min}{value})); } my $meta = $dpic; $meta .= ' '.$searchDB{$dpic}{COM} if (defined $searchDB{$dpic}{COM}); $meta .= ' '.$searchDB{$dpic}{EXIF} if (defined $searchDB{$dpic}{EXIF}); $meta .= ' '.$searchDB{$dpic}{IPTC} if (defined $searchDB{$dpic}{IPTC}); $meta .= ' '.$searchDB{$dpic}{KEYS} if (defined $searchDB{$dpic}{KEYS}); if ((defined $meta) and ($meta ne '')) { # replace newlines with space $meta =~ s/\n/ /g; if ($meta =~ m/(?$case).*$pat.*/) { # collect matching pics in a list push @pic_list, $dpic; } } } # while progressWinEnd($pw); return (sort(@pic_list)); } ############################################################## # format hash content to a printable string # considers only one level and assumes for best readability # that the keys consists of four chars ############################################################## sub hash_content { my $hash = shift; my $text = ''; my $len_keys = 0; my $len_values = 0; my $nr_keys = 0; foreach my $key (sort keys %$hash) { $nr_keys++; $len_keys += length($key); $text .= "$key: "; my $value = $$hash{$key}; if (defined $value) { $len_values += length($value); # replace newline by newline+spaces to preserve indentation # e.g. for multi line IPTC and EXIF data $value =~ s/\n/\n /g; $text .= "\"$value\""; } $text .= "\n"; } my $len_total = $len_keys + $len_values; my $len_keys_p = sprintf "%.1f",$len_keys/$len_total*100; $text .= "\nFound $nr_keys elements in hash. Hash size is $len_total chars (keys: ${len_keys_p}%)\n"; return $text; } ############################################################## # editDatabase ############################################################## sub editDatabase { my $buttext = "Remove picture(s) from database"; my $text = "This list shows all pictures of the search database.\nYou may select any number of pictures and remove them with the \"$buttext\" button.\nThe pictures won't be deleted, only the entry in the database is removed.\nIt's recommended to call the function \"Clean database\" first, because it will remove all invalid entries for you."; # open window my $ew = $top->Toplevel(); $ew->title("Edit search database"); $ew->iconimage($mapiviicon) if $mapiviicon; my $height = ($text =~ tr/\n//); $height += 3; $height = 10 if ($height > 10); # not to big, we have scrollbars my $rotext = $ew->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 110, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-expand => 0, -padx => 3, -pady => 3,-anchor => 'w'); $rotext->insert('end', $text); my $size = getFileSize($searchDBfile, FORMAT); my $keys = keys %searchDB; my ($first, $last) = get_date_limits(); my $info = "$keys pictures in the database between the years $first and $last (file size of database: $size)"; $ew->Label(-text => 'Database items:')->pack(); my $listBox = $ew->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, )->pack(-expand => 1, -fill =>'both', -padx => 3, -pady => 3); $listBox->insert('end', (sort keys %searchDB)); my $labF = $ew->Frame()->pack(-anchor => 'w', -padx => 3, -pady => 3); $labF->Label(-textvariable => \$info, -bg => $conf{color_bg}{value})->pack(-side => 'left'); my $bf = $ew->Frame()->pack(-fill => 'x', -padx => 3, -pady => 3); # remove button $bf->Button(-text => $buttext, -command => sub { my @sellist = $listBox->curselection(); # check selection args: widget, min, max, listref, itemkind (e.g. 'picture') return unless checkSelection($ew, 1, 0, \@sellist, lang("picture(s)")); foreach my $item (reverse @sellist) { my $dpic = $listBox->get($item); delete $searchDB{$dpic}; # delete key from hash $listBox->delete($item); } $keys = keys %searchDB; # display the new number of database entries $info = "$keys entries in the database"; } )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); my $showBut = $bf->Button(-text => 'Show picture database details', -command => sub { my @sellist = $listBox->curselection(); # check selection args: widget, min, max, listref, itemkind (e.g. 'picture') return unless checkSelection($ew, 1, 5, \@sellist, lang("picture(s)")); foreach my $item (@sellist) { my $dpic = $listBox->get($item); my $text = hash_content($searchDB{$dpic}); my $pic = basename($dpic); my $thumb = getThumbFileName($dpic); showText("Database content of $pic", $text, NO_WAIT, $thumb); } } )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); $listBox->bind("", sub { $showBut->Invoke(); }); $bf->Button(-text => 'Clean database ...', -command => sub { $ew->withdraw; $ew->destroy; cleanDatabase(); } )->pack(-side => 'right', -anchor => 'w', -padx => 3, -pady => 3); my $filter; my $ef = $ew->Frame()->pack(-fill => 'x', -padx => 3, -pady => 3); $ef->Label(-text => "Show only keys matching:", -anchor => 'w', -bg => $conf{color_bg}{value}, )->pack(-side => 'left', -padx => 3); my $entry = $ef->Entry(-textvariable => \$filter, -width => 20, )->pack(-fill => 'x', -padx => 3, -pady => 3); $entry->bind('', sub { return if (!defined $filter); $listBox->delete(0, 'end'); $keys = keys %searchDB; # display the new number of database entries if ($filter eq '') { $listBox->insert('end', (sort keys %searchDB)); $info = "$keys entries in the database (all visible)"; } else { my $count = 0; $filter = makePattern($filter); # create a windows like pattern foreach (sort keys %searchDB) { if ($_ =~ m!$filter!i) { $listBox->insert('end', $_); $count++; } } $info = "$keys entries in the database ($count visible)"; } } ); my $xBut = $ew->Button(-text => lang('Close'), -command => sub { $ew->withdraw; $ew->destroy; } )->pack(-expand => 0, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($ew, $xBut); $xBut->focus; $ew->Popup(); window_center($ew, 60, 90); $ew->waitWindow; return; } ############################################################## ############################################################## sub window_center { my $window = shift; # window widget my $w_percent = shift; # window width in percent (0-100%) of screen width my $h_percent = shift; # window height in percent of screen height # print "widget = --".ref($window)."--\n"; if ((not defined $window) or (ref($window) ne 'Tk::Toplevel') or (not defined $w_percent) or ($w_percent <= 0) or ($w_percent > 100) or (not defined $h_percent) or ($h_percent <= 0) or ($h_percent > 100)) { print "window_center: called with missing or wrong arguments: $w_percent, $h_percent\n"; return; } my $w = int($w_percent/100 * $window->screenwidth); my $h = int($h_percent/100 * $window->screenheight); my $x = int((1-$w_percent/100)/2); my $y = int((1-$h_percent/100)/2); $window->geometry("${w}x${h}+${x}+${y}"); $window->update(); return; } ############################################################## # checkDatabase - check the comment and iptc fields of all # database entries for problematic (non-ASCII) chars # will e.g. complain about the copyright sign ############################################################## sub checkDatabase { my $findings = ''; my $i = 0; my $problems = 0; foreach my $dpic (sort keys %searchDB) { $i++; my $com = $searchDB{$dpic}{COM}; my $iptc = $searchDB{$dpic}{IPTC}; my $keys = $searchDB{$dpic}{KEYS}; if ((defined $com) and ($com =~ m/[^\x00-\x7f]/)) { $findings .= "comment of $dpic\n"; $problems++; } if ((defined $iptc) and ($iptc =~ m/[^\x00-\x7f]/)) { $findings .= "IPTC of $dpic\n"; $problems++; } if ((defined $keys) and ($keys =~ m/[^\x00-\x7f]/)) { $findings .= "IPTC keyword of $dpic\n"; $problems++; } } my $text = "Check of IPTC keywords, IPTC data and JPEG comments in $i pictures finished.\nFound $problems problematic (non-ASCII) entries.\n\n$findings"; showText("Check database", $text, WAIT); } ############################################################## # searchDupName - search duplicate pics in the database by # same file name ############################################################## sub searchDupsName { my %pics; # hash of all file names key: file name or size value: directory+pic my $dpics = shift; # ref to hash of all file names key: file name value: list of dirs+pic containing this pic my $ignore_links = shift; my $filter = shift; my $ignore_filter = shift; undef %$dpics; #log_it("searching duplicates by file name ..."); # loop through all database entries foreach my $dpic (sort keys %searchDB) { next if (($filter ne '') and ($dpic !~ m!$filter!i)); next if (($ignore_filter ne '') and ($dpic =~ m!$ignore_filter!i)); next if ($ignore_links and -l $dpic); my $pic = basename($dpic); # new entry if (!defined $pics{$pic}) { $pics{$pic} = $dpic; } # duplicate found else { # if not defined in the dups hash, add first dir (was saved before) if (!defined $$dpics{$pic}) { $$dpics{$pic} = [$pics{$pic}]; } # and add the actual dir and pic push @{$$dpics{$pic}}, $dpic; } } } ############################################################## # searchDupSize - search duplicate pics in the database by # same file size ############################################################## sub searchDupsSize { my %pics; # hash of all file names key: file name or size value: directory+pic my $dpics = shift; # ref to hash of all file names key: file size value: list of dirs+pic containing this pic my $ignore_links = shift; my $filter = shift; my $ignore_filter = shift; undef %$dpics; #log_it("searching duplicates by file size ..."); # loop through all database entries foreach my $dpic (sort keys %searchDB) { next if (($filter ne '') and ($dpic !~ m!$filter!i)); next if (($ignore_filter ne '') and ($dpic =~ m!$ignore_filter!i)); next if ($ignore_links and -l $dpic); next if (!defined $searchDB{$dpic}{SIZE}); my $size = $searchDB{$dpic}{SIZE}; # size in Bytes # new entry if (!defined $pics{$size}) { $pics{$size} = $dpic; } # duplicate found else { # if not defined in the dups hash, add first dir (was saved before) if (!defined $$dpics{$size}) { $$dpics{$size} = [$pics{$size}]; } # and add the actual dir and pic push @{$$dpics{$size}}, $dpic; } } } ############################################################## # searchDupDate - search duplicate pics in the database by # same EXIF creation date ############################################################## sub searchDupsDate { my %pics; # hash of all file names key: file name or date value: directory+pic my $dpics = shift; # ref to hash of all file names key: file date value: list of dirs+pic containing this pic my $ignore_links = shift; my $filter = shift; my $ignore_filter = shift; undef %$dpics; #log_it("searching duplicates by file size ..."); # loop through all database entries foreach my $dpic (sort keys %searchDB) { next if (($filter ne '') and ($dpic !~ m!$filter!i)); next if (($ignore_filter ne '') and ($dpic =~ m!$ignore_filter!i)); next if ($ignore_links and -l $dpic); #next if (-l $dpic); unless (defined $searchDB{$dpic}{TIME}) { print "$dpic has no EXIF date/time!\n"; next; } my $date = $searchDB{$dpic}{TIME}; # EXIF creation date/time # new entry if (!defined $pics{$date}) { $pics{$date} = $dpic; } # duplicate found else { # if not defined in the dups hash, add first dir (was saved before) if (!defined $$dpics{$date}) { $$dpics{$date} = [$pics{$date}]; } # and add the actual dir and pic push @{$$dpics{$date}}, $dpic; } } } ############################################################## # findDups - find duplicate pics in the database ############################################################## sub findDups { if (Exists($dupw)) { $dupw->deiconify; $dupw->raise; $dupw->focus; return; } my %dup_thumbs; # hash to store all thumbnails displayed in the duplicate window my $pic; my $dir; my %dpics; # hash of all file names key: file name or size value: list of dirs+pic containing this pic my $searchForDups = 'Name'; my $ignore_links = 0; my $filter = ''; my $ignore_filter = ''; # open window $dupw = $top->Toplevel(); $dupw->title('Duplicate pictures'); $dupw->iconimage($mapiviicon) if $mapiviicon; my $subF = $dupw->Frame()->pack(-fill => 'x', -expand => 0, -padx => 3, -pady => 2); my $subF2 = $dupw->Frame()->pack(-fill => 'x', -expand => 0, -padx => 3, -pady => 2); my $dbsize = getFileSize($searchDBfile, FORMAT); my $label = ''; $dupw->Label(-textvariable => \$label, -justify => 'left',-bg => $conf{color_bg}{value})->pack(-anchor => 'w', -padx => 1, -pady => 2); my $filter_entry = labeledEntry($subF2, 'left', 7, "Include", \$filter, 15); $balloon->attach($filter_entry, -msg => "Enter a part of the file or path name to filter for.\nExample: If you enter \"photos/2012\" only duplicates\nfrom the folder ...photos/2012... will be shown."); my $ignore_filter_entry = labeledEntry($subF2, 'left', 6, "Ignore", \$ignore_filter, 15); $balloon->attach($ignore_filter_entry, -msg => "Enter a part of the file or path name to ignore.\nExample: If you enter \"photos/2012\" no duplicates\nfrom the folder ...photos/2012... will be shown."); my $duplb = makeThumbListbox($dupw); $subF->Button(-text => 'Search', -command => sub { # clean up $duplb->delete('all'); $label = 'cleaning up ...'; $duplb->update; # clean up memory - delete all found thumbnail photo objects delete_thumb_objects(\%dup_thumbs); $label = 'searching duplicates in database ...'; $duplb->update; my $filterP = makePattern($filter); # create a windows like pattern my $ignore_filterP = makePattern($ignore_filter); # create a windows like pattern if ($searchForDups eq 'Name') { searchDupsName(\%dpics, $ignore_links, $filterP, $ignore_filterP); } elsif ($searchForDups eq 'Size') { searchDupsSize(\%dpics, $ignore_links, $filterP, $ignore_filterP); } elsif ($searchForDups eq 'Date') { searchDupsDate(\%dpics, $ignore_links, $filterP, $ignore_filterP); } elsif ($searchForDups eq 'Cancel') { return; } else { warn "wrong searchForDups: $searchForDups\n"; return; } my $keys = keys %dpics; $label = " $keys duplicates found in the database (file size: $dbsize)."; my $pcount = 0; # pic count = keys %dpics my $dcount = 0; # dir count (if each pic has one duplicate this number is $pcount * 2) # to distinguish between sets of duplicates we use a darker background for these rows my $bg2 = $duplb->Darken($fileS->cget(-background), 120); # 120% darker = 20% brighter # we have to repeat the definition here, a copy does not work (see line containing "my $fileS") my $thumbS2 = $duplb->ItemStyle('imagetext', -anchor => 'w', -textanchor => 's', -foreground=>$conf{color_fg}{value}, -background=>$bg2, -font => $thumbCaptionFont); my $fileS2 = $duplb->ItemStyle('image', -anchor=>'w', -foreground=>$config{ColorFile}, -background=>$bg2); my $iptcS2 = $duplb->ItemStyle('text', -anchor=>'nw', -foreground=>$config{ColorIPTC}, -background=>$bg2); # save global styles to restore them later my $thumbS_save = $thumbS; my $fileS_save = $fileS; my $iptcS_save = $iptcS; # insert duplicates in hlist my $pw = progressWinInit($duplb, "Displaying duplicate pictures"); foreach my $item (sort keys %dpics) { last if progressWinCheck($pw); $pcount++; progressWinUpdate($pw, "Inserting duplicate ($pcount/$keys) ...", $pcount, $keys); foreach my $dpic (@{$dpics{$item}}) { last if progressWinCheck($pw); insertPic($duplb, $dpic, \%dup_thumbs); $dcount++; } # toggle style of name col to separate different duplicate sets if ($fileS == $fileS_save) { $thumbS = $thumbS2; $fileS = $fileS2; $iptcS = $iptcS2; } else { $thumbS = $thumbS_save; $fileS = $fileS_save; $iptcS = $iptcS_save; }; } progressWinEnd($pw); # reset global style $thumbS = $thumbS_save; $fileS = $fileS_save; $iptcS = $iptcS_save; $label = " found $pcount duplicates with $dcount files."; $duplb->update(); })->pack(-side => 'left', -anchor => 'w', -fill => 'both'); $subF->Label(-text => "duplicates by same ", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w', -fill => 'both'); $subF->Optionmenu(-variable => \$searchForDups, -textvariable => \$searchForDups, -options => [ ['file name' => 'Name'], ['creation date and time' => 'Date'], ['file size' => 'Size'], ])->pack(-side => 'left', -anchor => 'w', -fill => 'both'); $subF->Checkbutton(-text => 'ignore links', -variable => \$ignore_links)->pack(-side => 'left', -anchor => 'w', -fill => 'both', -padx => 1,-pady => 1); my $Xbut = $subF->Button(-text => lang('Close'), -command => sub { $dupw->withdraw(); $dupw->destroy(); # clean up memory - delete all found thumbnail photo objects delete_thumb_objects(\%dup_thumbs); } )->pack(-side => 'left', -anchor => 'w', -fill => 'both', -expand => 1); # the context menu my $menu = $dupw->Menu(-title => "Duplicate pictures menu"); ############# open pic $menu->command(-label => "Open picture in new window", -accelerator => "Middle Mouse Button", -command => sub { my @pics = $duplb->info('children'); return unless (@pics); my @sellist = $duplb->info('selection'); if (@sellist != 1) { $dupw->messageBox(-icon => 'warning', -message => "Please select exactly one picture!", -title => "Wrong selection", -type => 'OK'); return; } my $dpic = $sellist[0]; my $dir = dirname($dpic); if (!-d $dir) { $dupw->messageBox(-icon => 'warning', -message => "Sorry, but the folder\n$dir\nis not availabale at the moment.\nMaybe you should clean your database\nor insert a removable media.", -title => "folder not found", -type => 'OK'); return; } $dupw->Busy; showPicInOwnWin($dpic); $dupw->Unbusy; }); ############# open dir $menu->command(-label => lang('open folder and show picture'), -accelerator => "", -command => sub { open_pic_in_main($duplb); }); ############# ignore dir $menu->command(-label => "ignore folder ...", -command => sub { my @pics = $duplb->info('children'); return unless (@pics); my @sellist = $duplb->info('selection'); if (@sellist != 1) { $dupw->messageBox(-icon => 'warning', -message => "Please select exactly one picture!", -title => "Wrong selection", -type => 'OK'); return; } my $ignoredir = dirname($sellist[0]); my $rc = myEntryDialog("Ignore folder", "Ignore all folders matching this pattern:", \$ignoredir); return if ($rc ne 'OK' or $ignoredir eq ''); my $count = 0; foreach my $i (@pics) { next unless ($duplb->info("exists", $i)); my $dir = dirname($i); if ($dir =~ m!$ignoredir!) { $count++; $label = "removing $dir ($count) ..."; #print "$dir remove $i $ignoredir\n"; $duplb->delete("entry", $i); } } $label = "removed $count entries by folders."; }); ############# select all $menu->command(-label => lang("Select all"), -command => sub { selectAll($duplb); } ); $menu->separator; ############# delete to trash $menu->command(-label => "delete picture to trash", -command => sub { deletePics($duplb, TRASH); $label = "pictures deleted"; } ); ############# copy $menu->command(-label => "copy selected pictures ...", -command => sub { copyPicsDialog(COPY, $duplb); $label = "ready! (pictures copied)"; $dupw->update; } ); ############# move $menu->command(-label => "move selected pictures ...", -command => sub { movePicsDialog($duplb); $label = "ready! (pictures moved)"; $dupw->update; } ); # mouse and button bindings addCommonKeyBindings($duplb, $duplb); $duplb->bind('', sub { $menu->Popup(-popover => "cursor", -popanchor => "nw"); } ); $duplb->bind('', sub { return unless ($duplb->info('children')); my $dpic = getNearestItem($duplb); my $dir = dirname($dpic); if (!-d $dir) { $dupw->messageBox(-icon => 'warning', -message => "Sorry, but the folder\n$dir\nis not availabale at the moment.\nMaybe you should clean your database\nor insert a removable media.", -title => "folder not found", -type => 'OK'); return; } $dupw->Busy; showPicInOwnWin($dpic); $dupw->Unbusy; } ); bind_exit_keys_to_button($dupw, $Xbut); $dupw->bind('', sub { open_pic_in_main($duplb); }); my $w = int(0.8 * $dupw->screenwidth); my $h = int(0.8 * $dupw->screenheight); $dupw->geometry("${w}x${h}+10+10"); $duplb->update(); $dupw->waitWindow; } ############################################################## # editHashDialog - let the user add or remove keys from a hash ############################################################## sub editHashDialog { my $title = shift; my $text = shift; my $hr = shift; # hash reference my $okB = shift; # Ok button text my $cancelB = shift; # Cancel button text ('' means no Cancel button) my $addB = shift; # bool - show a path entry and a Add Path button my $entry = ''; my $rc; # open window my $ew = $top->Toplevel(); $ew->title($title); $ew->iconimage($mapiviicon) if $mapiviicon; my $height = ($text =~ tr/\n//); $height += 2; $height = 10 if ($height > 10); # not to big, we have scrollbars my $rotext = $ew->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -tabs => '4', -width => 80, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-expand => 0, -padx => 3, -pady => 3); $rotext->insert('end', $text); my $keys = keys %{$hr}; my $listBoxY = $keys; $listBoxY = 30 if ($listBoxY > 30); # not higher than 30 entries my $listBox = $ew->Scrolled('Listbox', -scrollbars => 'osoe', -selectmode => 'extended', -exportselection => 0, -width => 80, -height => $listBoxY, )->pack(-expand => 1, -fill =>'both', -padx => 3, -pady => 3); $listBox->insert('end', (sort keys %{$hr})); my $labF = $ew->Frame()->pack(-anchor => 'w', -padx => 3, -pady => 3); $labF->Label(-textvariable => \$keys, -bg => $conf{color_bg}{value})->pack(-side => 'left'); $labF->Label(-text => " entries", -bg => $conf{color_bg}{value})->pack(-side => 'left'); $ew->Button(-text => "Remove marked", -command => sub { foreach (reverse $listBox->curselection()) { my $path = $listBox->get($_); delete $$hr{$path}; # delete key from hash $listBox->delete($_); } # refresh listbox #$listBox->delete(0, 'end'); #$listBox->insert('end', (sort keys %{$hr})); $keys = keys %{$hr}; # display the ne wnumber of database entries } )->pack(-anchor => 'w', -padx => 3, -pady => 3); if ($addB) { my $entryF = $ew->Frame()->pack(-fill =>'x'); $entryF->Entry(-textvariable => \$entry, -width => 40)->pack(-side => 'left', -fill => 'x', -padx => 3, -pady => 3); $entryF->Button(-text => 'Add path', -command => sub { $$hr{"$entry"} = 1; $listBox->delete(0, 'end'); $listBox->insert('end', (sort keys %{$hr})); })->pack(-side => 'left', -padx => 3, -pady => 3); } my $ButF = $ew->Frame()->pack(-fill =>'x'); my $OKB = $ButF->Button(-text => $okB, -command => sub { $rc = 'OK', })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $OKB->bind('', sub { $OKB->Invoke; } ); if ($cancelB ne '') { $ButF->Button(-text => $cancelB, -command => sub { $rc = 'Cancel'; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); } $OKB->focus; $ew->Popup(-popover => 'cursor'); repositionWindow($ew); $ew->waitVariable(\$rc); $ew->withdraw; $ew->destroy; return $rc; } ############################################################## # checkDateFormat - check if date string matches dd.mm.yyyy # and day is between 1..31 and month 1..12 ############################################################## sub checkDateFormat { my $date = shift; my $rc = 0; if ($date =~ /^(\d\d)\.(\d\d)\.(\d\d\d\d)$/) { # check format if ($1 >= 1 and $1 <= 31) { # check day range if ($2 >= 1 and $2 <= 12) { # check month range if ($3 >= 1901 and $3 <= 2038) { # check year range, 1901 and 2038 are save boundaries for 32 bit systems # check for valid dates (e.g. 31.02.2000 is invalid) eval { timelocal(0, 0, 0, $1, $2-1, $3-1900); }; $rc = 1 unless ($@); } } } } return $rc; } ############################################################## # checkNumberFormat - check if the argument is a number ############################################################## sub checkNumberFormat { my $nr = shift; my $rc = 0; if ($nr =~ /^\d+$/) { # check format if ($nr >= 0 and $nr <= 99999) { # check range $rc = 1; } } return $rc; } ############################################################## # getDateTime - returns the actual local time as a string, format yyyymmdd-hhmm ############################################################## sub getDateTime { my $time = shift; my ($s,$m,$h,$d,$mo,$y,undef,undef,undef,undef) = localtime($time); # do some adjustments $y += 1900; $mo++; return ($s,$m,$h,$d,$mo,$y); } ############################################################## # getDateTimeShortString - returns the actual local time as a string, format # yyyymmdd-hhmm ############################################################## sub getDateTimeShortString { my $time = shift; my ($s,$m,$h,$d,$mo,$y) = getDateTime($time); return sprintf "%04d%02d%02d-%02d%02d", $y, $mo, $d, $h, $m; } ############################################################## # getDateTimeISOString - returns the actual local time as a string, # ISO 8601 extended format # yyyy-mm-dd hh:mm:ss ############################################################## sub getDateTimeISOString { my $time = shift; my ($s,$m,$h,$d,$mo,$y) = getDateTime($time); return sprintf "%04d-%02d-%02d %02d:%02d:%02d", $y, $mo, $d, $h, $m, $s; } ############################################################## # getDateTimeDINString - UNIX date/time to DIN 5008 format # dd.mm.yyyy hh:mm:ss ############################################################## sub getDateTimeDINString { my $time = shift; my ($s,$m,$h,$d,$mo,$y) = getDateTime($time); return sprintf "%02d.%02d.%04d %02d:%02d:%02d", $d, $mo, $y, $h, $m, $s; } ############################################################## # getDateTimeEXIFString - UNIX date/time to EXIF format # yyyy:mm:dd hh:mm:ss ############################################################## sub getDateTimeEXIFString { my $time = shift; my ($s,$m,$h,$d,$mo,$y) = getDateTime($time); return sprintf "%04d:%02d:%02d %02d:%02d:%02d", $y, $mo, $d, $h, $m, $s; } ############################################################## # buildUnixTime - dd.mm.yyyy to UNIX date/time ############################################################## sub buildUnixTime { my $date_str = shift; my $time; if ($date_str =~ m/(\d\d)\.(\d\d)\.(\d\d\d\d)/) { my $mon = $2; my $year = $3; $mon--; $year -= 1900; # check for valid dates (e.g. 31.02.2000 is invalid) eval { timelocal(0, 0, 0, $1, $mon, $year); }; if ($@) { warn "buildUnixTime: $date_str is invalid, date does not exists.\n"; $time = 0; } else { # valid $time = timelocal(0, 0, 0, $1, $mon, $year); } } else { warn "buildUnixTime: wrong string format $date_str, should be dd.mm.yyyy\n"; $time = 0; } return $time; } ############################################################## # searchFileName ############################################################## sub searchFileName { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 1, \@sellist, lang("picture(s)")); my $fileName = basename($sellist[0]); #resetAllSearchOptions(); # todo: write this sub $config{SearchPattern} = $fileName; $config{SearchName} = 1; searchMetaInfo(); } ############################################################## # search_by_date_time ############################################################## sub search_by_date_time { my $listbox = shift; my @sellist = getSelection($listbox); return unless checkSelection($listbox, 1, 1, \@sellist, lang("picture(s)")); my $dest_dpic = $sellist[0]; if ($searchDB{$dest_dpic}{TIME}) { my $dest_date_time = $searchDB{$dest_dpic}{TIME}; my $dest_date_time_str = getDateTimeISOString($dest_date_time); # open window my %thumbs; # hash to store all thumbnails displayed in the listbox my $tlb; # thumb list box my $win = $top->Toplevel(); window_size($win, 80); $win->title(lang('Pictures with creation date/time').' '.$dest_date_time_str); $win->iconimage($mapiviicon) if $mapiviicon; my $text = lang('Searching ...'); log_it(lang('Searching for pictures with creation date/time: ').$dest_date_time_str); my $butF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $Xbut = $butF->Button(-text => lang('Close'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 0, -fill => 'x', -padx => 3, -pady => 3); $butF->Label(-textvariable => \$text)->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $delta_number = 0; my $delta_unit = 1; $butF->Label(-text => '+/-')->pack(-side => 'left', -padx => 3, -pady => 3); my $deltaM = $butF->Optionmenu(-variable => \$delta_number, -textvariable => \$delta_number, -options => [qw(0 1 2 5 10 15 30)] )->pack(-side => 'left', -padx => 3, -pady => 3); my $deltaunitM = $butF->Optionmenu(-options => [ [lang('Seconds') => 1], [lang('Minutes') => 60], [lang('Hours') => 3600], [lang('Days') => 86400] ], -textvariable => \$delta_unit)->pack(-side => 'left', -padx => 3, -pady => 3); my $updateB = $butF->Button(-image => $mapivi_icons{'Update'}, -command => sub { clean_listbox($tlb); delete_thumb_objects(\%thumbs); my $delta = $delta_number * $delta_unit; my $found = search_by_date_time_add($tlb, $dest_date_time, $delta, \%thumbs); $text = langf("Found %d pictures (+/-%d seconds)", $found, $delta); })->pack(-side => 'left', -padx => 3, -pady => 3); $balloon->attach($updateB, -msg => lang('Update search with new time interval')); $tlb = makeThumbListbox($win); # key bindings bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { selectAll($tlb); } ); $win->bind('', sub { return if (!$tlb->info('children')); my $dpic = getNearestItem($tlb); showPicInOwnWin($dpic); }); $win->bind('', sub { my @sellist = getSelection($tlb); return unless checkSelection($win, 1, 0, \@sellist, lang("picture(s)")); show_multiple_pics(\@sellist, 0); }); # show picture in main window and in lighttable $win->bind('', sub { open_pic_in_main($tlb); }); $win->bind('', sub { light_table_add_from_lb($tlb); } ); # todo: add context menu $win->Popup(-popover => 'cursor'); repositionWindow($win); my $delta = $delta_number * $delta_unit; my $found = search_by_date_time_add($tlb, $dest_date_time, $delta, \%thumbs); $text = langf("Found %d pictures", $found); $win->waitWindow; # clean up memory - delete all found thumbnail photo objects delete_thumb_objects(\%thumbs); } else { log_it(lang('Error').': '.basename($dest_dpic).' '.lang('has no creation date/time!')); } } ############################################################## # get selected pic (must be exaclty one!) from listbox and # open it in main window ############################################################## sub open_pic_in_main { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 1, \@sellist, lang("picture(s)")); my $dpic = $sellist[0]; my $dir = dirname($dpic); my $pic = basename($dpic); if (!-d $dir) { $lb->messageBox(-icon => 'warning', -message => "Sorry, but the folder\n$dir\nis not availabale at the moment.\nMaybe you should clean your database\nor insert a removable media.", -title => 'folder not found', -type => 'OK'); return; } $top->deiconify; $top->raise; $top->focus; openDirPost($dir) if ($dir ne $actdir); showPic($dpic); } ############################################################## # deletes all (picture) entries of a listbox # does not free memory for thumbnails (use delete_thumb_objects) ############################################################## sub clean_listbox { my $lb = shift; my @elements = $lb->info('children'); foreach my $element (@elements) { $lb->delete('entry', $element) if ($lb->info('exists', $element)); } } ############################################################## # search for pictures with given creation date/time and # insert them in the given listbox widget ############################################################## sub search_by_date_time_add { my $lb = shift; my $dest_date_time = shift; my $delta = shift; # delta time in seconds my $thumbs = shift; # hash ref of thumbnail objects my $nr_of_pics = 0; my $pics = search_by_date_time_int($dest_date_time, $delta); if (@$pics) { my $i = 0; $nr_of_pics = scalar @$pics; my $pw = progressWinInit($lb, "Displaying pictures"); # insert pictures in hlist foreach my $dpic (sort @$pics) { last if progressWinCheck($pw); progressWinUpdate($pw, "Inserting picture ($i/$nr_of_pics) ...", $i, $nr_of_pics); insertPic($lb, $dpic, $thumbs); } progressWinEnd($pw); $lb->update(); log_it("Found $nr_of_pics pictures with identical date/time."); } else { log_it("Warning: Found no pictures with date/time ".getDateTimeISOString($dest_date_time)."!"); } return $nr_of_pics; } ############################################################## # search_by_date_time_int ############################################################## sub search_by_date_time_int { my $date_time = shift; my $delta = shift; # delta time in seconds print "search_by_date_time_int $date_time, delta=$delta\n" if $verbose; my $i = 0; my $keys = keys %searchDB; my @pics; my $pw = progressWinInit($top, "Search pictures with same creation date and time"); my $timemax = $date_time+$delta; my $timemin = $date_time-$delta; #foreach my $dpic (keys %searchDB) { while (my ($dpic, undef) = each %searchDB) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Searching pictures ($i/$keys), found ".scalar(@pics)."", $i, $keys); my $time = $searchDB{$dpic}{TIME}; if ($time) { if ($time <= $timemax and $time >= $timemin) { push @pics, $dpic; } } } progressWinEnd($pw); return \@pics; } ############################################################## # searchMetaInfo ############################################################## sub searchMetaInfo { use bytes; use locale; if (Exists($sw)) { $sw->deiconify; $sw->raise; $sw->focus; $sw->{entry}->focus; $sw->{entry}->selectionRange(0,'end'); # select all return; } my $start_dir = getRightDir(); my $pattern = $config{SearchPattern}; my $exclude = $config{SearchExPattern}; my $pat = ''; my $exl = ''; my $OKB; my $keys = keys %searchDB; my $stop = 0; my $stopB; if (!$config{SaveDatabase}) { my $rc = $top->messageBox(-message => "The save database to file option is off. The search will only cover the folders visited during this session.\nShould I switch the save option on?\nYou can change this option also in the options dialog.", -icon => 'question', -title => "Switch save option", -type => 'OKCancel'); $config{SaveDatabase} = 1 if ($rc =~ m/Ok/i); } # open window $sw = $top->Toplevel(); $sw->withdraw; $sw->title("Search picture database"); $sw->iconimage($mapiviicon) if $mapiviicon; my $topF = $sw->Frame()->pack(-fill => 'x', -expand => 0, -pady => 1); my $leftF = $topF->Frame()->pack(-fill => 'x', -side => 'left', -padx => 3, -pady => 3); my $pf1 = $leftF->Frame()->pack(-fill => 'x', -padx => 3, -pady => 3); $pf1->Label(-text => "Search pattern", -width => 15, -anchor => 'w', -bg => $conf{color_bg}{value})->pack(-side => 'left', -padx => 3); # the search pattern $sw->{entry} = $pf1->Entry(-textvariable => \$pattern, -width => 25, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); my $pf2 = $leftF->Frame()->pack(-fill => 'x', -padx => 3, -pady => 3); $pf2->Label(-text => "Exclude pattern", -width => 15, -anchor => 'w', -bg => $conf{color_bg}{value})->pack(-side => 'left', -padx => 3); my $exentry = $pf2->Entry(-textvariable => \$exclude, -width => 25)->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); #$pf2->Button(-text => "clear", -command => sub {$exclude = '';})->pack(-side => 'left', -padx => 3, -pady => 0); $balloon->attach($sw->{entry}, -msg => 'Use * for zero or more chars, ? for exactly one char. Use \* to search for the star sign (*) and \? to search for a questionmark (?) itself. To search for a backslash (\) use two backslashes (\\\). Examples: "I * home" will match e.g. "I go home", "I run home" but also "I do not go home" "Tr?ck" will match "Trick" or "Track" "who\?" will match "who?" "\*\* Party \*\*" will match "** Party **"'); $balloon->attach($exentry, -msg => 'Enter the patterns to exclude here. Separate them with one space. All patterns will be joined by or. Hint: Use an empty search pattern and the exlude pattern "?*" to search for pictures without comments, EXIF or IPTC infos.'); $sw->bind('', sub { fullscreen($sw);}); $sw->{entry}->bind('', sub { $OKB->Invoke; } ); $exentry->bind('', sub { $OKB->Invoke; } ); $sw->{entry}->focus; $sw->{entry}->selectionRange(0,'end'); # select all # what to search: keywords, IPTC, comments, ... my $f1 = $topF->Frame(-relief => 'groove', -bd => $config{Borderwidth})->pack(-side => 'left', -anchor => 'n', -fill =>'both',-padx => 3,-pady => 5); # different search options my $f0 = $leftF->Frame()->pack(-anchor => 'w', -padx => 0,-pady => 0); # local search + more options my $locSF = $leftF->Frame(-relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 5); $locSF->Checkbutton(-variable => \$config{SearchOnlyInDir}, -text => "local search in")->pack(-side => 'left', -anchor => 'w', -padx => 4, -padx => 2); $locSF->Label(-textvariable => \$start_dir)->pack(-side => 'left', -anchor => 'w', -padx => 4, -padx => 2); $locSF->Button(-text => 'Set', -command => sub { my $dir = $sw->chooseDirectory(-title => 'Select folder to search in', -initialdir => dirname($start_dir)); if (defined $dir and -d $dir) { $start_dir = $dir; $config{SearchOnlyInDir} = 1; } }, )->pack(-side => 'left'); $balloon->attach($locSF, -msg => 'When this option is enabled, the search will only take place in folders matching the displayed string. When the option is disabled a global search will take place.'); my ($addMF, $addF); $locSF->Checkbutton(-variable => \$config{SearchMore}, -text => 'more options', -command => sub { if ($config{SearchMore}) { $addF->pack(-in => $addMF, -side => 'left', -expand => 0);# if (!ismapped($addF)); } else { $addF->packForget();# if (ismapped($addF)); } })->pack(-side => 'right', -padx => 5); my $ButF = $f0->Frame(-relief => 'groove', -bd => $config{Borderwidth})->pack(-side => 'left', -anchor => 'n', -expand => 1, -fill =>'both',-padx => 3,-pady => 0); $balloon->attach($f1, -msg => "Search in JPEG comments, EXIF info,\nIPTC info, IPTC keywords, file name and/or in folder name"); my $f2 = $f0->Frame(-relief => 'groove', -bd => $config{Borderwidth})->pack(-side => 'left', -anchor => 'n', -fill =>'both',-padx => 3,-pady => 0); my $f3 = $f0->Frame(-relief => 'groove', -bd => $config{Borderwidth})->pack(-side => 'left', -anchor => 'n', -fill =>'both',-padx => 3,-pady => 0); my $f4 = $f0->Frame(-relief => 'groove', -bd => $config{Borderwidth})->pack(-side => 'left', -anchor => 'n', -fill =>'both',-padx => 3,-pady => 0); $f1->Checkbutton(-variable => \$config{SearchKeys}, -text => "Keywords")->pack(-anchor => 'w'); $f1->Checkbutton(-variable => \$config{SearchIptc}, -text => "IPTC info")->pack(-anchor => 'w'); $f1->Checkbutton(-variable => \$config{SearchCom}, -text => "Comments")->pack(-anchor => 'w'); $f1->Checkbutton(-variable => \$config{SearchExif}, -text => "EXIF info")->pack(-anchor => 'w'); $f1->Checkbutton(-variable => \$config{SearchName}, -text => "file name")->pack(-anchor => 'w'); $f1->Checkbutton(-variable => \$config{SearchDir}, -text => "folder name")->pack(-anchor => 'w'); my $sep = $f1->Checkbutton(-variable => \$config{SearchJoin}, -text => "join fields")->pack(-anchor => "nw"); $balloon->attach($sep, -msg => "If this option is selected all selected fields (keywords, IPTC, comments, ...) of a picture will be joined before the search starts, so it's e.g. possible to find a picture with keyword \"Tom\" and the comment \"at the beach\". If it is not selected, a all-search for \"Tom\" and \"Tim\" will only match, if all patterns are in one field (e.g. Tom and Tim are both in the keywords)."); my $sc1 = $f2->Checkbutton(-variable => \$config{SearchCase}, -text => "case sensitive")->pack(-anchor => "nw"); $balloon->attach($sc1, -msg => "Toggle between case sensitive/insensitive searching"); my $sw1 = $f2->Checkbutton(-variable => \$config{SearchWord}, -text => "complete word")->pack(-anchor => "nw"); $balloon->attach($sw1, -msg => "search only for complete words, not for parts"); my $stf = $f2->Frame()->pack(-anchor => 'w'); $stf->Label(-text => "match", -bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w'); my $st1 = $stf->Optionmenu(-variable => \$config{SearchType}, -textvariable => \$config{SearchType}, -options => [qw(exactly all any)] )->pack(-side => 'left', -anchor => 'w'); $balloon->attach($st1, -msg => 'Match search pattern exactly, match all words or try to match any of the given words. e.g. "Tim Tom" with search type match exactly will find all pictures containing exactly this string (string-search) match all will find this but also "Tom Tim" or "Tim and Tom" (and-search) match any will find all pictures containing "Tim" or "Tom" or both (or-search)'); my $urgF = $f3->Frame()->pack(-anchor=>'w', -padx => 0, -pady => 0); $urgF->Checkbutton(-variable => \$conf{search_rating_on}{value}, -text => 'rating')->pack(-side => 'left', -anchor => 'w'); rating_button_min_max($urgF, \$conf{search_rating_max}{value}, \$conf{search_rating_min}{value}); # todo search for empty urgency tags: , [Empty => ''] $balloon->attach($urgF, -msg => "Search only for pictures with a rating\n(IPTC urgency) between min and max."); #$f3->Checkbutton(-variable => \$config{SearchUrgencyIg}, -text => "ignore pictures without urgency")->pack(-anchor => 'nw'); my $popF = $f3->Frame()->pack(-anchor=>'w', -padx => 0, -pady => 0); $popF->Checkbutton(-variable => \$config{SearchPopOn}, -text => 'viewed')->pack(-side => 'left', -anchor => 'w'); $popF->Optionmenu(-variable => \$config{SearchPopRel}, -textvariable => \$config{SearchPopRel}, -options => [ qw(= <= >=) ] )->pack(-side => 'left', -anchor => 'w'); my $popE = $popF->Entry(-textvariable => \$config{SearchPop}, -width => 10, -validate => 'focus', -validatecommand => sub { checkNumberFormat($_[0]); }, -invalidcommand => sub {$config{SearchPop} = 5; $sw->messageBox(-icon => 'warning', -message => 'Please enter a natural number (0,1,2,3,...) in the viewed field', -title => 'Wrong format', -type => 'OK');})->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 1); $balloon->attach($popF, -msg => "Search only for pictures with have been viewed\nthis numer of times."); my $justCount = 0; my $countOp = $f3->Checkbutton(-variable => \$justCount, -text => 'just count pictures')->pack(-anchor => 'nw'); $balloon->attach($countOp, -msg => "Just count the matching pictures, do not display them.\nWith this option the search is much faster."); $f4->Checkbutton(-variable => \$config{SearchDate}, -text => 'search by EXIF date', -width => 19, -anchor => 'w')->pack(-anchor => 'w'); my $datetext = 'Please use date format: dd.mm.yyyy and check if you entered a valid date. dd (day) is between 01 and 31 mm (month) is between 01 and 12 yyyy (year) is between 1901 and 2038 Example 25.02.2012'; my $fromF = $f4->Frame()->pack(-anchor => 'w'); $fromF->Button(-text => 'from', -anchor => 'w', -width => 4, -command => sub { setFromTo(); } )->pack(-side => 'left', -anchor => 'w', -padx => 3); my $fromdate = $fromF->Entry( -textvariable => \$config{SearchDateStart}, -width => 11, -validate => 'focus', -validatecommand => sub { checkDateFormat($_[0]); }, -invalidcommand => sub { $config{SearchDateStart} = "01.01.2004"; $sw->messageBox(-icon => 'warning', -message => $datetext, -title => 'Wrong date format', -type => 'OK'); } )->pack(-side => 'left', -padx => 3); my $toF = $f4->Frame()->pack(-anchor => 'w'); $toF->Button(-text => 'to', -anchor => 'w', -width => 4, -command => sub { setFromTo(); } )->pack(-side => 'left', -anchor => 'w', -padx => 3); my $todate = $toF->Entry( -textvariable => \$config{SearchDateEnd}, -width => 11, -validate => 'focus', -validatecommand => sub { checkDateFormat($_[0]); }, -invalidcommand => sub { $config{SearchDateEnd} = "01.01.2012"; $sw->messageBox(-icon => 'warning', -message => $datetext, -title => 'Wrong date format', -type => 'OK'); } )->pack(-side => 'left', -padx => 3); $balloon->attach($fromdate, -msg => "Search only for pictures with a creation date\nwith or after this date.\nFormat: dd.mm.yyyy (example: 21.12.2001)"); $balloon->attach($todate, -msg => "Search only for pictures with a creation date\nbefore or with this date.\nFormat: dd.mm.yyyy (example: 31.01.2012)"); $addMF = $sw->Frame()->pack(-fill => 'x', -expand => 0, -pady => 0, -padx => 3); # this empty frame is needed, else the frame won't shrink after removing the other content my $empty_frame = $addMF->Frame()->pack(); $addF = $addMF->Frame(); # pixel size my $pixF = $addF->Frame(-bd => 1, -relief => 'sunken')->pack(-side => 'left', -anchor=>'w', -padx => 0, -pady => 0); $pixF->Checkbutton(-variable => \$config{SearchPixelOn}, -text => 'pixel size')->pack(-side => 'left', -anchor => 'w'); $pixF->Optionmenu(-variable => \$config{SearchPixelRel}, -textvariable => \$config{SearchPixelRel}, -options => [ qw(= <= >=) ] )->pack(-side => 'left', -anchor => 'w'); $pixF->Entry(-width => 8,-textvariable => \$config{SearchPixel})->pack(-side => 'left', -anchor => 'w', -fill => 'both', -padx => 2, -pady => 2); $balloon->attach($pixF, -msg => "Search only for pictures with a certain size.\nEnter the number of total pixels, width multiplicated with height.\nExample: For a 1500x1000 picture enter 1500000."); # picture format my $formatF = $addF->Frame(-bd => 1, -relief => 'sunken')->pack(-side => 'left', -anchor=>'w', -padx => 8, -pady => 0); $balloon->attach($formatF, -msg => "Search only for pictures with a certain aspect ratio."); $formatF->Checkbutton(-variable => \$conf{search_format_on}{value}, -text => 'format')->pack(-side => 'left', -anchor => 'w'); foreach my $form (qw(landscape square portrait)) { $formatF->Radiobutton(-text => $form, -variable => \$conf{search_format}{value}, -value => $form)->pack(-side => 'left', -anchor => 'w'); } my $panoB = $formatF->Checkbutton(-variable => \$conf{search_format_pano}{value}, -text => 'panorama')->pack(-side => 'left', -anchor => 'w'); $balloon->attach($panoB, -msg => "Search only for pictures with an aspect ratio of 2 greater or equal to 1\n(horizonal or vertical panoramas)."); if ($config{SearchMore}) { $addF->pack(-in => $addMF, -side => 'left', -expand => 0);# if (!ismapped($addF)); } else { $addF->packForget();# if (ismapped($addF)); } my $label = get_database_info(); my $subF = $sw->Frame()->pack(-fill => 'x', -expand => 0, -pady => 1); my $progress = 0; my $progBar = my $progB = $subF->ProgressBar(-takefocus => 0, -borderwidth => 1, -relief => 'sunken', -length => 100, -padx => 0, -pady => 0, -variable => \$progress, -colors => [0 => $config{ColorProgress}], -resolution => 1, -blocks => 10, -anchor => 'w', -from => 0, -to => 100, )->pack(-side => 'left', -fill => 'both', -expand => 0, -padx => 8, -pady => 0); $balloon->attach($progB, -msg => 'Displays the search progress'); $subF->Label(-textvariable => \$label, -justify => 'left',-bg => $conf{color_bg}{value})->pack(-side => 'left', -anchor => 'w', -padx => 8); my $findLB = makeThumbListbox($sw); $balloon->attach($findLB, -msg => "left click : select\nmiddle click: Open picture in new window\nright click : open context menu"); addCommonKeyBindings($findLB, $findLB); $findLB->bind('', sub { my @sellist = getSelection($findLB); return unless checkSelection($sw, 1, 0, \@sellist, lang("picture(s)")); show_multiple_pics(\@sellist, 0); } ); $findLB->bind('', sub { deletePics($findLB, TRASH); } ); $findLB->bind('', sub { deletePics($findLB, REMOVE); } ); # the context menu my $menu = $sw->Menu(-title => 'Search menu'); ############# select all $menu->command(-label => lang('Select all'), -command => sub {selectAll($findLB);}, -accelerator => '' ); $menu->separator; ############# file operations addFileActionsMenu($menu, $findLB); $menu->separator; ############# remove pictures from searchDB $menu->command(-label => "remove pictures from search database", -command => sub { my @sellist = getSelection($findLB); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $rc = $sw->messageBox(-icon => 'question', -message => "Please press OK to remove the ".scalar @sellist." selected picture(s) from the search data base.\nThe picture file(s) won't be deleted. They may be added to the search database again anytime.", -title => "Remove ".scalar @sellist." picture(s) from search database?", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); foreach (@sellist) { delete $searchDB{$_}; } }); ############# open pic $menu->command(-label => 'show pictures in new window', -accelerator => '', -command => sub { my @sellist = getSelection($findLB); return unless checkSelection($sw, 1, 0, \@sellist, lang("picture(s)")); show_multiple_pics(\@sellist, 0); }); ############# open dir $menu->command(-label => "open picture in main window", -accelerator => '', -command => sub { open_pic_in_main($findLB); }); # key-desc,m,show picture in main window (from search window) $findLB->bind('', sub { open_pic_in_main($findLB); }); ############# open in external viewer $menu->command(-label => lang('Open pictures in external viewer'), -command => sub { openPicInViewer($findLB); }, -accelerator => ''); $menu->separator; ############# display IPTC $menu->command(-label => 'show IPTC', -command => sub { displayIPTCData($findLB); }, -accelerator => ''); ############# edit IPTC $menu->command(-label => 'edit IPTC ...', -command => sub { editIPTC($findLB); }, -accelerator => ''); addRatingMenu($menu, $findLB); # todo: is editIPTCCategories still needed? $menu->command(-label => 'add/remove categories ...', -command => sub { editIPTCCategories($findLB); } , -accelerator => ''); $menu->command(-label => 'EXIF histogram', -command => sub { exif_histogram($findLB); } , -accelerator => ''); $menu->separator; ############# add comment $menu->command(-label => 'add comment ...', -command => sub { addComment($findLB); }, -accelerator => '
'); ############# edit comment $menu->command(-label => 'edit comment ...', -command => sub { editComment($findLB); }, -accelerator => ''); ############# search/replace comment $menu->command(-label => 'search/replace comment ...', -command => sub { replaceComment($findLB); }, ); $menu->separator; ############# sort my $sort_menu = $menu->cascade(-label => 'sort by ...'); $menu->separator; $menu->command(-label => lang("Add to collection"), -command => sub {light_table_add_from_lb($findLB);}, -accelerator => ''); $sort_menu->command(-label => 'file name', -command => sub { my @pics = $findLB->info('children'); $findLB->delete('all'); delete_thumb_objects(\%searchthumbs); sortPics('name', 0, \@pics); foreach (@pics) { insertPic($findLB, $_, \%searchthumbs); } }, ); $sort_menu->command(-label => 'urgency', -command => sub { my @pics = $findLB->info('children'); $findLB->delete('all'); delete_thumb_objects(\%searchthumbs); sortPics('urgency', 0, \@pics); foreach (@pics) { insertPic($findLB, $_, \%searchthumbs); } }, ); $sort_menu->command(-label => 'file date', -command => sub { my @pics = $findLB->info('children'); $findLB->delete('all'); delete_thumb_objects(\%searchthumbs); sortPics('date', 0, \@pics); foreach (@pics) { insertPic($findLB, $_, \%searchthumbs); } }, ); $sort_menu->command(-label => 'EXIF date', -command => sub { my @pics = $findLB->info('children'); $findLB->delete('all'); delete_thumb_objects(\%searchthumbs); sortPics('exifdate', 0, \@pics); foreach (@pics) { insertPic($findLB, $_, \%searchthumbs); } }, ); # mouse and button bindings $findLB->bind('', sub { $menu->Popup(-popover => 'cursor', -popanchor => 'nw'); } ); $findLB->bind('', sub { return unless ($findLB->info('children')); my $dpic = getNearestItem($findLB); my $dir = dirname($dpic); if (!-d $dir) { $sw->messageBox(-icon => 'warning', -message => "Sorry, but the folder\n$dir\nis not availabale at the moment.\nMaybe you should clean your database\nor insert a removable media.",-title => 'folder not found', -type => 'OK'); return; } $sw->Busy; showPicInOwnWin($dpic); $sw->Unbusy; } ); my $SButF = $ButF->Frame(-bd => 0)->pack(-side => 'top', -anchor => 'n', -expand => 1, -fill =>'both',-padx => 0,-pady => 0); $OKB = $SButF->Button(-text => 'Search', -command => sub { my $searchStart = Tk::timeofday(); my $count = 0; my ($thumb, $thumbP, $last_time, $start_time, $end_time); if (($config{SearchCom} == 0 and $config{SearchName} == 0 and $config{SearchDir} == 0 and $config{SearchExif} == 0 and $config{SearchKeys} == 0 and $config{SearchIptc} == 0)) { $sw->messageBox(-icon => 'warning', -message => 'Please select at least on field (keywords, comments, ...) to search in.', -title => 'No search field selected', -type => 'OK'); return; } unless (checkNumberFormat($config{SearchPop})) { $config{SearchPop} = 5; $sw->messageBox(-icon => 'warning', -message => 'Please enter a natural number (0,1,2,3,...) in the viewed field', -title => 'Wrong format', -type => 'OK'); return; } # store the patterns before we process them $config{SearchPattern} = $pattern; $config{SearchExPattern} = $exclude; # replace (german) umlaute by corresponding letters $pattern =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $exclude =~ s/([$umlaute])/$umlaute{$1}/g if ($config{ConvertUmlaut}); $label = "searching pattern in $keys pictures."; $sw->update; $pat = makePattern($pattern);# support windows like search patterns $exl = makePattern($exclude);# support windows like search patterns if ($config{SearchWord}) { $pat = "\\b$pat"; $pat =~ s/\s+/\\b \\b/g; # replace one or more whitespaces with \b \b the word boundary $pat .= '\\b'; } if ($config{SearchType} eq 'any') { # or-function "Tim Tom" -> "Tim|Tom" $pat =~ s/\s+/|/g; # replace one or more whitespaces with | } elsif ($config{SearchType} eq 'all') { $pat = '(?=.*'.$pat; # and-function with look-ahead $pat =~ s/\s+/)(?=.*/g; # replace one or more whitespaces with )(?=.* $pat .= ')'; # "Tom Tim" is now: "(?=.*Tom)(?=.*Tim)" } else { # do nothing (normal string search) } #my $qrpat; # todo, but seems not to work with and searches #if ($config{SearchCase}) { $qrpat = qr/'$pat'2/io; } else { $qrpat = qr/'$pat'/o; } #print "pat = $pat qrpat = $qrpat\n"; # the exclude patterns are always combined with or $exl =~ s/ /|/g; # or-function "Tim Tom" -> "Tim|Tom" print "searchMetaInfo: pattern: $pattern -> -$pat-\n" if $verbose; print "searchMetaInfo: exclude pattern: $exclude -> -$exl-\n" if $verbose; if ($config{SearchDate}) { if (!checkDateFormat($config{SearchDateStart})) { $sw->messageBox(-icon => 'warning', -message => $datetext, -title => 'Wrong from-date', -type => 'OK'); return; } if (!checkDateFormat($config{SearchDateEnd})) { $sw->messageBox(-icon => 'warning', -message => $datetext, -title => 'Wrong to-date', -type => 'OK'); return; } $start_time = buildUnixTime($config{SearchDateStart}); $end_time = buildUnixTime($config{SearchDateEnd}); #print "$start_time .. $end_time\n"; if ($end_time < $start_time) { $sw->messageBox(-icon => 'warning', -message => 'Search from date must be before search to date', -title => 'Wrong search date', -type => 'OK'); return; } } $findLB->delete('all'); # clear listbox $sw->Busy; my $case = 'i'; $case = '' if $config{SearchCase}; $stopB->configure(-state => 'normal'); $stopB->update(); my $i = 0; #################################################### # loop through all database entries foreach my $dpic (sort keys %searchDB) { last if $stop; $i++; # show progress and found pics every 0.5 seconds - idea from Slaven if (!defined $last_time || Tk::timeofday()-$last_time > 0.3) { $progress = int($i/$keys*100); $sw->update; $last_time = Tk::timeofday(); } if ($config{SearchOnlyInDir}) { # search only in subdirs of actual/selected dir next unless ($dpic =~ m/^$start_dir/); } my $urg = $searchDB{$dpic}{URG}; my $time = $searchDB{$dpic}{TIME}; # skip if wrong urgency if ($conf{search_rating_on}{value}) { next if (not rating_valid($urg, $conf{search_rating_max}{value}, $conf{search_rating_min}{value})); } # skip if wrong format / aspect ratio if ($conf{search_format_on}{value}) { next unless (defined $searchDB{$dpic}{PIXX}); next unless (defined $searchDB{$dpic}{PIXY}); if ($conf{search_format}{value} eq 'landscape') { next if ($searchDB{$dpic}{PIXX} <= $searchDB{$dpic}{PIXY}); if ($conf{search_format_pano}{value}) { next if ($searchDB{$dpic}{PIXX} <= 2*$searchDB{$dpic}{PIXY}); } } elsif ($conf{search_format}{value} eq 'square') { next if ($searchDB{$dpic}{PIXX} != $searchDB{$dpic}{PIXY}); # ignore panorama constraint for square pictures } elsif ($conf{search_format}{value} eq 'portrait') { next if ($searchDB{$dpic}{PIXX} >= $searchDB{$dpic}{PIXY}); if ($conf{search_format_pano}{value}) { next if (2*$searchDB{$dpic}{PIXX} >= $searchDB{$dpic}{PIXY}); } } else { warn "unsupported search format: $conf{search_format}{value}"; } } # skip if wrong pixel sum size if ($config{SearchPixelOn}) { next unless (defined $searchDB{$dpic}{PIXX}); next unless (defined $searchDB{$dpic}{PIXY}); my $pixy = $searchDB{$dpic}{PIXX} * $searchDB{$dpic}{PIXY}; if ($config{SearchPixelRel} eq '=') { # equal next if ($pixy != $config{SearchPixel}); } else { # handle bigger and lower if ($config{SearchPixelRel} eq '>=') { # bigger next if ($pixy < $config{SearchPixel}); } if ($config{SearchPixelRel} eq '<=') { # lower next if ($pixy > $config{SearchPixel}); } } } # fill in the POP key if it's missing (will cost about 6 Bytes per picture in the searchDB $searchDB{$dpic}{POP} = 0 unless (defined $searchDB{$dpic}{POP}); # skip if wrong numer of views (popularity) if ($config{SearchPopOn}) { if ($config{SearchPopRel} eq '=') { # equal next if ($searchDB{$dpic}{POP} != $config{SearchPop}); } else { # handle bigger and lower if ($config{SearchPopRel} eq '>=') { # bigger next if ($searchDB{$dpic}{POP} < $config{SearchPop}); } if ($config{SearchPopRel} eq '<=') { # lower next if ($searchDB{$dpic}{POP} > $config{SearchPop}); } } } # skip if wrong date if ($config{SearchDate} and defined($time)) { next if ($time < $start_time); next if ($time > $end_time); } my $com = $searchDB{$dpic}{COM}; my $exif = $searchDB{$dpic}{EXIF}; my $iptc = $searchDB{$dpic}{IPTC}; my $keys = $searchDB{$dpic}{KEYS}; # replace newlines with space $com =~ s/\n/ /g if (defined $com); $exif =~ s/\n/ /g if (defined $exif); $iptc =~ s/\n/ /g if (defined $iptc); my $allMeta = ''; if ($config{SearchJoin}) { # join all selected meta info with a space $allMeta = $com if ($config{SearchCom} and $com); $allMeta .= ' '.$exif if ($config{SearchExif} and $exif); $allMeta .= ' '.$iptc if ($config{SearchIptc} and $iptc); $allMeta .= ' '.$keys if ($config{SearchKeys} and $keys); $allMeta .= ' '.basename($dpic) if ($config{SearchName}); $allMeta .= ' '.dirname($dpic) if ($config{SearchDir}); $allMeta =~ s/\n/ /g; # replace newlines with space } if ((($config{SearchJoin} and ($allMeta ne '') and ($allMeta =~ m/(?$case).*$pat.*/) ) ) or (($config{SearchCom} and (defined $com) and ($com =~ m/(?$case).*$pat.*/)) or ($config{SearchExif} and (defined $exif) and ($exif =~ m/(?$case).*$pat.*/)) or ($config{SearchIptc} and (defined $iptc) and ($iptc =~ m/(?$case).*$pat.*/)) or ($config{SearchKeys} and (defined $keys) and ($keys =~ m/(?$case).*$pat.*/)) or ($config{SearchKeys} and (not defined $keys) and ($pat eq '')) or # empty keywords ($config{SearchName} and (basename($dpic) =~ m/(?$case).*$pat.*/)) or ($config{SearchDir} and (dirname($dpic) =~ m/(?$case).*$pat.*/)))) { # skip if exclude pattern matches if ((defined $exl) and ($exl ne '')) { next if ((($config{SearchJoin} and ($allMeta ne '') and ($allMeta =~ m/(?$case).*$exl.*/ )) ) or (($config{SearchCom} and (defined $com) and ($com =~ m/(?$case).*$exl.*/)) or ($config{SearchExif} and (defined $exif) and ($exif =~ m/(?$case).*$exl.*/)) or ($config{SearchIptc} and (defined $iptc) and ($iptc =~ m/(?$case).*$exl.*/)) or ($config{SearchKeys} and (defined $keys) and ($keys =~ m/(?$case).*$exl.*/)) or ($config{SearchName} and (basename($dpic) =~ m/(?$case).*$exl.*/)) or ($config{SearchDir} and (dirname($dpic) =~ m/(?$case).*$exl.*/)))); } unless ($justCount) { insertPic($findLB, $dpic, \%searchthumbs); } $count++; $label = "found pattern in $count pictures."; } } # foreach #################################################### $stopB->configure(-state => 'disabled'); $progress = 100; $findLB->update; my $searchDuration = sprintf "%.2f", (Tk::timeofday() - $searchStart); if ($count == 0) { my $msg = "Found no pictures containing \"$pattern\""; $msg .= " with rating (urgency) between ".iptc_rating_stars_urg($conf{search_rating_max}{value})." and ".iptc_rating_stars_urg($conf{search_rating_min}{value}) if ($conf{search_rating_on}{value}); $msg .= " with pixel size ".$config{SearchPixelRel}." ".$config{SearchPixel} if ($config{SearchPixelOn}); my $pano = ''; $pano = ' panorama' if ($conf{search_format_pano}{value}); $msg .= " in $conf{search_format}{value}$pano format" if ($conf{search_format_on}{value}); $msg .= " with views ".$config{SearchPopRel}." ".$config{SearchPop} if ($config{SearchPopOn}); $msg .= " dated between ".$config{"SearchDateStart"}." and ".$config{"SearchDateEnd"} if ($config{"SearchDate"} != 0); $msg .= " in folders matching $start_dir" if ($config{"SearchOnlyInDir"} != 0); $msg .= " in the database."; $sw->messageBox(-icon => 'warning', -message => $msg, -title => "Pattern not found", -type => 'OK'); $label = "pattern not found (duration: $searchDuration sec)."; $sw->Unbusy; $stop = 0; return; } $sw->Unbusy; my $end_str = "finished"; $end_str = "canceled" if $stop; $label = "Search $end_str: found $count pictures (duration: $searchDuration sec)."; $stop = 0; })->pack(-side => 'left', -anchor => 'w', -expand => 1, -fill => 'both',-padx => 1,-pady => 1); $stopB = $SButF->Button(-text => "Stop", -command => sub { $stop = 1; } )->pack(-side => 'left', -anchor => 'w', -fill => 'both', -expand => 0,-padx => 1,-pady => 1); $stopB->configure(-image => $mapivi_icons{Stop}, -borderwidth => 0); $stopB->configure(-state => 'disabled'); # would be usefull here, but needs to much space #$ButF->Button(-text => "Clean database ...", # -command => sub {cleanDatabase();})->pack(-side => 'top', -anchor => 'w', -expand => 1,-fill => 'x',-padx => 1,-pady => 1); my $Xbut = $ButF->Button(-text => "Close", -command => sub { $stop = 1; $config{SearchGeometry} = $sw->geometry; $sw->withdraw; delete_thumb_objects(\%searchthumbs); $sw->destroy; } )->pack(-side => 'top', -anchor => 'w', -expand => 1,-fill => 'both',-padx => 1,-pady => 1); bind_exit_keys_to_button($sw, $Xbut); $sw->bind('', sub { showHistogram($findLB); }); $sw->Popup; checkGeometry(\$config{SearchGeometry}); $sw->geometry($config{SearchGeometry}); $sw->waitWindow; return; } ############################################################## # delete_thumb_objects ############################################################## sub delete_thumb_objects { my $thumbs = shift; # hash ref to store the thumbnails # clean up memory - delete all found thumbnail photo objects foreach (keys %$thumbs) { print "delete_thumb_objects: deleting thumb $_\n" if $verbose; delete_photo_object($$thumbs{$_}); delete $$thumbs{$_}; } } ############################################################## # insertPic # todo: the -stlye for each column should be an optional argument (e.g. for sub findDups()) ############################################################## sub insertPic { my $lb = shift; my $dpic = shift; my $thumbs = shift; # hash ref to store the thumbnails my $thumb = getThumbFileName($dpic); # create new row $lb->add($dpic); my $pic = basename($dpic); if (-f $thumb) { $$thumbs{$thumb} = $lb->Photo(-file => $thumb, -gamma => $config{Gamma}); if (defined $$thumbs{$thumb}) { $lb->itemCreate($dpic, $lb->{thumbcol}, -image => $$thumbs{$thumb}, -itemtype => "imagetext", -text => getThumbCaption($dpic), -style => $thumbS); } } else { $lb->itemCreate($dpic, $lb->{thumbcol}, -itemtype => "imagetext", -text => $pic, -style => $thumbS); print "insertPic: no thumb for $dpic ($thumb)\n" if $verbose; } my $dir = dirname($dpic); my $iptc; $iptc = displayIPTC($dpic); my $com = formatString($searchDB{$dpic}{COM}, 30, $config{LineLimit}); # format the comment for the list my $exif = formatString(date_iso_to_relative($searchDB{$dpic}{EXIF}), 30, $config{LineLimit}); # format the EXIF info for the list $iptc = formatString($iptc, 30, $config{LineLimit}); # format the IPTC info for the list my $rating_size = get_rating_and_size($dpic, $lb); $lb->itemCreate($dpic, $lb->{filecol}, -itemtype => "image", -image => $rating_size, -style => $fileS) if (defined $lb->{filecol}); $lb->itemCreate($dpic, $lb->{iptccol}, -text => $iptc, -style => $iptcS); $lb->itemCreate($dpic, $lb->{comcol}, -text => $com, -style => $comS); $lb->itemCreate($dpic, $lb->{exifcol}, -text => $exif, -style => $exifS); $lb->itemCreate($dpic, $lb->{dircol}, -text => $dir, -style => $dirS); } ############################################################## # makePattern - create a regex from windows like search patterns # * for zero or more chars # ? for exactly one char # \* to search for the star sign (*) # \? to search for a questionmark (?) # . for a point (.) ############################################################## sub makePattern { my $pattern = shift; $pattern =~ s/\(/\\(/g; # replace ( with \( $pattern =~ s/\)/\\)/g; # replace ) with \) $pattern =~ s/\[/\\[/g; # replace ( with \( $pattern =~ s/\]/\\]/g; # replace ) with \) $pattern =~ s/\{/\\{/g; # replace ( with \( $pattern =~ s/\}/\\}/g; # replace ) with \) $pattern =~ s/\./\\./g; # replace . with \. (a point) $pattern =~ s/\\\*/\377/g; # replace \* with \377 (\377 is an unlikly char) $pattern =~ s/\*/.*/g; # replace * with .* (zero or more chars) $pattern =~ s/\377/\\*/g; # replace \377 with \* (the star iteself) $pattern =~ s/\\\?/\377/g; # replace \? with \377 $pattern =~ s/\?/.{1}/g; # replace ? with .{1} (one char) must be after { -> \{ $pattern =~ s/\377/\\?/g; # replace \377 with \? (the questionmark iteself) $pattern =~ s/\+/\\+/g; # replace + with \+ $pattern =~ s/\^/\\^/g; # replace ^ with \^ $pattern =~ s/\$/\\\$/g; # replace $ with \$ $pattern =~ s/\|/\\|/g; # replace | with \| #print "makePattern: $pattern\n"; return $pattern; } ############################################################## # getMemoryUsage - get the actual memory usage of mapivi in Bytes ############################################################## sub getMemoryUsage { my $pid = (defined($_[0])) ? $_[0] : $$; # $$ = PID of current process my $pt = Proc::ProcessTable->new; my %info = map { $_->pid => $_ } @{$pt->table}; return $info{$$}->rss; } ############################################################## # png_show - show PNG info using Image::ExifTool ############################################################## sub png_show { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; log_it("extracting PNG information of $selected pictures"); my $exifTool = new Image::ExifTool; my $i = 0; my $pw = progressWinInit($lb, "Extracting PNG information"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Extracting PNG ($i/$selected) ...", $i, $selected); my $xmp = ''; my $info = $exifTool->ImageInfo($dpic, 'PNG:*'); foreach (sort keys %$info) { my $val = $$info{$_}; if (ref $val eq 'ARRAY') { $val = join(', ', @$val); } elsif (ref $val eq 'SCALAR') { $val = '(Binary data)'; } $xmp .= sprintf("%-24s : %s\n", $_, $val); } $xmp .= "desc:\n"; $info = $exifTool->ImageInfo($dpic, 'PNG:Description'); foreach (sort keys %$info) { my $val = $$info{$_}; if (ref $val eq 'ARRAY') { $val = join(', ', @$val); } elsif (ref $val eq 'SCALAR') { $val = '(Binary data)'; } $xmp .= sprintf("%-24s : %s\n", $_, $val); } $xmp .= "Thumb::URI:\n"; $info = $exifTool->ImageInfo($dpic, 'PNG:Thumb::URI'); foreach (sort keys %$info) { my $val = $$info{$_}; if (ref $val eq 'ARRAY') { $val = join(', ', @$val); } elsif (ref $val eq 'SCALAR') { $val = '(Binary data)'; } $xmp .= sprintf("%-24s : %s\n", $_, $val); } $xmp = 'No PNG data found.' if ($xmp eq ''); showText("PNG data of $dpic", $xmp, NO_WAIT); } progressWinEnd($pw); log_it(lang('Ready!')." ($i of $selected)"); } ############################################################## # Extract embedded JPEG from raw file using Image::ExifTool ############################################################## sub extract_jpeg { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; log_it("extracting JPEG (Preview) from $selected pictures"); my $exifTool = new Image::ExifTool; my $i = 0; my $errors = ''; my $extracted = 0; my $pw = progressWinInit($lb, "Extracting JPEGs from RAW"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Extracting JPEG ($i/$selected) ...", $i, $selected); # parts of this code is based on a script from Phil Harvey, # see http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,19.0.html my $info = $exifTool->ImageInfo($dpic,'JpgFromRaw','PreviewImage'); my $val = $$info{JpgFromRaw} || $$info{PreviewImage}; if ($val) { my ($basename,$dir,$suffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) # JPEG file in same directory my $out = $dir.$basename.".jpg"; if (-f $out) { $out = $dir.findNewName($out); } my $fh; if (open($fh,'>',$out)) { if (print $fh $$val and close $fh) { $extracted++; log_it(" extracted $dpic to $out"); generateOneThumb($out); # insert new pic in listbox after $dpic addOneRow($lb, $out, 1, $dpic); } else { $errors .= "Error writing $out\n"; } } else { $errors .= "Error creating $out\n"; } } else { $errors .= "No embedded JPG in $dpic\n"; } } progressWinEnd($pw); showText("Errors while extracting JPEGs", $errors, NO_WAIT) if ($errors ne ''); log_it(lang('Ready!')." (extracted $extracted of $selected)"); } ############################################################## # xmp_show - show XMP info using Image::ExifTool ############################################################## sub xmp_show { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; log_it("extracting XMP information of $selected pictures"); #my $exifTool = new Image::ExifTool; my $i = 0; my $pw = progressWinInit($lb, "Extracting XMP information"); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Extracting XMP ($i/$selected) ...", $i, $selected); my $xmp = xmp_get($dpic); $xmp = 'No XMP data found.' if ($xmp eq ''); showText("XMP data of $dpic", $xmp, NO_WAIT); } progressWinEnd($pw); log_it(lang('Ready!')." ($i of $selected)"); } ############################################################## # xmp_get - returns XMP info as string using Image::ExifTool ############################################################## sub xmp_get { return exiftool_tostring(exiftool_get(shift, "XMP:*")); } ############################################################## # iptc_get - returns IPTC info as string using Image::ExifTool ############################################################## sub iptc_get { return exiftool_tostring(exiftool_get(shift, "IPTC:*")); } ############################################################## # exif_get - returns EXIF info as string using Image::ExifTool ############################################################## sub exif_get { return exiftool_tostring(exiftool_get(shift, "EXIF:*")); } ############################################################## # exiftool_get - returns EXIF info as string using Image::ExifTool ############################################################## sub exiftool_get { my $dpic = shift; # e.g. "EXIF:*" for all EXIF information, "XMP:*" for XMP ... # "XMP-xmp:Rating" for rating ... my $type = shift; my $exifTool = new Image::ExifTool; return $exifTool->ImageInfo($dpic, $type); } ############################################################## ############################################################## sub exiftool_tostring { my $info = shift; my $string = ''; foreach (sort keys %$info) { my $val = $$info{$_}; if (ref $val eq 'ARRAY') { $val = join(', ', @$val); } elsif (ref $val eq 'SCALAR') { $val = '(Binary data)'; } $string .= sprintf("%-14s: %s\n", $_, $val); } return $string; } ############################################################## # gps_get (taken from photoGalery.pl) ############################################################## sub gps_get { my $dpic = shift; my $exifTool = new Image::ExifTool; #set a few parameters $exifTool->Options(Charset => 'UTF8', CoordFormat => "%.6f"); my $imgInfo = $exifTool->ImageInfo($dpic,"GPSLatitude","GPSLatitudeRef","GPSLongitude","GPSLongitudeRef","GPSAltitude","GPSAltitudeRef"); #get individual parameters my @tags = $exifTool->GetRequestedTags(); my $lat = Encode::encode("utf8",$exifTool->GetValue($tags[0])); my $latRef = Encode::encode("utf8",$exifTool->GetValue($tags[1])); my $lon = Encode::encode("utf8",$exifTool->GetValue($tags[2])); my $lonRef = Encode::encode("utf8",$exifTool->GetValue($tags[3])); my $alt = Encode::encode("utf8",$exifTool->GetValue($tags[4])); my $altRef = Encode::encode("utf8",$exifTool->GetValue($tags[5])); #print "GPS of $dpic:\nlat = $lat latref = $latRef lon = $lon lonRef = $lonRef alt = $alt altRef = $altRef\n" if (defined $lat); # strip off " N" or " W" suffixes $lat =~ s/ \S$// if defined $lat; $lon =~ s/ \S$// if defined $lon; return ($lat, $lon, $latRef, $lonRef); } ############################################################## ############################################################## sub gps_set { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; my ($ok, $lat, $lon, $lat_ref, $lon_ref) = gps_dialog("Enter GPS coordinates for the $selected picture(s).\nFormat: DD.DDDDDD or DD MM.MMM or DD MM SS (D=Degree, M=Minutes, S=Seconds)\nExamples: \"52.2625\" or \"52 15.75\" or \"52 15 45\" (for 52.2625 or 52 15.75' or 52 15' 45'')\n\nNote: Existing GPS coordinates will be overwritten!"); return if !$ok; log_it("adding/overwriting GPS coordinates to $selected pictures"); my $exifTool = new Image::ExifTool; my $i = 0; my $success = 0; my $error = ''; my $pw = progressWinInit($lb, 'Adding/overwriting GPS coordinates'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Adding/overwriting GPS coordinates ($i/$selected) ...", $i, $selected); $exifTool->SetNewValue('GPSLatitude' => $lat); $exifTool->SetNewValue('GPSLatitudeRef' => $lat_ref); $exifTool->SetNewValue('GPSLongitude' => $lon); $exifTool->SetNewValue('GPSLongitudeRef' => $lon_ref); my $ok = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $ok, $dpic); if ($ok) { showImageInfoCanvas($dpic) if ($dpic eq $actpic); updateOneRow($dpic, $lb); $success++; } } progressWinEnd($pw); log_it(lang('Ready!')." Added GPS coodinates to $success of $selected pictures."); showText("Errors while adding/overwriting GPS coordinates", $error, NO_WAIT) if ($error ne ''); } ############################################################## # get location (county, state, city, ...) from GPS coordinates # and store them in IPTC ############################################################## sub gps_to_location { my $lb = shift; my @sellist = getSelection($lb); # note: service may be limited to 1 access per second # so we limit to max 100 pictures at once; todo: maybe add a delay with after? return unless checkSelection($lb, 1, 100, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; # check before overwriting return if (not allow_location_overwrite($top, \@sellist)); my $exifTool = new Image::ExifTool; my $i = 0; my $success = 0; my $error = ''; my $pw = progressWinInit($lb, 'Set location from GPS coordinates'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "setting GPS coordinates ($i/$selected) ...", $i, $selected); my ($lat, $lon, $lat_ref, $lon_ref) = gps_get($dpic); if (defined $lat and defined $lon) { $lat *= -1 if (defined $lat_ref and $lat_ref eq 'South'); $lon *= -1 if (defined $lon_ref and $lon_ref eq 'West'); #web_browser_open('http://maps.google.com/maps?q='."$lat,$lon"); # note: service may be limited to 1 access per second # note2: we use the Mapivi language setting to get the location in the selected language; todo: should maybe be a separate option my $loc_service = 'http://nominatim.openstreetmap.org'; my $url = $loc_service.'/reverse?format=xml&lat='.$lat.'&lon='.$lon.'&zoom=10&accept-language='.$config{Language}.'&addressdetails=1'; #print "gps_to_location: \"$url\"\n\n"; use LWP::Simple; my $xml = LWP::Simple::get($url); #print "XML = $xml\n\n"; # --- example output #$xml = 'Vico, Deux-Sorru, Corse-du-Sud, Korsika, Metropolitanes Frankreich, 20160VicoCorse-du-SudKorsikaMetropolitanes Frankreich20160fr'; # --- example end if ($xml) { $exifTool->ImageInfo($dpic, 'IPTC:*'); my ($country, $state, $county, $city); # depending on the location it could make sense to use the county as province/state value # but here we use state and add county in sublocation as addition info # We all know there are better ways to parse XML, but for this case it should be sufficent if ($xml =~ m|(.+)|) { $country = $1; $country =~ s/([$umlaute])/$umlaute{$1}/g; $exifTool->SetNewValue('Country-PrimaryLocationName' => $country);} if ($xml =~ m|(.+)|) { $state = $1; $state =~ s/([$umlaute])/$umlaute{$1}/g; $exifTool->SetNewValue('Province-State' => $state); } if ($xml =~ m|(.+)|) { $county = $1; $county =~ s/([$umlaute])/$umlaute{$1}/g; $exifTool->SetNewValue('Sub-location' => $county); } if ($xml =~ m|(.+)|) { $city = $1; $city =~ s/([$umlaute])/$umlaute{$1}/g; $exifTool->SetNewValue('City' => $city); } my $ok = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $ok, $dpic); if ($ok) { showImageInfoCanvas($dpic) if ($dpic eq $actpic); updateOneRow($dpic, $lb); log_it("added location: $country, $state, $county, $city to $dpic"); $success++; } } else { $error .= "Location service ($loc_service) returned nothing for $dpic\n"; } } else { $error .= "Found no GPS coordinates in picture $dpic\n"; } } progressWinEnd($pw); log_it(lang('Ready!')." Set location info in $success of $selected pictures."); showText("Errors while setting location from GPS", $error, NO_WAIT) if ($error ne ''); } ############################################################## # gps_dialog - get GPS coordinates from the user ############################################################## sub gps_dialog { my $text = shift; my $lat = 48.778505; my $lat_ref = 'North'; my $lon = 9.179915; my $lon_ref = 'East'; my $rc = 0; # open window my $win = $top->Toplevel(); $win->title('Enter GPS coordinates'); $win->iconimage($mapiviicon) if $mapiviicon; # determine the heigt of the textbox by counting the number of lines my $height = ($text =~ tr/\n//); $height += 2; $height = 10 if ($height > 10); # not to big, we have scrollbars my $rotext = $win->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -width => 80, -height => $height, -relief => 'flat', -bg => $conf{color_bg}{value}, -bd => 0 )->pack(-fill => 'both', -expand => 1, -padx => 3, -pady => 3); $rotext->insert('end', $text); my $latf = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $latf->Label(-text=> 'GPS Latitude', -width => 15)->pack(-side => 'left', -padx => 3); my $lat_e = $latf->Entry(-textvariable => \$lat, -width => 12)->pack(-side => 'left', -fill => 'x', -padx => 3); $latf->Optionmenu(-textvariable => \$lat_ref, -options => [qw(North South)], -width => 8)->pack(-side => 'left', -padx => 3); my $lonf = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $lonf->Label(-text=> 'GPS Longitude', -width => 15)->pack(-side => 'left', -padx => 3); $lonf->Entry(-textvariable => \$lon, -width => 12)->pack(-side => 'left', -fill => 'x', -padx => 3); $lonf->Optionmenu(-textvariable => \$lon_ref, -options => [qw(East West)], -width => 8)->pack(-side => 'left', -padx => 3); $lat_e->selectionRange(0,'end'); # select all #$lat_e->bind('', sub { $OKB->Invoke; } ); $lat_e->focus; my $ButF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $rc = 1; $win->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $XBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $win->destroy; } )->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $XBut); $win->Popup; repositionWindow($win); $win->waitWindow(); return ($rc, $lat, $lon, $lat_ref, $lon_ref); } ############################################################## # get the selected picture and show the GPS position of it in a web browser ############################################################## sub gps_map_open { my $lb = shift; my @sellist = $lb->info('selection'); # check selection args: widget, min, max, listref, itemkind (e.g. "picture") return unless checkSelection($top, 1, 1, \@sellist, lang("picture(s)")); gps_pic_map_open($sellist[0]); } ############################################################## # show the GPS position of a single picture in web browser ############################################################## sub gps_pic_map_open { my $dpic = shift; return if !defined $dpic; return if !-f $dpic; my ($lat, $lon, $lat_ref, $lon_ref) = gps_get($dpic); if (defined $lat and defined $lon) { $lat *= -1 if (defined $lat_ref and $lat_ref eq 'South'); $lon *= -1 if (defined $lon_ref and $lon_ref eq 'West'); web_browser_open('http://maps.google.com/maps?q='."$lat,$lon"); } else { $top->messageBox(-icon => 'error', -message => "Found no GPS coordinates in picture\n$dpic", -title => "No GPS coordinates", -type => 'OK'); } } ############################################################## # returns string explaining how XMP rating is mapped to IPTC ############################################################## sub convert_iptc_to_xmp_text { my $string = lang("IPTC urgency to XMP rating mapping (IPTC -> XMP):")."\n"; foreach my $iptc (0 .. 8) { my $xmp = convert_iptc_to_xmp($iptc); $string .= "$iptc -> $xmp\n"; } return $string; } ############################################################## # convert IPTC urgency value to XMP rating value ############################################################## sub convert_iptc_to_xmp { my $iptc_urgency = shift; $iptc_urgency = 0 if (! $iptc_urgency); # 5 = best XMP rating = 5 stars = IPTC urgency 1, 4 = 2, 3 = 3, 2 = 4, 1 = 5, 0 = 6-8 my $xmp_rating = 6 - $iptc_urgency; $xmp_rating = 0 if ($xmp_rating < 0); $xmp_rating = - 1 if ($iptc_urgency == 0); return $xmp_rating; } ############################################################## # returns string explaining how XMP rating is mapped to IPTC ############################################################## sub convert_xmp_to_iptc_text { my $string = lang("XMP rating to IPTC urgency mapping (XMP -> IPTC):")."\n"; foreach my $xmp (0 .. 5) { my $iptc = convert_xmp_to_iptc($xmp); $string .= "$xmp -> $iptc\n"; } return $string; } ############################################################## # convert XMP rating value to IPTC urgency value ############################################################## sub convert_xmp_to_iptc { my $xmp = shift; $xmp = 0 if (! $xmp); # 5 = best XMP rating = 5 stars = IPTC urgency 1, 4 = 2, 3 = 3, 2 = 4, 1 = 5, 0 = 6-8 my $urgency = 6 - $xmp; $urgency = 0 if ($xmp == -1); return $urgency; } ############################################################## # xmp_set_rating - set XMP Rating of the given picture to a IPTC urgency using Image::ExifTool ############################################################## sub xmp_set_rating { my ($dpic , $iptc_urgency) = @_; my $xmp_rating = convert_iptc_to_xmp($iptc_urgency); return xmp_set($dpic, 'Rating', $xmp_rating); } ############################################################## # xmp_set - set any XMP single item of the given picture to a value using Image::ExifTool ############################################################## sub xmp_set { my $error = ''; my ($dpic , $xmp_item, $xmp_value) = @_; return $error if (! -f $dpic); # todo: support more XMP tags return $error if ($xmp_item ne 'Rating'); # we support only Rating at the moment log_it("Setting XMP item $xmp_item to $xmp_value in $dpic ..."); my $exifTool = new Image::ExifTool; my $info = $exifTool->ImageInfo($dpic, 'XMP-xmp:*'); # set XMP item to value my ($ok, $et_error) = $exifTool->SetNewValue('XMP-xmp:'.$xmp_item => $xmp_value); if ($ok) { my $rc = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $rc, $dpic); } else { # SetNewValue has thtrown an errors $error .= $et_error; } my $log_entry = ''; $log_entry = 'There have been errors!' if ($error); print "Errors = $error\n" if ($error); log_it("... ready! $log_entry"); return $error; } ############################################################## # xmp_add_keyword - add XMP keyword using Image::ExifTool ############################################################## sub xmp_add_keyword { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; my $keyword = ''; my $rc = myEntryDialog('Add XMP keyword', "Please enter a new keyword to add to the $selected pictures.\n(To add hierachical keywords use dot notation, i.e. Person.Simpson.Homer)", \$keyword); return if (($rc ne 'OK') or ($keyword eq '')); log_it("adding XMP keyword to $selected pictures"); # detect hierarchical keywords (containing a dot) my $getvalue = 'Subject'; my $setvalue = 'XMP-dc:Subject'; if ($keyword =~ m|.+\..+|) { $keyword =~ s/\./\|/g; # replace all dots with | $getvalue = 'HierarchicalSubject'; $setvalue = 'XMP:HierarchicalSubject'; } my $exifTool = new Image::ExifTool; my $i = 0; my $error = ''; my $pw = progressWinInit($lb, 'Adding XMP keyword'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Adding XMP keyword ($i/$selected) ...", $i, $selected); my $info = $exifTool->ImageInfo($dpic, 'XMP:*'); # get exsisting keywords my @keywords = $exifTool->GetValue($getvalue); # add new keyword to list push @keywords, $keyword; # remove double entries and sort alphabetical uniqueArray(\@keywords); # add XMP keywords $exifTool->SetNewValue($setvalue => \@keywords); #$exifTool->SetNewValue('XMP-dc:Title' => 'Mapivi can write XMP!'); #$exifTool->SetNewValue('XMP:Urgency' => 3); my $ok = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $ok, $dpic); showImageInfoCanvas($dpic) if ($ok and ($dpic eq $actpic)); } progressWinEnd($pw); log_it(lang('Ready!')." ($i of $selected)"); showText("Errors while adding XMP keywords", $error, NO_WAIT) if ($error ne ''); } ############################################################## # convert exifTool return code into error string ############################################################## sub exiftool_get_error { my $exifTool = shift; # handle my $rc = shift; # return code my $dpic = shift; my $error = ''; if ($rc != 1) { # error if ($rc == 2) { $error = "$dpic written, but no changes made\n"; } else { $error = "Error writing $dpic: $rc\n"; # retrieve error and warning messages $error .= sprintf "error: %s\n", $exifTool->GetValue('Error') if $exifTool->GetValue('Error'); $error .= sprintf "warning: %s\n", $exifTool->GetValue('Warning') if $exifTool->GetValue('Warning'); } } return $error; } ############################################################## # xmp_remove - remove complete XMP section using Image::ExifTool ############################################################## sub xmp_remove { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; my $rc = $top->messageBox(-icon => 'question', -message => "Really remove all XMP infomation of $selected pictures?", -title => "Remove XMP information", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); log_it("removeing XMP information of $selected pictures"); my $exifTool = new Image::ExifTool; my $i = 0; my $error = ''; my $pw = progressWinInit($lb, 'Remove XMP information'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Remove XMP information ($i/$selected) ...", $i, $selected); my $info = $exifTool->ImageInfo($dpic, 'XMP:*'); # remove XMP section $exifTool->SetNewValue('XMP:*'); my $ok = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $ok, $dpic); showImageInfoCanvas($dpic) if ($ok and ($dpic eq $actpic)); } progressWinEnd($pw); log_it(lang('Ready!')." ($i of $selected)"); showText("Errors while removing XMP information", $error, NO_WAIT) if ($error ne ''); } ############################################################## # xmp_add_title - add XMP title using Image::ExifTool ############################################################## sub xmp_add_title { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; my $item = ''; my $rc = myEntryDialog('Add XMP title', "Please enter a new title to add to the $selected picture(s)", \$item); return if ($rc ne 'OK'); log_it("adding XMP title to $selected picture(s)"); my $exifTool = new Image::ExifTool; my $i = 0; my $error = ''; my $pw = progressWinInit($lb, 'Adding XMP title'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "Adding XMP title ($i/$selected) ...", $i, $selected); my $info = $exifTool->ImageInfo($dpic, 'XMP:*'); # add XMP title $exifTool->SetNewValue('XMP-dc:Title' => $item); my $ok = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $ok, $dpic); showImageInfoCanvas($dpic) if ($ok and ($dpic eq $actpic)); } progressWinEnd($pw); log_it(lang('Ready!')." ($i of $selected)"); showText("Errors while adding XMP title", $error, NO_WAIT) if ($error ne ''); } ############################################################## # xmp_edit_title - edit XMP title using Image::ExifTool ############################################################## sub xmp_edit_title { my $lb = shift; my @sellist = getSelection($lb); return unless checkSelection($lb, 1, 0, \@sellist, lang("picture(s)")); my $selected = scalar @sellist; log_it("adding XMP title to $selected picture(s)"); my $exifTool = new Image::ExifTool; my $i = 0; my $error = ''; my $pw = progressWinInit($lb, 'Adding XMP title'); foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; my $item = ''; my $info = $exifTool->ImageInfo($dpic, 'XMP:*'); $item = $$info{Title} unless (ref $$info{Title} eq 'SCALAR'); my $rc = myEntryDialog('Edit XMP title', "Please edit title of $dpic", \$item); next if ($rc ne 'OK'); progressWinUpdate($pw, "Edit XMP title ($i/$selected) ...", $i, $selected); # add XMP title $exifTool->SetNewValue('XMP-dc:Title' => $item); my $ok = $exifTool->WriteInfo($dpic); $error .= exiftool_get_error($exifTool, $ok, $dpic); showImageInfoCanvas($dpic) if ($ok and ($dpic eq $actpic)); } progressWinEnd($pw); log_it(lang('Ready!')." ($i of $selected)"); showText("Errors while adding XMP title", $error, NO_WAIT) if ($error ne ''); return; } ############################################################## # checkTrash ############################################################## sub checkTrash { my @files = getFiles($trashdir); my $sum = 0; foreach (@files) { $sum += getFileSize("$trashdir/$_", NO_FORMAT); # get size in Bytes } my $msum = sprintf "%.1f", $sum/(1024*1024); # size in MB return if ($msum < $config{MaxTrashSize}); my $dialog = $top->Dialog(-title => "Trash full!", -text => "The trash contains $msum MB in ".scalar @files." files!", -buttons => ["Do nothing", "Show trash in main window", "Empty trash ..."]); my $rc = $dialog->Show(); if ($rc eq "Do nothing") { $top->focusForce; return; } elsif ($rc eq "Show trash in main window") { openDirPost($trashdir); $top->focusForce; return; } elsif ($rc eq "Empty trash ...") { emptyTrash(); } else { warn "this should never be reached!"; } $top->focusForce; return; } ############################################################## # emptyTrash - remove all files from the trash ############################################################## sub emptyTrash { # open window my $win = $top->Toplevel(); $win->title(lang('Empty trash?')); $win->iconimage($mapiviicon) if $mapiviicon; my $w = int($top->screenwidth * 0.5); my $h = int($top->screenheight * 0.90); $win->geometry("${w}x${h}+0+0"); my $text = lang("loading ..."); $win->Label(-textvariable => \$text)->pack(-expand => 0, -fill => 'x'); my $tlb = $win->Scrolled("HList", -header => 1, -separator => ';', # todo here we hope that ; will never be in a folder or file name -pady => 0, -columns => 4, -scrollbars => 'osoe', -selectmode => 'extended', -background => $conf{color_bg}{value}, #8fa8bf -width => 80, -height => 30, )->pack(-expand => 1, -fill => 'both'); $tlb->header('create', 0, -text => lang('Thumbnail'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 1, -text => lang('Name'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 2, -text => lang('Size'), -headerbackground => $conf{color_entry}{value}); $tlb->header('create', 3, -text => lang('Original folder'), -headerbackground => $conf{color_entry}{value}); my $butF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $butF->Button(-text => lang('Remove all'), -command => sub { my @files = getFiles($trashdir); foreach (@files) { removeFile("$trashdir/$_"); } updateThumbsPlus() if ($actdir eq $trashdir); $userinfo = lang("Trash is now empty!"); $win->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $rmB = $butF->Button(-text => lang('Remove'), -command => sub { my @sellist = getSelection($tlb); return unless checkSelection($win, 1, 0, \@sellist, lang("picture(s)")); foreach (@sellist) { removeFile($_); $tlb->delete('entry', $_); } $text = langf("Removed %d file(s) from trash!", scalar(@sellist)); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($rmB, -msg => lang("Remove selected files from trash.\nThere is no undo!")); my $reB = $butF->Button(-text => lang('Restore'), -command => sub { my @sellist = getSelection($tlb); return unless checkSelection($win, 1, 0, \@sellist, lang("picture(s)")); my $error = ''; my %changed_dirs; foreach my $dpic (@sellist) { # if original dir (odir) is defined and not unknown and the folder exists, we move the picture back if ($searchDB{$dpic}{odir} and ($searchDB{$dpic}{odir} ne 'unknown') and ( -d $searchDB{$dpic}{odir})) { my @list; # we need a dummy list here with one element push @list, $dpic; $changed_dirs{$searchDB{$dpic}{odir}}++; movePics($searchDB{$dpic}{odir}, $tlb, @list); } else { $error .= langf("Could not restore %s (no folder information available)\n",$dpic); } } foreach my $dir (keys %changed_dirs) { smart_update() if ($actdir eq $dir); # updateThumbsPlus() } if ($error ne '') { $error = langf("Errors while restoring selected picture(s):\n%s",$error); showText(lang("Errors"), $error, NO_WAIT); } $text = langf("Restored %s file(s) from trash!", scalar(@sellist)); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($reB, -msg => lang("Restore selected files from trash to original folder.")); my $Xbut = $butF->Button(-text => lang('Cancel'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { selectAll($tlb); } ); $win->bind('', sub { return if (!$tlb->info('children')); my $dpic = getNearestItem($tlb); showPicInOwnWin($dpic); }); $win->Popup(-popover => 'cursor'); repositionWindow($win); my $sum = 0; my %thumbs; my @files = getFiles($trashdir); foreach my $pic (sort { uc($a) cmp uc($b); } @files) { my $dpic = "$trashdir/$pic"; $sum += getFileSize($dpic, NO_FORMAT); # get size in Bytes my $size = getFileSize($dpic, FORMAT); my $thumb = getThumbFileName($dpic); my $odir = 'unknown'; $odir = $searchDB{$dpic}{odir} if ($searchDB{$dpic}{odir}); $tlb->add($dpic); if (-f $thumb) { $thumbs{$thumb} = $top->Photo(-file => $thumb, -gamma => $config{Gamma}); if (defined $thumbs{$thumb}) { $tlb->itemCreate($dpic, 0, -image => $thumbs{$thumb}, -itemtype => 'imagetext', -style => $thumbS); } } $tlb->itemCreate($dpic, 1, -text => $pic, -style => $comS); $tlb->itemCreate($dpic, 2, -text => $size, -style => $iptcS); $tlb->itemCreate($dpic, 3, -text => $odir, -style => $comS); } my $msum = sprintf "%.1f", $sum/(1024*1024); # size in MB $text = langf("Press \"%s\" to delete all files (%d MB in %d files) from the trash.\nWarning: There is no undelete!\n\n(Trash folder: %s)", lang("Remove all"), $msum, scalar(@files), $trashdir); $win->waitWindow; foreach (keys %thumbs) { delete_photo_object($thumbs{$_}); } # free memory return; } ############################################################## # setFromTo - dialog to set search from and search to date ############################################################## sub setFromTo { # open window my $win = $top->Toplevel(); $win->title('Set from/to search dates'); $win->iconimage($mapiviicon) if $mapiviicon; my @fdate = split /\./, $config{SearchDateStart}; my $from_day = $fdate[0]; my $from_month = $fdate[1]; my $from_year = $fdate[2]; my @tdate = split /\./, $config{SearchDateEnd}; my $to_day = $tdate[0]; my $to_month = $tdate[1]; my $to_year = $tdate[2]; # ranges my (@day, @month, @year); push @day, sprintf "%02d",$_ for ( 1 .. 31); push @month, sprintf "%02d",$_ for ( 1 .. 12); push @year, sprintf "%4d", $_ for ( 1990 .. 2020); # it is still possible to add other year numbers in the search window itself! my $f1 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $f1->Label(-text => 'from', -width => 4)->pack(-side => 'left', -anchor => 'w'); $f1->Optionmenu(-variable => \$from_day, -textvariable => \$from_day, -options => \@day)->pack(-side => 'left', -anchor => 'w'); $f1->Optionmenu(-variable => \$from_month, -textvariable => \$from_month, -options => \@month)->pack(-side => 'left', -anchor => 'w'); $f1->Optionmenu(-variable => \$from_year, -textvariable => \$from_year, -options => \@year)->pack(-side => 'left', -anchor => 'w'); $f1->Button(-text => 'today', -command => sub { my (undef,undef,undef,$d,$M,$y) = getDateTime(time()); $from_day = sprintf "%02d", $d; $from_month = sprintf "%02d", $M; $from_year = sprintf "%4d", $y;})->pack(-side => 'left', -anchor => 'w'); my $f2 = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $f2->Label(-text => 'to', -width => 4)->pack(-side => 'left', -anchor => 'w'); $f2->Optionmenu(-variable => \$to_day, -textvariable => \$to_day, -options => \@day)->pack(-side => 'left', -anchor => 'w'); $f2->Optionmenu(-variable => \$to_month, -textvariable => \$to_month, -options => \@month)->pack(-side => 'left', -anchor => 'w'); $f2->Optionmenu(-variable => \$to_year, -textvariable => \$to_year, -options => \@year)->pack(-side => 'left', -anchor => 'w'); $f2->Button(-text => 'today', -command => sub { my (undef,undef,undef,$d,$M,$y) = getDateTime(time()); $to_day = sprintf "%02d", $d; $to_month = sprintf "%02d", $M; $to_year = sprintf "%4d", $y;})->pack(-side => 'left', -anchor => 'w'); my $butF = $win->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $butF->Button(-text => lang('OK'), -command => sub { $config{SearchDateStart} = "$from_day.$from_month.$from_year"; $config{SearchDateEnd} = "$to_day.$to_month.$to_year"; $win->destroy; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $butF->Button(-text => lang('Cancel'), -command => sub { $win->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($win, $Xbut); $win->bind('', sub { $OKB->Invoke; }); $win->Popup(-popover => 'cursor'); repositionWindow($win); $win->waitWindow; return; } ############################################################## # showFile ############################################################## sub showFile { my $file = shift; return if (!-f $file); my $fileH; if (!open($fileH, '<', $file)) { warn "Sorry, I couldn't open the file $file: $!"; return; } my $buffer; read $fileH, $buffer, 32768; close($fileH); $buffer =~ s/\r//g; showText(basename($file), $buffer, WAIT) if ($buffer ne ''); return; } ############################################################## # showText ############################################################## sub showText { my $title = shift; my $text = shift; my $wait = shift; # WAIT = wait for the window to close or NO_WAIT my $thumbnail = shift; # optional file name my $icon; $text = ' ' if ((!defined $text) or ($text eq '')); # open window my $win = $top->Toplevel(); $win->withdraw; $win->title($title); $win->iconname($title); $win->iconimage($mapiviicon) if $mapiviicon; my $xBut = $win->Button(-text => lang('Close'), -pady => 0, -command => sub { $icon->delete if $icon; $win->withdraw(); $win->destroy(); }, )->pack(-fill => 'x'); # 50 ways to leave your window ;) bind_exit_keys_to_button($win, $xBut); my $f = $win->Frame()->pack(-fill => 'both', -expand => 1); my $fl = $f->Frame()->pack(-anchor => 'n', -side => 'left'); my $fr = $f->Frame()->pack(-anchor => 'n', -side => 'left', -fill => 'both', -expand => 1); if ((defined $thumbnail) and (-f $thumbnail)) { $icon = $win->Photo(-file => $thumbnail, -gamma => $config{Gamma}); if ($icon) { $fl->Label(-image => $icon, -bg => $conf{color_bg}{value}, -relief => 'sunken', )->pack(-padx => 1, -pady => 2); } } # determine the height of the textbox by counting the number of lines my $height = ($text =~ tr/\n//); $height += 3; $height = 50 if ($height > 50); # not to big, we have scrollbars my $rotext = $fr->Scrolled('ROText', -scrollbars => 'oe', -wrap => 'word', -tabs => '4', -width => 90, -height => $height, )->pack(-fill => 'both', -expand => 1); $rotext->insert('end', $text); $xBut->focus; $win->Popup; repositionWindow($win); $win->waitWindow if ($wait == WAIT); return; } ############################################################## # exportFilelist ############################################################## sub exportFilelist { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $addPath = 0; my $useQuotes = 0; # open window my $myDiag = $top->Toplevel(); $myDiag->title("Export file list"); $myDiag->iconimage($mapiviicon) if $mapiviicon; $myDiag->Label(-text => "Write a filelist containing the ".scalar @sellist." selected pictures", -bg => $conf{color_bg}{value} )->pack(-fill => 'x', -padx => 3, -pady => 3); labeledEntryButton($myDiag,'top',37,"path/name of file list",'Set',\$config{PicListFile}); $myDiag->Checkbutton(-variable => \$addPath, -text => "add the complete path to every file")->pack(-anchor=>'w'); $myDiag->Checkbutton(-variable => \$useQuotes, -text => "add quotes around each file")->pack(-anchor=>'w'); my $ButF = $myDiag->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { if (-f $config{PicListFile}) { my $rc = $myDiag->messageBox(-icon => 'warning', -message => "file $config{'PicListFile'} exist. Ok to overwrite?", -title => "Export file list", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } my $exfile; if (!open($exfile, '>', $config{PicListFile})) { warn "exportFilelist: Couldn't open $config{PicListFile}: $!"; return; } foreach my $dpic (@sellist) { my $pic = basename($dpic); print $exfile "\"" if $useQuotes; print $exfile "$actdir/" if $addPath; print $exfile "$pic"; print $exfile "\"" if $useQuotes; print $exfile ", "; } close $exfile; log_it("File list exported!"); $myDiag->withdraw(); $myDiag->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $myDiag->withdraw(); $myDiag->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($myDiag, $xBut); $myDiag->Popup(-popover => 'cursor'); repositionWindow($myDiag); $myDiag->waitWindow; } ############################################################## # edit_pic ############################################################## sub edit_pic { my $widget = shift; my @sellist = @_; # optional list of pictures (each with path) if (not @sellist) { @sellist = getSelection($widget); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); } # ask before starting many editors return unless askSelection(\@sellist, 10, "external editor"); my $i = 0; my $rc = 1; foreach my $dpic (@sellist) { $i++; # normal picture editor my $editor = $conf{external_pic_editor}{value}; # picture editor for RAW pictures if (defined $conf{external_raw_editor}{value} and $conf{external_raw_editor}{value} ne '') { $editor = $conf{external_raw_editor}{value} if (is_raw_file($dpic)); } log_it(langf("Opening picture in %s (%d/%d).",basename($editor),$i,scalar(@sellist))); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $command = "$editor \"$dpic\" "; $command .= "2>&1 1>/dev/null &" if (!$EvilOS); if ((system "$command") != 0) { warn "$command failed: $!"; log_it("$command failed: $!"); $rc = 0; last; } #execute($command); # does not work for Windows } $top->after(800, sub { log_it(lang('Ready!')); }) if $rc; } ############################################################## # getSelection - get the selected items from a Canvas (e.g. light # table) or a HList (e.g. thumbnail table in main window) ############################################################## sub getSelection { my $widget = shift; my @sellist; if (ref($widget) eq 'Tk::Canvas') { my @sel = $widget->find('withtag', 'THUMBSELECT_MH'); foreach my $id (@sel) { push @sellist, get_path_from_id($widget,$id); } } else { @sellist = $widget->info('selection'); } return @sellist; } ############################################################## # selection_get_sort - get the selected items from # the widget (e.g. light_table) in the order as in $pic_list_ref ############################################################## sub selection_get_sort { my $widget = shift; my $pic_list_ref = shift; my @sel_unsorted = getSelection($widget); my @sel_sorted; # use the order given in pic_list foreach my $dpic (@{$pic_list_ref}) { if (isInList($dpic, \@sel_unsorted)) { push @sel_sorted, $dpic; } } return @sel_sorted; } ############################################################## # openPicInViewer ############################################################## sub openPicInViewer { my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $maxnr = 20; if (!$conf{external_pic_viewer_multi}{value} and (@sellist > $maxnr)) { my $rc = $lb->messageBox(-icon => "question", -message => "You have selected more than $maxnr pictures.\nPlease confirm to start ".scalar @sellist." pictures viewer processes.\nPlease press Ok to continue.", -title => "Start a lot of viewers?", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } my $piclist; my $i = 0; foreach my $dpic (@sellist) { $i++; log_it("opening picture in viewer ($i/".scalar @sellist.")"); increasePicPopularity($dpic); updateOneRow($dpic, $lb) if (($config{trackPopularity}) and (ref($lb) ne 'Tk::Canvas')); $dpic =~ s/\//\\/g if $EvilOS; # windows needs backslashes if ($conf{external_pic_viewer_multi}{value}) { $piclist .= "\"$dpic\" "; } else { my $command = "$conf{external_pic_viewer}{value} $dpic "; $command = "\"$conf{external_pic_viewer}{value}\" \"$dpic\" " if $EvilOS; # windows needs quotes # instead of the & for UNIX windows needs a "start" in front of the application to run in the background if ($EvilOS) { $command = "start $command"; } else { $command .= "2>&1 1>/dev/null &"; } (system "$command") == 0 or warn "$command failed: $!"; #execute($command); this is no good choice, because it waits for the viewer to finish } } if ($conf{external_pic_viewer_multi}{value}) { my $command = "$conf{external_pic_viewer}{value} $piclist"; $command = "\"$conf{external_pic_viewer}{value}\" $piclist" if $EvilOS; # windows needs quotes # instead of the & for UNIX windows needs a "start" in front of the application to run in the background if ($EvilOS) { $command = "start $command"; } else { $command .= "2>&1 1>/dev/null &"; } (system "$command") == 0 or warn "$command failed: $!"; } $top->after(800, sub { log_it(lang('Ready!')); }); } ############################################################## # setBackground - set the current picture as desktop background ############################################################## sub setBackground { my @sellist = $picLB->info('selection'); if (@sellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select exactly one picture.", -title => "set desktop background", -type => 'OK'); return; } my $dpic = $sellist[0]; my $pic = basename($dpic); log_it("Setting $pic as desktop background using ".$config{ExtBGApp}); # check if file is a link and get the real target return if (!getRealFile(\$dpic)); my $command = $config{ExtBGApp}." \"$dpic\" "; execute($command); log_it($config{ExtBGApp}." ".lang("Ready!")); } ############################################################## # identifyPic - display the output of identify ############################################################## sub identifyPic { return if (!checkExternProgs("identifyPic", "identify")); my @sellist = $picLB->info('selection'); if (@sellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select exactly one picture.", -title => "Show picture infos", -type => 'OK'); return; } my $dpic = $sellist[0]; my $pic = basename($dpic); my $thumb = getThumbFileName($dpic); log_it("getting infos about $pic ..."); # check if file is a link and get the real target return if (!getRealFile(\$dpic)); my $command = "identify -verbose \"$dpic\" "; my $buffer = `$command`; showText("Information about $pic", $buffer, NO_WAIT, $thumb); log_it(lang('Ready!')); } ############################################################## # showSegments ############################################################## sub showSegments { my @sellist = $picLB->info('selection'); if (@sellist != 1) { $top->messageBox(-icon => 'warning', -message => "Please select exactly one picture.", -title => "Show segments", -type => 'OK'); return; } my $dpic = $sellist[0]; my $pic = basename($dpic); my $thumb = getThumbFileName($dpic); # check if file is a link and get the real target return if (!getRealFile(\$dpic)); my $meta = getMetaData($dpic); # get all segments return unless ($meta); my $segments = $meta->{segments}; my $win = $top->Toplevel(); $win->withdraw; $win->title("JPEG segments of $pic"); $win->iconimage($mapiviicon) if $mapiviicon; my $xBut = $win->Button(-text => "Close", -command => sub { $win->destroy(); })->pack(-fill => 'x'); foreach (@$segments) { my $segInfo = $_->get_description(); my $segname = $_->{name}; my $title = sprintf "%-16s %8s Bytes",$segname,$_->size(); $win->Button(-text => $title, -anchor => "nw", -command => sub { showText("Segment $segname of $pic", $segInfo, NO_WAIT); })->pack(-fill => 'x'); } $xBut->focus; $win->Popup; } ############################################################## # showHistogram - display the histogram of a picture ############################################################## sub showHistogram { return if (!checkExternProgs("showHistogram", "convert")); my $lb = shift; my @sellist = $lb->info('selection'); if (@sellist != 1) { $lb->messageBox(-icon => 'warning', -message => "Please select exactly one picture.", -title => "Show picture histogram", -type => 'OK'); return; } my $dpic = $sellist[0]; my $pic = basename($dpic); my $thumb = getThumbFileName($dpic); log_it("building histogram of $pic ..."); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); my $hist = getHistogram($lb, $dpic); if (($hist eq '') or (!-f $hist)) { log_it("Error building histogram of $pic!"); return; } log_it("Histogram ready!"); my $but = "Save histogram"; my $rc = myPicDialog("Histogram", "Histogram of $pic", $but, $thumb, $hist); if ($rc eq $but) { my $file = $lb->FileSelect(-title => "Save histogram of $pic (GIF format)", -directory => $actdir, -initialfile => basename($hist), -create => 1, -width => 30, -height => 30)->Show; if ((defined $file) and ($file ne '')) { if (mycopy($hist, $file, ASK_OVERWRITE)) { # ask before overwrite log_it("histogram saved!"); } else { log_it("error while saving histogram"); } } } removeFile($hist); return; } ############################################################## # getHistogram - generate a histogram of the given picture # returns the path and file to the histogram # file or '' if no success ############################################################## sub getHistogram { my $widget = shift; my $dpic = shift; my $rc = ''; return $rc unless (-f $dpic); my $pic = basename($dpic); # temp PNM or GIF file in the trash directory my $hist = "$trashdir/$pic-histogram.gif"; # exchange pnm with gif if needed if (-f $hist) { my $urc = $top->messageBox(-icon => 'question', -message => "Histgram file $hist exists already.\nShould I overwrite it?", -title => "Overwrite?", -type => 'OKCancel'); return $rc if ($urc !~ m/Ok/i); } # with the -comment '' option the file size of the histogram shrinks from ~1MB to ~5kB # because convert saves the complete color table in the comment (at least when GIF format is used) my $command = "convert \"$dpic\" HISTOGRAM:- | convert -comment \"\" - \"$hist\" "; $widget->Busy; #execute($command); (system "$command") == 0 or warn "getHistogram: $command failed: $!"; $widget->Unbusy; $rc = $hist if (-f $hist); return $rc; } ############################################################## # showHistogram2 - display the histogram of a picture with builtin histogram function ############################################################## sub showHistogram2 { return if (!checkExternProgs("showHistogram", "convert")); my $lb = shift; my @sellist = $lb->info('selection'); if (@sellist != 1) { $lb->messageBox(-icon => 'warning', -message => "Please select exactly one picture.", -title => "Show picture histogram", -type => 'OK'); return; } my $dpic = $sellist[0]; my $pic = basename($dpic); my $thumb = getThumbFileName($dpic); log_it("building histogram of $pic ..."); # check if file is a link and get the real target next if (!getRealFile(\$dpic)); buildHistogram($dpic); } ############################################################## # buildHistogram ############################################################## sub buildHistogram { my $dpic = shift; my $photo = $top->Photo(-file => $dpic); # no gamma correction here! my (@red, @green, @blue); foreach (0 .. 255) { $red[$_] = 0; } foreach (0 .. 255) { $green[$_] = 0; } foreach (0 .. 255) { $blue[$_] = 0; } my $w = $photo->width; my $h = $photo->height; # if the picture is to big, it will take very long, so we shrink them first. # some color information may be lost this way! my $subsample = int($w*$h/500000); print "$dpic: subsample: $subsample\n" if $verbose; if ($subsample > 1) { my $zoomed = $top->Photo; $zoomed->blank; $zoomed->copy($photo, -zoom => 1); $photo->delete; $photo = undef; $photo = $top->Photo; $photo->copy($zoomed, -subsample => $subsample); $zoomed->delete; $zoomed = undef; $w = $photo->width; $h = $photo->height; print "$dpic new size: $w x $h\n" if $verbose; } if ($w <= 0 or $h <= 0) { warn "buildHistogram: wrong size: $w $h\n"; return; } my $pw = progressWinInit($top, "Calculating histogram of ".$w*$h." pixels"); # get and add rgb values of each pixel foreach my $x (0 .. $w-1) { last if progressWinCheck($pw); progressWinUpdate($pw, "calculating column ($x/$w) ...", $x, $w); foreach my $y (0 .. $h-1) { my @rgb = $photo->get($x,$y); $red[$rgb[0]]++; $green[$rgb[1]]++; $blue[$rgb[2]]++; } } progressWinEnd($pw); # find the maximal value my $max = 0; foreach (0 .. 255) { $max = $red[$_] if ($red[$_] > $max); $max = $green[$_] if ($green[$_] > $max); $max = $blue[$_] if ($blue[$_] > $max); }; # open window my $win = $top->Toplevel(); $win->title("Histogram of $dpic"); $win->iconimage($mapiviicon) if $mapiviicon; $h = 255; # height is now the height of the canvas my $canvas = $win->Canvas(-width => 256, -height => $h+1, -background => 'black', -relief => 'sunken', -bd => $config{Borderwidth})->pack(-side => 'top', -padx => 3, -pady => 3); # draw a line for red, green and blue foreach my $x (0 .. 255) { $canvas->createLine( $x, $h, $x, $h-int($h*$red[$x]/$max), -fill => 'red'); $canvas->createLine( $x, $h, $x, $h-int($h*$green[$x]/$max), -fill => 'green', -stipple => 'transp2'); $canvas->createLine( $x, $h, $x, $h-int($h*$blue[$x]/$max), -fill => 'blue', -stipple => 'transp3'); } $win->Button(-text => lang('Close'), -command => sub { $win->destroy(); } )->pack(-side => 'top',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); $win->bind('', sub { $win->destroy; } ); $win->Popup; return; } ############################################################## # checkSelection ############################################################## sub checkSelection { my $win = shift; # widget used as parent for messageBox my $min = shift; my $max = shift; # use 0 for any number my $listref = shift; my $itemkind = shift; # optional string, e.g. "picture" or "keyword", ... $itemkind = lang('item(s)') unless defined $itemkind; my $ok = 0; my $message = ''; if (($min == $max) and ($min != 0) and (@$listref != $min)) { $ok = 0; $message = langf("Please select exactly %d %s!",$min,$itemkind); } elsif (@$listref < $min) { $ok = 0; $message = langf("Please select at least %d %s!",$min,$itemkind); } elsif (($max != 0) and (@$listref > $max)) { $ok = 0; $message = langf("Please select not more than %d %s!",$max,$itemkind); } else { $ok = 1; } if ($ok != 1) { $win->messageBox(-icon => 'warning', -message => $message, -title => lang("Wrong selection"), -type => 'OK'); } return $ok; } ############################################################## # askSelection ############################################################## sub askSelection { my $listRef = shift; my $max = shift; my $text = shift; # ask only for more than $max pictures return 1 if (@{$listRef} < $max); my $rc = $top->messageBox(-icon => 'question', -message => "You have selected ".scalar @{$listRef}." pictures. This function will open an $text window for each selected picture.\nPlease press Ok to continue.", -title => "Show $text of ".scalar @{$listRef}." pictures", -type => 'OKCancel'); if ($rc =~ m/Ok/i) { return 1; } return 0; } ############################################################## # indexPrint - generate indexPrints/montages of the selected pictures ############################################################## sub indexPrint { return if (!checkExternProgs("indexPrint", "montage")); if (Exists($indexW)) { $indexW->deiconify; $indexW->raise; return; } my $pic_list_ref = shift; #foreach (@$pic_list_ref) { print "list::: $_\n"; } my @sellist = @$pic_list_ref; return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $index = $sellist[0]; $index = dirname($sellist[0]).'/'.findNewName($index); if (-f $index) { # just for safety, we don't want to overwrite something warn "$index exists: aborting - this should never happen!!!\n"; return; } # get size of first pic my ($pic0x, $pic0y) = getSize($sellist[0]); # open window $indexW = $top->Toplevel(); #$indexW->grab(); $indexW->title(langf("Collage/index print of %d pictures", scalar(@sellist))); $indexW->iconimage($mapiviicon) if $mapiviicon; my $w = 20; $indexW->Label(-text => lang('Generate a picture containing several pictures in a grid layout.'))->pack(-padx => 3, -pady => 3); labeledEntry($indexW, 'top', $w, lang('File name'), \$index); labeledEntryColor($indexW,'top',$w,lang('Background color'),'Set',\$config{indexBG}); labeledEntry2($indexW, 'top', $w, 4, lang('Columns'),\$config{indexCols}, lang('Rows'),\$config{indexRows}); labeledEntry2($indexW, 'top', $w, 4, lang('Distance x in pixels'), \$config{indexDisX}, 'y', \$config{indexDisY}); my $sbb = $indexW->Button(-text => lang('Symmetric borders'), -command => sub { $config{indexDisX} = $config{indexBorderWidth}; $config{indexDisY} = $config{indexBorderWidth}; })->pack(-side => 'top',-anchor => 'e', -padx => 3, -pady => 3); $balloon->attach($sbb, -msg => "This button will set x and y distance to border width."); my $sizeF = $indexW->Frame(-relief => "raised")->pack(-expand => 1, -fill => 'x', -padx => 3); labeledEntry2($sizeF, 'top', $w, 4, lang('Picture width'), \$config{indexPicX}, lang('Height'), \$config{indexPicY}); my $sizeF2 = $sizeF->Frame()->pack(-anchor => 'e'); $sizeF2->Label(-text => lang('Presets'))->pack(-side => 'left',-anchor => 'e', -padx => 3, -pady => 3); foreach my $div (1, 2, 4, 10) { $sizeF2->Button(-text => int(100/$div).'%', -command => sub { $config{indexPicX} = int($pic0x/$div); $config{indexPicY} = int($pic0y/$div); })->pack(-side => 'left',-anchor => 'e', -padx => 3, -pady => 3); } #$sizeF2->Button(-text => "50%", -command => sub { $config{indexPicX} = int($pic0x/2); $config{indexPicY} = int($pic0y/2); })->pack(-side => 'left',-anchor => 'e', -padx => 3, -pady => 3); my $lF = $indexW->Frame(-relief => "raised")->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); $lF->Checkbutton(-variable => \$config{indexLabel}, -text => lang('Text to each picture'))->pack(-anchor=>'w'); my $labstr = labeledEntry($lF, 'top', $w, "Text", \$config{indexLabelStr}); $balloon->attach($labstr, -msg => "%b file size\n%c comment\n%d folder\n%e filename extention\n%f filename\n%h height\n%i input filename\n%l label\n%m magick\n%n number of scenes\n%o output filename\n%p page number\n%q quantum depth\n%s scene number\n%t top of filename\n%u unique temporary filename\n%w width\n%x x resolution\n%y y resolution"); my $fss = labeledScale($lF, 'top', $w, lang("Font size"), \$config{indexFontSize}, 0, 50, 1); $balloon->attach($fss, -msg => "The font size of the labels.\nIf you set this to 0, montage will\ntry to choose a appropriate size."); my $ibF = $indexW->Frame(-relief => "raised")->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ibF->Checkbutton(-variable => \$config{indexInnerBorder}, -text => lang('Border around each picture'))->pack(-anchor=>'w'); labeledScale($ibF, 'top', $w, lang("Width"), \$config{indexInnerBorderWidth}, 1, 1000, 1); labeledEntryColor($ibF, 'top', $w, lang("Color"),'Set',\$config{indexInnerBorderColor}); my $obF = $indexW->Frame(-relief => "raised")->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); $obF->Checkbutton(-variable => \$config{indexBorder}, -text => lang('Border around collage'))->pack(-anchor=>'w'); labeledScale($obF, 'top', $w, lang("Width"), \$config{indexBorderWidth}, 1, 1000, 1); labeledEntryColor($obF, 'top', $w, lang("Color"),'Set',\$config{indexBorderColor}); my $qS = labeledScale($indexW, 'top', 26, lang('Quality of collage'), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); buttonComment($indexW, 'top'); calcIndexInfo($indexW, scalar @sellist); my $f = $indexW->Frame(-bd => $config{Borderwidth}, -relief => 'groove',)->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); # add 3 labels for user feedback $f->Label(-textvar => \$indexW->{Label1})->pack(-anchor => 'w'); $f->Label(-textvar => \$indexW->{Label2})->pack(-anchor => 'w'); $f->Label(-textvar => \$indexW->{Label3})->pack(-anchor => 'w'); $f->Button(-image => $mapivi_icons{'Update'}, -command => sub { calcIndexInfo($indexW, scalar @sellist); } )->pack(); my $ButF = $indexW->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB; $OKB = $ButF->Button(-text => lang('OK'), -command => sub { my $nr = calcIndexInfo($indexW, scalar @sellist); # we need the nr of index prints here if ($nr == 1) { # just one index print, we leave the name if (-f $index) { my $rc = $indexW->messageBox(-icon => 'warning', -message => "file $index exist. Please press Ok to overwrite.", -title => "File exists!", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } } else { # there is more than one index print, montage will name them xxx01.jpg ... $index =~ /(.*)(\.jp(g|eg))/i; # split (we need base name and suffix) $index = "$1-%02d$2"; for (1 .. $nr) { my $name = sprintf "%s-%02d%s", $1, $_, $2; if (-f $name) { my $rc = $indexW->messageBox(-icon => 'warning', -message => "file $name exist. Please press Ok to overwrite.", -title => "File exists!", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } } } $indexW->destroy(); # close index window log_it("building index prints of ".scalar @sellist." pictures ..."); my $command = "montage "; if ($config{indexInnerBorder}) { $command .= "-bordercolor \"".$config{indexInnerBorderColor}."\" "; $command .= "-border ".$config{indexInnerBorderWidth}.'x'.$config{indexInnerBorderWidth}." "; } $command .= "-label \"$config{'indexLabelStr'}\" " if $config{indexLabel}; $command .= "-font \"-*-courier-medium-r-*-*-".$config{indexFontSize}."-*-*-*-*-*-iso8859-*\" " if ($config{indexLabel} and ($config{indexFontSize} > 0)); #$command .= "-pointsize ".$config{indexFontSize}." " if $config{indexLabel}; $command .= "-background \"$config{'indexBG'}\" -tile $config{'indexCols'}x$config{'indexRows'} -filter Lanczos -geometry $config{'indexPicX'}x$config{'indexPicY'}+$config{'indexDisX'}+$config{'indexDisY'} "; my $pic; # add the selected pictures to $command foreach my $dpic (@sellist) { $command .= "\"$dpic\" "; } # if there is a second process step (border) we use the lossless MIFF format my $tmpfile = "$trashdir/indexTmpFile.miff"; if (-f $tmpfile) { warn "tmp file $tmpfile exists! Mapivi tries to remove it"; return unless removeFile($tmpfile); } if ($config{indexBorder}) { $command .= "\"$tmpfile\""; } else { $command .= "-quality ".$config{PicQuality}." "; $command .= "\"$index\""; } print "$command\n" if $verbose; $top->Busy; if ($EvilOS) { (system $command) == 0 or warn "execute: $command failed: $!"; } else { execute($command); } # for win32 we need to wait for this process to finish if ($config{indexBorder}) { $command = "convert -bordercolor \"".$config{indexBorderColor}."\" "; $command .= "-border ".$config{indexBorderWidth}.'x'.$config{indexBorderWidth}." "; $command .= "-quality ".$config{PicQuality}." "; $command .= "\"$tmpfile\" "; $command .= "\"$index\""; print "$command\n" if $verbose; if ($EvilOS) { # do not use bgrun for windows (system $command) == 0 or warn "execute: $command failed: $!"; } else { execute($command); } } $top->Unbusy; removeFile($tmpfile) if (-f $tmpfile); if ($conf{add_tool_info}{value}) { addCommentToPic("Picture made with Mapivi $version ($mapiviURL)", $index, NO_TOUCH); } log_it(lang('Ready!')); if ($nr == 1) { # for one index we insert it in the listbox generateOneThumb($index); # insert index in listbox addOneRow($picLB, $index, 1, $sellist[0]); } else { # for several index we need a (slower) update updateThumbs(); } showPic($index); })->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $indexW->destroy(); } )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); bind_exit_keys_to_button($indexW, $xBut); $indexW->Popup; $indexW->waitWindow; return; } ############################################################## ############################################################## sub passport_dialog { if (Exists($passportW)) { $passportW->deiconify; $passportW->raise; return; } # open window $passportW = $top->Toplevel(); $passportW->title(lang("Passport prints")); $passportW->iconimage($mapiviicon) if $mapiviicon; my ($axp,$ayp) = @_; my $ok = 0; my $ax = 3.5; my $ay = 4.5; my $px = 15; my $py = 10; my $w = 25; my $status = ''; my $pxs = labeledScale($passportW, 'top', $w, lang("Print size x"), \$px, 1, 100, 0.1, sub{ $status = passport_check($axp,$ayp,$ax,$ay,$px,$py); }); $balloon->attach($pxs, -msg => "Width of photo print in physical units (e.g. cm or inch)"); my $pys = labeledScale($passportW, 'top', $w, lang("Print size y"), \$py, 1, 100, 0.1, sub{ $status = passport_check($axp,$ayp,$ax,$ay,$px,$py);}); $balloon->attach($pys, -msg => "Height of photo print in physical units (e.g. cm or inch)"); my $axs = labeledScale($passportW, 'top', $w, lang("Passport picture size x"), \$ax, 1, 50, 0.1, sub{ $status = passport_check($axp,$ayp,$ax,$ay,$px,$py);}); $balloon->attach($axs, -msg => "Width of passport photo in physical units (e.g. cm or inch)"); my $ays = labeledScale($passportW, 'top', $w, lang("Passport picture size y"), \$ay, 1, 50, 0.1, sub{ $status = passport_check($axp,$ayp,$ax,$ay,$px,$py);}); $balloon->attach($ays, -msg => "Height of passport photo in physical units (e.g. cm or inch)"); $passportW->Label(-textvariable => \$status)->pack(-side => 'top',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); my $ButF = $passportW->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB; $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $ok = 1; $passportW->destroy(); })->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $ok = 0; $passportW->destroy(); } )->pack(-side => 'left',-expand => 1,-fill => 'x',-padx => 3,-pady => 3); bind_exit_keys_to_button($passportW, $xBut); $passportW->Popup; $passportW->waitWindow; print "passport dialog: $ax * $ay $px * $py\n"; return ($ok, $ax, $ay, $px, $py); } ############################################################## ############################################################## sub passport_check { my ($axp,$ayp,$ax,$ay,$px,$py) = @_; my ($ok,$err,$n,$m,$bxp,$byp) = passport_border($axp,$ayp,$ax,$ay,$px,$py); my $check; if ($ok) { my $nr = $n * $m; $check = "OK, print will hold $nr (${n}x${m}) passport pictures. Border $bxp, $byp"; } else { $check = "Error, $err"; } return $check; } ############################################################## # generate a new picture with a certain phyiscal size (e.g. 10x15cm) # from one input picture. The given picture will be tiled onto # the given canvas as often as possible (in $n cols and $m rows). # outer size of print: $px * $py # inner size of picture: $ax * $ay # border around inner picture(s): $bx, $by # all sizes above are in physical units (e.g. cm or inch) # size in pixels end with a 'p', e.g. $bxp ############################################################## sub passport_border { #my $dpic = shift; my ($axp,$ayp,$ax,$ay,$px,$py) = @_; my $err = ''; if ((not defined $ax) or (not defined $ay) or (not defined $px) or (not defined $py)) {$err = "Some sizes are not defined!"; return (0,$err);} if (($ax <= 0) or ($ay <= 0)) {$err = "Passport picture has a size of 0 or less!"; return (0,$err);} if ($ax > $px) {$err = "Passport picture width is larger than print!"; return (0,$err);} if ($ay > $py) {$err = "Passport picture height is larger than print!"; return (0,$err);} my $arat = $ax/$ay; my $aratp = $axp/$ayp; print "ratio $arat pixel-ratio $aratp\n"; my $delta = 1/100; if (($arat > $aratp*(1+$delta)) or ($arat < $aratp*(1-$delta))) {$err = sprintf("Given ratio of passport picture (%.3f) does not fit to its pixel ratio (%.3f)!",$arat,$aratp); return (0,$err);} my $n = int($px/$ax); my $m = int($py/$ay); my $bx = ($px-$n*$ax)/(2*$n); my $by = ($py-$m*$ay)/(2*$m); my $bxp = round($axp/$ax*$bx); my $byp = round($ayp/$ay*$by); print "passport: n:$n, m:$m, bx:$bx = $bxp pixel, by:$by = $byp pixel\n"; print "error:$err\n"; return (1,$err,$n,$m,$bxp,$byp); } ############################################################## ############################################################## sub passport_print { my $widget = shift; my @pics = getSelection($widget); return 0 if (!checkExternProgs('passport_print', 'montage')); return 0 if not checkSelection($top, 1, 1, \@pics, lang('picture')); my $dpic = $pics[0]; if (not -f $dpic) {log_it("Error: $dpic is no file!"); return 0;} my ($axp, $ayp) = getSize($dpic); if (($axp == 0) or ($ayp == 0)) {log_it("Error: Picture has a pixel size of 0!"); return 0;} my ($ok, $ax, $ay, $px, $py); ($ok, $ax, $ay, $px, $py) = passport_dialog($axp,$ayp); return 0 if not $ok; my ($err,$n,$m,$bxp,$byp); ($ok,$err,$n,$m,$bxp,$byp) = passport_border($axp,$ayp,$ax,$ay,$px,$py); if (not $ok) { log_it("Passport print error: $err"); return 0; } my ($basename,$dir,$suffix) = fileparse($dpic, '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) # Build outfile name my $outfile = $dir.$basename.'Passport'.$suffix; $outfile = $dir.findNewName($outfile) if (-f $outfile); my $command = "montage "; $command .= "-bordercolor \"white\" "; #$command .= "-border ${bxp}x${byp} "; #$command .= "-background \"$config{'indexBG'}\"; $command .= " -tile ${n}x${m} -geometry ${axp}x${ayp}+${bxp}+${byp} "; #my $pic; # add the selected pictures to $command my $nr = $n*$m; foreach (1 .. $nr) { $command .= "\"$dpic\" "; } $command .= "-quality ".$config{PicQuality}." "; $command .= "\"$outfile\""; print "$command\n";# if $verbose; $top->Busy; log_it("Creating passport print with $n x $m pictures ..."); if ($EvilOS) { (system $command) == 0 or warn "execute: $command failed: $!"; } else { execute($command); } if (-f $outfile) { smart_update() if ($widget == $picLB); showPic($outfile); log_it("Passport print ready!"); } else { log_it("Passport print: no file created; reason unknown (please check console output)!"); } $top->Unbusy; return 1; } ############################################################## # calcIndexInfo ############################################################## sub calcIndexInfo { my $w = shift; # the window widget, we assume 3 labels with a variable reference to Label1, Label2 and Label3 here my $nrOfSel = shift; my $indexPics = $config{indexRows} * $config{indexCols}; $w->{Label1} = langf("One collage holds %d pictures.", $indexPics); my $indexNr = int($nrOfSel/$indexPics); $indexNr++ if (($nrOfSel % $indexPics) != 0); $w->{Label2} = langf("With %d pictures this results in %d collages.", $nrOfSel, $indexNr); my $sizex = $config{indexCols} * ($config{indexPicX} + (2*$config{indexDisX})); my $sizey = $config{indexRows} * ($config{indexPicY} + (2*$config{indexDisY})); if ($config{indexBorder}) { $sizex = $sizex + 2 * $config{indexBorderWidth}; $sizey = $sizey + 2 * $config{indexBorderWidth}; } if ($config{indexInnerBorder}) { $sizex = $sizex + $config{indexCols} * 2 * $config{indexInnerBorderWidth}; $sizey = $sizey + $config{indexRows} * 2 * $config{indexInnerBorderWidth}; } $w->{Label3} = langf("Each collage is about %dx%d pixels.", $sizex, $sizey); return ($indexNr); } ############################################################## # fisher_yates_shuffle - shuffle an array randomly ############################################################## sub fisher_yates_shuffle { my $deck = shift; # $deck is a reference to an array my $i = @$deck; while ($i--) { my $j = int rand ($i+1); @$deck[$i,$j] = @$deck[$j,$i]; } return; } ############################################################## # reloadPic ############################################################## sub reloadPic { deleteCachedPics($actpic); # we need to reread the picture, so we should remove it from the cachedPics list first showPic($actpic); # display the picture return; } ############################################################## # slideshow - start/stop slideshow ############################################################## sub slideshow { my $last_time; if ($slideshow) { log_it("slideshow started"); $top->after(500); # just a litte delay to show the message above until ($slideshow == 0) { if (!defined $last_time || Tk::timeofday()-$last_time > $config{SlideShowTime}) { my @savedselection = $picLB->info('selection'); showPic(nextSelectedPic($actpic)); log_it(basename($actpic)." (slideshow: ".$config{SlideShowTime}."sec)"); $last_time = Tk::timeofday(); $picLB->selectionClear(); reselect($picLB, @savedselection); } DoOneEvent(); # stay responsive last if (!$slideshow); } } log_it("slideshow stopped"); return; } ############################################################## # toggle - toggle the value of a boolean variable reference ############################################################## sub toggle { my $varRef = shift; if ($$varRef == 1) { $$varRef = 0; } elsif ($$varRef == 0) { $$varRef = 1; } else { warn "toggle: Reference has unexpected value: $$varRef\n"; } return; } ############################################################## # execute ############################################################## sub execute { my $string = shift; # command to execute my $actexe; # file handle to Tk::IO object (background process) print "execute: $string\n" if $verbose; if (!$EvilOS) { # running on a "good" OS like Linux we can use background processes :) # init a background process $actexe = Tk::IO->new(-linecommand => sub { nop(); }, -childcommand => sub { print "execute: child com\n" if $verbose; } ); # start the background process $actexe->exec($string); # the busy call made some problems with jhead and the autorot option # while it was enabled the $actexe->wait call sometimes never returned #$top->Busy; # waiting for current process to finish $actexe->wait(); #$top->Unbusy; } # we run on a evil OS like windows - no threading :( # Tk::IO is supposed to run under windows, but it does not with mine else { #$top->Busy; #(system "$string") == 0 or warn "execute: $string failed: $!"; #$top->Unbusy; bgRun($string); } return; } ############################################################## # findApp - find Windows-App-Name for Win32::Process # from Uwe Steffen ############################################################## sub findApp { my ($cmd)=@_; $cmd =~ /^\s*(\w+)/; my $cmdName=$1.".exe"; #print "cmdName:",$cmdName,"\n"; if (defined($winapps{$cmdName})) { return $winapps{$cmdName}; } my @path = split (/;/, $ENV{PATH}); print " adding \"$FindBin::Bin\" to path \"$ENV{PATH}\"\n" if $verbose; foreach my $dir (@path) { my $test=$dir."\\$cmdName"; #print "Test: $test \n"; if ( -x $test ) { $winapps{$cmdName}=$test; #print " Success!\n"; return $test; } } warn "findApp: Could not find application: \"$cmd\" \"$cmdName\"\n"; return ''; } ############################################################## # bgRun - run a process in background # from Uwe Steffen ############################################################## sub bgRun { my ($cmd) = @_; if (!$EvilOS) { warn "bgRun should not be called for non Windows systems!"; return 0; } if (Win32ProcAvail) { my ($dir,$pid,$proc); my ($bInherit) = 0; my ($flags) = Win32::Process::CREATE_NO_WINDOW() | Win32::Process::IDLE_PRIORITY_CLASS() | Win32::Process::DETACHED_PROCESS(); if ( $cmd =~ /^(\w+:[\w\\.]+)/) { print "Process with full path: ",$cmd," APP:", $1,"\n" if $verbose; $pid = Win32::Process::Create($proc, $1, $cmd, $bInherit, $flags, "." ); } else { print "Process without full path: ",$cmd," APP:", findApp($cmd),"\n" if $verbose; $pid = Win32::Process::Create($proc, findApp($cmd), $cmd, $bInherit, $flags, "." ); } if ($pid) { $proc->Wait(15000); print "bgRun: timeout\n"; return 1; } else { warn "Could not start $cmd.\n"; warn "Error: " . Win32::FormatMessage(Win32::GetLastError()); return 0; } } else { # Win32::Process module not available $top->Busy; (system "$cmd") == 0 or warn "bgRun: $cmd failed: $!"; $top->Unbusy; } return 1; } ############################################################## # cleanThumbDB - remove all old thumbnails in the thumbDB ############################################################## sub cleanThumbDB { # todo create dialog window and make e.g. the $days an adjustable option my $days = 30; my $thumbDB_quote = $thumbDB; $thumbDB_quote =~ s|\\|\\\\|g; # replace backslash with double backslashe \ -> \\ (quoting) my @thumbs; my $rc = $top->messageBox(-icon => "question", -message => "This function will display all stored thumbnails in the thumbnail data base which have no corresponding picture and are older than $days days. You may then select which of them to delete. Please press Ok to proceed.", -title => "Clean thumbnail database", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); log_it("searching outdated thumbnails ..."); find(sub { #print "dir: $File::Find::name\n"; if (-f and (-M >= $days)) { my $orig = $File::Find::name; # cut off the first path part (the path to the thumbdb) the rest is the real part. $orig =~ s|^$thumbDB_quote||; unless (-f $orig) { print "file: $File::Find::name -> $orig\n" if $verbose; push @thumbs, $File::Find::name; } } }, $thumbDB); # todo: ignore /mnt/cdrom (%ignorePaths) ... log_it("found ".@thumbs." outdated thumbnails ..."); if (@thumbs > 0) { my @sel_list; # user may select which to delete if (mySelListBoxDialog("Really delete?", "Please select which of these ".scalar @thumbs." thumbnails to delete.", MULTIPLE, 'OK', \@sel_list, @thumbs)) { foreach (@sel_list) { print "removing $thumbs[$_]\n" if $verbose; removeFile($thumbs[$_]); } } log_it(lang('Ready!')); } else { $top->messageBox(-icon => "info", -message => "Found no outdated thumbnails in $thumbDB. Seems like your thumbnails are up to date.", -title => "Thumbnail database is up to date", -type => 'OK'); } return; # todo: remove empty dirs in $thumbDB ... } ############################################################## # cleanDir - remove all dirs and files added by mapivi from # the given dir ############################################################## sub cleanDir { my $dir = shift; print "dir = $dir actdir = $actdir\n" if $verbose; return unless ((defined $dir) or (-d $dir)); my $rc; if (($cleanDirLevel == 0) or (not $cleanDirNoAsk)) { my $dia = $top->DialogBox(-title => "Clean folder ".basename($dir)."?", -buttons => ['OK', 'Cancel']); $dia->add("Label", -text => "Remove all sub folders and files from\n$dir\nwhich were created from Mapivi\nContinue?", -bg => $conf{color_bg}{value}, -justify => 'left')->pack; $dia->add("Checkbutton", -text => "Continue without asking again", -variable => \$cleanDirNoAsk)->pack; $rc = $dia->Show(); return if ($rc ne 'OK'); } my @subdirs = ("$dir/$thumbdirname", "$dir/$exifdirname"); foreach my $subdir (@subdirs) { if (-d $subdir) { my @fileDirList = readDir($subdir); unless ($cleanDirNoAsk) { $rc = $top->messageBox(-icon => 'question', -message => "There are ".scalar @fileDirList." files in the sub folder\n".basename($subdir)."\nRemove?", -title => "Remove sub folder?", -type => 'OKCancel'); next if ($rc !~ m/Ok/i); } log_it("cleaning $subdir ..."); foreach (@fileDirList) { if (-f "$subdir/$_") { removeFile("$subdir/$_") } else { $top->messageBox(-icon => 'warning', -message => "There is a non file in $subdir: $_!", -title => 'Warning', -type => 'OK') if ($_ ne ".."); } } if (! rmdir($subdir)) { $top->messageBox(-icon => 'warning', -message => "Could not remove $subdir: $_!", -title => 'Error', -type => 'OK'); } } } my @dirs = getDirs($dir); return if (@dirs == 0); my %dirh; # copy the list into a hash foreach (@dirs) { $dirh{$_} = 1; } # sort some special dirs out foreach ($thumbdirname, $exifdirname, ".xvpics") { if (defined $dirh{$_}) { delete $dirh{$_}; } } # are there some other dirs? my $nr = keys %dirh; if (($nr > 0) and (not $cleanDirNoAsk)) { $rc = $top->messageBox(-icon => 'question', -message => "There are $nr sub folders in\n$dir\n, should I clean them too?", -title => "Clean sub folders?", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } # recursive call of cleanDir() foreach (sort keys %dirh) { $cleanDirLevel++; cleanDir ("$dir/$_"); $cleanDirLevel--; } if ($cleanDirLevel == 0) { log_it(lang('Ready!')); } return; } ############################################################## # isInList - check if a string is element of a list reference ############################################################## sub isInList { my $e = shift; my $listRef = shift; my $found = 0; foreach (@$listRef) { if ($e eq $_) { $found = 1; last; } } return $found; } ############################################################## # screenshot ############################################################## sub screenshot { if (Exists($scsw)) { $scsw->deiconify; $scsw->raise; return; } return if (!checkExternProgs("screenshot", "xwd")); return if (!checkExternProgs("screenshot", "convert")); # open window $scsw = $top->Toplevel(); $scsw->title("Make screenshot"); $scsw->iconimage($mapiviicon) if $mapiviicon; my $root = ''; my $frame = "-frame"; my $tmpfile = "$trashdir/screenshot.jpg"; $tmpfile = "$trashdir/".findNewName($tmpfile); my $file = "$actdir/screenshot.jpg"; $file = "$actdir/".findNewName($file); my $hideMapivi = 0; my $showPic = 1; my $ifB; my $f1 = $scsw->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x',-padx => 3,-pady => 3); $f1->Radiobutton(-text => "single window (select window with mouse click after pressing OK)", -variable => \$root, -value => '', -command => sub { $ifB->configure(-state => 'normal');} )->pack(-anchor => 'w'); $f1->Radiobutton(-text => "complete desktop", -variable => \$root, -value => "-root", -command => sub { $frame = ''; $ifB->configure(-state => 'disabled');} )->pack(-anchor => 'w'); my $f2 = $scsw->Frame(-bd => $config{Borderwidth}, -relief => 'groove')->pack(-fill => 'x',-padx => 3,-pady => 3); $ifB = $f2->Checkbutton(-variable => \$frame, -onvalue => "-frame", -offvalue => '', -anchor => 'w', -text => "include window border" )->pack(-anchor => 'w'); $f2->Checkbutton(-variable => \$hideMapivi, -anchor => 'w', -text => "hide Mapivi window" )->pack(-anchor => 'w'); $f2->Checkbutton(-variable => \$showPic, -anchor => 'w', -text => "show screenshot in Mapivi when finished" )->pack(-anchor => 'w'); buttonComment($f2, 'top'); labeledEntryButton($scsw,'top',23,"file name",'Set',\$file); my $qS = labeledScale($scsw, 'top', 23, lang('Quality of picture (%)'), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); my $ButF = $scsw->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { if (-f $file) { my $rc = $scsw->messageBox(-icon => 'warning', -message => "file\n\"$file\"\nexist.\nOk to overwrite?", -title => "Screenshot", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } if (-f $tmpfile) { my $rc = $scsw->messageBox(-icon => 'warning', -message => "file $tmpfile exist. Ok to overwrite?", -title => "Screenshot", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); } $top->iconify() if $hideMapivi; $scsw->withdraw(); $scsw->destroy(); $top->update if (!$hideMapivi); # call external command jpegtran and rotate to the temp file my $command = "xwd $frame $root -out \"$tmpfile\" "; #(system "$command") == 0 or warn "screenshot: $! ($command)"; execute($command); $top->deiconify if $hideMapivi; if (!-f $tmpfile) { warn "nothing to convert!"; return; } $command = "convert -quality ".$config{PicQuality}." \"$tmpfile\" \"$file\""; log_it("converting to JPEG format ..."); $top->Busy; #(system "$command") == 0 or warn "convert: $! ($command)"; execute($command); $top->Unbusy; removeFile($tmpfile); if ($conf{add_tool_info}{value}) { addCommentToPic("Screenshot made with Mapivi $version ($mapiviURL)", $file, NO_TOUCH); } log_it(lang('Ready!')); if ($showPic) { my $dir = dirname($file); if ($actdir ne $dir) { openDirPost($dir); } else { updateThumbs(); } showPic($file); } })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $balloon->attach($OKB, -msg => 'In "single window" mode the mouse cursor will turn into a cross after pressing OK. Just make a left mouse click on the desired window. In "desktop" mode the screenshot will be taken immediatelly after pressing the OK button. There may be two beeps in both modes if sound is enabled.'); my $xBut = $ButF->Button(-text => lang('Cancel'), -command => sub { $scsw->withdraw(); $scsw->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($scsw, $xBut); $scsw->Popup; $scsw->waitWindow; return; } ############################################################## # drag keyword(s) from keyword tree or clipboard to picture(s) ############################################################## sub drag_keyword { my ($w, $token) = @_; #my $w = $token->parent; # $widget my $e = $w->XEvent; $w->update; my @sellist; # determine drag source widget # source: keyword tree if ($w == $nav_F->{key_frame}->{tree}) { #(ref($w) eq 'Tk::Tree') { print "drag_keyword: source: tree\n"; @sellist = $w->info('selection'); } # source: keyword clipboard # todo: ->{hot} should be a Tk::Listbox but it shows up as Tk::Frame, clarify why elsif ($w == $nav_F->{key_frame}->{hot}) { # (ref($w) eq 'Tk::Frame') { print "drag_keyword: source: frame (listbox)\n"; foreach ($w->curselection()) { print "drag_keyword: adding $_: ".$w->get($_)."\n"; push @sellist, $w->get($_); } } else { print "drag_keyword: unknown source widget ref($w)\n"; return; } if (@sellist < 1) { print "drag_keyword: no selection\n"; return; } # check selection # only one picture selected if (@sellist == 1) { #my $tokentext = $w->itemCget($sellist[0], 1, -text); my $tokentext = $sellist[0]; # Configure the dnd token to show the keyword #if (!$w->info("exists", $sellist[0])) { print "drag_keyword: item $tokentext\n"; # return; #} $token->configure(-text => $tokentext); # store keyword in token $token->{keyword} = $tokentext; #log_it($tokentext); } # more than one pictures selected else { print "drag_keyword: please select just one keyword\n"; return; #$token->configure(-text => scalar @sellist." keywords"); } # Show the token my($X, $Y) = ($e->X, $e->Y); $token->MoveToplevelWindow($X, $Y); $token->raise; $token->deiconify; $token->FindSite($X, $Y, $e); Tk->break; # stop default binding of this event return; } ############################################################## # drop keywords on actual picture in canvas ############################################################## sub drop_keyword { my ($w, $token) = @_; $token->withdraw; my (@pics, @keys); if (not defined $token->{keyword}) { print "drop_keyword: no keyword defined!\n"; return; } push @keys, $token->{keyword}; print "drop_keyword: \"$token->{keyword}\"\n"; # add keyword to picture(s) if (($w == $picLB) or ($w == $c)) { # drop on thumbnail list if ($w == $picLB) { @pics = $picLB->info('selection'); } # drop on canvas (actual picture) else { push @pics, $actpic; } print "drop_keyword: $token->{keyword} to picture(s)\n"; add_keywords_to_pics($picLB, \@keys, \@pics); } elsif ($w == $nav_F->{key_frame}->{hot}) { print "drop_keyword: $token->{keyword} to hotlist\n"; add_keyword_to_hotlist($nav_F->{key_frame}->{hot}, \@keys); } else { print "drop_keyword: error wrong widget $w\n"; } return; } ############################################################## # dragFromPicLB - drag pictures from the thumb table ############################################################## sub dragFromPicLB { my($token) = @_; my $w = $token->parent; # $w is the $picLB hlist my @sellist = getSelection($w); my $e = $w->XEvent; my($x, $y) = ($e->X, $e->Y); # compare drag coordinates with press coords to distinguish picture dragging my ($mx, $my) = ($Tk::event->x(), $Tk::event->y()); my $dx = $picLB->{lastx} - $mx; $dx *= -1 if ($dx < 0); my $dy = $picLB->{lasty} - $my; $dy *= -1 if ($dy < 0); #print "drag: diff $dx $dy $mx $my last $picLB->{lastx} $picLB->{lasty}\n"; if ($dx < 20 and $dy < 20) { #print "move to small\n"; Tk->break; # stop default binding of this event return; } #print "move OK\n"; $w->update; if ($w->{DnDThumbnail}) { # clear memory from last drag $w->{DnDThumbnail}->delete; # delete the photo object delete $w->{DnDThumbnail}; # delete the hash item } return unless checkSelection($w, 1, 0, \@sellist, lang("picture(s)")); #print "Drag from with ".scalar @sellist." selected pictures\n"; if ($EvilOS) { log_it("copy or move "); } else { log_it("copy, link, or move "); } # only one picture selected if (@sellist == 1) { #my $dpic = $w->itemCget($sellist[0], 1, -text); my $dpic = $sellist[0]; # Configure the dnd token to show the listbox entry if (!$w->info("exists", $sellist[0])) { print "dragFromPicLB: item not available\n"; return; } my $icon = $dragAndDropIcon1; my $thumb = getThumbFileName($dpic); if (-f $thumb) { $w->{DnDThumbnail} = $top->Photo(-file => $thumb, -gamma => $config{Gamma}); $icon = $w->{DnDThumbnail} if $w->{DnDThumbnail}; } if ($icon) { $token->configure(-image => $icon); } else { $token->configure(-text => basname($dpic)); } log_it('Drag and drop '.$dpic); } # more than one pictures selected # todo: generate a stack of the first two selected thumbnails using imagemagick as icon # see e.g. http://www.imagemagick.org/Usage/montage/#index else { if ($dragAndDropIcon2) { $token->configure(-image => $dragAndDropIcon2); } else { $token->configure(-text => " ".scalar @sellist." pictures"); } log_it('Drag and drop '.scalar @sellist." pictures"); } # Show the token $token->MoveToplevelWindow($x, $y); $token->raise; $token->deiconify; $token->FindSite($x, $y, $e); #Tk->break; # stop default binding of this event return; } ############################################################## # dropToDirTree - drop pictures on the dirtree (copy or move) ############################################################## sub dropToDirTree { $token->withdraw; log_it(''); my @sellist = $picLB->info('selection'); my $targetdir = getNearestItem($dirtree); my $details; return if (@sellist < 1); my $dirtreeNoScroll = $dirtree->Subwidget("scrolled"); return unless ($top->containing($top->pointerxy) eq $dirtreeNoScroll); $targetdir =~ s/\/\//\//g; # replace all // with / foreach my $dpic (@sellist) { warn "$dpic n.a." unless ($picLB->info("exists", $dpic)); my $pic = basename($dpic); my $size = getFileSize($dpic, FORMAT); $details .= sprintf "%-30s %20s\n", $pic, $size; } my $text = "Should I "; if ($EvilOS) { $text .= "copy or move "; } else { $text .= "copy, link, or move "; } if (@sellist == 1) { $text .= "this picture"; } else { $text .= "these ".scalar @sellist." pictures"; } $text .= " to $targetdir?\n\n$details"; my $rc = 'Cancel'; if ($EvilOS) { $rc = myButtonDialog("Copy/Move", $text, undef, "Copy", "Move", 'Cancel'); } else { $rc = myButtonDialog("Copy/Link/Move", $text, undef, "Copy", "Link", "Move", 'Cancel'); } if ($rc eq 'Cancel') { return; } elsif ($rc eq "Copy") { dirSave($targetdir); copyPics($targetdir, COPY, $picLB, @sellist); } elsif ($rc eq "Link") { dirSave($targetdir); linkPics($targetdir, @sellist); } elsif ($rc eq "Move") { dirSave($targetdir); movePics($targetdir, $picLB, @sellist); } else { warn "unexpected rc: $rc"; return; } return; } ############################################################## #dragAndDropExtern - todo # 2009-08-27: This code works under Windows XP ############################################################## sub dragAndDropExtern { my($widget, $selection) = @_; print "dragAndDropExtern\n"; my $item; eval { if ($^O eq 'MSWin32') { $item = $widget->SelectionGet(-selection => $selection, 'STRING'); } else { $item = $widget->SelectionGet(-selection => $selection, 'FILE_NAME'); } }; if (!defined $item) { log_it("Drag-and-drop: Sorry, filename is not defined!"); print "dragAndDropExtern: filename is not defined!\n"; return; } print "drop extern received: $item\n"; # $top->messageBox(-icon => 'warning', # -message => "drop extern received: $item", # -title => "Drag and drop", -type => 'OK'); # is the dropped item a file .... if (-f $item) { if (is_a_JPEG($item)) { showPic($item); } elsif (is_a_slideshow_file($item)) { # open collection (light table) window if needed light_table_open_window() unless (Exists($ltw)); # add the pictures from the slideshow file to light table window light_table_open(ADD, $item); } else { log_it("Drag-and-drop: Sorry, only JPEG pictures are supported!"); print "Sorry, only JPEG pictures are supported!\n"; } } # ... or a directory? elsif (-d $item) { openDirPost($item); } else { log_it("Drag-and-drop: Sorry, $item is no dir and no file!"); print "$item is no dir and no file\n"; } return; } ############################################################## # light_table_dragAndDropExtern - todo # 2009-08-27: This code works under Windows XP ############################################################## sub light_table_dragAndDropExtern { my($widget, $selection) = @_; my $item; eval { if ($^O eq 'MSWin32') { $item = $widget->SelectionGet(-selection => $selection, 'STRING'); } else { $item = $widget->SelectionGet(-selection => $selection, 'FILE_NAME'); } }; if (!defined $item) { log_it("Drag-and-drop: Sorry, filename is not defined!"); print "dragAndDropExtern: filename is not defined!\n"; return; } # is the dropped item a file .... if (-f $item) { if (is_a_JPEG($item)) { # open light table window if needed light_table_open_window(); my @list; push @list, $item; light_table_add(\@list); } elsif (is_a_slideshow_file($item)) { # open light table window if needed light_table_open_window(); # add the pictures from the slideshow file to light table window light_table_open(ADD, $item); } else { log_it("Drag-and-drop: Sorry, only JPEG pictures and slideshow files are supported!"); print "Sorry, only JPEG pictures are supported!\n"; } } # ... or a directory? todo #elsif (-d $item) { # todo: function not yet implemented; should add all pictures of a folder to the light box #light_table_add_folder($item); #} else { log_it("Drag-and-drop: Sorry, $item is no file!"); print "$item is no dir and no file\n"; } return; } ############################################################## # checkWriteable ############################################################## sub checkWriteable { my $dpic = shift; my $pic = basename($dpic); my $dir = dirname($dpic); my $thumb = getThumbFileName($dpic); return 0 if (! -f $dpic); # no file return 1 if (-w $dpic); # OK, file is writable if (!-w $dpic) { my $message = "The picture $pic is write proteced!\nShould I try to overwrite the write protection?"; my $rc = myButtonDialog("$pic is write protected", $message, $thumb, 'OK', 'Cancel'); if ($rc eq 'OK') { my $mode = (lstat $dpic)[2]; # get the actual access mode $mode = $mode | oct(200); # set user write (+uw) return (chmod($mode, $dpic)); # try to change the mode } else { return 0; # file is still write protected } } } ############################################################## # checkWriteableMulti ############################################################## sub checkWriteableMulti { my @dpics = @_; my @protected = (); foreach (@dpics) { if ((-f $_) and (not -w $_)) { push @protected, $_; } } return '' unless (@protected); # nothing to do my $text = "The following pictures are write protected:\n\n"; foreach (@protected) { $text .= "$_\n"; } $text .= "\nShould I try to overwrite the write protection?"; my $rc = myButtonDialog(scalar @protected." pictures are write protected", $text, undef, 'OK', 'Cancel', 'Cancel all'); if ($rc eq 'OK') { foreach (@protected) { my $mode = (lstat $_)[2]; # get the actual access mode $mode = $mode | oct(200); # set user write (+uw) chmod($mode, $_); # try to change the mode } } return $rc; } ############################################################## # set input focus on widget if mouse pointer enters the widget area ############################################################## sub focus_on_enter { my $w = shift; # in Windows this causes troubles, because it moves the main window # in foreground and a sub window to background when the mouse pointer # leaves the sub window and touches the main window if (not $EvilOS) { $w->bind('', sub { $w->focus; } ); } return; } ############################################################## ############################################################## sub add_mousewheel_zoom { my $w = shift; # widget if ($EvilOS) { $w->CanvasBind('' => sub { print "Ctrl-Mousewheel Windows\n"; } ); #[ sub { $_[0]->yview('scroll', -($_[1] / 120) * 3, 'units') }, #Ev('D') ]); } else { $w->CanvasBind('' => sub { print "Ctrl-Mousewheel Non-Windows up\n"; #$_[0]->yview('scroll', -3, 'units') unless $Tk::strictMotif; }); $w->CanvasBind('' => sub { print "Ctrl-Mousewheel Non-Windows down\n"; #$_[0]->yview('scroll', +3, 'units') unless $Tk::strictMotif; }); } return; } ############################################################## # diffPics - create a new picture containing the difference # between two pictures ############################################################## sub diffPics { return if (!checkExternProgs("diffPics", "composite")); my @sellist = $picLB->info('selection'); return unless checkSelection($top, 2, 2, \@sellist, lang("picture(s)")); my $dpicA = $sellist[0]; my $dpicB = $sellist[1]; my $dpicDiff = $dpicA; $dpicDiff =~ s/(.*)(\.jp(g|eg))/$1-diff$2/i; # pic.jpg -> pic-diff.jpg $dpicDiff = dirname($dpicA).'/'.findNewName($dpicDiff); # pic-diff.jpg -> pic-diff-03.jpg log_it("creating difference picture ..."); #my $command = "composite -compose difference \"$dpicA\" \"$dpicB\" \"$dpicDiff\""; my $command = "convert \"$dpicA\" \"$dpicB\" -compose difference -composite -normalize \"$dpicDiff\""; print "diffPics: $command\n" if $verbose; $top->Busy; execute($command); $top->Unbusy; log_it(lang('Ready!').' '.langf("Created difference picture %s.",basename($dpicDiff))); generateOneThumb($dpicDiff); # insert diff pic in listbox addOneRow($picLB, $dpicDiff, 1, $dpicA); #updateThumbs(); showPic($dpicDiff); return; } ############################################################## # hdr_pic - generate a High Dynamic Range (HDR) Image from several pictures # using external program luminance-hdr-cli # luminance-hdr-cli -a AIS -o hdr.jpg -q 90 Bil1.jpg Bild2.jpg Bild3.jpg ############################################################## sub hdr_pic { my @sellist = $picLB->info('selection'); return if (!checkExternProgs('hdr_pic', 'luminance-hdr-cli')); return unless checkSelection($top, 2, 10, \@sellist, lang('picture(s)')); my $selected = @sellist; my ($basename,$dir,$suffix) = fileparse($sellist[0], '\.[^.]*'); # suffix = . and not-. (one dot and zero or more non-dots) # Build outfile name my $outfile = $dir.$basename.'HDR'.$suffix; $outfile = $dir.findNewName($outfile) if (-f $outfile); log_it("Buildung a High Dynamic Range (HDR) image from $selected pictures. May take a while ..."); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $input_files = join( " ", @sellist ); # align input pictures with AIS, output picture JPEG quality 90% # todo: add a dialog to select parameters my $command = "luminance-hdr-cli -a AIS -o $outfile -q 90 $input_files"; if (not $EvilOS) { execute($command); } else { # else we run in a timeout (system "$command") == 0 or warn "High Dynamic Range Picture: $command failed: $!"; } if (-f $outfile) { addProcessInfoToPicComment($command, $outfile); generateOneThumb($outfile); # insert $outfile in listbox addOneRow($picLB, $outfile, 1, $sellist[0]); showPic($outfile); log_it("ready! (Created $outfile from $selected pictures)"); } else { log_it("Error: Could not create $outfile!"); } return; } ############################################################## # addProcessInfoToPicComment ############################################################## sub addProcessInfoToPicComment { my $info = shift; my $dpic = shift; if ($conf{add_tool_info}{value}) { $info =~ s/\"//g; # remove double quotes $info = "Picture processed by Mapivi $version ($mapiviURL):\n".$info; addCommentToPic($info, $dpic, NO_TOUCH); } return; } ############################################################## # fuzzyBorder - add a fuzzy border to the selected pics ############################################################## sub fuzzyBorder { my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; return if (!fuzzyBorderDialog()); my $frame = "$trashdir/framePic.miff"; # we need MIFF or PNG because of the alpha channel removeFile($frame); return if (!checkExternProgs("fuzzyBorder", "convert", "composite")); log_it(langf("Adding fuzzy border to %d pictures",$selected)); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, lang("Adding fuzzy border")); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); progressWinUpdate($pw, lang("creating border")." ($i/$selected) ...", $i, $selected); next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); # get size of pic my ($x, $y) = getSize($dpic); my $min = min($x, $y); # shortest picture side my $bw = $config{FuzzyBorderWidth}; my $blur = $config{FuzzyBorderBlur}; if ($config{FuzzyBorderRelative}) { $bw = round($min*$bw/100); $blur = round($min*$blur/100); } # create an empty picture with a fuzzy frame my $command = "convert -size ${x}x${y} xc:none -fill ".$config{FuzzyBorderColor}." "; # windows needs " instead of ' $command .= "-draw \"rectangle 0,0 $x,$bw\" "; # upper $command .= "-draw \"rectangle 0,".($y-$bw)." $x,$y\" "; # lower $command .= "-draw \"rectangle 0,0 $bw,$y\" "; # left $command .= "-draw \"rectangle ".($x-$bw).",0 $x,$y\" "; # right border $command .= "-blur 0x".$blur." \"$frame\" "; if (not $EvilOS) { execute($command); } else { # else we run in a timeout (system "$command") == 0 or warn "fuzzy frame: $command failed: $!"; } unless (-f $frame) { warn "fuzzyBorder: could not create fuzzy border, skipping $dpic!\n"; next; } progressWinUpdate($pw, lang("adding border")." ($i/$selected) ...", $i, $selected); # compose the frame on top of the picture $command = "composite -quality ".$config{PicQuality}." -compose Atop \"$frame\" \"$dpic\" \"$dpic\" "; if (not $EvilOS) { execute($command); } else { # else we run in a timeout (system "$command") == 0 or warn "fuzzy frame: $command failed: $!"; } $i++; progressWinUpdate($pw, lang("adding process info")." ($i/$selected) ...", $i, $selected); addProcessInfoToPicComment($command, $dpic); updateOneRow($dpic, $picLB); deleteCachedPics($dpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one } progressWinEnd($pw); removeFile($frame); reselect($picLB, @sellist); log_it(langf("Added fuzzy border to %d of %d pictures.",$i,$selected)); generateThumbs(ASK, SHOW); return; } ############################################################## # fuzzyBorderDialog ############################################################## sub fuzzyBorderDialog { if (Exists($fuzzybw)) { $fuzzybw->deiconify; $fuzzybw->raise; return; } my $rc = 0; my $bwf; # border width frame with labeled scale my $brf; # blur radius frame with labeled scale # open window $fuzzybw = $top->Toplevel(); $fuzzybw->title(lang("Fuzzy border")); $fuzzybw->iconimage($mapiviicon) if $mapiviicon; $fuzzybw->Radiobutton(-text => lang("use absolute value (pixel)"), -variable => \$config{FuzzyBorderRelative}, -value => 0, -command => sub { $config{FuzzyBorderWidth} = 10; $config{FuzzyBorderBlur} = 10; $bwf->{scale}->configure(-from => 1, -to => 200, -resolution => 1); $brf->{scale}->configure(-from => 1, -to => 200, -resolution => 1);})->pack(-anchor => 'w'); $fuzzybw->Radiobutton(-text => lang("use relative value (%)"), -variable => \$config{FuzzyBorderRelative}, -value => 1, -command => sub { $config{FuzzyBorderWidth} = 1; $config{FuzzyBorderBlur} = 1; $bwf->{scale}->configure(-from => 0.1, -to => 50, -resolution => 0.1); $brf->{scale}->configure(-from => 0.1, -to => 50, -resolution => 0.1);})->pack(-anchor => 'w'); my $from = 1; my $to = 200; my $res = 1; if ($config{FuzzyBorderRelative}) { $from = 0.1; $to = 50; $res = 0.1; } $bwf = labeledScale($fuzzybw, 'top', 23, lang("Border width"), \$config{FuzzyBorderWidth}, $from, $to, $res); $brf = labeledScale($fuzzybw, 'top', 23, lang("Blur radius"), \$config{FuzzyBorderBlur}, $from, $to, $res); labeledEntryColor($fuzzybw,'top',23,lang("Border color"),'Set',\$config{FuzzyBorderColor}); my $qS = labeledScale($fuzzybw, 'top', 23, lang("Quality of picture (%)"), \$config{PicQuality}, 10, 100, 1); qualityBalloon($qS); buttonBackup($fuzzybw, 'top'); buttonComment($fuzzybw, 'top'); my $presetF = $fuzzybw->Frame()->pack(-anchor => 'w', -padx => 3, -pady => 3); $presetF->Label(-text => lang('Presets'))->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); foreach my $preset (0.5,1,2,5) { $presetF->Button(-text => $preset.'%', -command => sub { $config{FuzzyBorderRelative} = 1; $config{FuzzyBorderWidth} = $preset; $config{FuzzyBorderBlur} = $preset; })->pack(-side => 'left', -padx => 3, -pady => 3); } my $ButF = $fuzzybw->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $fuzzybw->withdraw(); $fuzzybw->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $fuzzybw->withdraw(); $fuzzybw->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($fuzzybw, $Xbut); $fuzzybw->Popup; $fuzzybw->waitWindow; return $rc; } ############################################################## # losslessBorder - add a frame to the selected pics without # recompressing the picture ############################################################## sub losslessBorder { my $mode = shift; # PIXEL, ASPECT_RATIO, RELATIVE (%) # check if jpegtran supports lossless dropping my $usage = `jpegtran -? 2>&1`; if ($usage !~ m/.*-drop.*/) { $top->messageBox(-icon => 'warning', -message => "Sorry, but your version of jpegtran does not support lossless dropping!\nTry to get jpegtran with the lossless drop patch from http://jpegclub.org.", -title => "Wrong jpegtran version", -type => 'OK'); return; } return if (!checkExternProgs("losslessBorder", "convert")); my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; my $aspectdelta = 1 + ($config{AspectSloppyFactor} / 100); # delta factor for aspect ratio my $info = ''; my $bix = 0; # inner width X my $biy = 0; # inner width Y my $bwx = 0; # complete width X my $bwy = 0; # complete width Y if ($mode == PIXEL) { my ($w, $h) = getSize($sellist[0]); # get size of first picture return if (!losslessBorderDialogPixel($w, $h)); $bix = $config{llBorderWidthIX}; # inner width X $biy = $config{llBorderWidthIY}; # inner width Y $bwx = $config{llBorderWidthX}; # complete width X $bwy = $config{llBorderWidthY}; # complete width Y # no frame width-> nothing to do. return if ($bwx == 0 and $bwy == 0); } elsif ($mode == ASPECT_RATIO) { return if (!losslessBorderDialogAspect()); } elsif ($mode == RELATIVE) { return if (!losslessBorderDialogRelative()); } else { warn "Sorry mode $mode is not supported!"; return; } my $frame = "$trashdir/framePic.jpg"; if (-f $frame) { warn "file $frame exists! Please delete it first!"; return; } log_it("adding lossless border to $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $pw = progressWinInit($top, "Adding lossless border"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); $i++; progressWinUpdate($pw, "adding border ($i/$selected) ...", $i, $selected); if ($mode == ASPECT_RATIO) { # get size of dpic my ($w, $h) = getSize($dpic); my $n = $config{AspectBorderN}; my $m = $config{AspectBorderM}; # skip pictures which have (nearly) the right aspect ratio (either n/m or m/n) # and be a little bit sloppy about this (aspectdelta) if (((($w/$h) <= ($n/$m)*$aspectdelta) and (($w/$h) >= ($n/$m)/$aspectdelta)) or ((($w/$h) <= ($m/$n)*$aspectdelta) and (($w/$h) >= ($m/$n)/$aspectdelta))) { $info .= "$dpic has correct aspect ratio - skipping\n"; next; } if ($w > $h) { # landscape picture if ($w > $h*$n/$m) { # panorama picture (too wide) $bwx = 0; $bwy = int(($w*$m/$n -$h)/2); } elsif ($w < $h*$n/$m) { # too narrow $bwx = int(($h*$n/$m -$w)/2); $bwy = 0; } else { # already right aspect ratio next; } } else { # portrait and square picture if ($w > $h*$m/$n) { # panorama picture (too small) $bwx = 0; $bwy = int(($w*$n/$m -$h)/2); } elsif ($w < $h*$m/$n){ # too tall $bwx = int(($h*$m/$n -$w)/2); $bwy = 0; } else { # already right aspect ratio $info .= "$dpic has correct aspect ratio - skipping\n"; next; } } # we need 16 pixel steps for the complete border width $bwx = sprintf("%.0f", $bwx / 16) * 16; # int() does not round! $bwy = sprintf("%.0f", $bwy / 16) * 16; } # add a border relative to the picture size if ($mode == RELATIVE) { # get size of dpic my ($w, $h) = getSize($dpic); # we need 16 pixel steps for the complete border width $bwx = sprintf("%.0f",($config{RelativeBorderX} * $w / (100 * 16))) * 16; # int() does not round! $bwy = sprintf("%.0f",($config{RelativeBorderY} * $h / (100 * 16))) * 16; if (($bwx == 0) and ($bwy == 0)) { $info .= "$dpic border would be 0 pixel - skipping\n"; next; } $bix = sprintf("%.0f",($config{RelativeBorderIX} * $w / 100)); $biy = sprintf("%.0f",($config{RelativeBorderIY} * $h / 100)); # correction: add at least one pixel #$bwx = 1 if ($config{RelativeBorderX} > 0 and $bwx == 0); #$bwy = 1 if ($config{RelativeBorderY} > 0 and $bwy == 0); $bix = 1 if ($config{RelativeBorderIX} > 0 and ($bix == 0)); $biy = 1 if ($config{RelativeBorderIY} > 0 and ($biy == 0)); if ($config{RelativeBorderEqual}) { $bix = $biy if ($biy > $bix); $biy = $bix; $bwx = $bwy if ($bwy > $bwx); $bwy = $bwx; } } next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); # approach 1: # create an empty picture with a frame # this is the better approach as a new background is generated, but something with the color resolution(?) is wrong # because when the other picture is dropped on this one jpegtran changes the whole picture to grayscale #my $command = "convert -size ${cx}x${cy} xc:\"".$config{llBorderColor}."\" -fill \"".$config{llBorderColorI}."\" "; #$command .= "-draw \'rectangle $r1,$r1 $rx2,$ry2\' -quality 95 \"$frame\" "; # approach 2: # add a lossy frame to the original picture # not the fastes way, but it works my $box = $bwx - $bix; # outer border width my $boy = $bwy - $biy; # outer border width #print "losslessBorder: bwx $bwx bwy $bwy box $box boy $boy bix $bix biy $biy\n"; my $command = "convert "; $command .= "-bordercolor \"".$config{llBorderColorI}."\" -border ${bix}x${biy} " if (($bix > 0) or ($biy > 0)); $command .= "-bordercolor \"".$config{llBorderColor}."\" -border ${box}x${boy} -quality 95 \"$dpic\" \"$frame\" "; execute($command); unless (-f $frame) { $info .= "$dpic: could not create lossless border - skipping\n"; next; } progressWinUpdate($pw, "adding border ($i/$selected) ...", $i, $selected); # drop the picture lossless! on top of the frame # no recompression of the picture! $command = "jpegtran -copy all -drop +${bwx}+${bwy} \"$dpic\" -outfile \"$dpic\" \"$frame\" "; execute($command); addProcessInfoToPicComment($command, $dpic); updateOneRow($dpic, $picLB); deleteCachedPics($dpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one } progressWinEnd($pw); removeFile($frame); reselect($picLB, @sellist); log_it("ready! (added lossless border to $i of $selected)"); if ($info ne '') { showText('Add Border Information', $info, NO_WAIT); } generateThumbs(ASK, SHOW); return; } ############################################################## # losslessBorderDialogPixel ############################################################## sub losslessBorderDialogPixel { my $w = shift; # pixel size of first selected picture for preview my $h = shift; if (Exists($ll_b_w)) { $ll_b_w->deiconify; $ll_b_w->raise; return; } my $min = min($w, $h); # shortest picture side # copy the config values to an hash for easy handling my %border; $border{out}{x} = $config{llBorderWidthX}; $border{out}{y} = $config{llBorderWidthY}; $border{out}{c} = $config{llBorderColor}; $border{in}{x} = $config{llBorderWidthIX}; $border{in}{y} = $config{llBorderWidthIY}; $border{in}{c} = $config{llBorderColorI}; my $rc = 0; my $preview_size = 200; my $c; # canvas # open window $ll_b_w = $top->Toplevel(); $ll_b_w->title(lang("Add lossless border")); $ll_b_w->iconimage($mapiviicon) if $mapiviicon; my $fb = $ll_b_w->Frame(-relief => 'groove')->pack(-fill => 'x', -padx => 6, -pady => 6); $fb->Label(-text => "Border size and color")->pack(-fill => 'x', -padx => 3, -pady => 3); $balloon->attach($fb, -msg => "This is the overall width of the border in x- and y-direction.\nAs JPEGs are organized in 8 or 16 pixel blocks only 16 pixel-steps are allowed.\nUse the add-border function if you need other sizes."); labeledScale($fb, 'top', 35, "Overall border width x-direction", \$border{out}{x}, 0, 1000, 16, sub {$border{in}{x} = $border{out}{x} if ($border{in}{x} > $border{out}{x});draw_preview($c, $preview_size, $w, $h, \%border);}); labeledScale($fb, 'top', 35, "Overall border width y-direction", \$border{out}{y}, 0, 1000, 16, sub {$border{in}{y} = $border{out}{y} if ($border{in}{y} > $border{out}{y});draw_preview($c, $preview_size, $w, $h, \%border);}); my $bordercolor = labeledEntryColor($fb,'top',35,"Border color",'Set',\$border{out}{c}); my $fbi = $ll_b_w->Frame(-relief => 'groove')->pack(-fill => 'x', -padx => 6, -pady => 6); $fbi->Label(-text => "Inner border (optional) size and color")->pack(-fill => 'x', -padx => 3, -pady => 3); $balloon->attach($fbi, -msg => "This is the width of the inner border in x- and y-direction.\nSet to 0 for no inner border."); labeledScale($fbi, 'top', 35, "Border width x-direction", \$border{in}{x}, 0, 1000, 1, sub { $border{in}{x} = $border{out}{x} if ($border{in}{x} > $border{out}{x}); draw_preview($c, $preview_size, $w, $h, \%border); }); labeledScale($fbi, 'top', 35, "Border width y-direction", \$border{in}{y}, 0, 1000, 1, sub {$border{in}{y} = $border{out}{y} if ($border{in}{y} > $border{out}{y}); draw_preview($c, $preview_size, $w, $h, \%border);}); my $ibordercolor = labeledEntryColor($fbi,'top',35,"Border color",'Set',\$border{in}{c}); # lower frame with left and right sub frame my $lf = $ll_b_w->Frame()->pack(-fill => 'x', -padx => 0, -pady => 0); my $lf_left = $lf->Frame()->pack(-side => 'left', -fill => 'x', -padx => 0, -pady => 0); my $lf_right = $lf->Frame()->pack(-side => 'right', -fill => 'x', -padx => 0, -pady => 0); buttonBackup($lf_left, 'top'); buttonComment($lf_left, 'top'); my $rel_border = sprintf("%.0f",($min*5/100)); # preset border width in % # todo: replace preset buttons by a loop canvas buttons and generate icon using draw_preview() my $preF = $lf_left->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); $preF->Label(-text => 'Presets ')->pack(-side => 'left'); $preF->Button(-image => $mapivi_icons{'Frame-bw'}, #-text => '2 White-Black', -command => sub { $border{out}{x} = $rel_border; $border{out}{y} = $rel_border; $border{out}{c} = 'white'; $border{in}{x} = 1; $border{in}{y} = 1; $border{in}{c} = 'black'; $bordercolor->{button}->configure(-bg => $border{out}{c}); $ibordercolor->{button}->configure(-bg => $border{in}{c}); draw_preview($c, $preview_size, $w, $h, \%border); })->pack(-side => 'left', -padx => 3); $preF->Button(-image => $mapivi_icons{'Frame-wb'}, #-text => '1 Black-White', -command => sub { $border{out}{x} = $rel_border; $border{out}{y} = $rel_border; $border{out}{c} = 'black'; $border{in}{x} = 1; $border{in}{y} = 1; $border{in}{c} = 'white'; $bordercolor->{button}->configure(-bg => $border{out}{c}); $ibordercolor->{button}->configure(-bg => $border{in}{c}); draw_preview($c, $preview_size, $w, $h, \%border); })->pack(-side => 'left', -padx => 3); $preF->Button(-image => $mapivi_icons{'Frame-wbp'}, #-text => '3 Pano White-Black', -command => sub { $border{out}{x} = 0; $border{out}{y} = $rel_border; $border{out}{c} = 'black'; $border{in}{x} = 0; $border{in}{y} = 1; $border{in}{c} = 'white'; $bordercolor->{button}->configure(-bg => $border{out}{c}); $ibordercolor->{button}->configure(-bg => $border{in}{c}); draw_preview($c, $preview_size, $w, $h, \%border); })->pack(-side => 'left', -padx => 3); $preF->Button(-image => $mapivi_icons{'Frame-bwp'}, #-text => '4 Pano Black-White', -command => sub { $border{out}{x} = 0; $border{out}{y} = $rel_border; $border{out}{c} = 'white'; $border{in}{x} = 0; $border{in}{y} = 1; $border{in}{c} = 'black'; $bordercolor->{button}->configure(-bg => $border{out}{c}); $ibordercolor->{button}->configure(-bg => $border{in}{c}); draw_preview($c, $preview_size, $w, $h, \%border); })->pack(-side => 'left', -padx => 3); # preview $c = $lf_right->Canvas(-width => $preview_size, -height => $preview_size, -borderwidth => 0, -highlightthickness => 0, -relief => 'flat')->pack(); # draw preview draw_preview($c, $preview_size, $w, $h, \%border); my $font = $top->Font(-family => $config{FontFamily}, -size => 10, -weight => 'bold'); $c->createText(100,100, -text => 'Picture', -fill => 'black', -font => $font, -anchor => 'c'); my $ButF = $ll_b_w->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $config{llBorderWidthX} = $border{out}{x}; $config{llBorderWidthY} = $border{out}{y}; $config{llBorderColor} = $border{out}{c}; $config{llBorderWidthIX} = $border{in}{x}; $config{llBorderWidthIY} = $border{in}{y}; $config{llBorderColorI} = $border{in}{c}; $ll_b_w->withdraw(); $ll_b_w->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Preview'), -command => sub { border_preview($w, $h, \%border); })->pack(-side => 'left', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Help'), -command => sub { showText('Help for lossless border', "This function can be used to add a border to a JPEG without losing quality due to recompressing.\n\nHow it works:\nIt will first create a background with the border and then drop the original picture on top of it. The picture is not recompressed and thus every pixel stays exactly the same. The tool jpegtran with the lossless drop patch is used for this task. See http://jpegclub.org.\n\nYou can verify this by adding a border to a picture (don't forget to save the original picture by selecting \"Create Backup\") then you crop the border away and compare the original and the processed picture e.g. using the Mapivi function \"build difference picture\". If the resulting difference picture is complety black no pixel was changed.\nYou may check this again with a lossy border to see the differences.", NO_WAIT); })->pack(-side => 'left', -expand => 0, -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $ll_b_w->withdraw(); $ll_b_w->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($ll_b_w, $Xbut); $ll_b_w->Popup; $ll_b_w->waitWindow; return $rc; } ############################################################## # border_preview - quick preview in correct proportions, but # without rescaling the real picture (would # take too much time). ############################################################## sub border_preview { my $w = shift; # picture size my $h = shift; my $b = shift; # border hash ref #my $c; # Canvas unless (Exists($bpw)) { # open window $bpw = $top->Toplevel(); $bpw->title('Border Preview'); $bpw->iconimage($mapiviicon) if $mapiviicon; my $fa = $bpw->Frame(-relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $bpw->{c} = $fa->Canvas(-width => 100, -height => 100, -background => 'gray', -relief => 'sunken', )->pack(-padx => 3, -pady => 3); my $Xbut = $bpw->Button(-text => lang('Close'), -command => sub { $bpw->withdraw(); $bpw->destroy(); })->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); } $bpw->deiconify; $bpw->raise; my $per = 0.8; # preview canvas should be 80% of the min screen size my $preview_size = int($per * $top->screenwidth); $preview_size = int($per * $top->screenheight) if ($top->screenheight < $top->screenwidth); my ($scale, $w_all, $h_all) = calc_preview_scale($preview_size, $w, $h, $$b{out}{x}, $$b{out}{y}); # resize canvas $bpw->{c}->configure(-width => sprintf("%.0f",($w_all*$scale)), -height => sprintf("%.0f",($h_all*$scale)),); # draw preview draw_preview($bpw->{c}, $preview_size, $w, $h, $b); return; } ############################################################## ############################################################## sub calc_preview_scale { my ($preview_size, $w, $h, $bx, $by) = @_; my $w_all = $w + 2 * $bx; # complete width my $h_all = $h + 2 * $by; # complete height my $max_side = $w_all; $max_side = $h_all if ($h_all > $w_all); # longest side if ($max_side == 0) { warn "border_preview: Error max_side = $max_side"; return; } my $scale = $preview_size / $max_side; $scale = 1 if ($scale > 1); # we don't want to magnify small pictures return ($scale, $w_all, $h_all); } ############################################################## ############################################################## sub draw_preview { my $c = shift; # canvas widget my $preview_size = shift; my $w = shift; # picture size my $h = shift; my $b = shift; # border hash ref my ($scale, $w_all, $h_all) = calc_preview_scale($preview_size, $w, $h, $$b{out}{x}, $$b{out}{y}); # clear canvas $c->delete('all'); # outer border $c->createRectangle( 0, 0, sprintf("%.0f",($w_all*$scale)), sprintf("%.0f",($h_all*$scale)), -fill => $$b{out}{c}, -width => 0, ); # calc picture coordinates my $px1 = sprintf("%.0f",($$b{out}{x}*$scale)); my $py1 = sprintf("%.0f",($$b{out}{y}*$scale)); my $px2 = sprintf("%.0f",(($$b{out}{x}+$w)*$scale)); my $py2 = sprintf("%.0f",(($$b{out}{y}+$h)*$scale)); # inner border if (($$b{in}{x} > 0) or ($$b{in}{y} > 0)) { my $ix1 = sprintf("%.0f",(($$b{out}{x}-$$b{in}{x})*$scale)); my $iy1 = sprintf("%.0f",(($$b{out}{y}-$$b{in}{y})*$scale)); my $ix2 = sprintf("%.0f",(($$b{out}{x}+$w+$$b{in}{x})*$scale)); my $iy2 = sprintf("%.0f",(($$b{out}{y}+$h+$$b{in}{y})*$scale)); # adjust picture coordinates to show at least a one pixel wide inner border, # even if scaling should hide it $px1++ if (($px1 == $ix1) and ($$b{in}{x} > 0)); $px2-- if (($px2 == $ix2) and ($$b{in}{x} > 0)); $py1++ if (($py1 == $iy1) and ($$b{in}{y} > 0)); $py2-- if (($py2 == $iy2) and ($$b{in}{y} > 0)); # draw inner frame as block on top of outer frame $c->createRectangle( $ix1, $iy1, $ix2, $iy2, -fill => $$b{in}{c}, -width => 0); } # draw picture as gray block on top of frame blocks $c->createRectangle($px1 , $py1, $px2, $py2, -fill => 'gray50', -width => 0); # picture text my $font_size = 50; my $font = $top->Font(-family => $config{FontFamily}, -size => $font_size, -weight => 'bold'); my $id = $c->createText(int(($$b{out}{x}+$w/2)*$scale), int(($$b{out}{y}+$h/2)*$scale), -text => 'Picture', -fill => 'black', -font => $font, -anchor => 'c'); fit_text($c, $id, $config{FontFamily}, $font_size, 'bold', sprintf("%.0f",($w*$scale))); return; } ############################################################## # shrink font size until text fits into a certain width, delete text if it doesn't fit with smallest font size ############################################################## sub fit_text { my $c = shift; # canvas widget my $id = shift; # canvas id of text my $font_fam = shift; my $font_size = shift; # start size my $weight = shift; my $w = shift; # max width to fit into # get coordintes of text box to check if the font size fits into the circle my ($tx1, $ty1, $tx2, $ty2) = $c->bbox($id); while ($tx2 - $tx1 >= $w) { # decrease font size until minimum of 8pt $font_size -= 2; if ($font_size < 8) { $c->delete($id); # then delete text last; } my $font = $c->Font(-family => $font_fam, -size => $font_size, -weight => $weight); $c->itemconfigure($id, -font => $font); # measure new text box size ($tx1, $ty1, $tx2, $ty2) = $c->bbox($id); } return; } ############################################################## # losslessBorderDialogRelative ############################################################## sub losslessBorderDialogRelative { if (Exists($ll_r_w)) { $ll_r_w->deiconify; $ll_r_w->raise; return; } my $rc = 0; # open window $ll_r_w = $top->Toplevel(); $ll_r_w->title("Add relative border (lossless)"); $ll_r_w->iconimage($mapiviicon) if $mapiviicon; my $fb = $ll_r_w->Frame(-relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $balloon->attach($fb, -msg => "This is the overall width of the border in x- and y-direction.\nAs JPEGs are organized in 8 or 16 pixel blocks only 16 pixel-steps are allowed.\nUse the add-border function if you need other sizes."); my $fbi = $ll_r_w->Frame(-relief => 'groove')->pack(-fill => 'x', -padx => 3, -pady => 3); $balloon->attach($fbi, -msg => "This is the width of the inner border in x- and y-direction.\nSet to 0 for no inner border."); labeledScale($fb, 'top', 37, "Complete border width x-direction (%)", \$config{RelativeBorderX}, 0, 100, 0.1); labeledScale($fb, 'top', 37, "Complete border width y-direction (%)", \$config{RelativeBorderY}, 0, 100, 0.1); labeledEntryColor($fb,'top',37,"Border color",'Set',\$config{llBorderColor}); labeledScale($fbi, 'top', 37, "Inner border width x-direction (%)", \$config{RelativeBorderIX}, 0, 100, 0.01); labeledScale($fbi, 'top', 37, "Inner border width y-direction (%)", \$config{RelativeBorderIY}, 0, 100, 0.01); labeledEntryColor($fbi,'top',37,"Inner border color",'Set',\$config{llBorderColorI}); $ll_r_w->Checkbutton(-text => 'Symmetric border (biggest wins)', -variable => \$config{RelativeBorderEqual})->pack(-anchor => 'w', -padx => 5, -pady => 5); buttonBackup($ll_r_w, 'top'); buttonComment($ll_r_w, 'top'); my $ButF = $ll_r_w->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { # some checks if (($config{RelativeBorderIX} > $config{RelativeBorderX}) or ($config{RelativeBorderIY} > $config{RelativeBorderY})) { $ll_r_w->messageBox(-icon => 'warning', -message => 'The inner border must be smaller than the complete border.', -title => 'Lossess border - Error', -type => 'OK'); return; } $ll_r_w->withdraw(); $ll_r_w->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Help'), -command => sub { showText('Help for relative border (lossless)', "This function can be used to add a border to a JPEG without losing quality due to recompressing.\nThe actual border width in pixel will be calculated depending on the picture size. As JPEGs are organized in 8 or 16 pixel blocks the complete border width will be in 16 pixel-steps.\nThe inner border may be have any width, set it to 0 to have just one frame. If the inner border is bigger than 0, then it will be at least one pixel.\n\nHow it works:\nIt will first create a background with the border and then drop the original picture on top of it. The picture is not recompressed and thus every pixel stays exactly the same. The tool jpegtran with the lossless drop patch is used for this task. See http://jpegclub.org.\n\nYou can check this by adding a border to a picture (don't forget to save the original picture by selecting \"Create Backup\") then you crop the border away and compare the original and the processed picture using the Mapivi function \"build difference picture\". If the resulting difference picture is complety black no pixel was changed.\nYou may check this again with a lossy border to see the differences.", NO_WAIT); })->pack(-side => 'left', -expand => 0, -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $ll_r_w->withdraw(); $ll_r_w->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($ll_r_w, $Xbut); $ll_r_w->Popup; $ll_r_w->waitWindow; return $rc; } ############################################################## # losslessBorderDialogAspect ############################################################## sub losslessBorderDialogAspect { if (Exists($ll_a_w)) { $ll_a_w->deiconify; $ll_a_w->raise; return; } my $rc = 0; # open window $ll_a_w = $top->Toplevel(); $ll_a_w->title("Add border to aspect ratio (lossless)"); $ll_a_w->iconimage($mapiviicon) if $mapiviicon; my $oF = $ll_a_w->Frame(-relief => 'groove')->pack(-expand => 1, -fill => 'x', -padx => 3, -pady => 3); $oF->Label(-text => 'Aspect ratio ')->pack(-side => 'left', -padx => 3, -pady => 3); $oF->Entry(-textvariable => \$config{AspectBorderN}, -width => 5, -justify => 'right')->pack(-side => 'left', -padx => 3, -pady => 3); $oF->Label(-text => ':')->pack(-side => 'left', -padx => 3, -pady => 3); $oF->Entry(-textvariable => \$config{AspectBorderM}, -width => 5)->pack(-side => 'left', -padx => 3, -pady => 3); #labeledEntry($oF,'left',17,': Aspect ratio M',\$config{AspectBorderM}); my $aF = $ll_a_w->Frame(-relief => 'groove')->pack(-padx => 3, -pady => 3); $aF->Label(-text => 'Presets')->pack(); $aF->Button(-text => "3:2 (e.g. 10x15)", -anchor => 'w', -command => sub { $config{AspectBorderN} = 3; $config{AspectBorderM} = 2; } )->pack(-anchor => 'w', -fill => 'x', -padx => 3, -pady => 3); $aF->Button(-text => "4:3", -anchor => 'w', -command => sub { $config{AspectBorderN} = 4; $config{AspectBorderM} = 3; } )->pack(-anchor => 'w', -fill => 'x', -padx => 3, -pady => 3); $aF->Button(-text => "5:4 (PAL)", -anchor => 'w', -command => sub { $config{AspectBorderN} = 5; $config{AspectBorderM} = 4; } )->pack(-anchor => 'w', -fill => 'x', -padx => 3, -pady => 3); $aF->Button(-text => "7:5 (e.g. 13x18)", -anchor => 'w', -command => sub { $config{AspectBorderN} = 7; $config{AspectBorderM} = 5; } )->pack(-anchor => 'w', -fill => 'x', -padx => 3, -pady => 3); $aF->Button(-text => "16:9", -anchor => 'w', -command => sub { $config{AspectBorderN} = 16; $config{AspectBorderM} = 9; } )->pack(-anchor => 'w', -fill => 'x', -padx => 3, -pady => 3); $aF->Button(-text => "1:1", -anchor => 'w', -command => sub { $config{AspectBorderN} = 1; $config{AspectBorderM} = 1; } )->pack(-anchor => 'w', -fill => 'x', -padx => 3, -pady => 3); labeledEntryColor($ll_a_w,'top',12,'Border color','Set',\$config{llBorderColor}); buttonBackup($ll_a_w, 'top'); buttonComment($ll_a_w, 'top'); my $ButF = $ll_a_w->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { # some checks if (($config{AspectBorderM} !~ m|^\d+$|) or # must be an integer ($config{AspectBorderN} !~ m|^\d+$|)) { $ll_a_w->messageBox(-icon => 'warning', -message => 'Aspect ratio must be a natural number', -title => 'Aspect ratio border - Error', -type => 'OK'); return; } if (($config{AspectBorderM} <= 0) or ($config{AspectBorderN} <= 0)) { $ll_a_w->messageBox(-icon => 'warning', -message => 'Aspect ratio must be positive and bigger than 0', -title => 'Aspect ratio border - Error', -type => 'OK'); return; } $ll_a_w->withdraw(); $ll_a_w->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Help'), -command => sub { showText('Help for lossless aspect ratio border', "This function can be used to add a border to a JPEG to fit the selected aspect ratio without losing quality due to recompressing.\nAs JPEGs are organized in 8 or 16 pixel blocks the complete border width will be in 16 pixel-steps. Thus the resulting picture will not always match the selected aspect ratio.\n\nHow it works:\nIt will first create a background with the border and then drop the original picture on top of it. The picture is not recompressed and thus every pixel stays exactly the same. The tool jpegtran with the lossless drop patch is used for this task. See http://jpegclub.org.\n\nYou can check this by adding a border to a picture (don't forget to save the original picture by selecting \"Create Backup\") then you crop the border away and compare the original and the processed picture using the Mapivi function \"build difference picture\". If the resulting difference picture is complety black no pixel was changed.\nYou may check this again with a lossy border to see the differences.", NO_WAIT); })->pack(-side => 'left', -expand => 0, -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $ll_a_w->withdraw(); $ll_a_w->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($ll_a_w, $Xbut); $ll_a_w->Popup; $ll_a_w->waitWindow; return $rc; } ############################################################## # losslessWatermark - drop a picture onto the selected pics # without recompressing the whole picture ############################################################## sub losslessWatermark { # check if jpegtran supports lossless dropping my $usage = `jpegtran -? 2>&1`; if ($usage !~ m/.*-drop.*/) { $top->messageBox(-icon => 'warning', -message => "Sorry, but your version of jpegtran does not support lossless dropping!\nTry to get jpegtran with the lossless drop patch from http://jpegclub.org.", -title => "Wrong jpegtran version", -type => 'OK'); return; } # todo: # 1. Select a part of the picture with e.g. the crop dialog # 2. Select a font and size and enter a text # 3. crop the selected part out of the picture # 4. add the text to the crop: # convert crop.jpg -pointsize 120 -fill white -gravity center # -annotate 0 'Mapivi' -quality 95 crop2.jpg # 5. lossless drop the crop at the same position # benefit: as color sampling is from original picture there should # be no problem with lossless drop my @sellist = $picLB->info('selection'); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); my $selected = @sellist; return if (!losslessWatermarkDialog($sellist[0])); my $wmx = $config{llWatermarkX}; # X position my $wmy = $config{llWatermarkY}; # Y position my $file = $config{llWatermarkFile}; # the picture to add # get size of watermark pic my ($wmw, $wmh) = getSize($file); log_it("adding lossless watermark to $selected pictures"); # check if some files are links return if (!checkLinks($picLB, @sellist)); my $error = ''; my $pw = progressWinInit($top, "Adding lossless watermark"); my $i = 0; foreach my $dpic (@sellist) { last if progressWinCheck($pw); progressWinUpdate($pw, "adding watermark ($i/$selected) ...", $i, $selected); next if (!checkWriteable($dpic)); next if (!makeBackup($dpic)); # todo: either just drop a existing pic or # 1. crop a part of the picture -> cropPic($dpic,$w,$h,$x,$y,95); # 2. write a text on this crop -> convert crop.jpg -pointsize 50 -gravity south -stroke '#000C' -strokewidth 2 -annotate 0 'Martin' -stroke none -fill white -annotate 0 'Martin' crop-text.jpg # 3. drop it back on the same position # other idea/option: scale the picture to drop to a percentage (e.g. max 5%) of the main picture # to avoid big logos on small pictures # drop the watermark lossless! on top of the picture # no recompression of the picture! my ($ok, $drop_error) = drop_pic($file, $dpic, $dpic, $wmx, $wmy); $error .= $drop_error." ($dpic)\n" if ($drop_error); $i++; progressWinUpdate($pw, "adding watermark ($i/$selected) ...", $i, $selected); if ($ok) { updateOneRow($dpic, $picLB); deleteCachedPics($dpic); showPic($dpic) if ($dpic eq $actpic); # redisplay the picture if it is the actual one } } progressWinEnd($pw); if ($error ne '') { $error = "Some pictures caused errors:\n\n".$error; showText('Watermark errors', $error, NO_WAIT); } reselect($picLB, @sellist); log_it("ready! (added lossless watermark to $i of $selected)"); generateThumbs(ASK, SHOW); return; } ############################################################## # drop a pic on top of another lossless! no recompression of the picture! ############################################################## sub drop_pic { my $top_pic = shift; # picture to drop my $bottom_pic = shift; # picture to drop onto my $out_pic = shift; # final picture my $x = shift; # drop position x my $y = shift; # drop position y # check arguments return (0, 'Drop picture $top_pic does not exists') if (not -f $top_pic); return (0, 'Drop picture $top_pic is not in JPEG format') if (not is_a_JPEG($top_pic)); return (0, 'Picture does not exists') if (not -f $bottom_pic); return (0, 'Picture is not in JPEG format') if (not is_a_JPEG($bottom_pic)); my ($tw, $th) = getSize($top_pic); my ($bw, $bh) = getSize($bottom_pic); return (0, 'Drop picture $top_pic must be smaller than picture') if (($tw > $bw) or ($th > $bh)); return (0, 'Drop position must be a positive value') if (($x < 0) or ($y < 0)); return (0, 'Drop position out of picture (over right border)') if (($x + $tw) > $bw); return (0, 'Drop position out of picture (over bottom border)') if (($y + $th) > $bh); my $position = ''; if ($x >= 0) { $position = "+"; } $position .= $x; if ($y >= 0) { $position .= "+"; } $position .= $y; # todo: still unclear what the -trim and -perfect switch does #my $command = "jpegtran -copy all -trim -perfect -drop $position \"$top_pic\" -outfile \"$out_pic\" \"$bottom_pic\" "; my $command = "jpegtran -copy all -drop $position \"$top_pic\" -outfile \"$out_pic\" \"$bottom_pic\" "; print "com = $command\n"; execute($command); addProcessInfoToPicComment($command, $out_pic); if (-f $out_pic) { return (1, ''); } return (0, 'unknown error'); } ############################################################## # losslessWatermarkDialog ############################################################## sub losslessWatermarkDialog { my $dpic = shift; # used for preview if (Exists($ll_w_w)) { $ll_w_w->deiconify; $ll_w_w->raise; return; } my $rc = 0; # forward references my $preview_button; my $x_scale; my $y_scale; my $c; # canvas widget # open window $ll_w_w = $top->Toplevel(); $ll_w_w->title("Drop picture (lossless) - ".basename($dpic)); $ll_w_w->iconimage($mapiviicon) if $mapiviicon; # horizontal buttons and slider my $horizf = $ll_w_w->Frame()->pack(-fill => 'x', -padx => 3, -pady => 6); $preview_button = $horizf->Button(-image => $mapivi_icons{'Update'}, -command => sub { # some checks return if (not check_drop_picture($ll_w_w, $config{llWatermarkFile})); $ll_w_w->Busy; my ($dw, $dh) = getSize($config{llWatermarkFile}); my ($w, $h) = getSize($dpic); $x_scale->configure(-to => $w-$dw); $y_scale->configure(-to => $h-$dh); $c->delete('all'); my $out_file = "$trashdir/dropXYZ554.jpg"; # will be overwritten removeFile($out_file); my ($ok, $error) = drop_pic($config{llWatermarkFile}, $dpic, $out_file, $config{llWatermarkX}, $config{llWatermarkY}); if ($ok) { $ll_w_w->{preview} = $ll_w_w->Photo(-file => $out_file); if ($ll_w_w->{preview}) { # insert pic $c->createImage(0,0, -image => $ll_w_w->{preview}, -anchor => 'nw'); my ($w, $h) = getSize($out_file); $c->configure(-scrollregion => [0, 0, $w, $h]); } else { print "Could not create photo object from $out_file\n"; } } else { $ll_w_w->Dialog(-title => 'Error dropping picture', -text => "Could not drop picture using the external tool jpegtran.\n$error\nSee also: help button in drop dialog.", -buttons => ['Ok'])->Show(); print "Drop error: $error\n"; } $ll_w_w->Unbusy; })->pack(-side => 'left', -padx => 6); $balloon->attach($preview_button, -msg => "Update preview"); my $hlb = $horizf->Button(-image => $mapivi_icons{'GoFirst'}, -command => sub {$config{llWatermarkX} = 0; $preview_button->Invoke;})->pack(-side => 'left'); $balloon->attach($hlb, -msg => "Align with left border"); my $hcb = $horizf->Button(-image => $mapivi_icons{'MediaStop'}, -command => sub { if (-f $config{llWatermarkFile}) { my ($dw, $dh) = getSize($config{llWatermarkFile}); my ($w, $h) = getSize($dpic); $config{llWatermarkX} = int($w/2-$dw/2); $preview_button->Invoke; }})->pack(-side => 'left'); $balloon->attach($hcb, -msg => "Center horizontal"); my $hrb = $horizf->Button(-image => $mapivi_icons{'GoLast'}, -command => sub { if (-f $config{llWatermarkFile}) { my ($dw, $dh) = getSize($config{llWatermarkFile}); my ($w, $h) = getSize($dpic); $config{llWatermarkX} = $w-$dw; $preview_button->Invoke; }})->pack(-side => 'left'); $balloon->attach($hrb, -msg => "Align with right border"); $horizf->Label(-textvariable => \$config{llWatermarkX}, -width => 4)->pack(-side => 'left'); $x_scale = $horizf->Scale(-variable => \$config{llWatermarkX}, -from => 0, -to => 100, -resolution => 1, -sliderlength => 10, -orient => 'horizontal', -showvalue => 0, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 3,-pady => 3); $balloon->attach($x_scale, -msg => "x position from the right border in pixel"); # lower frame with bottons and canvas my $canvasf = $ll_w_w->Frame()->pack(-expand => 1, -fill => 'both', -padx => 0); # vertical frame with buttons and vertical slider my $vertif = $canvasf->Frame()->pack(-expand => 0, -fill => 'y', -side => 'left', -padx => 3); my $vtb = $vertif->Button(-image => $mapivi_icons{'GoTop'}, -command => sub {$config{llWatermarkY} = 0; $preview_button->Invoke;})->pack; $balloon->attach($vtb, -msg => "Align with top border"); my $vcb = $vertif->Button(-image => $mapivi_icons{'MediaStop'}, -command => sub { if (-f $config{llWatermarkFile}) { my ($dw, $dh) = getSize($config{llWatermarkFile}); my ($w, $h) = getSize($dpic); $config{llWatermarkY} = int($h/2-$dh/2); $preview_button->Invoke; }})->pack; $balloon->attach($vcb, -msg => "Center vertical"); my $hbb = $vertif->Button(-image => $mapivi_icons{'GoBottom'}, -command => sub { if (-f $config{llWatermarkFile}) { my ($dw, $dh) = getSize($config{llWatermarkFile}); my ($w, $h) = getSize($dpic); $config{llWatermarkY} = $h-$dh; $preview_button->Invoke; }})->pack; $balloon->attach($hbb, -msg => "Align with bottom border"); $vertif->Label(-textvariable => \$config{llWatermarkY}, -width => 4)->pack; $y_scale = $vertif->Scale(-variable => \$config{llWatermarkY}, -from => 0, -to => 100, -resolution => 1, -sliderlength => 10, -orient => 'vertical', -showvalue => 0, )->pack(-side => 'top', -fill => 'y', -expand => 1, -padx => 3,-pady => 3); $balloon->attach($y_scale, -msg => "y position from the top border in pixel"); # file selector for drop picture labeledEntryButton($ll_w_w,'top',20,"Picture to drop",'Set', \$config{llWatermarkFile}); # canvas $c = $canvasf->Scrolled("Canvas", -width => 600, -height => 400, -scrollbars => 'osoe')->pack(-side => 'left', -expand => 1, -fill => 'both'); # aux buttons buttonBackup($ll_w_w, 'top'); buttonComment($ll_w_w, 'top'); # OK + Help + Cancel button my $ButF = $ll_w_w->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { # some checks return if (not check_drop_picture($ll_w_w, $config{llWatermarkFile})); # clean up preview photo object (free mem) $ll_w_w->{preview}->delete if ($ll_w_w->{preview}); $ll_w_w->withdraw(); $ll_w_w->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); $ButF->Button(-text => lang('Help'), -command => sub { showText('Help for drop picture', "This function can be used to drop a picture (e.g. a small logo) onto another picture without losing quality due to recompression.\nThis can be used to add any label (e.g. name of the photographer, the location or the date) onto a picture.\nTherefore both pictures have to be in JPEG format and must have the same JPEG sampling factors!\n\nThe original picture is not recompressed and thus every pixel stays exactly the same - except where the drop picture is added, of course.\nThe tool jpegtran with the lossless drop patch is used for this function. See http://jpegclub.org.\n\nIf you have troubles with the sampling factors you may crop a part of the picture first, add a text to it and then use this picture as drop file.\n\nYou can check that this function is really lossless by comparing the original and the processed picture using the Mapivi function \"compare pictures\". If the resulting difference picture is complety black (except the region where the picture was dropped) no other pixel was changed.", NO_WAIT); })->pack(-side => 'left', -expand => 0, -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; # clean up preview photo object (free mem) $ll_w_w->{preview}->delete if ($ll_w_w->{preview}); $ll_w_w->withdraw(); $ll_w_w->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($ll_w_w, $Xbut); $ll_w_w->Popup; # show preview $preview_button->Invoke; $ll_w_w->waitWindow; return $rc; } ############################################################## ############################################################## sub check_drop_picture { my $window = shift; my $file = shift; unless (-f $file) { $window->messageBox(-icon => 'error', -message => 'The picture to drop could not be found.', -title => 'File not found', -type => 'OK'); return 0; } unless (is_a_JPEG($file)) { $window->messageBox(-icon => 'error', -message => "The picture to drop ($file) is not in JPEG format.", -title => 'Wrong picture format', -type => 'OK'); return 0; } return 1; } ############################################################## ############################################################## sub get_valid_parent_folder { my $dir = shift; return $dir if -d $dir; while (File::Spec->splitdir( $dir ) >= 1) { $dir = dirname($dir); last if -d $dir; } # fallback solution if (!-d $dir) { $dir = File::Spec->rootdir(); } return $dir; } ############################################################## # importWizard - Dialog window ############################################################## sub importWizard { if (Exists($wizW)) { $wizW->deiconify; $wizW->raise; return; } my $pics = shift; my $rc = 0; # open window $wizW = $top->Toplevel(); $wizW->title(lang('Import pictures wizard')); $wizW->iconimage($mapiviicon) if $mapiviicon; my $i_text = $wizW->Scrolled('ROText', -scrollbars => 'osoe', -wrap => 'word', -width => 70, -height => 5, -relief => 'flat', -bd => 0 )->pack(-fill => 'both', -expand => 0, -padx => 3, -pady => 3); $i_text->insert('end', lang("Import pictures from a removable device like e.g. a camera or a card reader connected via e.g. USB.\nThe selected actions will take place in the displayed order.\nMapivi is rather paranoid when importing pictures to be on the safe side.\nIf there are any errors during import (like a mismatch in the number of files or file size) you will be asked how to proceed.")); my ($s,$m,$ho,$d,$mo,$y) = getDateTime(time()); # build up the date string for the dir structure (e.g. "2007/10/29") my $date = sprintf "%04d/%02d/%02d", $y, $mo, $d; my $w = 32; my $w2 = $w - 3; $config{ImportSource} = get_valid_parent_folder($config{ImportSource}); labeledEntryButton($wizW,'top',$w,lang("Source folder / Import from"),lang('Set'),\$config{ImportSource}, 1); $wizW->Checkbutton(-variable => \$config{ImportSubdirs}, -anchor => 'w', -text => lang('Import from all sub folders, too') )->pack(-side => 'top', -anchor => 'w', -padx => 3, -pady => 3); labeledEntryButton($wizW,'top',$w,lang('Target folder (fix part)'),lang('Set'),\$config{ImportTargetFix}, 1); my $varF = $wizW->Frame()->pack(-anchor => 'w', -fill => 'x'); labeledEntry($varF,'left',$w,lang("Target folder (variable part)"),\$config{ImportTargetVar}); $varF->Button(-text => 'Set' , -command => sub {$config{ImportTargetVar} = $date;})->pack(-side => 'right', -padx => 3, -pady => 3); $varF->Label(-text => lang('Actual date:'), -anchor => "e", -bg => $conf{color_bg}{value})->pack(-side => 'right', -anchor => 'w', -padx => 3, -pady => 3); my $moreF = $wizW->Frame(-relief => 'groove'); my $more_button; $more_button = $wizW->Checkbutton(-variable => \$config{ImportMore}, -anchor => 'w', -text => lang('More options'), -command => sub { if ($config{ImportMore}) { $moreF->pack(-after => $more_button, -fill => 'x', -expand => 0, -padx => 4, -pady => 3); } else { $moreF->packForget(); } })->pack(-padx => 3, -anchor => 'w'); if ($config{ImportMore}) { $moreF->pack(-after => $more_button, -expand => 0, -fill => 'x', -padx => 4, -pady => 3); } else { $moreF->packForget(); } # jjjj my $rotF = $wizW->Frame()->pack(-anchor => 'w', -fill => 'x'); my $rot = $rotF->Checkbutton(-variable => \$config{ImportRotate}, -anchor => 'w', -text => lang('Picture rotation (lossless)') )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); my @deg = qw(auto horizontal vertical 90 180 270); $rotF->Optionmenu(-variable => \$conf{import_rotate_deg}{value}, -options => \@deg)->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); if (missingProgs("Automatic rotation", "jhead", "jpegtran") > 0) { $config{ImportRotate} = 0; # disabled if jhead and jpegtran are not available $rot->configure(-state => 'disabled'); $rot->configure(-disabledforeground => 'gray30'); $balloon->attach($rot, -msg => explainMissingProg('Automatic rotation', 'jhead').explainMissingProg('Automatic rotation', 'jpegtran')); } my $comF = $moreF->Frame()->pack(-anchor => 'w', -fill => 'x'); $comF->Checkbutton(-variable => \$config{NameComment}, -anchor => 'w', -text => lang('Add original file name to comment (') )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); $comF->Checkbutton(-variable => \$config{NameComRmSuffix}, -anchor => 'w', -text => lang('remove file suffix )') )->pack(-side => 'left', -anchor => 'w', -padx => 0, -pady => 3); my $headF = $moreF->Frame()->pack(-anchor => 'w', -fill => 'x'); $headF->Checkbutton(-variable => \$conf{import_iptc_headline}{value}, -anchor => 'w', -text => '', )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); labeledEntry($headF,'left',$w,lang('Add IPTC headline'),\$conf{import_iptc_headline_content}{value}); my $acomF = $moreF->Frame()->pack(-anchor => 'w', -fill => 'x'); $acomF->Checkbutton(-variable => \$config{ImportAddCom}, -anchor => 'w', -text => '', )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); labeledEntry($acomF,'left',$w,lang('Add comment to each picture'),\$config{ImportAddComment}); my $iptcF = $moreF->Frame()->pack(-anchor => 'w', -fill => 'x'); my $addiptc = $iptcF->Checkbutton(-variable => \$config{ImportAddIPTC}, -anchor => 'w', -text => '', )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); $balloon->attach($addiptc, -msg => lang("To generate a IPTC template (*.iptc2) edit IPTC of any (dummy) picture and save the IPTC info via menu: IPTC->").lang("Save template ...")); labeledEntryButton($iptcF,'top',$w,lang('Add IPTC info to each picture'),lang('Set'),\$config{ImportIPTCTempl}); my $addiptcdt = $moreF->Checkbutton(-variable => \$config{ImportAddIPTCDateTime}, -anchor => 'w', -text => lang('Add EXIF date/time to IPTC'), )->pack(-anchor => 'w', -padx => 3, -pady => 3); $balloon->attach($addiptcdt, -msg => lang("Add EXIF date and time to IPTC date / time created tags")); my $addiptcow = $moreF->Checkbutton(-variable => \$config{ImportAddIPTCByLine}, -anchor => 'w', -text => lang('Add EXIF owner to IPTC ByLine'), )->pack(-anchor => 'w', -padx => 3, -pady => 3); $balloon->attach($addiptcow, -msg => lang("Add EXIF owner or artist or user comment to IPTC ByLine")); my $lockB = $moreF->Checkbutton(-variable => \$config{ImportMarkLocked}, -anchor => 'w', -text => lang('Add high rating to locked pictures') )->pack(-anchor => 'w', -padx => 3, -pady => 3); $balloon->attach($lockB, -msg => lang("Some digital cameras allow to lock pictures.\nThis feature can be used to mark important pictures already in the camera.\nIf this function is enabled Mapivi will add a high rating to all locked pictures\n(files with write protection).")); $moreF->Checkbutton(-variable => \$config{ImportDeleteCameraJunk}, -anchor => 'w', -text => lang('Delete camera junk files in target folder after copy (e.g. *.CTG)') )->pack(-anchor => 'w', -padx => 3, -pady => 3); my $renF = $wizW->Frame()->pack(-anchor => 'w', -fill => 'x'); $renF->Checkbutton(-variable => \$config{ImportRename}, -anchor => 'w', -text => lang('Smart Rename with this pattern:') )->pack(-side => 'left', -anchor => 'w', -padx => 2, -pady => 3); $renF->Label(-textvariable => \$config{FileNameFormat}, -bg => $conf{color_bg}{value}, -anchor => 'w', #-width => ($w2-2), )->pack(-side => 'left', -anchor => 'w', -padx => 3, -pady => 3); $renF->Button(-text => lang('Set'), -command => sub { getRenameFormat(); })->pack(-side => 'right', -anchor => 'w', -padx => 3, -pady => 3); $wizW->Checkbutton(-variable => \$config{ImportDelete}, -anchor => 'w', -text => lang("Delete files in source folder after copy") )->pack(-anchor => 'w', -padx => 3, -pady => 3); $wizW->Checkbutton(-variable => \$config{ImportShowPics}, -anchor => 'w', -text => lang("Show pictures when import finished") )->pack(-anchor => 'w', -padx => 3, -pady => 3); my $ButF = $wizW->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $wizW->withdraw(); $wizW->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $wizW->withdraw(); $wizW->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($wizW, $Xbut); $wizW->Popup; $wizW->waitWindow; return if ($rc != 1); my $ok = importPictures(); openDirPost($config{ImportTargetFix}."/".$config{ImportTargetVar}) if $config{ImportShowPics}; if ($ok) { log_it(lang("Picture import finished successfully!")); } else { log_it(lang("Picture import finished with errors!")); } return; } my $printW; ############################################################## # copyToPrint - copy pics to print folders # (e.g. 2_times_5x7/ or 1_times_13x18/) ############################################################## sub copyToPrint { my $lb = shift; # the reference to the active listbox widget my @sellist = getSelection($lb); return unless checkSelection($top, 1, 0, \@sellist, lang("picture(s)")); if (Exists($printW)) { $printW->deiconify; $printW->raise; return; } my $pics = shift; my $rc = 0; # open window $printW = $lb->Toplevel(); $printW->title("copy pictures to print folder"); $printW->iconimage($mapiviicon) if $mapiviicon; $printW->Label(-text => "Copy ".scalar @sellist." pictures to a according print folder.", -bg => $conf{color_bg}{value}, -justify => 'left')->pack(-anchor => 'w', -padx => 3, -pady => 3); my $w = 32; my $w2 = $w - 3; my $times = 1; my $timesStr = "times"; my $size = "10x15"; labeledEntryButton($printW,'top',$w,"Print base folder",'Set',\$config{PrintBaseDir}, 1); my $sf = $printW->Frame()->pack(); $sf->Label(-text => "numer, string and size", -width => $w, -bg => $conf{color_bg}{value}, -justify => 'left')->pack(-side => 'left'); $sf->Optionmenu(-textvariable => \$config{PrintTimes}, -options => [qw(1 2 3 4 5 6 7 8 9 10)], -command => sub { $config{PrintVarDir} = $config{PrintTimes}.$config{PrintTimesStr}.$config{PrintSize}; }, )->pack(-side => 'left', -anchor => 'w'); $sf->Optionmenu(-textvariable => \$config{PrintTimesStr}, -options => [qw(times mal - x _x_ _times_ _mal_ _prints_in_ _Abzuege_in_)], -command => sub { $config{PrintVarDir} = $config{PrintTimes}.$config{PrintTimesStr}.$config{PrintSize}; }, )->pack(-side => 'left', -anchor => 'w'); $sf->Optionmenu(-textvariable => \$config{PrintSize}, -options => [qw(4x6 5x7 8x10 11x14 9x13 10x15 13x18 18x27 30x40 50x70)], -command => sub { $config{PrintVarDir} = $config{PrintTimes}.$config{PrintTimesStr}.$config{PrintSize}; }, )->pack(-side => 'left', -anchor => 'w'); labeledEntry($printW,'top',$w,"folder",\$config{PrintVarDir}); my $ButF = $printW->Frame()->pack(-fill =>'x', -padx => 3, -pady => 3); my $OKB = $ButF->Button(-text => lang('OK'), -command => sub { $printW->withdraw(); $printW->destroy(); $rc = 1; })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); my $Xbut = $ButF->Button(-text => lang('Cancel'), -command => sub { $rc = 0; $printW->withdraw(); $printW->destroy(); })->pack(-side => 'left', -expand => 1, -fill => 'x', -padx => 3, -pady => 3); bind_exit_keys_to_button($printW, $Xbut); $printW->Popup; $printW->waitWindow; return if ($rc != 1); if (!-d $config{PrintBaseDir}) { my $rc = $top->messageBox(-icon => 'question', -message => $config{PrintBaseDir}." does not exist. Should I create it?", -title => "Create print base folder?", -type => 'OKCancel'); return if ($rc !~ m/Ok/i); eval { mkpath($config{PrintBaseDir}, 0, oct(755)) }; # 0 = no output, 0755 = access rights if ($@) { warn "Couldn't create ",$config{PrintBaseDir},": $@"; return; } } my $printdir = $config{PrintBaseDir}."/".$config{PrintVarDir}; print "copy pics to $printdir\n" if $verbose; makeDir($printdir, NO_ASK); # do not ask my $pw = progressWinInit($top, "Copy to print"); my $i = 0; foreach my $spic (@sellist) { last if progressWinCheck($pw); $i++; my $pic = basename($spic); my $tpic = "$printdir/$pic"; progressWinUpdate($pw, "copy ($i/".scalar @sellist.") ...", $i, scalar @sellist); if (!mycopy($spic, $tpic, ASK_OVERWRITE)) { # ask before overwrite warn "error in copy $pic!\n"; } } progressWinEnd($pw); log_it("copy finished! ($i/".scalar @sellist.")"); return; } ############################################################## # importPictures ############################################################## sub importPictures { my $source = $config{ImportSource}; ##### check source dir log_it("checking folders ..."); if (!-d $source) { $top->messageBox(-icon => 'warning', -message => "Sorry, but the source folder\n$source\ndoes not exists!\nPlease check, if the device is mounted.", -title => "Import pictures - Error", -type => 'OK'); return 0; } my @sdirs; # all dirs to process # add the sub dirs if ($config{ImportSubdirs}) { push @sdirs, getDirsRecursive($source); } push @sdirs, $source unless isInList($source, \@sdirs); # the source dir is the minimum # the target dir my $tdir = $config{ImportTargetFix}."/".$config{ImportTargetVar}; ##### check if target is available, create it if not makeDir($tdir, ASK) if (!-d $tdir); ##### check if target is now available if (!-d $tdir) { warn "$tdir not created!!!"; return 0; } #### get the IPTC template only once, before starting loop my $iptc; if ($config{ImportAddIPTC}) { if (defined $config{ImportIPTCTempl} and -f $config{ImportIPTCTempl}) { $iptc = retrieve($config{ImportIPTCTempl}); unless (defined $iptc) { $top->messageBox(-icon => 'warning', -message => "Sorry, but Mapivi could not retrieve IPTC template $config{ImportIPTCTempl}!\nPlease check, if the file is available.", -title => "Import pictures - Error", -type => 'OK'); return 0; } } else { $top->messageBox(-icon => 'warning', -message => "Sorry, but Mapivi could not find the IPTC template $config{ImportIPTCTempl}!\nPlease check, if the file is available.", -title => "Import pictures - Error", -type => 'OK'); return 0; } } # open log window if (Exists($impW)) { $impW->deiconify; $impW->raise; return 0; } # open window $impW = $top->Toplevel(); $impW->title(lang("Import pictures report")); $impW->iconimage($mapiviicon) if $mapiviicon; my ($s,$m,$ho,$d,$mo,$y) = localtime(time()); my $time = sprintf "%02d:%02d:%02d", $ho, $m, $s; my $butF = $impW->Frame()->pack(-expand => 1, -fill =>'x'); $butF->Button(-text => lang("Close"), -command => sub { $impW->withdraw(); $impW->destroy(); }, )->pack(-expand => 1, -side => 'left', -fill => 'x'); my $stop_import = 0; my $stop_importB = $butF->Button(-text => lang("Stop"), -command => sub { $stop_import = 1; } )->pack(-side => 'left', -anchor => 'w', -expand => 0,-padx => 1,-pady => 1); $stop_importB->configure(-image => $mapivi_icons{Stop}, -borderwidth => 0); $stop_importB->configure(-state => 'disabled'); my $dcount = 0; # progress of dirs my $pcount = 0; # progress of pics my $rating_count = 0; # counter for locked pictures with successfull added rating my $progF = $impW->Frame()->pack(-expand => 1, -fill =>'x'); $progF->Label(-text => lang("progress folders "), -bg => $conf{color_bg}{value})->pack(-side => 'left'); $progF->ProgressBar(-takefocus => 0, -borderwidth => 1, -relief => 'sunken', -length => 100, -padx => 0, -pady => 0, -variable => \$dcount, -colors => [0 => $config{ColorProgress}], -resolution => 1, -blocks => scalar @sdirs, -anchor => 'w', -from => 0, -to => scalar @sdirs, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 2, -pady => 3); $progF->Label(-text => lang(" pictures "), -bg => $conf{color_bg}{value})->pack(-side => 'left'); my $picProg = $progF->ProgressBar(-takefocus => 0, -borderwidth => 1, -relief => 'sunken', -length => 100, -padx => 0, -pady => 0, -variable => \$pcount, -colors => [0 => $config{ColorProgress}], -resolution => 1, -anchor => 'w', -from => 0, -to => 100, )->pack(-side => 'left', -fill => 'x', -expand => 1, -padx => 2, -pady => 3); my $rotext = $impW->Scrolled('ROText', -scrollbars => 'oe', -wrap => 'word', -tabs => '4', -width => 90, -height => 30, -bg => 'gray90', -fg => 'black', )->pack(-fill => 'both', -expand => 1, -padx => 1, -pady => 1); $rotext->tagConfigure("R",-foreground => "red"); $rotext->tagConfigure("G",-foreground => "DeepSkyBlue4"); $rotext->tagConfigure("B",-foreground => "blue4"); #$impW->Popup; $rotext->insert('end', $time.lang(" starting import ...\n"), "B"); $impW->update; $stop_importB->configure(-state => 'normal'); foreach my $source (@sdirs) { last if $stop_import; $dcount++; $rotext->insert('end', lang("in folder ")."($dcount/".scalar @sdirs.") $source ...\n", "G"); $impW->update; ##### get and check files to import my @importfiles = getFiles($source); print "In dir $source are ".@importfiles." files\n" if $verbose; if (@importfiles <= 0) { $rotext->insert('end', " ".lang("no pictures - skipping folder\n"), "R"); $rotext->see('end'); next; } $picProg->configure(-to => scalar @importfiles, -blocks => scalar @importfiles); ##### copy all files from source to target $pcount = 0; my $sum = 0; # the sum of all files copied in MegaBytes my $startTime = Tk::timeofday(); foreach my $file (@importfiles) { last if $stop_import; $pcount++; my $size = getFileSize("$source/$file", NO_FORMAT)/(1024*1024); # get size in MegaBytes my $sizeF = sprintf "%.2f", $size; $rotext->insert('end', " ($pcount/".scalar @importfiles.") ".lang("copy")." $file ($sizeF MB)\n"); $rotext->see('end'); $impW->update; mycopy("$source/$file", "$tdir/$file", ASK_OVERWRITE); if ($config{ImportMarkLocked}) { # if source file is write protected and a JPEG by file suffix (this is much faster than using is_a_JPEG()) if ((!-w "$source/$file") and ($file =~ m/.*\.jp(g|eg)$/i)) { # add rating 1 to target file my $urgency = 1; my $errors = ''; my $ok = set_IPTC_urgency_file("$tdir/$file", $urgency, \$errors); if ($ok) { # urgency changed successfully! # set also XMP rating, if option is set xmp_set_rating("$tdir/$file", $urgency) if $conf{xmp_rating}{value}; $rotext->insert('end', " ".lang("locked picture, setting high rating!\n")); $rating_count++; } else { $rotext->insert('end', " ".lang("locked picture, but writing of rating failed!\n")); } $rotext->see('end'); } } $sum += $size if (-f "$tdir/$file"); } if ($stop_import) { $rotext->insert('end', $time.' '.lang("Import aborted by user!")."\n", "R"); } my $duration = Tk::timeofday() - $startTime; # in seconds my $rate = 0; $rate = $sum/$duration if ($duration > 0); # MegaBytes/second my $string = langf("The transfer of %d pictures (%.2f MB) took %.2f seconds; transferrate %.2f MB/s\n", $pcount, $sum, $duration, $rate); $rotext->insert('end', $string); $rotext->see('end'); return 0 if ($stop_import); ##### check if the copy was successfull my $filediff = 0; my $sizediff = 0; # check if every source file is in the target dir and if the file size is the same foreach (@importfiles) { if (!-f "$tdir/$_") { $filediff++; } else { $sizediff++ if (getFileSize("$tdir/$_", NO_FORMAT) != getFileSize("$source/$_", NO_FORMAT)); } } if (($filediff > 0) or ($sizediff > 0)) { my $rinfo = ''; $rinfo = "$rating_count locked pictures found and rating added. This will increase the file size and may explain the difference.\n" if ($rating_count > 0); my $fdinfo = ''; $fdinfo = "$filediff files are missing.\n" if ($filediff > 0); my $sdinfo = ''; $sdinfo = "$sizediff files have another size.\n" if ($sizediff > 0); my $rc = $top->messageBox(-icon => 'question', -message => "Not all files in the source and target folder are eqal.\n${fdinfo}${sdinfo}${rinfo}Should I continue to process $pcount imported pictures?", -title => 'Continue importing pictures?', -type => 'OKCancel'); $impW->raise; return 0 if ($rc !~ m/Ok/i); } ##### get the imported JPEG pictures (from the source dir!!!) # no questions about NON-JPEGS while importing please! my @piclist = getPics($source, JUST_FILE, NO_CHECK_JPEG); # no sort needed ##### process JPEGS if ($config{ImportRotate} or $config{ImportRename} or $config{NameComment} or $config{ImportAddCom} or $config{ImportAddIPTC} or $config{ImportAddIPTCDateTime} or $config{ImportAddIPTCByLine}) { my $command = ''; my @renamed; $pcount = 0; foreach (@piclist) { last if $stop_import; $pcount++; my $pic = $_; my $dpic = "$tdir/$pic"; my $is_a_jpeg = 0; $is_a_jpeg = 1 if ($pic =~ m/.*\.jp(g|eg)$/i); $rotext->insert('end', " ($pcount/".scalar @piclist.") $pic ", "G"); $rotext->see('end'); if (!-f $dpic) { my $info = ''; $info = lang("(maybe already renamed with JPEG file?) ") if (not $is_a_jpeg); $rotext->insert('end', "*** ".lang("is missing - skipping!")." $info***\n", "R"); $rotext->see('end'); warn "importPictures: $dpic is missing - skipping!\n"; next; } #my $tmppic = "$dpic"."-cjpg"; # temporary file ############################################################## ##### auto rotate pics if ($config{ImportRotate}) { $rotext->insert('end', lang("rotate, ")); $rotext->see('end'); print "import rotate $pic: $conf{import_rotate_deg}{value}\n"; my ($ok, $error) = rotate_pic($dpic, $conf{import_rotate_deg}{value}); warn "Error while import rotate $pic: $error\n" if (not $ok); } ############################################################## ##### add file name to comment if ($config{NameComment} and $is_a_jpeg) { $rotext->insert('end', lang("add name to comment, ")); $rotext->see('end'); my $com = $pic; if (($config{NameComRmSuffix}) and ($pic =~ /(.*)(\.jp(g|eg))/i)) { $com = $1; # just the file name without .jp(e)g suffix } # add the filename as comment addCommentToPic($com, $dpic, NO_TOUCH) if ($com ne ''); } ############################################################## ##### add IPTC template to picture if ($config{ImportAddIPTC} and (defined $config{ImportIPTCTempl}) and (-f $config{ImportIPTCTempl}) and $is_a_jpeg) { $rotext->insert('end', lang("add IPTC, ")); $rotext->see('end'); # add IPTC to pic my $meta = getMetaData($dpic, 'APP13'); if (defined $meta) { # todo, we could also use UPDATE or REPLACE here $meta->set_app13_data($iptc, 'ADD', 'IPTC'); # make the SupplementalCategories and Keywords unique and sorted uniqueIPTC($meta); $meta->save(); } } ############################################################## ##### add IPTC Headline if ($conf{import_iptc_headline}{value} and $is_a_jpeg) { if (defined $conf{import_iptc_headline_content}{value}) { if ($conf{import_iptc_headline_content}{value} ne '') { $rotext->insert('end', lang("add headline, ")); $rotext->see('end'); my $iptc = { 'Headline' => $conf{import_iptc_headline_content}{value} }; my ($ok, $error) = applyIPTCint($dpic, $iptc); warn "Adding headline to $pic error: $error\n" if (not $ok); } } } ############################################################## ##### add EXIF date/time to IPTC if ($config{ImportAddIPTCDateTime} and $is_a_jpeg) { $rotext->insert('end', lang("add EXIF date/time, ")); $rotext->see('end'); my $meta = getMetaData($dpic, 'APP1'); # APP1 includes EXIF and IPTC (APP13) if (defined $meta) { my $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); my $iptc = $meta->get_app13_data('TEXTUAL', 'IPTC'); my $date = getEXIFDate($dpic, $er); # date format YYYY:MM:DD HH:mm:SS my ($ok, $IPTCdate, $IPTCtime) = EXIFtoIPTCdatetime($date); if ($ok) { # according to IPTC - NAA INFORMATION INTERCHANGE MODEL, Version No. 4, 1999, http://www.iptc.org/IIM/ ${$iptc->{DateCreated}}[0] = $IPTCdate; # format CCYYMMDD ${$iptc->{TimeCreated}}[0] = $IPTCtime; # format HHMMSS+HHMM # todo: better use applyIPTCint??? $meta->set_app13_data($iptc, 'ADD', 'IPTC'); $meta->save(); } else { warn "picture has an unusual EXIF date: \"$date\" ($dpic)\n" if $config{MetadataWarn}; } } } ############################################################## ##### add EXIF owner to IPTC ByLine if ($config{ImportAddIPTCByLine} and $is_a_jpeg) { $rotext->insert('end', lang("add EXIF owner, ")); $rotext->see('end'); my $meta = getMetaData($dpic, 'APP1'); # APP1 includes EXIF and IPTC (APP13) if (defined $meta) { my $er = $meta->get_Exif_data('ALL', 'TEXTUAL'); my $iptc = $meta->get_app13_data('TEXTUAL', 'IPTC'); my $owner = getEXIFowner($er); if ($owner ne '') { ${$iptc->{ByLine}}[0] = $owner; $meta->set_app13_data($iptc, 'ADD', 'IPTC'); $meta->save(); } } } ############################################################## ##### add comment to picture if ($config{ImportAddCom} and (defined $config{ImportAddComment}) and ($config{ImportAddComment} ne '') and $is_a_jpeg) { $rotext->insert('end', lang("add comment, ")); $rotext->see('end'); # add comment to pic addCommentToPic($config{ImportAddComment}, $dpic, NO_TOUCH); } ############################################################## ##### smart rename pics my $errors = ''; if ($config{ImportRename} and $is_a_jpeg) { my $newname = ''; my $doForAll = 1; # use the file date, if there is no EXIF date without asking my $rc = applyRenameFormat($dpic, $config{FileNameFormat}, \$newname, \$doForAll); $rotext->insert('end', langf("rename to %s ",$newname)); $rotext->see('end'); $newname = findNewName("$tdir/$newname"); if (($rc ne "Skip this picture") and ($rc ne "Cancel all")) { if (-f "$tdir/$newname") { # just a safety check warn "$newname already exists - skipping\n"; next; } print "renaming from $pic to $newname\n" if $verbose; # rename the picture if (!renamePicInt(undef, $dpic, "$tdir/$newname", \$errors)) { # rename failed $top->messageBox(-icon => 'warning', -message => "Could not rename $pic to $newname: $!", -title => 'Error', -type => 'OK'); } else { push @renamed, "$tdir/$newname"; } } } #$rotext->insert('end', "Error: $errors") if ($errors ne ''); # 2010-02-09: disabled because this gives an error about not beeing able to rename the thumbnail $rotext->insert('end', "\n"); $rotext->see('end'); $rotext->update; } # foreach pics end my $errors = ''; renameSmartFix(\$errors, @renamed) if $config{ImportRename}; $rotext->insert('end', "Error: $errors\n") if ($errors ne ''); } $stop_importB->configure(-state => 'disabled'); ############################################################## ##### delete worthless camera state files if ($config{ImportDeleteCameraJunk}) { my @junkfiles = grep {m/.*\.($cameraJunkSuffixes)$/i} @importfiles; $pcount = 0; $stop_importB->configure(-state => 'normal'); foreach (@junkfiles) { last if $stop_import; $pcount++; $rotext->insert('end', " ($pcount/".scalar @junkfiles.") ".lang("deleting")." $_\n"); $rotext->see('end'); $rotext->update; removeFile("$tdir/$_"); } $stop_importB->configure(-state => 'disabled'); } ############################################################## ##### delete imported pics if ($config{ImportDelete}) { # check if everything is alright if (($filediff > 0) or ($sizediff > 0)) { my $rc = $top->messageBox(-icon => 'question', -message => "There have been some warnings while importing.\nShould I continue and delete the ".scalar @importfiles." files in the source folder?", -title => "Continue?", -type => 'OKCancel'); return 0 if ($rc !~ m/Ok/i); } $pcount = 0; $stop_importB->configure(-state => 'normal'); # remove the pics on the source dir foreach (@importfiles) { last if $stop_import; $pcount++; $rotext->insert('end', " ($pcount/".scalar @importfiles.") ".lang("deleting")." $_\n"); $rotext->see('end'); $rotext->update; removeFile("$source/$_"); } } } # foreach dirs end $stop_importB->configure(-state => 'disabled'); ($s,$m,$ho,$d,$mo,$y) = localtime(time()); $time = sprintf "%02d:%02d:%02d", $ho, $m, $s; my $ok = 0; if ($stop_import) { $rotext->insert('end', $time.' '.lang("Import aborted by user!")."\n", "R"); } else { $rotext->insert('end', $time.' '.lang("Import finished!")."\n", "B"); $ok = 1; } $rotext->insert('end', lang("You may now close this window.")."\n", "B"); $rotext->see('end'); $rotext->update; return $ok; } ############################################################## # sets widget to normal if state = true, else to disabled ############################################################## sub set_child_normal { my ($widget, $state) = @_; if ($state) { setChildState($widget, 'normal'); } else { setChildState($widget, 'disabled'); } return; } ############################################################## # setChildState - changes the state of a widget and # all his descendants (if possible) ############################################################## sub setChildState { my $widget = shift; my $state = shift; # 'normal' or 'disabled' $widget->Walk( sub { print "changing widget ",ref($_[0])," to state $state\n" if $verbose; eval { $_[0]->configure(-state => $state); } }); return; } ############################################################## # progressWinInit ############################################################## sub progressWinInit { my $widget = shift; my $title = shift; # open window my $pw = $widget->Toplevel(); $pw->withdraw; $pw->title("Mapivi: $title"); $pw->iconimage($mapiviicon) if $mapiviicon; $pw->iconname(lang("Mapivi progress")); # init the values $pw->{stop} = 0; $pw->{percent} = 0; $pw->{label} = ''; $pw->{label2} = lang("0% done\n\n\n"); $pw->{start_time} = Tk::timeofday(); $pw->Label(-textvariable => \$pw->{label}, -width => 80, -anchor => 'w', -bg => $conf{color_bg}{value})->pack(-padx => 3, -pady => 10); $pw->Label(-textvariable => \$pw->{label2}, -anchor => 'w', -bg => $conf{color_bg}{value})->pack(-padx => 3, -pady => 10); $pw->{progbar} = $pw->ProgressBar(-takefocus => 0, -borderwidth => 1, -relief => 'sunken', #-width => (2*$config{FontSize}), # try to guess the height of the labels #-length => 30, -padx => 0, -pady => 0, -variable => \$pw->{percent}, -colors => [0 => $config{ColorProgress}], -resolution => 1, -blocks => 10, -anchor => 'w', -from => 0, -to => 100, )->pack(-fill => 'both', -expand => 0, -padx => 3, -pady => 10); $pw->Button(-text => lang('Cancel'), -command => sub { $pw->{stop} = 1; $pw->{label} = lang("Try to stop, please wait ..."); $pw->update(); })->pack(-fill => 'x', -expand => 1, -padx => 3, -pady => 10); centerWindow($pw); $pw->deiconify; $pw->raise; return $pw; } ############################################################## # progressWinCheck ############################################################## sub progressWinCheck { my $pw = shift; return 0 unless (Exists($pw)); warn "pw->stop undefined!" unless defined($pw->{stop}); return ($pw->{stop}); } ############################################################## # progressWinUpdate ############################################################## sub progressWinUpdate { my $pw = shift; return unless (Exists($pw)); # show progress and found pics every 0.3 seconds - idea from Slaven return unless (!defined $pw->{last_time} || Tk::timeofday()-$pw->{last_time} > 0.3); my $string = shift; my $index = shift; my $total = shift; $pw->{label} = $string; if ($total > 0) { my $add_str = ''; my $percent = int(($index/$total)*100); my $min = 0; my $sec = int(Tk::timeofday() - $pw->{start_time}); # try to estimate the time to go, after 3% are finished and 10 seconds are over if (($percent > 3) and ($sec > 5)) { my $to_go = int($sec * $total / $index) - $sec; # time to go in seconds my $totalt = $to_go + $sec; my $tgmin = 0; my $total_min = 0; if ($to_go > 59) { $tgmin = int($to_go / 60); $to_go = $to_go % 60; } # modulo if ($totalt > 59) { $total_min = int($totalt / 60); $totalt = $totalt % 60; } # modulo $add_str = langf("\n\nTotal time about %d:%02d, finished in about %d:%02d", $total_min, $totalt, $tgmin, $to_go); } if ($sec > 59) { $min = int($sec / 60); $sec = $sec % 60; } # modulo $pw->{label2} = langf("%d%% done, time elapsed %d:%02d%s", $percent, $min, $sec, $add_str); $pw->{percent} = $percent; $pw->iconname(langf("%d%% done", $percent)); } else { $pw->{label2} = ''; } $pw->update(); $pw->{last_time} = Tk::timeofday() if ($total > 0); return; } ############################################################## # progressWinEnd ############################################################## sub progressWinEnd { my $pw = shift; if (Exists($pw)) { $pw->withdraw; $pw->destroy; } return; } ############################################################## # fullscreen - toggle any window to fullscreen and back to old # size and position # all information is stored inside the $win hash # # other sources: # Mai 2007: $win->attributes(-fullscreen => 1); should also work with # 804.027_500 but it doesn't (at least not under windows) # see also http://objectmix.com/perl/19715-mainwindow-fullscreen.html # todo: do we need this, too? http://www.tek-tips.com/faqs.cfm?fid=6265 ############################################################## sub fullscreen { my $win = shift; if ((not defined $win->{my_fullscreen_flag}) or ($win->{my_fullscreen_flag} == 0)) { print "fullscreen: full \n" if $verbose; # save the actual window geometry $win->{my_last_geometry} = $win->geometry; # this should also work: (packPropagate must be before Fullscreen call!!!) $win->packPropagate(0); my $w = $win->screenwidth; # - 20; my $h = $win->screenheight; # - 80; $win->geometry("${w}x${h}+0+0"); #$win->FullScreen; # remove window decoration (has to be after Fullscreen call!!!) $win->overrideredirect(1) if $config{ToggleBorder}; $win->{my_fullscreen_flag} = 1; } else { print "fullscreen: normal \n" if $verbose; $win->packPropagate(1); $win->geometry($win->{my_last_geometry}); # add window decoration $win->overrideredirect(0) if $config{ToggleBorder}; $win->{my_fullscreen_flag} = 0; } $win->update; $win->focusForce; # info_window() has to be called after update and focusForce! if ($win->{my_fullscreen_flag}) { info_window($win, lang("Fullscreen On")); } else { info_window($win, lang("Fullscreen Off")); } return; } ############################################################## # mapiviUpdate - called if the mapivi version number changed # between two starts of mapivi (introduced with # version 0.7.3) ############################################################## sub mapiviUpdate { my $ver = 'unknown'; $ver = $config{Version} if ((defined $config{Version}) and ($config{Version} ne '000')); print "Mapivi up/downgrade from version $ver to version $version detected\n"; return; } ############################################################## # beep - play a beep sound (bell) ############################################################## sub beep { print "\a"; # this is a beep # if this won't work, try this: #print "\007"; return; } ############################################################## # round ############################################################## sub round { # int() does not round! return sprintf "%d", shift; } ############################################################## # about - display some infos about the application ############################################################## sub about { my $title = lang('About Mapivi')." $version $svnrevision"; my $nrs = $config{NrOfRuns}; my $about = << "EOA"; Mapivi - Martin\'s Picture Viewer and Manager EOA $about .= lang("Open-source and cross-platform picture manager with IPTC, EXIF and Comment support."); $about .= << "EOA"; Mapivi Version: $version SVN Revision: $svnrevision Date: $version_date File: $mapivi_file Author: Martin Herrmann email: Martin-Herrmann\@gmx.de www: $mapiviURL download: http://sourceforge.net/projects/mapivi EOA $about .= langf("You have used Mapivi %d times.", $nrs); $about .= "\n\n".lang("Mapivi is free software.\n"); $about .= "\n".lang("Thanks to the Tango Desktop Project for the nice icons!")." http://tango.freedesktop.org/Tango_Icon_Library\n"; $about .= "\n".lang("I am always happy to receive some feedback about Mapivi!\n"); showText($title, $about, WAIT); return; } ############################################################## # systemInfo - show some infos about the system to the user ############################################################## sub systemInfo { my $sec = time() - $^T; my $min = 0; my $hou = 0; my $day = 0; my $line = '------------------------------------'; # some modula calculations if ($sec > 59) { $min = int($sec / 60); $sec = $sec % 60; } # modulo if ($min > 59) { $hou = int($min / 60); $min = $min % 60; } if ($hou > 24) { $day = int($hou / 24); $hou = $hou % 24; } my $uptime = sprintf "%d day(s) %02d:%02d:%02d", $day, $hou, $min, $sec; my $perlversion = sprintf "%vd",$^V; my $exiftool_version = 'not available'; $exiftool_version = $Image::ExifTool::VERSION; my $string = << "EOA"; $line Paths: Mapivi user data: $user_data_path Mapivi program data: $program_data_path Perl executable: $^X $line Versions: Perl version: $perlversion Perl/Tk version: $Tk::VERSION Tcl/Tk version: $Tk::version Tcl/Tk patch level: $Tk::patchLevel Tk::JPEG version: $Tk::JPEG::VERSION MetaData version: $Image::MetaData::JPEG::VERSION ExifTool version: $exiftool_version $line Process: Process ID (PID): $$ Running since: $uptime $line System: System (OS): $^O EOA my $procTabAvail = (eval {require Proc::ProcessTable}) ? 1 : 0 ; my $mem = 'n.a.'; $mem = int(getMemoryUsage()/(1024*1024))."MB" if $procTabAvail; $string .= " memory usage: ".$mem."\n" if $procTabAvail; $string .= " OS type: ".$ENV{OS}."\n" if ($ENV{OS}); $string .= " OS: ".$ENV{PC_OS}."\n" if ($ENV{PC_OS}); $string .= " OS type: ".$ENV{OSTYPE}."\n" if ($ENV{OSTYPE}); $string .= " System name: ".$ENV{HOSTNAME}."\n" if ($ENV{HOSTNAME}); $string .= " System name: ".$ENV{COMPUTERNAME}."\n" if ($ENV{COMPUTERNAME}); $string .= " System type: ".$ENV{HOSTTYPE}."\n" if ($ENV{HOSTTYPE}); $string .= " # of processors: ".$ENV{NUMBER_OF_PROCESSORS}."\n" if ($ENV{NUMBER_OF_PROCESSORS}); $string .= " Processor: ".$ENV{CPU}."\n" if ($ENV{CPU}); $string .= " Processor: ".$ENV{PROCESSOR_ARCHITECTURE}."\n" if ($ENV{PROCESSOR_ARCHITECTURE}); $string .= " Processor type: ".$ENV{PROCESSOR_IDENTIFIER}."\n" if ($ENV{PROCESSOR_IDENTIFIER}); $string .= " Processor type: ".$ENV{MACHTYPE}."\n" if ($ENV{MACHTYPE}); $string .= " Processor rev.: ".$ENV{PROCESSOR_REVISION}."\n" if ($ENV{PROCESSOR_REVISION}); $string .= " Screen resolution: ".$top->screenwidth."x".$top->screenheight."\n"; $string .= "\n".$line."\nOptional Perl modules:\n"; $string .= "not " if (not MatchEntryAvail); $string .= "available: "; $string .= "Tk::MatchEntry\n"; $string .= "not " if (not $resizeAvail); $string .= "available: "; $string .= "Tk::ResizeButton\n"; $string .= "not " if (not $splashAvail); $string .= "available: "; $string .= "Tk::Splash\n"; $string .= "not " if (not ProcBackgroundAvail); $string .= "available: "; $string .= "Proc::Background\n"; $string .= "not " if (not $procTabAvail); $string .= "available: "; $string .= "Proc::ProcessTable\n"; $string .= "not " if (not Win32ProcAvail); $string .= "available: "; $string .= "Win32::Process\n"; $string .= "\n".$line."\nExternal programs (required or optional):\n"; foreach my $prog (sort keys %exprogs) { if ($exprogs{$prog}) { $string .= " "; } else { $string .= " not "; } $string .= sprintf("available: %-12s - %s\n", $prog, $exprogscom{$prog}); } showText("System Information", $string, WAIT); return; } ############################################################## # gratulation ############################################################## sub gratulation { my $nr = $config{NrOfRuns}; my $text = <<"EOT"; Gratulation!!! You\'ve started Mapivi $nr times! You are a real Mapivi Power User! I would be really glad to receive an email about this event. Martin Herrmann (author of Mapivi) email: Martin-Herrmann\@gmx.de EOT showText("Mapivi start nr. $nr", $text, NO_WAIT); return; } ############################################################## # showCopyright ############################################################## sub showCopyright { print < Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. Tk/0000775000000000000000000000000012600550353010133 5ustar rootrootTk/MhColorChooser.pm0000644000000000000000000002376012230036210013353 0ustar rootrootpackage Tk::MhColorChooser; use 5.008008; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); use Tk; use Tk::Balloon; use Tk::Pane; # Items to export into callers namespace by default. Note: do not export # names by default without a very good reason. Use EXPORT_OK instead. # Do not simply export all your public functions/methods/constants. our @EXPORT_OK = qw(color_chooser); our $VERSION = '0.01'; ############################################################## # color_chooser - open a window and offer some colors to select ############################################################## sub color_chooser { my $w = shift; # parent Tk widget (e.g. main window) my $additional_colors = shift; # list reference (optional) my @all_colors = qw/black gray10 gray20 gray30 gray40 gray50 gray60 gray70 gray80 gray85 gray90 gray95 white snow2 snow3 snow4 seashell1 seashell2 seashell3 seashell4 AntiqueWhite1 AntiqueWhite2 AntiqueWhite3 AntiqueWhite4 bisque1 bisque2 bisque3 bisque4 PeachPuff1 PeachPuff2 PeachPuff3 PeachPuff4 NavajoWhite1 NavajoWhite2 NavajoWhite3 NavajoWhite4 LemonChiffon1 LemonChiffon2 LemonChiffon3 LemonChiffon4 cornsilk1 cornsilk2 cornsilk3 cornsilk4 ivory1 ivory2 ivory3 ivory4 honeydew1 honeydew2 honeydew3 honeydew4 LavenderBlush1 LavenderBlush2 LavenderBlush3 LavenderBlush4 MistyRose1 MistyRose2 MistyRose3 MistyRose4 azure1 azure2 azure3 azure4 SlateBlue1 SlateBlue2 SlateBlue3 SlateBlue4 RoyalBlue1 RoyalBlue2 RoyalBlue3 RoyalBlue4 blue1 blue2 blue3 blue4 DodgerBlue1 DodgerBlue2 DodgerBlue3 DodgerBlue4 SteelBlue1 SteelBlue2 SteelBlue3 SteelBlue4 DeepSkyBlue1 DeepSkyBlue2 DeepSkyBlue3 DeepSkyBlue4 SkyBlue1 SkyBlue2 SkyBlue3 SkyBlue4 LightSkyBlue1 LightSkyBlue2 LightSkyBlue3 LightSkyBlue4 SlateGray1 SlateGray2 SlateGray3 SlateGray4 LightSteelBlue1 LightSteelBlue2 LightSteelBlue3 LightSteelBlue4 LightBlue1 LightBlue2 LightBlue3 LightBlue4 LightCyan1 LightCyan2 LightCyan3 LightCyan4 PaleTurquoise1 PaleTurquoise2 PaleTurquoise3 PaleTurquoise4 CadetBlue1 CadetBlue2 CadetBlue3 CadetBlue4 turquoise1 turquoise2 turquoise3 turquoise4 cyan1 cyan2 cyan3 cyan4 DarkSlateGray1 DarkSlateGray2 DarkSlateGray3 DarkSlateGray4 aquamarine1 aquamarine2 aquamarine3 aquamarine4 DarkSeaGreen1 DarkSeaGreen2 DarkSeaGreen3 DarkSeaGreen4 SeaGreen1 SeaGreen2 SeaGreen3 SeaGreen4 PaleGreen1 PaleGreen2 PaleGreen3 PaleGreen4 SpringGreen1 SpringGreen2 SpringGreen3 SpringGreen4 green1 green2 green3 green4 chartreuse1 chartreuse2 chartreuse3 chartreuse4 OliveDrab1 OliveDrab2 OliveDrab3 OliveDrab4 DarkOliveGreen1 DarkOliveGreen2 DarkOliveGreen3 DarkOliveGreen4 khaki1 khaki2 khaki3 khaki4 LightGoldenrod1 LightGoldenrod2 LightGoldenrod3 LightGoldenrod4 LightYellow1 LightYellow2 LightYellow3 LightYellow4 yellow1 yellow2 yellow3 yellow4 gold1 gold2 gold3 gold4 goldenrod1 goldenrod2 goldenrod3 goldenrod4 DarkGoldenrod1 DarkGoldenrod2 DarkGoldenrod3 DarkGoldenrod4 RosyBrown1 RosyBrown2 RosyBrown3 RosyBrown4 IndianRed1 IndianRed2 IndianRed3 IndianRed4 sienna1 sienna2 sienna3 sienna4 burlywood1 burlywood2 burlywood3 burlywood4 wheat1 wheat2 wheat3 wheat4 tan1 tan2 tan3 tan4 chocolate1 chocolate2 chocolate3 chocolate4 firebrick1 firebrick2 firebrick3 firebrick4 brown1 brown2 brown3 brown4 salmon1 salmon2 salmon3 salmon4 LightSalmon1 LightSalmon2 LightSalmon3 LightSalmon4 orange1 orange2 orange3 orange4 DarkOrange1 DarkOrange2 DarkOrange3 DarkOrange4 coral1 coral2 coral3 coral4 tomato1 tomato2 tomato3 tomato4 OrangeRed1 OrangeRed2 OrangeRed3 OrangeRed4 red1 red2 red3 red4 DeepPink1 DeepPink2 DeepPink3 DeepPink4 HotPink1 HotPink2 HotPink3 HotPink4 pink1 pink2 pink3 pink4 LightPink1 LightPink2 LightPink3 LightPink4 PaleVioletRed1 PaleVioletRed2 PaleVioletRed3 PaleVioletRed4 maroon1 maroon2 maroon3 maroon4 VioletRed1 VioletRed2 VioletRed3 VioletRed4 magenta1 magenta2 magenta3 magenta4 orchid1 orchid2 orchid3 orchid4 plum1 plum2 plum3 plum4 MediumOrchid1 MediumOrchid2 MediumOrchid3 MediumOrchid4 DarkOrchid1 DarkOrchid2 DarkOrchid3 DarkOrchid4 purple1 purple2 purple3 purple4 MediumPurple1 MediumPurple2 MediumPurple3 MediumPurple4 thistle1 thistle2 thistle3 thistle4/; my $title = 'Please select a color'; # open window my $win = $w->Toplevel(); # we need a fixed font for a nice layout in the balloon tooltips my $font = $win->Font(-family => 'Courier', -size => 8); my $balloon = $win->Balloon(-bg => 'gray90', -initwait => 1000, -font => $font); $balloon->Subwidget('message')->configure(-justify => 'left'); $win->{balloon} = $balloon; $win->withdraw; $win->title($title); #$win->iconimage($icon) if $icon; $win->iconname($title); my $return_color = 0; if ((defined $additional_colors) and @{$additional_colors}) { $win->Label(-text => 'Preselected colors')->pack(-padx => 0, -pady => 0); add_color_buttons($win, $additional_colors, \$return_color, 27, 'left'); # my $colorFB = $win->Frame()->pack(-fill => 'both', -expand => 1); # foreach (@{$additional_colors}) { # my $but; # $but = # $colorFB->Button( # -text => ' ', # -height => 0, # -width => 0, # -padx => 0, # -pady => 0, # -relief => 'groove', # -background => $_, # -command => sub { # my $col = $but->cget(-bg); # $return_color = $col; # } # )->pack(-padx => 0, -pady => 0, -side => 'left'); # $balloon->attach($but, -msg => $_); # } # label only needed when additional colors are defined $win->Label(-text => 'Other colors')->pack(-padx => 0, -pady => 0); } add_color_buttons($win, \@all_colors, \$return_color, 12, 'top'); # my $colorF = $win->Frame()->pack(-fill => 'both', -expand => 1); # my $i = 0; # foreach (@all_colors) { # $i++; # if ($i == 1 or $i % 12 == 1) { # a frame for the first and every 12th button (modulo) # $frame = $colorF->Frame()->pack(-side => 'left', -anchor => 'n'); # } # my $but; # $but = # $frame->Button( # -text => ' ', # -height => 0, # -width => 0, # -padx => 0, # -pady => 0, # -relief => 'groove', # -background => $_, # -command => sub { # my $col = $but->cget(-bg); # $return_color = $col; # } # )->pack(-padx => 0, -pady => 0); # $balloon->attach($but, -msg => $_); # } my $xBut = $win->Button(-text => 'Close', -command => sub { print "returning: undef\n"; $return_color = undef; },)->pack(-fill => 'x'); # 50 ways to leave your window ;) $win->bind('' , sub {$xBut->invoke;}); $win->bind('' , sub {$xBut->invoke;}); $win->protocol("WM_DELETE_WINDOW" => sub {$xBut->invoke;} ); $xBut->focus; $win->Popup; #repositionWindow($win); $win->waitVariable(\$return_color); $win->withdraw; $win->destroy; return $return_color; } ############################################################## ############################################################## sub add_color_buttons { my $w = shift; # parent widget my $colors = shift; # list reference my $return_color = shift; # scalar reference my $modulo = shift; # number of buttons in a row or column my $top_or_left = shift; # string: color buttons in rows ('left') or in columns ('top') my $side = 'top'; $side = 'left' if ($top_or_left eq 'top'); my $frame; my $colorF = $w->Frame()->pack(-fill => 'both', -expand => 1); my $i = 0; foreach (@{$colors}) { $i++; if ($i == 1 or $i % $modulo == 1) { # a frame for the first and every 12th button (modulo) $frame = $colorF->Frame()->pack(-side => $side, -anchor => 'n'); } my $but; $but = $frame->Button( -text => ' ', -height => 0, -width => 0, -padx => 0, -pady => 0, -relief => 'groove', -background => $_, -command => sub { my $col = $but->cget(-bg); $$return_color = $col; } )->pack(-side => $top_or_left, -padx => 0, -pady => 0); $w->{balloon}->attach($but, -msg => $_); } } ############################################################## ############################################################## # Preloaded methods go here. 1; __END__ =head1 NAME Tk::MhColorChooser - Perl/Tk user interface (dialog window) to choose a color from a given set of colors =head1 SYNOPSIS # simple usage use Tk::MhColorChooser qw(color_chooser); my $color = color_chooser($top); if (defined $color) { print "Color $color selected\n"; } else { print "Nothing selected\n"; } # advanced usage use Tk::MhColorChooser qw(color_chooser); my @more_colors = ('#707070', '#808080', '#909090'); my $color = color_chooser($top, \@more_colors); if (defined $color) { print "Color $color selected\n"; } else { print "Nothing selected\n"; } =head1 DESCRIPTION Tk::MhColorChooser provides a simple methode to choose a color. The only methode color_chooser() opens a window and presents 324 different colors to choose from. The first argument is used as parent widget for the window. The optional second argument has to be a list reference to additional colors (see SYNOPSIS above). These colors are shown as additional buttons above the default colors. =head2 EXPORT None by default. =head1 SEE ALSO Tk::ColorEditor from Slaven Rezic =head1 AUTHOR Martin Herrmann =head1 COPYRIGHT AND LICENSE Copyright (C) 2010 by Martin Herrmann This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available. =cut Tk/MhConfig.pm0000644000000000000000000004142512230036210012155 0ustar rootrootpackage Tk::MhConfig; use 5.008008; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); use Storable qw(nstore retrieve dclone); use File::Basename; use Tk; use Tk::Balloon; use Tk::Pane; use Tk::Font; use Tk::NoteBook; use Tk::MhColorChooser qw(color_chooser); # Items to export into callers namespace by default. Note: do not export # names by default without a very good reason. Use EXPORT_OK instead. # Do not simply export all your public functions/methods/constants. our @EXPORT_OK = qw(configuration_edit configuration_store configuration_restore); our $VERSION = '0.01'; ############################################################## # load the configuration from the given file ############################################################## sub configuration_restore { my $file = shift; # config file with path my $configref = shift; # hash reference # try to get the saved config hash if (-f $file) { my $hashref = retrieve($file); unless (defined $hashref) { return (0, "could not retrieve configuration from $file"); } # the config is a hash of hash (2 levels) # we copy each element for its own, so we do not loose any new options and still use all existing user settings foreach my $key1 (keys %{$hashref}) { if (not defined $configref->{$key1}) { warn "MhConfig: Ignoring unknown key \"$key1\" (file:$file)\n"; next; } foreach my $key2 (keys %{$hashref->{$key1}}) { if (not defined $configref->{$key1}->{$key2}) { warn "MhConfig: Unknown key \"$key1->$key2\" (value: $hashref->{$key1}->{$key2}) (file:$file)\n"; next; } $configref->{$key1}->{$key2} = $hashref->{$key1}->{$key2}; } } } return (1, ''); } ############################################################## # store the configuration in the given file ############################################################## sub configuration_store { my $file = shift; my $configref = shift; # hash reference if (nstore($configref, $file)) { return (1, ''); } else { return (0, "Could not store configuration in file $file: $!\n"); } } ############################################################## # open a dialog to edit the configuration options ############################################################## sub configuration_edit { my $w = shift; # main window widget my $config = shift; # reference to config hash my $tab_order = shift; # reference to array containing the tabs in the prefered order my $apply_callback = shift; # code reference, callback function to be called when user presses the apply button my $reset_callback = shift; # code reference, callback function to be called when user presses the reset button my $additional_colors = shift; # list reference (additional color s for color_chooser() my $inactivebackground = shift; # optional color fot the inactive notebook tabs my $icon = shift; # optional window icon (Tk::Photo reference) # the configuration toplevel is named $w->{'config_win'} if (Exists($w->{'config_win'})) { $w->{'config_win'}->deiconify; $w->{'config_win'}->raise; $w->{'config_win'}->focus; return; } # open window my $conf_win = $w->Toplevel(); my $title = ''; $title = $w->{tool_name} if (defined $w->{tool_name}); $title .= ' Options'; $conf_win->title($title); # calc and set window size my $winw = 700; # optimal size, but this depends on OS and font size my $winh = 600; my $screenw = $conf_win->screenwidth; my $screenh = $conf_win->screenheight; # limit window to to screensize $winw = $screenw if ($screenw < $winw); $winh = $screenh if ($screenh < $winh); # center window on screen my $xoffset = ($screenw - $winw)/2; my $yoffset = ($screenh - $winh)/2; $conf_win->geometry("${winw}x${winh}+${xoffset}+${yoffset}"); # number of ASCII chars per line (is used to calculate the length of text entries) my $total_length = 70; # we need a fixed font for a nice layout in the balloon tooltips my $font = $conf_win->Font(-family => 'Courier', -size => 8); my $balloon = $conf_win->Balloon(-initwait => 1000, -font => $font); $balloon->Subwidget('message')->configure(-justify => 'left'); $conf_win->iconimage($icon) if ($icon and (ref($icon) eq "Tk::Photo")); # store config window widget in main window widget $w->{'config_win'} = $conf_win; # get background color from window widget my $background_color = $conf_win->cget(-bg); # create notebook my $notebook = $conf_win->NoteBook(-background => $background_color, -backpagecolor => $background_color)->pack(-expand => 1, -fill => 'both', -padx => 3, -pady => 3); $notebook->configure(-inactivebackground => $inactivebackground) if (defined $inactivebackground); # generate notebook tabs in predefined order if (@{$tab_order}) { foreach my $tab (@{$tab_order}) { $conf_win->{$tab} = $notebook->add($tab, -label => $tab); # we use a pane to support small screen resolutions, the user is still able to scroll e.g. to the buttons at the bottom of the window $conf_win->{$tab}->{pane} = $conf_win->{$tab}->Scrolled('Pane', -scrollbars => 'osoe', #-width => $pane_w, -height => $pane_h )->pack(-expand => 1, -fill => 'both'); } } foreach my $item (sort { my $ord_a = 100; my $ord_b = 100; $ord_a = $$config{$a}{ord} if defined $$config{$a}{ord}; $ord_b = $$config{$b}{ord} if defined $$config{$b}{ord}; $ord_a <=> $ord_b || uc($a) cmp uc($b) } keys %{$config}) { # default tab is called Extra (all options without a tab value will be placed there!) my $tab = 'Extra'; $tab = $$config{$item}{tab} if defined $$config{$item}{tab}; # the config item will not be shown if the item tab is set to "no" next if ($tab eq 'no'); # if the tab still does not exists we create it if (not defined $conf_win->{$tab}) { $conf_win->{$tab} = $notebook->add($tab, -label => $tab); } # the option widget will be placed in widget $w which is either the tab itself or a frame inside the tab my $w = $conf_win->{$tab}->{pane}; # options may be grouped into frames my $frame; $frame = $$config{$item}{frame} if defined $$config{$item}{frame}; # if the tab does not exists we create it if (defined $frame) { if (not defined $conf_win->{$tab}->{pane}->{$frame}) { $conf_win->{$tab}->{pane}->{$frame} = $conf_win->{$tab}->{pane}->Frame(-bd => 1, -relief => 'groove')->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); $conf_win->{$tab}->{pane}->{$frame}->Label(-text => $frame, -width => $total_length)->pack(); } $w = $conf_win->{$tab}->{pane}->{$frame}; # widget w is now the frame } my $item_name = $item; $item_name = $$config{$item}{long} if defined $$config{$item}{long}; my $but; if (not defined $$config{$item}{kind}) { next; # to display a config option we need to know its nature } ### BOOL ### elsif ($$config{$item}{kind} eq 'bool') { $but = $w->Checkbutton(-variable => \$$config{$item}{value}, -text => $item_name, -anchor => 'w')->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); } ### NUMBER (INTEGER) ### elsif ($$config{$item}{kind} eq 'int') { if (defined $$config{$item}{value} and defined $$config{$item}{from} and defined $$config{$item}{to}) { $but = $w->Scale( -variable => \$$config{$item}{value}, -label => $item_name, -from => $$config{$item}{from}, -to => $$config{$item}{to}, -resolution => 1, -orient => 'horizontal', -showvalue => 1, -width => 15, )->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); } else { warn "Configuration option $item \"$item_name\": needs a \"value\" a \"from\" and a \"to\" value"; } } ### NUMBER (FLOAT) ### elsif ($$config{$item}{kind} eq 'float') { if (defined $$config{$item}{value} and defined $$config{$item}{from} and defined $$config{$item}{to} and defined $$config{$item}{res}) { $but = $w->Scale( -variable => \$$config{$item}{value}, -label => $item_name, -from => $$config{$item}{from}, -to => $$config{$item}{to}, -resolution => $$config{$item}{res}, -orient => 'horizontal', -showvalue => 1, -width => 15, )->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); } else { warn "Configuration option $item \"$item_name\": needs a \"value\" a \"from\" a \"to\" and a \"res\" value"; } } ### COLOR ### elsif ($$config{$item}{kind} eq 'color') { my $frame = $w->Frame(-bd => 1)->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); $but = $frame->Button(-text => '..', -bg => $$config{$item}{value}, -command => sub { my $rc = color_chooser($conf_win, $additional_colors); if (defined $rc) { $but->configure(-bg => $rc); $$config{$item}{value} = $rc; } })->pack(-side => 'left'); $frame->Label(-text => $item_name, -anchor => 'w')->pack(-side => 'left', -expand => 1, -fill => 'x',); } ### FILE ### elsif ($$config{$item}{kind} eq 'file') { my $frame = $w->Frame(-bd => 1)->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); $frame->Label(-text => $item_name, -anchor => 'w')->pack(-side => 'left', -expand => 1, -fill => 'x',); $but = $frame->Entry(-textvariable => \$$config{$item}{value}, -width => ($total_length-length($item_name)), )->pack(-side => 'left', -padx => 5, -fill => 'y'); $frame->Button(-text => '..', -command => sub { my $file = $w->getOpenFile(-title => $item_name, -initialdir => dirname($$config{$item}{value})); if ((defined $file) and (-f $file)) { $$config{$item}{value} = $file; } })->pack(-side => 'left', -padx => 5); } ### DIR ### elsif ($$config{$item}{kind} eq 'dir') { my $frame = $w->Frame(-bd => 1)->pack(-side => 'top', -expand => 1, -fill => 'x', -padx => 4, -pady => 4); $frame->Label(-text => $item_name, -anchor => 'w')->pack(-side => 'left', -expand => 1, -fill => 'x',); $but = $frame->Entry(-textvariable => \$$config{$item}{value}, -width => ($total_length-length($item_name)), )->pack(-side => 'left', -padx => 5); $frame->Button(-text => '..', -command => sub { my $dir = $w->chooseDirectory(-title => $item_name, -initialdir => $$config{$item}{value}); if ((defined $dir) and (-d $dir)) { $$config{$item}{value} = $dir; } })->pack(-side => 'left', -padx => 5); } ### STRING ### elsif ($$config{$item}{kind} eq 'string') { $but = $w->Frame(-bd => 1)->pack(-side => 'top', -expand => 0, -fill => 'x', -padx => 4, -pady => 4); $but->Label(-text => $item_name, -anchor => 'w')->pack(-side => 'left', -expand => 1, -fill => 'x',); $but->Entry(-textvariable => \$$config{$item}{value}, -width => ($total_length-length($item_name)), )->pack(-side => 'left', -padx => 5); } ### RADIO ### elsif ($$config{$item}{kind} eq 'radio') { $but = $w->Frame(-bd => 1, -relief => 'groove')->pack(-expand => 0, -fill => 'x', -padx => 4, -pady => 4); $but->Label(-text => $item_name, -anchor => 'w')->pack(-expand => 1, -fill => 'x',); foreach my $option (keys %{$config->{$item}->{options}}) { $but->Radiobutton(-variable => \$$config{$item}{value}, -text => $$config{$item}{options}{$option}, -value => $option, -anchor => 'w')->pack(-expand => 1, -fill => 'x', -padx => 4, -pady => 4); } } else { warn "Configuration option $item \"$item_name\": unsupported config type = $$config{$item}{kind}\n"; next; } if ((exists $$config{$item}{info}) and ($$config{$item}{info} ne '')) { $balloon->attach($but, -msg => $$config{$item}{info}); } } # button frame for close button etc. my $bframe = $conf_win->Frame(-bd => 1)->pack(-side => 'top', -expand => 0, -fill => 'x'); if ((defined $reset_callback) and (ref($reset_callback) eq 'CODE')) { my $reset_but = $bframe->Button(-text => 'Reset', -command => sub { my $ok = $conf_win->messageBox(-icon => 'question', -message => "Really reset all options to default settings?", -title => "Reset all options?", -type => 'OKCancel'); if ($ok =~ m/Ok/i) { &$reset_callback(); # execute callback function $conf_win->destroy; } })->pack(-side => 'left', -padx => 3, -pady => 3); $balloon->attach($reset_but, -msg => 'Reset all options to default settings'); } my $OKB = $bframe->Button(-text => 'Close', -command => sub { $conf_win->destroy; })->pack(-side => 'right', -padx => 3, -pady => 3); if ((defined $apply_callback) and (ref($apply_callback) eq 'CODE')) { $bframe->Button(-text => 'Apply', -command => sub { &$apply_callback(); # execute callback function })->pack(-side => 'right', -padx => 3, -pady => 3); } $conf_win->bind('', sub { $OKB->invoke; }); $conf_win->Popup; $conf_win->waitWindow(); } # Preloaded methods go here. 1; __END__ # Below is stub documentation for your module. You'd better edit it! =head1 NAME Tk::MhConfig - Perl extension to read, store and edit the configuration of a program =head1 SYNOPSIS use Tk::MhConfig qw(configuration_edit configuration_store configuration_restore); my %config; # global configuration hash my ($ok, $err) = configuration_restore($config_file, \%config); warn $err if (not $ok); my ($ok, $err) = configuration_store($config_file, \%config); warn $err if (not $ok); configuration_edit($top, \%config, \@config_tab_order); sub configuration_set_default { # this defines the order of the tabs in the configuration_edit dialog: @config_tab_order = qw(General Behavior Extra); %config = ( # General tab 'make_backup' => { 'value' => 1, 'kind' => 'bool', 'long' => 'Make backup', 'tab' => 'General', 'frame' => 'Behavior', 'info' => "When selected the tool will always make a backup.", 'ord' => 1}, 'show_file_name' => { 'value' => 1, 'kind' => 'bool', 'long' => 'Show file name', 'tab' => 'General', 'frame' => 'Display', 'info' => "Show the file name (if available)", 'ord' => 2}, 'option_b' => { 'value' => 9, 'kind' => 'int', 'from' => 1, 'to' => 12, 'long' => 'Option B', 'tab' => 'Extra', 'info' => "Informative text ... blah blah\nblah blah", 'ord' => 30}, =head1 DESCRIPTION Tk::MhConfig is a framework to read, store and edit tool configurations (parameters of a software program, like e.g. the window background color). Tk::MhConfig supports different kinds of options: bool - boolean value (on/off) number - integer number (0, 1, 2, 3, ...) string - ("Hello world") color - a valid color ("black", "red", "#0405AF") file - a file with complete path (/usr/local/bin/mapivi) dir - a directory (/usr/local/bin) These options may be arranged in any number of tabs and grouped in labeled frames (see 'tab' and 'frame' in the examples above). The order of the options inside the tabs is controlled by 'ord': low numbers are shown in the upper and high numbers are shown in the lower part of the tab. =head2 EXPORT Tk::MhConfig exports no function by default. The following three functions are exported on demand: configuration_edit() configuration_store() configuration_restore() =head1 SEE ALSO =head1 AUTHOR Martin Herrmann =head1 COPYRIGHT AND LICENSE Copyright (C) 2010 by Martin Herrmann This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available. =cut PlugIns/0000775000000000000000000000000012754672046011154 5ustar rootrootPlugIns/Channel-Separator.txt0000664000000000000000000000027012754672046015222 0ustar rootrootChannel-Separator + separate channels (RGB, CMY, etc) + 1 + this plugin will split the red, gree, blue, matte, opacity, cyan, ... channels of all selected pictures to separate files PlugIns/Join-RGB.txt0000664000000000000000000000017612754672046013230 0ustar rootrootJoin-RGB + join RGB channels + 1 + this plugin will join the three selected red, gree and blue channel pictures to one files PlugIns/filelist-plugin.txt0000664000000000000000000000015512754672046015025 0ustar rootrootfilelist-plugin + write file list + 0 + this plugin will write the names of all selected pictures to a file PlugIns/Channel-Separator0000664000000000000000000000464012754672046014411 0ustar rootroot#!/usr/bin/perl -w # include perl packages use strict; use warnings; use diagnostics; use Tk; use Tk::FileSelect; use File::Basename; my $nr = @ARGV; if ($nr < 1) { print "Error: $0 called with just $nr arguments\n"; usage(); exit(); } my $dir = $ARGV[0]; if (-d $dir) { print "Error: first argument ($dir) is a valid directory.\nThis Mapivi Plug-In is developed for Mapivi version >= 0.7.5\n"; usage(); exit(); } my $top = MainWindow->new; $top->title("MaPiVi Plugin RGB Separator"); $top->Label(-text => "Create a", )->pack(); $top->Button(-text => "red, green and blue (RGB)", -command => sub { my @list = qw/Red Green Blue/; export(@list); })->pack(); $top->Button(-text => "cyan, magenta and yellow (CMY)", -command => sub { my @list = qw/Cyan Magenta Yellow/; export(@list); })->pack(); $top->Button(-text => "matte, opacity and black", -command => sub { my @list = qw/Matte Opacity Black/; export(@list); })->pack(); $top->Label(-text => "channel picture of the $nr selected pictures", )->pack(); $top->Button(-text => "exit plugin", -command => \&exit)->pack(); $top->MainLoop; sub export { my @list = @_; for (0 .. $nr-1) { my $dpic = $ARGV[$_]; next if (!-f $dpic); print "processing $dpic ($_/$nr) ...\n"; foreach my $color (@list) { my $rgb = $dpic; $rgb =~ s/(.*)(\.jp(g|eg))/$1-$color$2/i; print "rgb = $rgb\n"; if (-f $rgb) { my $rc = $top->messageBox(-icon => 'warning', -message => "file $rgb exist. Ok to overwrite?", -title => "Plugin", -type => "OKCancel"); next if ($rc !~ m/Ok/i); } my $command = "convert -quality 95 -channel $color \"$dpic\" \"$rgb\" "; (system "$command") == 0 or print "Error: $command failed: $!\n"; } } print "Plugin finished successfully!\n"; exit(); } ############################################################## # usage ############################################################## sub usage { my $prog = basename($0); print "\nUsage: $prog file1 [file2] [file3] [...]\n\n"; print "This is a plugin for mapivi (see http://mapivi.de.vu)\n"; print "It will split the selected pictures from mapivi\n"; print "into RGB or other channel pictures\n"; print "Author: Martin Herrmann \n"; print "License: GNU General Public License, version 2\n"; } # Tell Emacs that this is really a perl script # Local Variables: # mode:perl # End: PlugIns/checkDir-plugin.txt0000664000000000000000000000016712754672046014731 0ustar rootrootcheckDir-plugin + check directory + 0 + this plugin will check the current directory for empty files and broken links PlugIns/Join-RGB0000664000000000000000000000623212754672046012411 0ustar rootroot#!/usr/bin/perl -w # include perl packages use strict; use warnings; use diagnostics; use Tk; use Tk::FileSelect; use File::Basename; my $nr = @ARGV; if ($nr != 3) { print "Error: $0 called with wrong number of arguments (should be 3).\n"; usage(); exit(); } my $red = ""; my $green = ""; my $blue = ""; foreach (0 .. $nr-1) { my $pic = $ARGV[$_]; if ($pic =~ m/.*red.*/i) { $red = $pic; } if ($pic =~ m/.*green.*/i) { $green = $pic; } if ($pic =~ m/.*blue.*/i) { $blue = $pic; } } if ($red eq "") { print "$0 called with no red channel!\n"; usage(); exit(); } if ($green eq "") { print "$0 called with no green channel!\n"; usage(); exit(); } if ($blue eq "") { print "$0 called with no blue channel!\n"; usage(); exit(); } my $dir = dirname($red); if (!-d $dir) { print "$dir is no valid directory\n"; usage(); exit(); } my $top = MainWindow->new; $top->title("MaPiVi Plugin RGB Seperator"); $top->Label(-text => "Create a", )->pack(); $top->Button(-text => "RGB picture", -command => sub { export(); })->pack(); $top->Label(-text => "picture of the three selected channel pictures", )->pack(); $top->Button(-text => "exit plugin", -command => \&exit)->pack(); $top->MainLoop; sub export { my $rgb = $red; $rgb =~ s/(.*)(red)(\.jp(g|eg))/$1RGB$3/i; print "rgb outoput file = $rgb\n"; if (-f "$rgb") { my $rc = $top->messageBox(-icon => 'warning', -message => "output file $rgb exist. Ok to overwrite?", -title => "Plugin", -type => "OKCancel"); return if ($rc !~ m/Ok/i); } if (-f "$rgb.ppm") { my $rc = $top->messageBox(-icon => 'warning', -message => "temp file $rgb.ppm exist. Ok to overwrite?", -title => "Plugin", -type => "OKCancel"); return if ($rc !~ m/Ok/i); } my $command = "composite -compose copyred $red $green $rgb.ppm"; (system "$command") == 0 or print "Error: $command failed: $!\n"; $command = "composite -quality 95 -compose copyblue $blue $rgb.ppm $rgb"; (system "$command") == 0 or print "Error: $command failed: $!\n"; if (-f "$rgb.ppm") { if ( unlink("$rgb.ppm") != 1) { # unlink returns the number of successfull removed files $top->messageBox(-icon => 'warning', -message => "Could not delete file \"$rgb.ppm\": $!", -title => "Error", -type => "OK"); print "Plugin $0 finished with errors!\n"; exit(); } } print "Plugin $0 finished successfully!\n"; exit(); } ############################################################## # usage ############################################################## sub usage { my $prog = basename($0); print "\nUsage: $prog file1-Red.jpg file2-Green.jpg file3-Blue.jpg\n\n"; print "This is a plugin for mapivi (see http://mapivi.de.vu)\n"; print "It will join the three selected RGB channel pictures from mapivi\n"; print "into one RGB picture\n"; print "The file names must include the corresponding color\n"; print "(red, green, blue; in upper or lower case)\n"; print "Author: Martin Herrmann \n"; print "License: GNU General Public License, version 2\n"; } # Tell Emacs that this is really a perl script # Local Variables: # mode:perl # End: PlugIns/filelist-plugin0000664000000000000000000001202712754672046014210 0ustar rootroot#!/usr/bin/perl -w # include perl packages use strict; use warnings; use diagnostics; use Tk; use Tk::FileSelect; use File::Basename; my $xnview_slideshow_header = '# Slide Show Sequence Loop = 1 FullScreen = 1 TitleBar = 0 View = 1 CenterWindow = 0 ReadErrors = 1 HideMouse = 1 RandomOrder = 0 ShowFilename = 0 BackgroundColor = 0'; my $nr = @ARGV; if ($nr < 1) { print "Error: $0 called with just $nr arguments\n"; usage(); exit(); } my $dir = $ARGV[0]; if (-d $dir) { print "Error: first argument ($dir) is a valid directory.\nThis Mapivi Plug-In is developed for Mapivi version >= 0.7.5\n"; usage(); exit(); } my $top = MainWindow->new; $top->title("MaPiVi Plugin file list"); my $file = glob("~/tmp/filelist"); labeledEntryButton($top,"top",24,"path/name of file list","Set",\$file); my $f1 = $top->Frame()->pack(-anchor => 'w'); my $separator = "\n"; $f1->Label(-text => "separator")->pack(-side => 'left'); $f1->Optionmenu(-options => [ ['newline' => "\n"],['newline and quotas' => "\"\n\""],['comma', ','],['comma and space', ', '],['comma and newline', ",\n"]], -variable => \$separator, -textvariable => \$separator)->pack(-side => 'left'); my $write_xnview_slideshow = 0; $top->Checkbutton(-variable => \$write_xnview_slideshow, -text => "Write XnView slide show file (*.sld)", -command => sub { # change to separator needed from XnView $separator = "\"\n\""; # add right file extension *.sld $file .= '.sld' if ( $file !~ m/.*\.sld/); } )->pack(-anchor => 'w'); my $f2 = $top->Frame()->pack(-anchor => 'w'); $f2->Button(-text => "export file list", -command => \&export)->pack(-side => 'left'); $f2->Button(-text => "exit plugin", -command => \&exit)->pack(-side => 'left'); $top->MainLoop; sub export { if (-f $file) { my $rc = $top->messageBox(-icon => 'warning', -message => "file $file exist. Ok to overwrite?", -title => "Plugin", -type => "OKCancel"); return if ($rc !~ m/Ok/i); } if (!open(FILE, ">$file")) { print "could not open $file for write access!: $!\n"; return; } if ($write_xnview_slideshow) { print FILE "$xnview_slideshow_header\n"; } print FILE '"' if ($separator eq "\"\n\""); for (0 .. $nr-1) { $separator = "\"\n" if (($_ == $nr-1) and ($separator eq "\"\n\"")); print FILE $ARGV[$_].$separator; } close FILE; print "Plugin finished successfully!\n"; exit(); } ############################################################## # usage ############################################################## sub usage { my $prog = basename($0); print "\nUsage: $prog file1 [file2] [file3] [...]\n\n"; print "This is a example plugin for mapivi (see http://mapivi.de.vu)\n"; print "It will just write a file list (the selected pictures from mapivi)\n"; print "Author: Martin Herrmann \n"; print "License: GNU General Public License, version 2\n"; } ############################################################## # labeledEntryButton - build a frame containing a labeled entry # and a button with a file selector ############################################################## sub labeledEntryButton { # input values my ($parentWidget, $position, $width, $label, $buttext, $varRef, $dir) = @_; my $frame = labeledEntry($parentWidget, $position, $width, $label, $varRef); setFileButton($frame,"right",$buttext,$label,$varRef, $dir); return $frame; } ############################################################## # labeledEntry - build a frame containing a labeled entry ############################################################## sub labeledEntry { # input values my ($parentWidget, $position, $width, $label, $varRef) = @_; my $frame = $parentWidget->Frame(-relief=>"groove", -bd => 2)->pack(-side => $position, -fill => "x", -padx => 3, -pady => 3); $frame->Label(-text => $label, -width => $width, -anchor => "w", )->pack(-side => "left", -padx => 3); my $entry = $frame->Entry(-textvariable => $varRef, -width => $width, )->pack(-side => "left", -fill => "x", -expand => "1", -padx => 1); $entry->xview("end"); $entry->icursor("end"); return $frame; } ############################################################## # setFileButton - open a file selector and set file name ############################################################## sub setFileButton { # input values my ($parentWidget, $position, $butlabel, $fileselLabel, $varRef, $dir) = @_; # $dir is optional, if defined and true a dir will be selected instead of a file $parentWidget->Button(-text => $butlabel, -command => sub { my $fileSelect = $top->FileSelect(-title => $fileselLabel, -directory => dirname($$varRef), -width => 30, -height => 30); my $file = $fileSelect->Show; if (defined $file and $file ne "") { if (-f $file) { $$varRef = $file; } } }, )->pack(-side => $position); } # Tell Emacs that this is really a perl script # Local Variables: # mode:perl # End: PlugIns/checkDir-plugin0000664000000000000000000001252312754672046014112 0ustar rootroot#!/usr/bin/perl -w # include perl packages use strict; use warnings; use diagnostics; use Tk; use Tk::FileSelect; use File::Basename; use Cwd; my $nr = @ARGV; my $verbose = 0; if ($nr < 1) { warn "$0 called with just $nr+1 arguments\n"; usage(); exit(); } my $dir = $ARGV[0]; if (-f $dir) { $dir = dirname($dir); } my $top = MainWindow->new; $top->title("MaPiVi Plugin check dir"); my @emptyFiles = getEmptyFiles ($dir); my @brokenLinks = getBrokenLinks($dir); $top->Label(-text => "There are ". scalar @emptyFiles." empty files and ".scalar @brokenLinks." broken links in $dir")->pack(); my $ce = 0; my $cb = 0; if (@emptyFiles > 0) { $top->Button(-text => "remove empty files", -command => sub { foreach (@emptyFiles) { if ( unlink("$dir/$_") != 1) { # unlink returns the number of successfull removed files $top->messageBox(-icon => 'warning', -message => "Could not delete file \"$_\": $!", -title => "Error", -type => "OK"); } else { $ce++; } } print "removed $ce empty files\n"; })->pack(); } if (@brokenLinks > 0) { $top->Button(-text => "remove broken links", -command => sub { foreach (@brokenLinks) { if ( unlink("$dir/$_") != 1) { # unlink returns the number of successfull removed files $top->messageBox(-icon => 'warning', -message => "Could not delete file \"$_\": $!", -title => "Error", -type => "OK"); } else { $cb++; } } print "removed $ce broken links\n"; })->pack(); } $top->Button(-text => "Exit", -command => \&exit)->pack(); $top->MainLoop; ############################################################## # usage ############################################################## sub usage { my $prog = basename($0); print "\nUsage: $prog directory|file\n\n"; print "This is a example plugin for mapivi (see http://mapivi.de.vu)\n"; print "It will check, if there are some empty files or\n"; print "broken links in the given directory and ask to\n"; print "remove them.\n"; print "Author: Martin Herrmann \n"; print "License: GNU General Public License, version 2\n"; } ############################################################## # getEmptyFiles - returns a list of empty files ############################################################## sub getEmptyFiles { my $dir = shift; print " getEmptyFiles: in $dir\n" if $verbose; my @fileDirList = readDir($dir); my @fileList; foreach (@fileDirList) { # put only files which are empty into the filelist push @fileList, $_ if (-z "$dir/$_"); } return @fileList; } ############################################################## # getBrokenLinks - returns a list of broken links ############################################################## sub getBrokenLinks { my $dir = shift; print " getBrokenLinks: in $dir\n" if $verbose; my @fileDirList = readDir($dir); my @fileList; foreach (@fileDirList) { if (-l "$dir/$_") { my $real = getLinkTarget("$dir/$_"); print "$_ links to $real\n" if $verbose; if (!-f $real) { print "$real does not exists!\n" if $verbose; # put only files which are empty into the filelist push @fileList, $_; } } } return @fileList; } ############################################################## # readDir - reads the contents of the given directory ############################################################## sub readDir { my $dir = shift; if (! -d $dir) { warn "readDir: $dir is no dir!: $!"; return 0; } my @fileDirList; # open the directory if (!opendir ACTDIR, "$dir") { warn "Can't open directory $dir: $!"; return 0; } # show no files starting with a '.', but '..' @fileDirList = grep /^(\.\.)|^[^\.]+.*$/, readdir ACTDIR; closedir ACTDIR; return @fileDirList; } ############################################################## # getLinkTarget - returns the file a link is pointing to # input (directory, link) or (dirlink) where # dirlink consists of directory and link # works with relative and absolute links ############################################################## sub getLinkTarget { my ($dir, $link); if (@_ == 2) { $dir = shift; $link = shift; } elsif (@_ == 1) { $dir = dirname($_[0]); $link = basename($_[0]); } else { warn "getLinkTarget: wrong # of parameters!"; return ""; } # change first to the start dir (to handle relative links) return "" if !changeDir($dir); my $linktargetfile = readlink $link; my $linktargetdir = dirname $linktargetfile; # change to link target, this should now work for relative and absolute links return "" if !changeDir($linktargetdir); # get the current dir my $cwd = cwd(); $linktargetfile = $cwd."/".basename($linktargetfile); return $linktargetfile; } ############################################################## # changeDir ############################################################## sub changeDir { my $newDir = shift; return 0 unless defined $newDir; if ( !chdir $newDir ) { my $dialog = $top->Dialog( -title => "Changing to $newDir directory failed", -text => "Can't change to $newDir directory: $!", -buttons => ["OK"]); $dialog->Show(); warn "Can't change to $newDir directory: $!"; return 0; } return 1; } # Tell Emacs that this is really a perl script # Local Variables: # mode:perl # End: PlugIns/test-plugin.txt0000664000000000000000000000013712754672046014171 0ustar rootroottest-plugin + Test PlugIn + 0 + this plugin will show the file names of all selected pictures PlugIns/test-plugin0000664000000000000000000000204112754672046013347 0ustar rootroot#!/usr/bin/perl -w # include perl packages use strict; use warnings; use diagnostics; use File::Basename; my $nr = @ARGV; if ($nr < 1) { print "Error: $0 called with just $nr arguments\n"; usage(); exit(); } my $dir = $ARGV[0]; if (-d $dir) { print "Error: first argument ($dir) is a valid directory.\nThis Mapivi Plug-In is developed for Mapivi version >= 0.7.5\n"; usage(); exit(); } for (0 .. ($nr-1)) { print "picture $_: $ARGV[$_]\n"; } exit(); ############################################################## # usage ############################################################## sub usage { my $prog = basename($0); print "\nUsage: $prog file1 [file2] [file3] [...]\n\n"; print "This is a test plugin for mapivi (see http://mapivi.de.vu)\n"; print "It will just print out the files received from mapivi\n"; print "Author: Martin Herrmann \n"; print "License: GNU General Public License, version 2\n"; } # Tell Emacs that this is really a perl script # Local Variables: # mode:perl # End: languages/0000775000000000000000000000000013007661777011542 5ustar rootrootlanguages/mapivi-lang-fr0000644000000000000000000010155412230036207014257 0ustar rootroot# Mapivi french translation # Thanks to cgaetan for translating! Elements added by fiuzzy. # remove the #-sign to uncomment translated strings # Strings with 'XXX'are draft translation, to improve in the next versions # to translate more strings replace a string in mapivi.pl with the function call lang() # example: 'Copy selected' -> lang('Copy selected') # this works also with double quotes: "Copy selected" -> lang("Copy selected") # Warning: If the string contains control codes like newline (\n) double quotes have to be used! # Warning: If the string contains variables (e.g. $number) is has to be converted to a langf() statement # After the convertion the string must not contain a variable!!! The variables in the string # have to be replaced with format specifiers like e.g. %s. The variables are added spearated # by commas after the string. See "perldoc -f sprintf" for documentation. # Example: before: print "The answer is $string and $int_number!\n"; # after: print langf("The answer is %s and %d!\n", $string, $int_number); %messages = ( " Could not check actual Mapivi version. No internet connection.\n" => "XXX Impossible de vérifier la version de Mapivi. Pas de connexion internet.\n", " Not found in hash\n" => "XXX not found in hash\n", " pictures found." => " images trouvées", "0% done\n\n\n" => "0% effectué\n\n\n", "A ByLineTitle is the title of the creator or creators of an objectdata. Where used, a by-line title should follow the ByLine it modifies. Examples: Staff Photographer, Corresponsal, Envoye Special (max. 32 chars)" => "XXX A ByLineTitle is the title of the creator or creators of an objectdata. Where used, a by-line title should follow the ByLine it modifies. Examples: Staff Photographer, Corresponsal, Envoye Special (max. 32 chars)", "A code representing the location of original transmission according to practices of the provider. Examples: BER-5, PAR-12-11-01. (The Trans Reference field lists a call letter/number combination associated with a photo. It includes an originating transmit points call letters and picture number from that point's sequence of offerings for a given day. Example: NY105.) (max. 32 chars)" => "XXX A code representing the location of original transmission according to practices of the provider. Examples: BER-5, PAR-12-11-01. (The Trans Reference field lists a call letter/number combination associated with a photo. It includes an originating transmit points call letters and picture number from that point's sequence of offerings for a given day. Example: NY105.) (max. 32 chars)", "A code representing the location of original transmission according to practices of the provider. Examples: BER-5, PAR-12-11-01. (The Trans Reference field lists a call letter/number combination associated with a photo. It includes an originating transmit points call letters and picture number from that point's sequence of offerings for a given day. Example: NY105.) (max. 32 chars)" => "XXX A code representing the location of original transmission according to practices of the provider. Examples: BER-5, PAR-12-11-01. (The Trans Reference field lists a call letter/number combination associated with a photo. It includes an originating transmit points call letters and picture number from that point's sequence of offerings for a given day. Example: NY105.) (max. 32 chars)", "About Mapivi" => "A propos de Mapivi", "About" => "A propos", "Actual view as chosen in the navigation frame." => "XXX Actual view as chosen in the navigation frame.", "Add a comment to pictures created or processed with Mapivi" => "Ajouter un commentaire aux images créées ou modifiées avec Mapivi", "Add comment" => "Ajouter un commentaire", "Add IPTC to selection or actual picture?" => "XXX Add IPTC to selection or actual picture?", "Add IPTC to selection or only to actual picture?" => "XXX Add IPTC to selection or only to actual picture?", "Add lossless border" => "Ajouter une bordure sans perte", "Add or edit text and then save it by pressing the save button.\nPress F4 to show or hide this box." => "Ajouter ou modifier le texte puis le sauvegarder en cliquant sur le bouton 'Save'.\nAppuyer sur F4 pour montrer ou cacher cette boîte.", "Add selected keywords to keyword catalog" => "Ajouter les mots-clés sélectionnés au catalogue de mots-clés", "Add selected keywords to selected pictures" => "Ajouter les mots-clés sélectionnés aux images sélectionnées", "Add title and caption to:" => "Ajouter un titre ou une légende à :", "Add to database ..." => "Ajouter à la base...", "Add" => "Ajouter", "Advanced Search ..." => "Recherche avancée...", "Advanced Search" => "Recherche avancée", "All" => "Tous", "Aperture (F)" => "Ouverture (F)", "Ask later" => "Demander plus tard", "Attach" => "Attacher", "Auto rotate (lossless)" => "Rotation automatique (sans perte)", "Best rated pictures - TOP 100" => "XXX Best rated pictures - TOP 100", "Build thumbnails ..." => "Construire les vignettes...", "Build thumbnails" => "Construire les vignettes", "Busy (loading pic), please retry later" => "Occupé (chargement d'images), veuillez réessayer ultérieurement", "Calculate folder size" => "Calculer la taille du répertoire", "Calculation finished." => "Calcul terminé.", "Cancel" => "Abandonner", "Change the size/quality of pictures" => "Changer la taille/qualité des images", "Check database ..." => "Vérifier la base...", "Check for new keywords" => "Vérifier les nouveaux mots-clés", "Check online if a new Mapivi version is available." => "Vérifier si une nouvelle version de Mapivi est disponible en ligne.", "Clean database ..." => "Nettoyer la base...", "Close light table?" => "Fermer la table lumineuse ?", "Close" => "Fermer", "Color picker: last color picked by clicking\non the picture in the main window.\nPlease click to clear." => "XXX Color picker: last color picked by clicking\non the picture in the main window.\nPlease click to clear.", "Combine pictures e.g. thumbnails with a background" => "XXX combine pictures e.g. thumbnails with a background", "Combine pictures to e.g. index prints" => "XXX combine pictures to e.g. index prints", "Comment(s) of displayed picture" => "Commentaire de l'image affichée", "Comments" => "Commentaires", "Contains any necessary copyright notice. (max. 128 chars)" => "XXX Contains any necessary copyright notice. (max. 128 chars)", "Contains name of the creator of the objectdata, e.g. writer, photographer or graphic artist. Examples: Robert Capa, Ernest Hemingway, Pablo Picasso (max. 32 chars)" => "XXX Contains name of the creator of the objectdata, e.g. writer, photographer or graphic artist. Examples: Robert Capa, Ernest Hemingway, Pablo Picasso (max. 32 chars)", "Convert ..." => "Convertir...", "Convert non-JPEG pictures ..." => "Convertir les images non-JPEG...", "Copy from ..." => "Copier de ...", "Copy to ..." => "Copier vers...", "copy to print ..." => "Copier dans un répertoire pour l'impression...", "Could not retrieve dirProperties" => "XXX could not retrieve dirProperties", "Could not retrieve folder hotlist" => "XXX could not retrieve folder hotlist", "Could not retrieve keywords_hot" => "XXX could not retrieve keywords_hot", "Could not retrieve keywords_ignore" => "XXX could not retrieve keywords_ignore", "Create a backup of the original picture in the same folder named name-bak.jpg" => "Créer dans le même répertoire une copie de sauvegarde de l'image originale nommée 'nom-bak.jpg'", "Create backup" => "Créer une sauvegarde", "Crop picture (lossless)" => "XXX Crop picture (sans perte)", "Date" => "Date", "Delete ..." => "Supprimer...", "Delete folder ..." => "Supprimer le répertoire...", "Delete to trash" => "Envoyer vers la corbeille", "Describe the format and characteristics of a picture" => "XXX décrire le format et les caractéristiques d'une image", "Designates in the form CCYYMMDD the earliest date the provider intends the object to be used. Follows ISO 8601 standard. Example: 19890317 indicates data for release on 17 March 1989. (8 chars)" => "XXX Designates in the form CCYYMMDD the earliest date the provider intends the object to be used. Follows ISO 8601 standard. Example: 19890317 indicates data for release on 17 March 1989. (8 chars)", "Designates in the form CCYYMMDD the latest date the provider or owner intends the objectdata to be used. Follows ISO 8601 standard. Example: 19940317 indicates an objectdata that should not be used after 17 March 1994." => "XXX Designates in the form CCYYMMDD the latest date the provider or owner intends the objectdata to be used. Follows ISO 8601 standard. Example: 19940317 indicates an objectdata that should not be used after 17 March 1994.", "Designates in the form HHMMSS+HHMM (or -HHMM) the earliest time the provider intends the object to be used. Follows ISO 8601 standard. Example: 090000-0500 indicates object for use after 0900 in New York (five hours behind UTC)" => "XXX Designates in the form HHMMSS+HHMM (or -HHMM) the earliest time the provider intends the object to be used. Follows ISO 8601 standard. Example: 090000-0500 indicates object for use after 0900 in New York (five hours behind UTC)", "Designates in the form HHMMSS+HHMM (or -HHMM) the latest time the provider or owner intends the objectdata to be used. Follows ISO 8601 standard. Example: 090000-0500 indicates an objectdata that should not be used after 0900 in New York (five hours behind UTC)." => "XXX Designates in the form HHMMSS+HHMM (or -HHMM) the latest time the provider or owner intends the objectdata to be used. Follows ISO 8601 standard. Example: 090000-0500 indicates an objectdata that should not be used after 0900 in New York (five hours behind UTC).", "Detach" => "Détacher", "Display 1/3 crop grid" => "XXX display 1/3 crop grid", "Display folder sizes (graphic)" => "XXX Afficher la taille des répertoires (graphique/visuel)", "Do lossless rotation of pictures" => "Effectuer une rotation des images sans perte", "Do nearly lossless interpolation (remove dead pixels)" => "XXX do nearly lossless interpolation (remove dead pixels)", "Donations for Mapivi" => "Dons pour Mapivi", "Donations" => "Dons", "Edit ..." => "Editer ...", "Edit database ..." => "Editer la base...", "Edit" => "Edition", "Email to ..." => "Envoyer par courriel à...", "Empty trash ..." => "Vider la corbeille...", "Enter the command to start the external mail tool here.\nExamles: \"thunderbird\", \"mozilla-thunderbird\", \"evolution\", \"icedove\", or\n\"C:\\Program Files\\Microsoft Office\\OFFICE11\\OUTLOOK.EXE\"" => "XXX Enter the command to start the external mail tool here.\nExamles: \"thunderbird\", \"mozilla-thunderbird\", \"evolution\", \"icedove\", or\n\"C:\\Program Files\\Microsoft Office\\OFFICE11\\OUTLOOK.EXE\"", "Enter the command to start the external picture editor here.\nYou may also add options.\nExamples: \"gimp-remote\" for UNIX\n, \"gimp-win-remote gimp-2.6.exe\" for Windows and GIMP > 2.0\n, \"gimp-win-remote\" for Windows and GIMP <= 2.0" => "XXX Enter the command to start the external picture editor here.\nYou may also add options.\nExamples: \"gimp-remote\" for UNIX\n, \"gimp-win-remote gimp-2.6.exe\" for Windows and GIMP > 2.0\n, \"gimp-win-remote\" for Windows and GIMP <= 2.0", "Enter the command to start the external picture viewer here.\nYou may also add options.\nExamles: \"gqview -f\", \"gthumb --fullscreen\", \"C:\\Program Files\\IrfanView\\iview_32.exe\"" => "XXX Enter the command to start the external picture viewer here.\nYou may also add options.\nExamles: \"gqview -f\", \"gthumb --fullscreen\", \"C:\\Program Files\\IrfanView\\iview_32.exe\"", "Enter the command to start the external web browser here.\nExamples: \"firefox\"" => "XXX Enter the command to start the external web browser here.\nExamples: \"firefox\"", "EXIF aperture" => "Ouverture (EXIF)", "EXIF artist" => "Artiste (EXIF)", "EXIF camera maker/model" => "Fabricant/modèle d'appareil (EXIF)", "EXIF date" => "Date (EXIF)", "EXIF exposure time" => "Temps d'exposition (EXIF)", "EXIF histogram" => "Histogramme EXIF", "EXIF histograms" => "Histogrammes EXIF", "Existing but unused translations in language: $config{Language}" => "XXX Existing but unused translations in language: $config{Language}", "Exposure time (1/s)" => "Temps d'exposition (1/s)", "F3: show/hide overlay text" => "XXX F3: show/hide overlay text", "File date" => "Date de fichier", "File name (A - Z)" => "Nom de fichier (A - Z)", "File name" => "Nom de fichier", "File operations ..." => "Opérations sur le fichier...", "File size of actual picture in kByte" => "XXX file size of actual picture in kByte", "File size" => "Taille de fichier", "File" => "Fichier", "Filename (including path) of default thumbnail." => "XXX Filename (including path) of default thumbnail.", "Film speed (ISO)" => "XXX Film speed (ISO)", "Filter pics by exclude keyword list (space separated string)" => "XXX filter pics by exclude keyword list (space separated string)", "First run ...\n" => "Premier démarrage ...\n", "First" => "Premier", "Flip horizontal (lossless)" => "Retourner horizontalement (sans perte)", "Flip vertical (lossless)" => "Retourner verticalement (sans perte)", "Focal distance (mm)" => "XXX Focal distance (mm)", "Focal distance in 35mm (mm)" => "XXX Focal distance in 35mm (mm)", "Folder checklist ..." => "Liste des répertoires...", "Folder preview" => "Prévisualisation du répertoire", "Folder size" => "Taille du répertoire", "Folder" => "Répertoire", "Found additional dirProperties. Merging information ...\n" => "XXX Found additional dirProperties. Merging information ...\n", "Gallery produced by mapivi" => "Galerie générée avec mapivi", "Handle EXIF infos and embedded thumbnail pictures" => "XXX handle EXIF infos and embedded thumbnail pictures", "Help" => "Aide", "Hot folders ..." => "Répertoires préférés... ", "I am always happy to receive some feedback about Mapivi!\n" => "Je suis toujours content d'avoir des retours sur Mapivi !\n", "Iconify" => "Réduire", "Identification of the name of the person involved in the writing, editing or correcting the objectdata or caption/abstract. (max. 32 chars)" => "XXX Identification of the name of the person involved in the writing, editing or correcting the objectdata or caption/abstract. (max. 32 chars)", "Identifies objectdata that recurs often and predictably. Enables users to immediately find or recall such an object. Example: EUROWEATHER" => "XXX Identifies objectdata that recurs often and predictably. Enables users to immediately find or recall such an object. Example: EUROWEATHER", "Identifies the location within a city. Examples: Capitol Hill, Maple Leaf Gardens, Strandgateparken (max. 32 chars)" => "XXX Identifies the location within a city. Examples: Capitol Hill, Maple Leaf Gardens, Strandgateparken (max. 32 chars)", "Identifies the original owner of the intellectual content of the objectdata. This could be an agency, a member of an agency or an individual. (The Source field lists who is the original provider of a photo, such as: AP, an AP member, pool photo provider or handout photo provider.) (max. 32 chars)" => "XXX Identifies the original owner of the intellectual content of the objectdata. This could be an agency, a member of an agency or an individual. (The Source field lists who is the original provider of a photo, such as: AP, an AP member, pool photo provider or handout photo provider.) (max. 32 chars)", "Identifies the person or organisation which can provide further background information on the objectdata. (max. 128 chars)" => "XXX Identifies the person or organisation which can provide further background information on the objectdata. (max. 128 chars)", "Identifies the provider of the objectdata, not necessarily the owner/creator. (The Credit field is the name of the service transmitting the photo) (max. 32 chars)" => "XXX Identifies the provider of the objectdata, not necessarily the owner/creator. (The Credit field is the name of the service transmitting the photo) (max. 32 chars)", "Identifies the subject of the objectdata in the opinion of the provider. A list of categories will be maintained by a regional registry, where available, otherwise by the provider. Note: Use of this DataSet is Deprecated. It is likely that this DataSet will not be included in further versions of the IIM. The Category field lists codes that aid in a more detailed search. (max. 3 chars)" => "XXX Identifies the subject of the objectdata in the opinion of the provider. A list of categories will be maintained by a regional registry, where available, otherwise by the provider. Note: Use of this DataSet is Deprecated. It is likely that this DataSet will not be included in further versions of the IIM. The Category field lists codes that aid in a more detailed search. (max. 3 chars)", "If enabled this function will filter the pictures using a keyword list." => "XXX If enabled this function will filter the pictures using a keyword list.", "Ignore selected keywords" => "Ignorer les mots-clés sélectionnés", "Ignore" => "Ignorer", "Import from all sub folders, too" => "Importer aussi de tous les sous-répertoires", "Import source" => "Source d'import", "Import wizard ..." => "Interface d'import...", "Invert selection" => "Inverser la sélection", "IPTC by-line" => "XXX IPTC by-line", "IPTC caption of displayed picture.\n" => "XXX IPTC caption of displayed picture.\n", "IPTC headline (title) of displayed picture.\n" => "XXX IPTC headline (title) of displayed picture.\n", "IPTC urgency/rating" => "XXX IPTC urgency/rating", "Join" => "Joindre", "Key shortcuts" => "Raccourcis-clavier", "Keys" => "Raccourcis-clavier", "Keyword" => "Mot-clé", "Keywordcloud" => "Nuage de mots-clés", "Last" => "Dernier", "Light table (slideshow) ..." => "Table lumineuse (diaporama)...", "Link to ..." => "Relier à...", "Loading ..." => "Chargement ...", "Location" => "Lieu", "Make a screenshot of a window or desktop" => "Effectuer une capture d'écran du bureau ou d'une fenêtre", "Make backup" => "Copie de sauvegarde", "Make HTML ..." => "Créer une page HTML...", "Mapivi can not find a home directory" => "XXX Mapivi can not find a home directory", "Mapivi Home" => "Site web de mapivi", "Mapivi log information\nClick to see complete log history." => "XXX Mapivi log information\nClick to see complete log history.", "Mapivi progress" => "XXX Mapivi progress", "Move to ..." => "Déplacer vers...", "Name of sub folder to store original pictures" => "XXX Name of sub folder to store original pictures", "Navigation rating constraint on/off" => "XXX Navigation rating constraint on/off", "Navigation" => "Navigation", "New folder ..." => "Nouveau répertoire...", "New IPTC keywords" => "Nouveaux mots-clés IPTC", "New options ..." => "Nouvelles options ...", "Next" => "Suivant", "No pictures in picLB!" => "XXX no pictures in picLB!", "No" => "XXX no", "Not displayed - no picture frame (hint: try F9 or F11)" => "XXX not displayed - no picture frame (hint: try F9 or F11)", "Not displayed (picture frame too small)" => "XXX not displayed (picture frame too small)", "Not displayed (unsupported picture format)" => "Pas d'affichage (format d'image non supporté)", "Number of background processes\n(generating thumbnail pictures)" => "XXX Number of background processes\n(generating thumbnail pictures)", "Number of bits per pixels (b/p)" => "Bits/pixels", "Number of non-JPEG files in the actual folder" => "XXX number of non-JPEG files in the actual folder", "Number of pixels" => "Nombre de pixels", "Number of thumbnails to generate/refresh" => "Nombre de vignettes à générer/raffraîchir", "Number of views" => "Nombre de visualisations", "Ok" => "Ok", "Open folder ..." => "Ouvrir le répertoire...", "Open trash in main window" => "Ouvrir la corbeille dans la fenêtre principale", "Options ..." => "Options...", "Options" => "Options", "Path to picture import folder" => "XXX Path to picture import folder", "Path" => "Chemin", "Pictures" => "Images", "Please edit the list of keywords to exclude.\nSeparate different keywords with a space." => "XXX Please edit the list of keywords to exclude.\nSeparate different keywords with a space.", "Please press Ok to remove all IPTC info of the %d selected pictures. There is no undo!" => "Veuillez cliquer sur 'OK' pour enlever toutes les informations IPTC des %d images sélectionnées. Il n'y aura pas de possibilité d'annuler !", "Preview folder" => "Aperçu du répertoire", "Previous" => "Précédent", "Printing histogram ..." => "XXX printing histogram ...", "Priority 0 meaning None, 1 meaning High to 8 meaning Low" => "XXX priority 0 meaning None, 1 meaning High to 8 meaning Low", "Provides full, publishable, name of the country/primary location where the intellectual property of the objectdata was created, according to guidelines of the provider. (max. 64 chars)" => "XXX Provides full, publishable, name of the country/primary location where the intellectual property of the objectdata was created, according to guidelines of the provider. (max. 64 chars)", "Quality (%)" => "Qualité (%)", "Quality of picture\nAppropriate settings are between 50% and 95%,\n80% is often a good tradeoff between size and quality for web and email\nfor further processing and best quality 95% is recommended\nValues over 95% just increase file size, not quality" => "Qualité de l'image\nLes réglages appropriés sont situés entre 50% et 95%,\n80% est souvent un bon compromis entre la taille et la qualité pour le web et le courriel\nPour des usages plus aboutis, 95% est recommandé\nLes valeurs supérieures à 95% ne font qu'augmenter la taille de l'image, pas sa qualité.", "Quantity" => "Quantité", "Quit" => "Quitter", "Rating (IPTC urgency) of actual picture\nTo change click on stars or use keys Ctrl-5 .. -1 or Ctrl-F1, -F2, ... -F8" => "XXX Rating (IPTC urgency) of actual picture\nTo change click on stars or use keys Ctrl-5 .. -1 or Ctrl-F1, -F2, ... -F8", "Rating" => "Note", "Read/write meta information in image files" => "Lire/écrire les métadonnées des images", "Ready!" => "Prêt !", "Ready" => "Prêt", "Rebuild selected thumbnails" => "Reconstruire les vignettes sélectionnées", "Redo selection" => "Refaire la sélection", "Reload pictures" => "Recharger les images", "Remove ..." => "Enlever ...", "Remove all IPTC info?" => "Enlever toutes les informations IPTC ?", "Remove selected keywords from selected pictures" => "Enlever les mots-clés sélectionnés des images sélectionnées", "Rename ..." => "Renommer...", "Rename folder ..." => "Renommer le répertoire...", "Replace" => "Remplacer", "Represented in the form HHMMSS+HHMM (or -HHMM) to designate the time the intellectual content of the objectdata current source material was created rather than the creation of the physical representation. Follows ISO 8601 standard. Where the time cannot be precisely determined, the closest approximation should be used. Example: 133015+0100 indicates that the object intellectual content was created at 1:30 p.m. and 15 seconds Frankfurt time, one hour ahead of UTC. (11 chars HHMMSS+HHMM)" => "XXX Represented in the form HHMMSS+HHMM (or -HHMM) to designate the time the intellectual content of the objectdata current source material was created rather than the creation of the physical representation. Follows ISO 8601 standard. Where the time cannot be precisely determined, the closest approximation should be used. Example: 133015+0100 indicates that the object intellectual content was created at 1:30 p.m. and 15 seconds Frankfurt time, one hour ahead of UTC. (11 chars HHMMSS+HHMM)", "Restart" => "Redémarrer", "Rotate ..." => "Rotation...", "Rotate 90 - right (lossless)" => "Rotation 90° - vers la droite (sans perte)", "Rotate 180 (lossless)" => "Rotation 180° - (sans perte)", "Rotate 270 - left (lossless)" => "Rotation 270° - vers la gauche (sans perte)", "Save options" => "Enregistrer les options", "Save the IPTC headline and caption to the file and database.\nPlease press this button after adding or editing." => "XXX Save the IPTC headline and caption to the file and database.\nPlease press this button after adding or editing.", "Save" => "Enregistrer", "Search ..." => "Recherche...", "Search by keywords (tag cloud) ..." => "Recherche par mots-clés (nuage) ...", "Search by location ..." => "Recherche par lieu...", "Search by timeline ..." => "Recherche par chronologie...", "Search duplicates ..." => "Recherche de doublons...", "Search for file name ..." => "Recherche du nom de fichier...", "Search for pictures with a certain aspect ratio of 2 >= 1 (panorama format)" => "XXX Search for pictures with a certain aspect ratio of 2 >= 1 (panorama format)", "Search for pictures with a certain aspect ratio on/off" => "XXX Search for pictures with a certain aspect ratio on/off", "Search for pictures with a certain aspect ratio" => "XXX Search for pictures with a certain aspect ratio", "Search in folder ..." => "Recherche dans le répertoire...", "Search rating constraint maximum (also used for navigation rating)" => "XXX Search rating constraint maximum (also used for navigation rating)", "Search rating constraint minimum (also used for navigation rating)" => "XXX Search rating constraint minimum (also used for navigation rating)", "Search rating constraint on/off" => "XXX Search rating constraint on/off", "Search" => "Recherche", "Searching ..." => "Recherche en cours ...", "Select ..." => "Sélectionner ...", "Select all backups" => "Sélectionner toutes les sauvegardes", "Select all" => "Tout sélectionner", "Select none" => "Aucune sélection", "Send pictures via email" => "Envoyer les images par courriel", "Set" => "Régler", "Show all" => "Tout montrer", "Show all pictures?" => "Montrer toutes les images ?", "Show filter by keywords button in thumbnail frame (needs restart)." => "XXX Show filter by keywords button in thumbnail frame (needs restart).", "Show non-JPEG files in the actual folder" => "XXX show non-JPEG files in the actual folder", "Show" => "Montrer", "Shows an animated folder preview using a thumbnail slideshow" => "XXX Shows an animated folder preview using a thumbnail slideshow", "Smart rename ..." => "Renommage intelligent...", "Smart update" => "Mise-à-jour intelligente", "Sort randomly" => "XXX Tri aléatoire", "Sort reverse" => "Tri inverse", "Sort" => "Tri", "Source folder / Import from" => "Répertoire-source / Importer de", "Special searches" => "Recherche spéciale", "Start test suite?" => "XXX Start test suite?", "Status of the objectdata, according to the practice of the provider. Examples: Lead, CORRECTION (max. 64 chars)" => "XXX Status of the objectdata, according to the practice of the provider. Examples: Lead, CORRECTION (max. 64 chars)", "Store picture rating not only in IPTC urgency, but also in XMP rating tag" => "XXX Store picture rating not only in IPTC urgency, but also in XMP rating tag", "Sub startup ...\n" => "XXX sub startup ...\n", "System information" => "Informations système", "Test suite must be started in a folder with at least two picture!" => "XXX test suite must be started in a folder with at least two picture!", "Test suite" => "XXX Test suite", "Thanks to the Tango Desktop Project for the nice icons!" => "Merci au projet Tango Desktop pour les belles icônes !", "The Caption field is the text that accompanies the photo, containing the who, what, when, where and why information (max. 2000 chars)" => "XXX The Caption field is the text that accompanies the photo, containing the who, what, when, where and why information (max. 2000 chars)", "The Caption Writer field lists the initials of all the people who wrote or edited the caption, header fields or image file. This includes toning and pixel editing" => "XXX The Caption Writer field lists the initials of all the people who wrote or edited the caption, header fields or image file. This includes toning and pixel editing", "The City field lists where the photo was originally made. For file photos, do not use the transmission point's city (max. 32 chars)" => "XXX The City field lists where the photo was originally made. For file photos, do not use the transmission point's city (max. 32 chars)", "The Country field lists the three-letter country code where the photo was originally made. For file photos, do not put the transmission points country. Examples: USA (United States), GER (Germany), FRA (France), XUN (United Nations) (max. 3 chars)" => "XXX The Country field lists the three-letter country code where the photo was originally made. For file photos, do not put the transmission points country. Examples: USA (United States), GER (Germany), FRA (France), XUN (United Nations) (max. 3 chars)", "The Create Date field is the date the photo was originally made. For file photos, use the date the photo was originally made, if known. If the complete date is not known, leaf the field blank. The field will not accept a partial date. (8 chars CCYYMMDD)" => "XXX The Create Date field is the date the photo was originally made. For file photos, use the date the photo was originally made, if known. If the complete date is not known, leaf the field blank. The field will not accept a partial date. (8 chars CCYYMMDD)", "The Headline field lists keywords to aid in a more detailed search for a photo. Example: Lindbergh Lands In Paris (max. 256 chars)" => "XXX The Headline field lists keywords to aid in a more detailed search for a photo. Example: Lindbergh Lands In Paris (max. 256 chars)", "The Instructions field lists special notations that apply uniquely to a photo, such as file photo, correction, advance or outs Examples: SECOND OF FOUR STORIES, 3 Pictures follow, Argentina OUT (max. 256 chars)" => "XXX The Instructions field lists special notations that apply uniquely to a photo, such as file photo, correction, advance or outs Examples: SECOND OF FOUR STORIES, 3 Pictures follow, Argentina OUT (max. 256 chars)", "The slideshow will not be saved automatically.\nAll changes will be lost.\nReally close light table?" => "XXX The slideshow will not be saved automatically.\nAll changes will be lost.\nReally close light table?", "The State field lists the state where the photo was originally made. Use U.S. postal code abbreviations. For file photos, do not use the transmission point's state. Examples: WA, Sussex, Baden-Wuerttenberg (max. 32 chars)" => "XXX The State field lists the state where the photo was originally made. Use U.S. postal code abbreviations. For file photos, do not use the transmission point's state. Examples: WA, Sussex, Baden-Wuerttenberg (max. 32 chars)", "The Supplemental Categories field lists codes that aid in a more detailed search for a photo." => "XXX The Supplemental Categories field lists codes that aid in a more detailed search for a photo.", "TOP 100 (best rated)" => "TOP 100 (les mieux notés)", "Trash" => "Corbeille", "Try to stop, please wait ..." => "Tentative d'arrêt, veuillez patientez ...", "Unknown navigation modus!" => "XXX Mode de navigation inconnu !", "Update" => "Mettre à jour", "Use left mouse button to move and adjust the crop frame" => "XXX Use left mouse button to move and adjust the crop frame", "Used as a shorthand reference for the object. Changes to existing data, such as updated stories or new crops on photos, should be identified in Edit Status. Examples: Wall St., Ferry Sinks. The Object Name field lists the story slug associated with a photo. For photos without a story, Associated Press photographers or photo editors will make up a logical slug to aid in a search and note it as a stand alone photo in the INSTRUCTIONS field of the NAA/IPTC header. If a related story moves on Data-Stream, the photo will be retransmitted with the appropriate OBJECT NAME to match the story. (max. 64 chars)" => "XXX Used as a shorthand reference for the object. Changes to existing data, such as updated stories or new crops on photos, should be identified in Edit Status. Examples: Wall St., Ferry Sinks. The Object Name field lists the story slug associated with a photo. For photos without a story, Associated Press photographers or photo editors will make up a logical slug to aid in a search and note it as a stand alone photo in the INSTRUCTIONS field of the NAA/IPTC header. If a related story moves on Data-Stream, the photo will be retransmitted with the appropriate OBJECT NAME to match the story. (max. 64 chars)", "View" => "Affichage", "Warning: The calculation wasn't finished!\nReal size may be bigger than displayed." => "XXX Warning: The calculation wasn't finished!\nReal size may be bigger than displayed.", "Where: a = morning, p = evening, b = both. Virtually only used in North America. (1 char)" => "XXX Where: a = morning, p = evening, b = both. Virtually only used in North America. (1 char)", "Width and height of actual picture in pixels" => "XXX width and height of actual picture in pixels", "x/y (z, s) = first selected picture is number x\nfrom y pictures in the actual folder\nz pictures are selected\ns is the file size of all selected pictures" => "XXX x/y (z, s) = first selected picture is number x\nfrom y pictures in the actual folder\nz pictures are selected\ns is the file size of all selected pictures", "Zoom factor of actual picture" => "XXX zoom factor of actual picture", ); languages/mapivi-lang-de0000775000000000000000000013145513006065062014253 0ustar rootroot# Mapivi german translation # remove the #-sign to uncomment translated strings # to translate more strings replace a string in mapivi.pl with the function call lang() # example: 'Copy selected' -> lang('Copy selected') # this works also with double quotes: "Copy selected" -> lang("Copy selected") # Warning: If the string contains control codes like newline (\n) double quotes have to be used! # Warning: If the string contains variables (e.g. $number) is has to be converted to a langf() statement # After the conversion the string must not contain a variable!!! The variables in the string # have to be replaced with format specifiers like e.g. %s. The variables are added separated # by commas after the string. See "perldoc -f sprintf" for documentation. # Example: before: print "The answer is $string and $int_number!\n"; # after: print langf("The answer is %s and %d!\n", $string, $int_number); # # todo: Clarify translations: # selected = ausgewählt oder markiert? %messages = ( "Folder" => "Verzeichnis", "File" => "Datei", "Path" => "Pfad", "Edit" => "Bearbeiten", "Show" => "Anzeigen", "Remove ..." => "Entfernen ...", "View" => "Ansicht", "Search" => "Suchen", "Incremental search" => "Inkrementell suchen", "Options" => "Einstellungen", "Help" => "Hilfe", "Open folder ..." => "Öffne Verzeichnis ...", "Open ..." => "Öffnen ...", "Preview folder" => "Verzeichnis Vorschau", "Preview" => "Vorschau", "Search in folder ..." => "Suche im Verzeichnis", #"folder size" => "Verzeichnisgröße", "Folder size" => "Verzeichnisgröße", "Folder size of %s" => "Verzeichnisgröße von %s", "Calculation finished." => "Berechnung beendet.", "Warning: The calculation wasn't finished!\nReal size may be bigger than displayed." => "Warnung: Berechnung wurde abgebrochen!\nDie wirkliche Größe ist größer als dargestellt.", "%s\nThe folder size of \"%s\" including thumbnails is\n\n%s (%d Bytes)\n\n%d file(s)\n%d folder(s)" => "%s\nDie Verzeichnisgröße von \"%s\" inklusive Thumbnails ist\n\n%s (%d Bytes)\n\n%d Datei(en)\n%d Verzeichnis(se)", "Rename folder ..." => "Verzeichnis umbenennen", "New folder ..." => "Neues Verzeichnis ...", "Make new folder" => "Neues Verzeichnis erstellen", "newfolder" => "NeuesVerzeichnis", "Enter name of new folder in %s" => "Namen des neuen Verzeichnisses in %s eingeben", "Error making folder %s/%s: %s" => "Fehler beim Erstellen des Verzeichnisses %s/%s: %s", "Delete folder ..." => "Verzeichnis löschen ...", "Delete folder?" => "Verzeichnis löschen?", "Hot folders ..." => "Wichtige Verzeichnisse", "File operations ..." => "Datei Operationen ...", "Trash" => "Müll", "Folder checklist ..." => "Verzeichnis Checkliste", "Folder Checklist" => "Verzeichnis Checkliste", "Folder Checklist Menu" => "Verzeichnis Checklistenmenü", "Import wizard ..." => "Bilderimport ...", "Picture collection" => "Bildersammlung", "Convert non-JPEG pictures ..." => "Nicht-JPEGs umwandeln ...", "Non-JPEG pictures" => "Nicht-JPEGs Bilder", "Hidden files" => "Versteckte Dateien", "List of hidden files in folder " => "Liste der versteckten Dateien im Verzeichnis ", "Play video" => "Video abspielen", "Edit picture" => "Bild editieren", "Number of hidden files in actual folder" => "Anzahl der versteckten Dateien im aktuellen Verzeichnis", "Reload pictures" => "Bilder neu laden", "Reload picture" => "Bild neu laden", "Load pictures" => "Lade Bilder", "Smart update" => "Intelligent neu laden", "Rebuild thumbnails ..." => "Thumbnails neu erzeugen ...", "Build thumbnails ..." => "Thumbnails erzeugen ...", "Iconify" => "Fenster minimieren", "Restart" => "Neu starten", "Quit" => "Beenden", "Calculate folder size" => "Berechne Verzeichnisgröße", "Display folder sizes (graphic)" => "Verzeichnisgrößen grafisch darstellen", "Folder Sizes" => "Verzeichnisgrößen", "Copy to ..." => "Kopieren nach ...", "Copy from ..." => "Kopieren von ...", "Copy thumbnail to ..." => "Kopiere Thumbnail nach ...", "Move to ..." => "Verschieben nach ...", "Link to ..." => "Verlinken mit ...", "Email to ..." => "Email an ...", "Convert ..." => "Umwandeln ...", "Copy to print ..." => "Kopieren für Druck ...", "Rename ..." => "Umbenennen ...", "Smart rename ..." => "Intelligent Umbenennen ...", "Make backup" => "Kopie (Backup)", "Make HTML ..." => "Webseiten (HTML) erzeugen ...", "Delete to trash" => "In den Müll verschieben", "Delete ..." => "Löschen ...", "Select ..." => "Auswählen ...", "Select all" => "Alle auswählen", "Select to end" => "Bis zum Ende auswählen", "Select none" => "Nichts auswählen", "Select all backups" => "Alle Backups auswählen", "Invert selection" => "Auswahl invertieren", "Redo selection" => "Auswahl wiederherstellen", "Rotate ..." => "Drehen ...", "Rotate 90 - right (lossless)" => "Drehen 90 - rechts (verlustlos)", "Rotate 180 (lossless)" => "Drehen 180 (verlustlos)", "Rotate 270 - left (lossless)" => "Drehen 270 - links (verlustlos)", "Flip horizontal (lossless)" => "horizontal spiegeln(verlustlos)", "Flip vertical (lossless)" => "vertikal spiegeln (verlustlos)", "Auto rotate (lossless)" => "Automatisch (verlustlos)", "Clear rotate flag" => "Drehflag zurücksetzen", "Empty trash ..." => "Mülleimer leeren ...", "Remove all" => "Alle löschen", "Remove" => "Löschen", "Empty trash?" => "Mülleimer leeren?", "Restore" => "Wiederherstellen", "restore ..." => "Wiederherstellen ...", "loading ..." => "laden ...", "loading picture" => "Lade Bild", "Errors" => "Fehler", "Original folder" => "Originalverzeichnis", "Size" => "Größe", "Errors while restoring selected picture(s):\n%s" => "Fehler beim Wiederherstellen der markierten Bilder:\n%s", "Could not restore %s (no folder information available)\n" => "Konnte %s nicht wiederherstellen (keine Verzeichnis bekannt)\n", "Press \"%s\" to delete all files (%d MB in %d files) from the trash.\nWarning: There is no undelete!\n\n(Trash folder: %s)" => "Drücke \"%s\" um alle Dateien im Mülleimer zu löschen (%d MB in %d Dateien).\nAchtung: Das Löschen lässt sich nicht rückgängig machen!!\n\n(Müllverzeichnis: %s)", "Remove selected files from trash.\nThere is no undo!" => "Lösche markierte Dateien.\nDas Löschen lässt sich nicht rückgängig machen!", "Restore selected files from trash to original folder." => "Verschiebe markierte Dateien zurück in das Originalverzeichnis.", "Trash is now empty!" => "Mülleimer ist jetzt geleert!", "Removed %d file(s) from trash!" => "%d Datei(en) gelöscht!", "Restored %s file(s) from trash!" => "%d Datei(en) wieder hergestellt!", "Open trash in main window" => "Mülleimer im Hauptfenster öffnen", "Next" => "Nächstes", "Previous" => "Vorheriges", "First" => "Erstes", "Last" => "Letzes", "About" => "Über", "Keys" => "Tasten", "System information" => "Systeminformation", "Donations" => "Spenden", "Comments" => "Kommentare", "comment" => "Kommentar", "Comment" => "Kommentar", "Sort" => "Sortieren", "File name (A - Z)" => "Dateiname (A - Z)", "File name (Z - A)" => "Dateiname (Z - A)", "Date (new first)" => "Datum (neu zuerst)", "Date (old first)" => "Datum (alt zuerst)", "Rating (high first)" => "Bewertung (hoch zuerst)", "Rating (low first)" => "Bewertung (niedrig zuerst)", "File name" => "Datei Name", "File date" => "Datei Datum", "File size" => "Datei Größe", "IPTC urgency/rating" => "Bewertung (IPTC urgency)", "IPTC by-line" => "Ersteller (IPTC by-line)", "Number of views" => "Anzahl der Betrachtungen", "Number of pixels" => "Anzahl der Pixel", "Number of bits per pixels (b/p)" => "Bits pro Pixel", "EXIF date" => "Datum (EXIF)", "EXIF aperture" => "Blende (EXIF)", "EXIF exposure time" => "Belichtungszeit (EXIF)", "EXIF camera maker/model" => "Kamera Hersteller/Modell (EXIF)", "EXIF artist" => "Künstler (EXIF)", "Sort randomly" => "Zufällig sortieren", "Sort reverse" => "Rückwärts sortieren", "Search ..." => "Suche ...", "Advanced Search" => "Erweiterte Suche", "Advanced search ..." => "Erweiterte Suche ...", "Search by keywords (tag cloud) ..." => "Suche nach Stichwort (tag cloud) ...", "Search by timeline ..." => "Suche nach Datum ...", "Search by location ..." => "Suche nach Orten ...", "Search duplicates ..." => "Suche nach Duplikaten ...", "Special searches" => "Spezielle Suche", "TOP 100 (best rated)" => "TOP 100 (beste Bewertung)", "EXIF histogram" => "EXIF Histogramm", "Search for file name ..." => "Suche nach Dateiname ...", "Search for date/time" => "Suche nach Datum/Zeit", "Pictures with creation date/time" => "Bilder mit Erstellungsdatum/zeit", "Pictures with creation date/time" => "Bilder mit Erstellungsdatum/zeit", "Add to database ..." => "Zur Datenbank hinzufügen ...", "Clean database ..." => "Datenbank bereinigen ...", "Clean database" => "Datenbank bereinigen", "Start cleaning" => "Reinigung starten", "Cleaning database" => "Reinige Datenbank", "checking" => "prüfe", "ignoring" => "ignoriere", "Cleaning database - ready" => "Reinigung der Datenbank abgeschlossen", "Check database ..." => "Datenbank überprüfen ...", "Edit database ..." => "Datenbank editieren ...", "Database information ..." => "Datenbank Information ...", "Options ..." => "Einstellungen ...", "Other options ..." => "Weitere Einstellungen ...", "Save options" => "Einstellungen speichern", "Ready!" => "Fertig!", "Quality (%)" => "Qualität (%)", "Quality of picture\nAppropriate settings are between 50% and 95%,\n80% is often a good tradeoff between size and quality for web and email\nfor further processing and best quality 95% is recommended\nValues over 95% just increase file size, not quality" => "Qualität des Bildes\nSinnvolle Einstellungen liegen zwischen 50% und 95%,\n80% ist ein guter Kompromiss für Webseiten bzw. Email\nFür beste Qualität oder falls das Bild weiter bearbeitet werden soll sind 95% empfehlenswert.\nWerte über 95% vergrößern nur die Dateigröße, nicht die Qualität.", "Remove all IPTC info?" => "Alle IPTC Informationen entfernen?", "Please press Ok to remove all IPTC info of the %d selected pictures. There is no undo!" => "Bitte OK drücken um alle IPTC Informationen der %d ausgewählten Bilder zu entfernen. Achtung es gibt kein Undo!", "Use left mouse button to move and adjust the crop frame" => "Mit dem linken Mausknopf läßt sich der Rahmen verschieben und in der Größe verändern", "Crop picture (lossless)" => "Bild (verlustlos) beschneiden", "Help" => "Hilfe", "Display 1/3 crop grid" => "Zeige 1/3 Gitter", "Create backup" => "Erstelle Backup", "Create a backup of the original picture in the same folder named name-bak.jpg" => "Erstelle ein Backup des original Bildes im gleichen Verzeichnis. Name des Backups: name-bak.jpg", "Add comment" => "Füge Kommentar ein", "Add a comment to pictures created or processed with Mapivi" => "Füge einen Kommentar in Bilder die mit Mapivi bearbeitet oder erstellt wurden ein", "Busy (loading pic), please retry later" => "Ausgelastet (lade Bild), bitte später noch mal versuchen", "Mapivi Home" => "Mapivi im Web", "Rating" => "Bewertung", "rating" => "Bewertung", "Location" => "Ort", "Date" => "Datum", "date" => "Datum", "Keywordcloud" => "Stichwort Cloud", "Keyword" => "Stichwort", "Unknown navigation modus!" => "Unbekannter Navigationsmodus!", "Add" => "Anfügen", "Update" => "Aktualisieren", "Reload thumbnails" => "Thumbnails neu laden", "Replace" => "Ersetzen", "Attach" => "Anfügen", "Detach" => "Entfernen", "Join" => "Zusammen", "All" => "Alle", "Last" => "Letztes", "Add selected keywords to selected pictures" => "Ausgewählte Stichwörter in ausgewählte Bilder schreiben", "Remove selected keywords from selected pictures" => "Ausgewählte Stichwörter aus ausgewählten Bilder entfernen", "Pictures" => "Bilder", "pictures" => "Bilder", "\n\nThis button is intended to be used as\npersonal markers for the folder status.\nSee also Menu: " => "\n\nDiese Knöpfe können als persönliche Ordnerkennzeichnung benutzt werden.\nSiehe auch Menü: ", "You may use this marker e.g. to\nmark this folder as sorted out.%s" => "Diese Markierung kann z.B. dazu benützt werden\nden Ordner als aussortiert zu kennzeichnen.%s", "You may use this marker e.g. to\nmark this folder, if all meta information is added.%s" => "Diese Markierung z.B. kann dazu benützt werden\nOrdner mit vollständiger Metainformation zu kennzeichnen.%s", "You may use this marker e.g. to\nmark this folder, if all pictures are rated.%s" => "Diese Markierung z.B. kann dazu benützt werden\nOrdner mit vollständiger Bewertung zu kennzeichnen.%s", "Collection will not be saved automatically.\nAll changes will be lost.\nReally close collection?" => "Die Sammlung wird nicht automatisch gespeichert.\nAlle Änderungen gehen verloren.\nSammlung wirklich schließen?", "Close collection?" => "Sammlung schließen?", "About Mapivi" => "Über Mapivi", "I am always happy to receive some feedback about Mapivi!\n" => "Über Feedback zu Mapivi freue ich mich jederzeit!\n", "Close" => "Schließen", " Could not check actual Mapivi version. No internet connection.\n" => " Konnte aktuelle Mapivi Version nicht ueberpruefen. Keine Internetverbindung.\n", # Hier keine Umlaute, da Output auf Shell bzw. DOX-Box " A newer version of Mapivi is available (V%s), see http://mapivi.sourceforge.net/\n" => " Eine neuere Mapivi Version (V%s) ist verfuegbar, siehe http://mapivi.sourceforge.net/\n",# Hier keine Umlaute! " Mapivi %s is up-to-date!\n" => " Mapivi %s ist aktuell!\n",# Hier keine Umlaute! "Key shortcuts" => "Tastaturkürzel", "Donations for Mapivi" => "Spenden für Mapivi", "Mapivi is free software.\n" => "Mapivi ist freie Software.\n", "You have used Mapivi %d times." => "Sie haben Mapivi %d mal benutzt.", "Add or edit text and then save it by pressing the save button.\nPress F4 to show or hide this box." => "Text hinzufügen oder ändern und dann durch Drücken auf den Knopf speichern sichern.\nTaste F4 drücken um dieses Feld anzuzeigen oder zu verstecken.", "IPTC headline (title) of displayed picture.\n" => "IPTC headline (Überschrift) des aktuellen Bildes.\n", "IPTC caption of displayed picture.\n" => "IPTC caption (Bildtext) des aktuellen Bildes.\n", "save" => "Speichern", "Save" => "Speichern", "Save as ..." => "Speichern unter ...", "F3: show/hide overlay text" => "F3: Text anzeigen/verstecken", "\n\ntotal time about %d:%02d, finished in about %d:%02d" => "\n\nGesamtdauer ca. %d:%02d, fertig in ca. %d:%02d", "%d%% done, time elapsed %d:%02d%s" => "%d%% erledigt, Dauer bisher %d:%02d%s", "%d%% done" => "%d%% erledigt", "0% done\n\n\n" => "0% erledigt\n\n\n", "Mapivi progress" => "Mapivi Fortschritt", "Try to stop, please wait ..." => "Versuche anzuhalten, bitte warten ...", "Cancel" => "Abbrechen", "Apply" => "Anwenden", "New IPTC keywords" => "Neue IPTC Stichworte", "Found %d new IPTC keywords, please choose how to proceed." => "%d neue IPTC Stichworte gefunden, bitte weiteres Vorgehen wählen.", "Quantity" => "Anzahl", "Add" => "Hinzufügen", "Ignore" => "Ignorieren", "Add selected keywords to keyword catalog" => "Ausgewählte Stichworte zum Stichwortkatalog hinzufügen", "Ignore selected keywords" => "Ausgewählte Stichworte ignorieren", "Check for new keywords" => "Auf neue Stichwörter prüfen", "Ask later" => "Später noch mal fragen", "Add lossless border" => "Rahmen verlustlos hinzufügen", "Not displayed (unsupported picture format)" => "Nicht angezeigt (nicht unterstütztes Bildformat)", "Not displayed - no picture frame (hint: try F9 or F11)" => "Nicht angezeigt - kein Bildfeld (Hinweis: Versuche F9 oder F11)", "Not displayed (picture frame too small)" => "Nicht angezeigt (Bildfeld zu klein)", "The database contains %d pictures, database file size is %s" => "Die Datenbank enthält %d Bilder, die Dateigröße der Datenbank ist %s", "%d pictures in database" => "Die Datenbank enthält %d Bilder", "IPTC urgency to XMP rating mapping (IPTC -> XMP):" => "Abbildung der IPTC Urgency auf das XMP Rating (IPTC -> XMP):", "XMP rating to IPTC urgency mapping (XMP -> IPTC):" => "Abbildung des XMP Rating auf die IPTC Urgency (XMP -> IPTC):", "Copy IPTC urgency to XMP rating" => "Kopieren: IPTC Bewertung -> XMP Bewertung", "Copy XMP rating to IPTC urgency" => "Kopieren: XMP Bewertung -> IPTC Bewertung", "Copy XMP keywords to IPTC" => "Kopieren: XMP Stichworte -> IPTC Stichworte", "Select folders" => "Verzeichnisse auswählen", "copy keywords" => "Stichwörter kopieren", "copy rating" => "Bewertung kopieren", "Copy ratings XMP to IPTC ..." => "Bewertungen von XMP nach IPTC kopieren ...", "Copy ratings IPTC to XMP ..." => "Bewertungen von IPTC nach XMP kopieren ...", "Copy keywords XMP to IPTC ..." => "Stichworte von XMP nach IPTC kopieren ...", " Copy keywords in %s ..." => " Kopiere Stichworte in %s ...", " Copy ratings in %s ..." => " Kopiere Bewertungen in %s ...", "processing picture (%d/%d) %s" => "Bearbeite Bild (%d/%d) %s", " Copied XMP to IPTC keywords in %d pictures finished." => " Kopieren von Stichworten von XMP nach IPTC in %d Bildern beendet.", " Copy of %d IPTC ratings to XMP finished." => " Kopieren von %d Bewertungen von IPTC nach XMP beendet.", " Copy of %d XMP ratings to IPTC finished." => " Kopieren von %d Bewertungen von XMP nach IPTC beendet.", "Mapivi will first scan through all sub folders of %s and collect all folders containing pictures." => "Mapivi wird alle Unterverzeichnisse von %s durchsuchen und alle Bilderordner sammeln.", "Then you are able to select in which folders Mapivi should copy XMP keywords (HierachicalSubject) to IPTC keywords." => "Dann können die Verzeichnisse ausgewählt werden, in denen Mapivi Stichworte von XMP (HierachicalSubject) nach IPTC kopieren soll.", "Then you are able to select in which folders Mapivi should copy the XMP rating to IPTC urgency." => "Dann können die Verzeichnisse ausgewählt werden, in denen Mapivi die Bewertung von XMP nach IPTC kopieren soll.", "Then you are able to select in which folders Mapivi should copy the IPTC urgency to XMP rating." => "Dann können die Verzeichnisse ausgewählt werden, in denen Mapivi die Bewertung von IPTC nach XMP kopieren soll.", "Found %d folders with %d pictures." => "%d Verzeichnisse mit %d Bildern gefunden.", "IPTC tags will be set only in the selected folders." => "IPTC Tags werden nur in den ausgewählten Verzeichnissen geschrieben.", "XMP tags will be set only in the selected folders." => "XMP Tags werden nur in den ausgewählten Verzeichnissen geschrieben.", "Overwrite IPTC ratings?" => "IPTC Bewertungen überschreiben?", "Overwrite XMP ratings?" => "XMP Bewertungen überschreiben?", "Overwrite existing IPTC rating with XMP rating?\nHint: If no XMP rating is defined, the IPTC rating is not changed.\n" => "Sollen vorhandene IPTC Bewertungen mit XMP Bewertungen überschrieben werden?\nHinweis: Wenn keine XMP Bewertung vorliegt, wird die IPTC Bewertung nicht geändert.\n", "Overwrite existing XMP ratings with IPTC ratings?\nHint: If no IPTC rating is defined, the XMP rating is not changed.\n" => "Sollen vorhandene XMP Bewertungen mit IPTC Bewertungen überschrieben werden?\nHinweis: Wenn keine IPTC Bewertung vorliegt, wird die XMP Bewertung nicht geändert.\n", "No overwrite" => "Nicht überschreiben", "Overwrite" => "Überschreiben", "Copy rating XMP to IPTC errors and infos" => "Informationen und Fehler beim Kopieren von Bewertungen von XMP nach IPTC", "Copy rating IPTC to XMP errors and infos" => "Informationen und Fehler beim Kopieren von Bewertungen von IPTC nach XMP", "action canceled by user" => "Vorgang durch Benutzer abgebrochen", "Keyword clipboard menu" => "Stichwort Zwischenablagen Menü", "Clear keyword clipboard" => "Lösche Stichwort Zwischenablage", "Remove selected keyword(s) from clipboard" => "Entferne markierte Stichworte aus der Zwischenablage", "add to clipboard" => "In Zwischenablage kopieren", "add new item" => "Neuen Begriff hinzufügen", "add new sub item" => "Neuen Unterbegriff hinzufügen", "New item" => "Neuer Begriff", "New sub item" => "Neuer Unterbegriff", "Please enter the new item (below %s)" => "Bitte neuen Begriff eingeben (unterhalb von %s)", "Please enter the new sub item (below %s)" => "Bitte neuen Unterbegriff eingeben (unterhalb von %s)", "rename or move item" => "Begriff umbenennen oder verschieben", "delete item(s)" => "Begriff(e) löschen", "search for item(s)" => "Nach Begriff(en) suchen", "Tree edit menu" => "Baumbearbeitungsmenü", "Thumbnail Menu" => "Thumbnail Menü", "Picture Menu" => "Bilder Menü", "Open picture in new window" => "Bild in neuem Fenster öffnen", "Toggle layout" => "Layout umschalten", "Fullscreen" => "Vollbildmodus", "Fullscreen On" => "Vollbildmodus An", "Fullscreen Off" => "Vollbildmodus Aus", "Change size/quality ..." => "Größe/Qualität ändern ...", "Crop (lossless) ..." => "Beschneiden (verlustlos) ...", "Image processing ..." => "Bildbearbeitung ...", "Add border ..." => "Rahmen hinzufügen ...", "Border menu" => "Rahmen Menü", "Add border (lossless) ..." => "Rahmen hinzufügen (verlustlos) ...", "Add border aspect ratio (lossless) ..." => "Rahmen im Seitenverhältnis (verlustlos) ...", "Aspect ratio" => "Seitenverhältnis", "Actual aspect ratio:" => "Aktuelles Seitenverhältnis:", "Add relative border (lossless) ..." => "Rahmen mit relative Breite (verlustlos) ...", "Add border or copyright (lossy) ..." => "Rahmen oder Copyright (verlustbehaftet) ...", "Image processing extern" => "Bildbearbeitung extern", "Collage/index print ..." => "Collage/Indexprint ...", "Processing preview ..." => "Bearbeite Vorschau ...", "Preview finished" => "Vorschau fertig", "Presets" => "Voreinstellungen", "Convert to black and white" => "Nach Schwarz/Weiß wandeln", "Black and white preview" => "Schwarz/Weiß Vorschau", "Red channel (%)" => "Rotkanal (%)", "Green channel (%)" => "Grünkanal (%)", "Blue channel (%)" => "Blaukanal (%)", "Keep brightness" => "Helligkeit erhalten", "Add border or text (not visible in preview)" => "Rahmen oder Text hinzufügen (in Vorschau nicht sichtbar)", "Convert %d picture(s) to black and white.\nPress OK to continue." => "%d Bild(er) nach Schwarz/Weiß wandeln.\nZum Fortsetzen OK drücken.", "Converting %s using all %d presets. Press %s to stop.\n" => "Wandle %s mit allen %d Voreinstellungen. Drücke %s zum Beenden.\n", "Converting to %s (%d/%d) ..." => "Wandle nach %s (%d/%d) ...", "Save changes?" => "Änderungen speichern?", "Headline/caption of %s have been changed.\nShould Mapivi save the changes?", "Überschrift/Bildtext von %s wurden verändert.\nSoll Mapivi die Änderungen speichern?", "The selection (%s) does not contain the actual picture (%s)." => "Die Auswahl (%s) enthält nicht das aktuelle Bild (%s).", "Add headline and caption to:" => "Überschrift und Bildtext speichern in:", "Add to selection or to actual picture?" => "Zur Auswahl oder zum aktuellen Bild hinzufügen?", "%d pic(s)" => "%d Bild(er)", "actual" => "aktuelles", "Move to \"%s\" folder" => "Verschieben ins \"%s\" Verzeichnis", "Copy to \"%s\" folder" => "Kopieren ins \"%s\" Verzeichnis", "Operation cancelled\n" => "Vorgang abgebrochen\n", "Processed %d/%d picture(s)." => "%d/%d Bild(er) bearbeitet.", "\"%s\" exists. Do you want to overwrite it?" => "\"%s\" existiert. Wirklich überschreiben?", "this file" => "Diese Datei", "will overwrite this file" => "wird diese Datei überschreiben", "%d files to go ..." => "noch %d Dateien ...", "Overwrite" => "Überschreiben", "Overwrite all" => "Alle überschreiben", "Cancel all" => "Alles abbrechen", "Display a clock in the status bar" => "Aktuelle Uhrzeit in Statuszeile anzeigen", "Check for Mapivi updates" => "Auf Mapivi Updates prüfen", "Window layout" => "Fenster Layout", "3 columns: Navigation Thumbnails Picture" => "3 Spalten: Navigation Thumbnails Bild", "2 columns: Navigation Thumbnails" => "2 Spalten: Navigation Thumbnails", "1 column: Thumbnails" => "1 Spalte: Thumbnails", "2 columns: Thumbnails Picture" => "2 Spalten: Thumbnails Bild", "1 column: Picture" => "1 Spalte: Bild", "2 columns: Navigation Picture" => "2 Spalten: Navigation Bild", "Menu bar" => "Menüleiste", "Status bar" => "Statusleiste", "Picture metadata overlay" => "Bild mit Metadaten überlagern", "IPTC box" => "IPTC Feld", "Comment box" => "Kommentar Feld", "Display coordinates" => "Koordinaten anzeigen", "Wrong selection" => "Falsche Auswahl", "item(s)" => "Objekt(e)", "Please select exactly %d %s!" => "Bitte genau %d %s auswählen!", "Please select at least %d %s!" => "Bitte mindestens %d %s auswählen!", "Please select not more than %d %s!" => "Bitte nicht mehr als %d %s auswählen!", "picture(s)" => "Bild(er)", "picture" => "Bild", "keyword(s)" => "Stichwort(e)", "location(s)" => "Ort(e)", "Navigation by folders" => "Navigation nach Verzeichnis", "Navigation by date" => "Navigation nach Datum", "Navigation by picture collection" => "Navigation nach Bildersammlung", "Navigation by location" => "Navigation nach Ort", "Navigation by keyword cloud" => "Navigation nach Stichwort Cloud", "Navigation by keyword" => "Navigation nach Stichwort", "Picture search" => "Bildersuche", "Loading thumbnails ..." => "Lade Thumbnails ...", "Loading %d thumbnails ..." => "Lade %d Thumbnails ...", "user abord (not all pictures are loaded!)" => "Abbruch durch Benutzer (nicht alle Bilder wurden geladen!)", "Mapivi log information\nHint: Click to see complete log history." => "Mapivi Protokoll\nHinweis: Hier klicken um komplettes Protokoll anzuzeigen.", "Mapivi log" => "Mapivi Protokoll", "Compare folders" => "Verzeichnisse vergleichen", "Compare" => "Vergleiche", "Compare by " => "Vergleiche ", "Copy A->B" => "Kopiere A->B", "Copy A<-B" => "Kopiere A<-B", "Delete A" => "Lösche A", "Delete B" => "Lösche B", "Compare progress" => "Vergleichsfortschritt", "Error" => "Fehler", "Folder \"%s\" is not valid!" => "Verzeichnis \"%s\" ist ungültig!", "Please choose two different folders!" => "Bitte unterschiedliche Verzeichnisse auswählen!", "Found %d unique pictures in A, %d unique pictures in B and %d matching pictures" => "%d Einzelbilder in A, %d Einzelbilder in B und %d übereinstimmende Bilder gefunden", " (%d of them differ)" => " (%d davon sind unterschiedlich)", "Database Statistics" => "Datenbankstatistik", "No changes since last call." => "Keine Änderungen seit dem letzten Aufruf.", "pictures with " => "Bilder mit ", "Changes to last database statistic" => "Änderungen seit der letzten Statistik", ", which was %s ago" => " vor %s", "seconds" => "Sekunden", "second(s)" => "Sekunde(n)", "minutes" => "Minuten", "hours" => "Stunden", "days" => "Tage", "EXIF data" => "EXIF Daten", "IPTC data" => "IPTC Daten", "keywords" => "Stichworten", "stars" => "Sterne", "star" => "Stern", "Remove rating" => "Bewertung entfernen", "Export filelist ..." => "Dateiliste exportieren ...", "Compare pictures" => "Bilder vergleichen", "Interpolate dead pixels ..." => "Defekte Pixel interpolieren ...", "Fuzzy border (lossy) ..." => "Unscharfer Rahmen (verlustbehaftet) ...", "Fuzzy border" => "Unscharfer Rahmen", "Drop picture (lossless) ..." => "Bild überlagern (verlustlos) ...", "Generate logo ..." => "Logo erzeugen ...", "Make screenshot ..." => "Bildschirmfoto ...", "Build thumbnails ..." => "Thumbnails erzeugen ...", "Clean thumbnails ..." => "Thumbnails aufräumen ...", "Clean folder ..." => "Verzeichnis aufräumen ...", "Edit entry history ..." => "Eingabeprotokoll ändern ...", "Session info" => "Sitzungsinformationen", "Mapivi test suite" => "Mapivi Tests", "Translation scan" => "Übersetzungstest", "go to/select ..." => "Gehe zu / Auswählen ...", "Auto zoom (fit picture)" => "Zoom automatisch (Bild anpassen)", "fit" => "anpassen", "fill" => "auffüllen", "Zoom menu" => "Zoom Menü", "Open pictures in new window" => "Bilder in neuem Fenster öffnen", "Open pictures in external viewer" => "Bilder in externem Betrachter öffnen", "Open selected pictures in external viewer" => "Ausgewählte Bilder in externem Betrachter öffnen", "Picture information" => "Bildinformationen", "Histogram (ImageMagick)" => "Histogramm (ImageMagick)", "Histogram (Mapivi)" => "Histogramm (Mapivi)", "Show JPEG segments" => "JPEG Segmente anzeigen", "Start/stop slideshow" => "Diashow starten/anhalten", "Slideshow Start" => "Diashow Start", "Slideshow End" => "Diashow Ende", "Slideshow" => "Diashow", "Slideshow animation" => "Diashow mit Animation", "Slideshow menu" => "Diashow Menü", "Show slideshow" => "Diashow vorführen", "Show slideshow, start from selected picture" => "Diashow vorführen, ab markiertem Bild", "Show selected pictures" => "Markierte Bilder anzeigen", "Add list ..." => "Liste hinzufügen ...", "Clear" => "Löschen", "Context Menu" => "Kontextmenü", "move selected to top" => "Markierte an den Anfang verschieben", "move selected to bottom" => "Markierte an das Ende verschieben", "remove selected from collection" => "Markierte aus Sammlung entfernen", "copy and rename selected" => "Markierte kopieren und umbenennen", "Next picture" => "Nächstes Bild", "Previous picture" => "Vorheriges Bild", "Show picture info" => "Bilderinformation anzeigen", "Show picture list" => "Bilderliste anzeigen", "Picture as desktop background" => "Bild als Bildschirmhintergrund", "Thumbnail table" => "Thumbnail Tabelle", "Thumbnail caption" => "Thumbnail Beschriftung", "Show file info" => "Dateiinformation anzeigen", "Show IPTC" => "IPTC anzeigen", "Show comments" => "Kommentare anzeigen", "Show EXIF" => "EXIF anzeigen", "Show folder" => "Verzeichnis anzeigen", "None" => "Keine", "File name without suffix" => "Dateiname ohne Dateiendung", "File name with suffix" => "Dateiname mit Dateiendung", "IPTC object name" => "IPTC Objekt Name", "Reload picture meta information" => "Bild-Metainformation neu laden", "Add to collection" => "Zur Sammlung hinzufügen", "added to collection: " => "Zur Sammlung hinzugefügt: ", "Show GPS position in map" => "GPS Position auf Karte anzeigen", "Open folder" => "Verzeichnis öffnen", "Open this folder" => "Dieses Verzeichnis öffnen", "Folder list" => "Verzeichnisliste", "Show unfinished" => "Unerledigte anzeigen", "Show finished" => "Erledigte anzeigen", "1 Sort" => "1 Sortiert", "2 Meta" => "2 Meta", "3 Rating" => "3 Bewertet", "Add all sub folders to list" => "Alle Unterverzeichnisse zur Liste hinzufügen", "Remove selected from list" => "Entferne markierte Elemente aus der Liste", "Edit folder comment" => "Verzeichniskommentar bearbeiten", "set" => "Setzen", "reset" => "Zurücksetzen", "Deleted %d of %d pictures" => "%d von %d Bildern gelöscht", "Flags" => "Markierungen", "Red flag" => "Rote Markierung", "Green flag" => "Grüne Markierung", "Blue flag" => "Blaue Markierung", "Flag pictures used in collections" => "Markiere in Sammlungen verwendete Bilder", "Reset all flags" => "Alle Markierungen zurücksetzen", "Ready! Flag %d toggled in %d pictures." => "Fertig! Markierung %d in %d Bildern umgeschaltet.", "Import pictures wizard" => "Bilderimport Assistent", "Picture import finished successfully!" => "Bilderimport erfolgreich beendet!", "Picture import finished with errors!" => "Bilderimport mit Fehlern beendet!", "collapse all" => "Alle einklappen", "expand all" => "Alle ausklappen", "collapse to first sub level" => "Zur ersten Ebene einklappen", "Clear filter and collapse tree" => "Filter zurücksetzen und Baum einklappen", "Filter:\nEnter any word or part of it\nto filter tree." => "Filter:\nEin Wort oder ein Teil davon eingeben\num den Baum zu filtern.", "showPic: error %s not available!" => "showPic: Fehler %s nicht verfügbar!", "showPic: error loading %s!" => "showPic: Fehler beim Laden von %s!", "showPic: loading %s\n" => "showPic: lade %s\n", "Loading %s ..." => "Lade %s ...", "Error: No pictures in listbox" => "Fehler: Keine Bilder in der Liste", "zooming to %s ..." => "zoome auf %s ...", "Picture filter settings" => "Bilderfilter Einstellungen", "Filter all accessible pictures of your database to create a slideshow." => "Alle zugreifbaren Bilder aus der Datenbank filtern um eine Diashow zu erstellen.", "Exclude pictures ..." => "Bilder ausschließen ...", "Include pictures ..." => "Bilder einschließen ...", "Sort pictures ..." => "Bilder sortieren ...", "with rating lower than" => "mit Berwertung kleiner als", "without rating" => "ohne Bewertung", "shown more often than" => "öfters angesehen als", "matching one of these keywords (space separated, case insensitive)" => "nach Stichworten (getrennt mit Leerzeichen, Groß- und Kleinschreibung wird ignoriert)", "in folders matching (space separated, case insensitive)" => "nach Verzeichnissen (getrennt mit Leerzeichen, Groß- und Kleinschreibung wird ignoriert)", "by random order" => "in zufälliger Reihenfolge", "Slideshow all pictures with filter" => "Diashow alle Bilder mit Filter", "Start" => "Start", "Settings" => "Einstellungen", "Could not retrieve %s" => "Konnte %s nicht auffinden", "Actual view as chosen in the navigation frame.\nClick opens folder selection dialog." => "Aktuelle Ansicht entsprechend Navigationsmodus.\nAnklicken öffnet Verzeichnisdialog.", "Update locations from database" => "Orte aus Datenbank neu laden", "Best rated pictures - TOP 100" => "Die TOP 100 der besten Bilder", "empty" => "leer", "Add/remove categories ..." => "Kategorien bearbeiten", "Save template ..." => "Template speichern ...", "Merge template ..." => "Template hinzufügen ...", "show info" => "Zeige Informationen", "show GPS position in map" => "Zeige GPS Position auf Karte", "show thumbnail" => "Thumbnail anzeigen", "save thumbnail ..." => "Thumbnail speichern ...", "(re)build thumbnail ..." => "Thumbnail (neu) erzeugen ...", "remove saved info ..." => "Entferne gespeicherte Information ...", "set date/time ..." => "Datum/Zeit setzen ...", "set GPS position ..." => "GPS Position setzen ...", "set year from file name ..." => "Jahr aus Dateinamen setzen ...", "remove thumbnail ..." => "Thumbnail entfernen ...", "remove all ..." => "Alles entfernen ...", "Select target folder" => "Zielverzeichnis auswählen", "No folder selected. Please select a folder in the navigation frame first." => "Kein Verzeichnis ausgewählt. Bitte zuerst ein Verzeichnis im Navigationsfeld auswählen.", "Copy pictures to folder" => "Kopiere Bilder ins Verzeichnis", "Move pictures to folder" => "Verschiebe Bilder ins Verzeichnis", "Copy meta information from " => "Kopiere Metainformation von ", "Meta information from %s added to %d picture(s)." => "Metainformation von %s zu %d Bild(ern) hinzugefügt.", "Included" => "Berücksichtigt", "Excluded" => "Ausgeschlossen", "Included keywords" => "Berücksichtigte Stichworte", "Excluded keywords" => "Ausgeschlossene Stichworte", "Remove last keyword from list" => "Letzes Stichwort aus Liste entfernen", "Click on a keyword below to add it to this list" => "Zum Hinzufügen unten auf ein Stichwort klicken", "Right click on a keyword below to add it to this list" => "Zum Hinzufügen unten auf ein Stichwort mit rechtem Mausknopf klicken", "Clear list" => "Liste löschen", "Show these pictures" => "Diese Bilder anzeigen", "Reread keywords from database" => "Stichwörter aus Datenbank neu einlesen", "All" => "Alle", "Only %d" => "Nur %d", "Show all pictures?" => "Alle Bilder anzeigen?", "Show %d pictures?" => "%d Bilder anzeigen?", "%d pictures, %d/%d keywords" => "%d Bilder, %d/%d Stichwörter", "%d pictures, %d keywords" => "%d Bilder, %d Stichwörter", "Found %d pictures (+/-%d seconds)" => "%d Bilder gefunden (+/-%d Sekunden)", "Seconds" => "Sekunden", "Minutes" => "Minuten", "Hours" => "Stunden", "Days" => "Tage", "Update search with new time interval" => "Suche erneut mit neuem Zeitintervall", "open folder and show picture" => "Bild im Verzeichnis anzeigen", "Show thumbnails in picture frame" => "Thumbnails im Bildfeld anzeigen", "File %s used as %s icon is missing.\n" => "Datei %s verwendet als %s Icon wurde nicht gefunden.\n", "Errors during Mapivi startup:\n%s\n\nPlease copy the needed icons to %s and restart Mapivi.\n" => "Fehler während des Starts von Mapivi:\n%s\n\nBitte die fehlenden Icons nach %s kopieren und Mapivi neu starten.\n", "Resize" => "Größe ändern", "Change the size and/or quality of %d selected pictures" => "Größe und/oder Qualität der %d ausgewählten Bilder ändern", "Keep aspect ratio (original size %dx%d)" => "Seitenverhältnis beibehalten (Originalgröße %dx%d)", "Width" => "Breite", "Height" => "Höhe", "Resize method" => "Methode", "Recommendation: Lanczos filter for high quality pictures." => "Empfehlung: Lanczos Filter für Bilder hoher Qualität.", "Strip all meta information (EXIF, IPTC, ...)" => "Entferne alle Metainformation (EXIF, IPTC, ...)", "Unsharp mask" => "Unscharf Maskieren", "Sharpness (radius)" => "Schärfe (Radius)", "Blur (radius)" => "Unschärfe (Radius)", "Absolute size in pixel" => "Absolute Größe in Pixel", "Relative size in %" => "Relative Größe in %", "Picture width" => "Bildbreite", "Columns" => "Spalten", "Rows" => "Zeilen", "Distance x in pixels" => "Abstand x in Pixeln", "Symmetric borders" => "Symmetrische Rahmen", "Text to each picture" => "Text zu jedem Bild", "Border around each picture" => "Rahmen um jedes Bild", "Border around collage" => "Rahmen um die Collage", "Font size" => "Schriftgröße", "Color" => "Farbe", "Quality of collage" => "Qualität der Collage", "Collage/index print of %d pictures" => "Collage/Indexprint aus %d Bildern", "One collage holds %d pictures." => "Eine Collage beinhaltet %d Bilder.", "With %d pictures this results in %d collages." => "Mit %d Bildern ergibt das %d Collagen.", "Each collage is about %dx%d pixels." => "Jede Collage hat etwa %dx%d Pixel.", "Generate a picture containing several pictures in a grid layout." => "Erzeugt ein Bild aus mehreren im Raster angeordneten Bildern.", "Delete pictures used in collections?" => "Bilder aus Sammlung löschen?", "Collection" => "Sammlung", "Open collection" => "Sammlung öffnen", "Collection folder" => "Sammlungsverzeichnis", "File folder" => "Dateiverzeichnis", "%d of the %d selected pictures are used in a collection." => "%d der %d ausgewählten Bilder gehören zu einer Sammlung.", "Please confirm to delete %d pictures." => "Bitte das Löschen der %d Bildern bestätigen.", "Use or middle mouse button to view a picture." => "Bilder mit oder mittlerer Maustaste anzeigen.", "Use to open collection." => "Sammlung mit öffnen.", "Select collection to open" => "Zu öffnende Sammlung auswählen", "Picture is used in %d collections.\nPlease select which one to open." => "Bild wird in %d Sammlungen verwendet.\nBitte auswählen welche geöffnet werden soll.", "Delete high rated pictures?" => "Gut bewertete Bilder löschen?", "%d of the %d selected pictures have a rating higher or equal to %d." => "%d der %d ausgewählten Bilder sind mit %d oder besser bewertet.", "Delete" => "Löschen", "Restore backup?" => "Backup wiederherstellen?", "Picture \"%s\" has a backup file%s.\nShould I rename the backup \"%s\" to \"%s\"?" => "Für Bild \"%s\" existiert ein Backup%s.\nSoll das Backup \"%s\" zu \"%s\" umbenannt werden?", "Export to file" => "In Datei exportieren", "Export to file as ..." => "In Datei exportieren als ...", "Save collection" => "Sammlung speichern", "Please enter folder name" => "Bitte Verzeichnisname eingeben", "Please enter collection name in folder %s" => "Bitte Name der Sammlung im Verzeichnis %s eingeben", "Overwrite collection?" => "Sammlung überschreiben?", "Collection %s %s exists. Overwrite this collection?" => "Sammlung %s %s existiert. Diese Sammlung überschreiben?", "Picture collection" => "Bildersammlung", "Close current collection and open %s %s?" => "Aktuelle Sammlung schließen und %s %s öffnen?", "wrote collection: " => "Sammlung gespeichert: ", "Enter file name format" => "Dateinamensformat eingeben", "Insert ..." => "Einfügen ...", "Last comment" => "Letzter Kommentar", "File ..." => "Datei ...", "Folder Menu" => "Verzeichnis Menü", "Add actual folder" => "Aktuelles Verzeichnis hinzufügen", "Remove actual folder" => "Aktuelles Verzeichnis entfernen", "Step width" => "Schrittweite", "Use setting for all pictures" => "Einstellung für alle Bilder verwenden", "Border width" => "Rahmenbreite", "Blur radius" => "Unschärferadius", "Adding fuzzy border" => "Unscharfen Rahmen hinzufügen", "creating border" => "Rahmen erzeugen", "adding border" => "Rahmen hinzufügen", "adding process info" => "Prozessinformation hinzufügen", "Adding fuzzy border to %d pictures" => "Unscharfer Rahmen zu %d Bildern hinzufügen", "Added fuzzy border to %d of %d pictures." => "Unscharfer Rahmen zu %d von %d Bildern hinzugefügt.", "Border color" => "Rahmenfarbe", "use absolute value (pixel)" => "Absolute Größe benutzen (Pixel)", "use relative value (%)" => "Relative Größe benutzen (%)", "Quality of picture (%)" => "Bildqualität (%)", "Created difference picture %s." => "Differenzbild %s erzeugt.", "Sorting %d pictures by %s" => "Sortiere %d Bilder nach %s", "(reverse)" => "(rückwärts)", "Opening" => "Öffne", "Searching pictures ..." => "Suche Bilder ...", "Searching" => "Suche", "Searching for \"%s\" in database (%d/%d) ... Found %d picture(s)" => "Suche nach \"%s\" in Datenbank (%d/%d) ... %d Bilder gefunden", "Got locations in %s seconds." => "Orte in %s Sekunden geladen.", "Got date and time in %s seconds." => "Datum und Zeit in %s Sekunden geladen.", "Getting date and time from database ..." => "Lade Datum und Zeit aus Datenbank ...", "Getting locations from database ..." => "Lade Orte aus Datenbank ...", "Collections saved successfully!" => "Sammlungen erfolgreich gespeichert!", "Error saving collections! (see console for further information)" => "Fehler beim Speichern der Sammlungen! (Weitere Informationen siehe Terminal)", "Delete collection" => "Sammlung löschen", "Save all collections" => "Alle Sammlungen speichern", "Edit collection" => "Sammlung bearbeiten", "Import from file ..." => "Importiere von Datei ...", "Add from file ..." => "Hinzufügen von Datei ...", "Add folder or collection" => "Verzeichnis oder Sammlung hinzufügen", "Update collections" => "Sammlungen aktualisieren", "Opening picture in %s (%d/%d)." => "Öffne Bild in %s (%d/%d).", "Searching for pictures with creation date/time: " => "Suche nach Bildern mit Erstellungsdatum/zeit: ", "has no creation date/time!" => "hat kein Erstellungsdatum/zeit!", "Saving for exit" => "Speichere zum Beenden", "Color picker: last color picked by clicking\non the picture in the main window.\nPlease click to clear." => "Farbpipette: letzte ausgewählte Farbe\naus dem Bild im Hauptfenster.\nAnklicken zum Zurücksetzen.", "Double click to display" => "Doppelklick zum Anzeigen", " starting import ...\n" => " Starte Bilderimport ...\n", "in folder " => "im Verzeichnis ", "no pictures - skipping folder\n" => "Keine Bilder - überspringe Verzeichnis\n", "copy" => "kopiere", "locked picture, setting high rating!\n" => "Gesperrtes Bild, setzte hohe Bewertung!\n", "locked picture, but writing of rating failed!\n" => "Gesperrtes Bild, Fehler beim Schreiben der Bewertung!\n", "The transfer of %d pictures (%.2f MB) took %.2f seconds; transferrate %.2f MB/s\n" => "Der Import von %d Bildern (%.2f MB) dauerte %.2f Sekunden; Transferrate %.2f MB/s\n", "(maybe already renamed with JPEG file?) " => "(Vielleicht bereits mit dem JPEG umbenannt?) ", "is missing - skipping!" => "fehlt - überspringe!", "rotate, " => "drehe, ", "add name to comment, " => "Name->Kommentar, ", "add IPTC, " => "+IPTC, ", "add headline, " => "+Überschrift, ", "add EXIF date/time, " => "+EXIF Datum/Zeit, ", "add EXIF owner, " => "+Exif Besitzer, ", "add comment, " => "+Kommentar, ", "rename to %s " => "umbenennen in %s ", "deleting" => "lösche", "Import finished!" => "Import beendet!", "Import aborted by user!" => "Import durch Benutzer abgebrochen!", "You may now close this window." => "Das Fenster kann geschlossen werden.", "Import pictures report" => "Bilderimport Bericht", " pictures " => " Bilder ", "progress folders " => "Fortschritt Verzeichnisse ", "files" => "Dateien", "Folder %s does not exists!" => "Verzeichnis %s existiert nicht!", "scanning folder ..." => "Untersuche Verzeichnis", "folder scanned!" => "Verzeichnis untersucht!", " at least (scanning stopped by timeout)" => "Mindestens (Untersuchung durch Timeout gestoppt)\n", "Found%s\n%8d folders and\n%8d files with a total size of\n%8s in \"%s\".\nReally delete?\n" => "%s%8d Verzeichnisse und\n%8d Dateien mit eine Komplettgröße von\n%8s in \"%s\" gefunden.\nWirklich löschen?\n", "Warning: There is no undelete!" => "Achtung: Das Löschen lässt sich nicht rückgängig machen!", "Ready! Removed folder %s with %d files." => "Fertig! Verzeichnis %s mit %d Dateien gelöscht.", "Thanks to the Tango Desktop Project for the nice icons!" => "Vielen Dank an das Tango Desktop Projekt für die schönen Icons!", "Open-source and cross-platform picture manager with IPTC, EXIF and Comment support." => "Open-source und plattformübergreifende Bildverwaltung mit IPTC-, EXIF- und Kommentarunterstützung.", "Play selected videos with " => "Ausgewählte Videos abspielen mit ", "\nTool can be changed in Options->Tools." => "\nWerkzeug kann unter Einstellungen->Tools geändert werden.", "Edit selected pictures with " => "Ausgewählte Bilder bearbeiten mit ", "Click to see hidden files" => "Anklicken um versteckte Dateien anzuzeigen", ); icons/0000775000000000000000000000000013007661641010675 5ustar rootrooticons/EmptyThumb.jpg0000755000000000000000000000605412230036210013464 0ustar rootrootÿØÿàJFIFZZÿÛC  !"$"$ÿÛCÿÀdd"ÿÄÿÄB!1AQq"2Ba‘¡3Rb±Ác‚ð#C’Ññ$%45rsâÿÄÿÄ*1!A"Qa2qÑB‘ðÿÚ ?ÙtQETwXë]7¤ÚI¼\P‡Ö2ÔVÆûîwn qó8ú‡kÝ¡N“r‘¦tJÙ2XÊnWY…Þ”ôSŸ!ñÅrчl}×mý¤ë“§/Ý%žÑçÔŒò×¾·×C—Va¼{®ÓµtäØôì;DñuhihÏÁˆ–YqÅGÇ”s¹á^Ä>#ú;<7V\§O̳"t‰ ž8¥M:—u q) J‰® ƒ‘ƹ|3ÅW»ÕZ˜ã>äìÓ,f#Õâ¼h¯¡FJI4Q=ªÛlš®leGÑÚyþÊïrA[òèãrRýÊ<@óäqV Îl{uºMÂZ÷#ÆiO:®ä¤OÀVczë&[µÌ‹–¡x¸rYŒ8!ÝŒx­ôÙå†ð)l4–™Óö•õD+H_U+¿'úå‹"ɧmNÚ›¶ )vQû¶±½…tJG´¯!ó®[ Ó‰ ^¥#/9È>ÈäUû|{ê#¶+¶¸ÑÐk+¥©«ö‡ ž0Û&M«ßçÓAêz{°3S‰joPqÓ¬¿¿/ßáw}Ñ:c,Í“‰Wíã*uåZ!U–O÷Ê™^Ï€¤o@ŠÜe¶Âpâ¸ï©D’}ä×;EöÙ}´Ç»Yç36 ”o´óJÊT?b9xƒÀ×HÇZð“½×o™6å5Ýþ¶_„‘ÔPæ*è¾ „·<–âÆêý¤òêjDâi»E¯´±ï_µ:9^ÛIkºˆXû¬œ»#É'¦k-»´xŠënm (f¾ä'ŽE' (åʾcÆè·EÄ~¡Ç(¿KS¯”ušë(H(åÖ“Eâé)åšÞ/-Áw±}XÞêZY³ë³wW&AR‡Ö!¬dw¡\Èüª³¼[[ +†ø'…Tl›FÏd(®ˆNê@¢¾µMNÆ/²9må•ÏÒ*àämžk (zï1˜)#˜ ;ÊòÂóªˆ1ö–°bÛa¦J#¶‘ì„à~¦¬_¤S„Ý4lb}NuÒ=èJqúÔ#e IÚÜWR¾dþÕч¦¦È²ú‚Ëq£5”î¶Ò;€¯Z…¥*J†’2î5¹j‡aݸÈSM« *'x÷㺥8€¡ÈŒŠ¦d uþËoÛ<ºKÖ{#Œe[Qzï¥ÂüNÅü ǰ9òàšóHk[6¯³‹žIRAÜy‡ë±×Õ OB>¦kBÖjúHè˜éÚ†{BJsNêýY5زä²ÕÞŽÛ{îºëdaN') ð'ŽxàŽ'àÑÖÇš·Ë?~Ïóû-iõ.§‡Õ~Μí4á?ÇPý)õÃYóXèÍc±&þ¹ÒÛEÔ7Ö­…2.¶›ÛÉz<¶w€s³À’€9Ï.} ël¸1tµD¹E*,Kaµ¼0wV¡Ÿ#]-žZ}<*“ËŠÁ¦Ù©ÍÉw:»I”8j9{½¹qŽÓ)PHEYãž<*W骽bÈä‚“ŽÄWnˆz6“jï¨Òncð,s?Ìó¨ÆÅu èÚMŠKŽÕÚ#°Ýᾑ|r<ê{µ¢vÊ/ê)ôLVÞô!Ä(U!³wÔÕßI> »û(þU-93[ôZ =0~\pÉJr–æÌ¢Š*D ƒé(Ùi:Rä}Fne•îÑ?ü¯ö_ EÚ#­,ãyjüßèjæÛ½•ËÞÌ®hŽé0Âf³ŽylåX÷îï:Îq®‚¡¶ßHià•¬Žœ7V<@«uúªhYëm¾D¡%èÈ[£‘'Ž;Ç#NI]6[¥¢T6¤!AAi ð¥I]RÀºÊßM=¡‹v®Ñ¶&î³µÏLæ^ho©ôî%ßh¹JO@ŸÅZhºÝ)¡/Zˆ.OvÝ ÇÑ’â’8ŽCŒ­çо½)˜R†ëIñÁQÇæ§èõd2öŒ$„…FÓð;=þ…÷OèWð«µújlɤh¢Š¨`ùwp¶ àa@Œ‚+$k9ö¦¸é c-fU©dð(9ô3Þ8,Ö¯¹¨¦9ÅT;X°#PÛ‚R ÔèÊí"¿Õ î>ãò=+m6rK®ÀaØŽ¯KÌg.nI`Æùâ´gÅ=ÝÞ­pºÊï·)ù…ÖBá_!«.´éQO% ÷Õ§³½©Âž„Zõ+ˆ=>€¿E§Oæü ñááʧu?Ê µûJó|Ð\7ŽêWÍ*J‡Gx=iÙÈ·ºÓ*ÂÔ8qÆxòª p%c(PPï5ò¥Ó‹&*SÀ¡*“Ì÷Ó¸*Z‚P’¥@ “@}-tݨ¯ví1`‘¨o ÿ†cƒMg ’ï²Ú|zžƒ&‘kMY§´dbíú`T½ÜµnaAO¸zd{ ÷Ÿ,Ö~Õz†õ´K·Ú×§D4L¥†Q÷l§ð {K<2®¿[ª©ÍõØÎbã6õ|¸k[Ñí^q⦓Ñnž JGáHÀ÷;«Rl#M5¢D¤ÿÌç,ÊšO0¥rIðó&©M˜iïµî‘¯âö6¸_öøÊ±×>|}çÃŽÓÎ(„ƒ[/šûPcíQUÌä6h¦¡Ú‚Ú£½èšš×) 6úp´ùÐë\éîj ª<ƾéôzÃÜ{ÅVWØN°çc¨¡­—9"|tå*ÿÈuýkZ\ôò]¡ ÔN賈©ZÈRHÁ9¶×s‡NÀ 4åçZiÄoi«Ñ— ö)Xq¿6ÕÈøq©Tm·j©¼i(¨p*B]`Ÿ$|)æñ²«{®—c6ô'z*:·~\¾¦w6w©X8‹¨]Pîyãñ$Öîz§¹“éݼÉZH‡¢b%Τ§ðS-ßi;P¿´¶a„Ù#+Öú“?Wáÿ±D¯àiÙ:W¸påý OzcŒþÔ®.Ê—!AWK”é¸>©Vê~~F™¦; •cèi™¿5÷oSÜQ=„rT•+ó/™ò©Þ›ÑÓno³&ø„!†ñØÁlaÿôÿj³4þƒ‡o@LHHhd'‰ñ<ÍLíZgs£ßPíôA‘OZÊ„¥ªh‰Ø¶¡Ç¥}Á·3#€&–ÕsEPQ@â’•zÉÄQEÅq#/Öi5ÉVÈg›TQ@x-P‡øUÑ"£“BŠ(Èi´z¨Hò¯º(  (¢€(¢ŠÿÙicons/MapiviIcon.gif0000755000000000000000000000706312230036210013412 0ustar rootrootGIF89a@@÷ Œ’Ô ”dbœÔ \$2tÄÆü |,6ÔLNÔ,2”dnÔ¤¦Ì "l´ü< &”LNü &| ¤ läæü42ülnìŒ–Ô L$&ì&Ô¤¦ì|~ì<>t\bÔ¤dfüLJü"ôœžô"„DF´Ü¼|,6„&„"¬"l"Œ$*ô$*lÌÊü4:ÔTRüìêü„‚켺üÜÚüüúüŒŠü Œ´²ü œÔÒü\ZüôòüÄÂüäâüdbü¤¢ü”’ü¬ªü Tœšü"Ô ” \ |LRÔ,6”ü46üü$&ü4>„DFü,.ü$lnü4tvü,2tlnÔ <¤$&ÔdbÔlœDBÔdfÔ¬\^ÜDFÔdT$&tttׯMP`Øû—ÿMÿP^¨ûÆéw§d_Åûéww&øwÿ`_ÿäÿNéÿwÅ0ú95qéFw ¨:Æøw¨ñÆüw!ù,@@ÿÔHp Á‚*LȰ· #„H±aD‹&Ô„à¤:¬©±$Æ‚&'Š2cÇ“€ÄرæËš.9vdiÓ&Ï™U’œDôæË+N t²f̘"eöÊâÑ,A4Á€!FŒ1Ó¤Á³ªTŸ-úè@ --peàÁ 2&žžÝ‰Ã–­+‚}Q¥Ê 1bnŒ½ò¢‚c 1)¥ØëŠ0EÄv”)ÃjΫWcºÄ¸óx’d 8*‡Á è 1=zŒÉÞqâÎr•6›ö ¤^«Á¸ Åêó˜U«â °û1d¾À1øÚDlHŸØwÌQb[P®àŸÿ…ªpÑj_ôÌYÕª•Q£DÛá¼ñ“püÂP%˜)c;„Â+¬ŒQŠm”à@}#èöÜNi½°Ÿ/Š@ÁË*ðà‰']db[&(È cQÆ-ÁL  /ĽâÙc b*ˆAÆ‚ :¦WA“p ]…‰”ÑÊ rØÅw´B†.6☋L8C!nôQhcd9 wxÑǺÔpcx’Õ‘aô·‰*CŒ1ʆ]ÄÙ!(wŒñÆ50‰c‰Â€1kÚ½‚rcĤ~àÙä÷=(øiÊpC)§tŽÁwê9BG8Ü‚ž*Ä”!hÉ­rœ'£•¢€ÿ9¦cgÕ!AšÁ@±â+ovŠœ¨d2Ú+Oü°©¢€é&_èÊqqD‹*{Ë%ù¬5Ô° ƒRMâƒt˜BL(•újn‡£Í±Æº°¾q#”·€¡«¦&×J´ª®â JR,²#èXÓ%(®iÌBpˆÊÂæ {Ç+y³†±²vDI¼?›œ´„¶²\ «¼A±§´NB‰0UÙ† 3g&§ÜÁ y°Ë“ðæªŠ1õr¬*hûÞáÉß<ës:Dî¸qç¯qó4Ð<±¢£¨"Ïëq<†‹¯ÄáÅŸäPÌÈ 6ŠfÁ_ÃJ+oÂ)gpÉŠ4óD¬dÀÿ+ïΡ¬7h–¯øŠŒ ¬EÑbÖç,R*²&/cÀ=J‘nX·Ìx瑌±8AI`k:‹%¡…‡¢YœÜ‘ 3gsÊ$x+ü"ªäA²×žDn¨yëwW»» Æ;oàÆ!÷J} Á +ÜqÊÔ\Ýø|œF…ÆŒ!D+Yzy‘¨pJxÓ°ÆÞîÆ4z0¥ïpe壘°1‰saë¨R4˜!º‚ÑšPć¬Ð,G¤ò¯s3Zñþ–™ÎÊÀTS$b2¢‚¦ÖÀ)1EãD¾ØžV‘ % ¡±Ù÷Np¹.\ è«Úç8E÷aŒTò+N+ÿ2á\¨ XÚZ×…-ü ‹À€¨RÔT|bÀÐCa†ÀŠ8À'>©8úÌ1Šõ ¡’—kúÀ„}‚¸°Â®>Dïê"áf±š*€A0«ð Ž!‰/èj\«xOŠô­kbšàšˆÁÆ2Ìa¸Ð_~+äÒ(ˆf,,ð'…Š0Ƽ0ƒ'HbÌrÃùO¤‚ ¬»ˆ‡FŒ­q3—ä„’€FTÁ“­Ùw”ðuÕà”`×>‘ ¬á )ò`¼'„„y ;p$ûz(ºêO@Á,Å1 þüqzB N1Š&¬¡ÇŒ¦"Üpÿ/Ä¡kˆ…<È‹W¸Ç›œ'‡wÆ42‹’¡Ü(8‘ kÀ R•ŸTÁŠå\€V F4? …P(áŽXC2®1¥D Á Ý<ÁÃYµ¬’ä¼üHÅ"ÎL5ƒ„/E˜b'Í0„$&Ñ ÊYE3Š‘‡Y¤lM/Ã÷dz>¤Oo|“¤kXDœ‰vf0Ã0–1Eê¼I1Ø6Á¬àÒHF2Š éj lÏ P ¯êÒŒÊÚé9ËzOT"­fèÊ6Ê‹V”"J`…3C­âÅ *üʦ9Ä¡=£ ¬aէdžúR*&OpaÿàÍ (ª)ôð6O¼ÐQq€ D,4˜BÁJ{Z/– ¼Hß™ä>Ò†¬eB*NÐŒ´Ví‘Û„,a4V¨Ž\JhÅ ò€Ü<ŒVg€es?]á!¶—`x(/°“ÝTŒâé£ÁN´Ó!Xr¬(Cà<¡„P<¡½É]îÁ¤%%Ð7}6å›Õè†ý H^Ew«–‡˜ÒOPÈÌ€6£( j$m~ÙÄŠUC€à}z]þ¶Â B°ðÖP”X@b…0‡À˜f2ÖÕÁ66DK÷¦‘D#é\3ìëAN_Ížà£)uØË¯Ø l ã5¼×ÿ`5FÎK‘cûòP’=P+>ÑŠkXb? Eš¶×,çɰ «°„Zÿ,c8§ª(…1Ä©aÑE \ö²râÐŒ6“°–)ªk¤¯>$ûVCƒ„ãÌT(aÒyÛñk» M ¹Œ?ØÀ~AÔ*ü)”P±5¨úo<;N¡‚¥‡+ßLÆtuvЇP¬¢q(‰×·©Yôâa02•B1W{}Ç.]²âê Í5/Ó¨"`SÛÚ¥XÅ:÷¿7<•ٯUñŒEç­X¢°®Û· º5›fy0cuóû7X;«PÂ*ül5ŠÕ …(Ð0‹’A0ÿfoÑè°åd­á¨ †#[+V‹÷á:,Cø=2]„©×z¨ q@sŠå£h.ó6K¼œÓù‚Ko.ÃLè|ÛÆÊ“–ô† pAèCûŽ _*Ý8’¹.i®åüNʼ¸ù¬ÞéŽwjëdºÈ~öøýN5ˆ…Òš…<½É 4FÄÆÞ>K£ÇƒzàEÜ_q€W`cÈØjR l¤‹;mŠ Æâ”¬H1øÌÌÁ¼ò„:qÙ*ró2†ó怊9h›fÿÓä€%éýN€gR ^¹I±‘@;A"fÎ…,G©?rClÜÖVÔÐðT}Äù¡ç.R0eMñÚX^çŠëâöWÝ’«t^÷u~×.ž¢E¡©ø½G?@|wwBÔ,©(Ôà{]+%$&Ù(7Ñ<’ß±aÒ+xâ)ñ€Üç}@&zâ8a.’"ù tZ·{ŸòŒ¸È}x·{(y;Á·X“½w“2©;icons/MapiviIcon.png0000644000000000000000000002624312230036210013427 0ustar rootroot‰PNG  IHDRddpâ•TsRGB®ÎébKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÛ *¹´Í³ IDATxÚíyp×}ç?}Í…0ƒÜ' Þ”)ëL´Ž%Ù’uÖQèœr9Îf“³e+Ñ–;NR¥•í()¯M*‡BE¶cK²eÙº"‹%‹÷!^ â âæÀÜÓÓÝoÿ˜ƒpp%yW?Ô«Ìt¿é~ß÷»ï ¼GïÑ{ô½GïÑ[HBˆG„I!ÄËB÷5\ïBB¤…ýsòÌ_BÄ„_}'û—–¸Øøîw¿«ìÝ»¯×{avvvçž={bWñåšH$¾ò7ó7 cšæ#{öìyà]HòÛßþ¶ãµ×^C×õ¯íÙ³çO߉þå%®W^zé% Ã`vvvÝ©S§ŽîU~±üöO~ò“ðàà ¦ibYÖç€w;§üÓ¾}ûÐuÃ0þøê;Ñ¿¼ÌÀÇill\|j•_¼Iѽoß>SUÕì—È2À¿›Ñ$é3†a|Ã0 z{{þðè_]©£ááaº»»ñz½Ÿ…B__Åwß766F8öMMMQWW·êïz+è‰'žp¦R©›€I’¤«¼üõ?øƒ?(F£÷ÿç­¾·={ö|&÷òSKõ¿â ]¼x‘îînšššºC¡Ð:à âêW÷ïß<ÇŽãÞ{ï}Ûfùã?þÉT*õà-æò«¡¿û»¿ÀBˆ‡çw~'ÿ\2 H’”Y¥ØÖS’$«ÄÇŸÉ5® ¹¹9Ün7kÖ¬áÌ™3ZæôÍBˆõ ¦ÓéðÜÜœg•°5w“¿4æDép xø–$Iú׬oûÛ^UUõ/›7o–|>6› I’H¥RFGGI¥RËÞÇ®]»0 CzþùçMá>*„P€s–e•9r¤{×®]ó+ÅÌÌÌr`lþ~ß¾}òc=&›áóóóÌÏÏsöìÙUU>ðtîÝ»w®”X´;3ööv ÃXðùÌÌ {÷îeïÞ½ìÞ½›»îºkÉ{ààË_þò¯J’tÚ4ÍoY–õç™L¦êÒ¥KC@3X¢‹_?yò$ÑhT‰ÅbkOž-þm @¸f@€7vìØ1ÐÜܼè\B\…r3£ xbVçÔéÓ§ €óçÏKÀ×ÿ–cçîéé©~ðÁp¹\_ª³x<Ϋ¯¾Ê'>ñ }ôQ6mÚD&“A×u6oÞÌ7¾ñ „TUUñ…/|@ @8.´Å”Éd J°©©éŸššše™æææU¥ÄÕÑ£G±,Kšžž±X à[Wcå­ I’D{{û¿UTTSÔ¿ºè”­Bˆµ©T* žXE¿[‰Äù@ `D£Qìvû7…UBˆ…¿+„xx17ÓÈܸT}}}ø|>>÷¹Ï‰D :(߉÷ÜsOÁY}á…|¾˜bñ˜w÷î6ä]œ;î¸ÃhooWO !D3pûÁƒ­wHÀÏ€‹o9 y±uÛm·åÅÖo•W±X¬jppPÀÉÕôû¯ÿú¯w9rdnÆ ‘D"qDˆÅb‡ÿéÕW_ýÍo~ó›™GydÅøÀÄÄ»wï ‘H\ÑÂá0y1ê÷ûyöÙg|¾˜FF†1„í–ÜdîØ±ãEI’¨­­¥¼¼ü“‹Nßèïï—-˲†‡‡¿Z?èj¼çÓ;vìyî¹çZ›››7„ÃáN` '®îûÙÏ~|9qõ«‘›BˆÏ?Þ~àÀ¾öµ¯1::*‰Dñ — !XÓ‰Dø¥_ú%ª««1M³ä9555”••qôèQ|>ß’ý…B!$¸%ç áñxþ¥»»ûž³gÏÒÒÒrcOOO0–;ý7Ž9’rDÖu=¿îº"I’0MóqÇógEÖÖÿ¶ !::J&“P(ä+e”c7ðÈ‹/¾ÈSO=e™¦™çVI×u …¤¹¹9&&&˜˜˜ ï5/G™Lo•§Ó¹Ô3àp8²®ªLMMáv¯3½¥èõó·ß~{ììÙ³îööv©§§g7ð×BˆÀÖýû÷€š±ÏáëÉ!ykëÏÂá0ç7Ãáðÿî%‹yC@pfÝ}æðáÃ|ï{ßÇÆÆ¬ .ȳ³³$“I)÷0=ÀÑçž{î<ùäÊ“MÓ4þÊCÔÔÔ8¡˜š›š¹ÿw>Y/‹ñÈß^B?üÐWœŸóì·Ýyçe/½ôR\’¤t2™üަi¿çóù¨ªªúT0ükà×ÇÇÇWÓé´1>>®^‹¸º–€ßÙ;v\|öÙg×477oL$kû^ýõ¼¸òß\¥™·å'?ù gΜáøñã2ððÏÀ!`:ßχ?üáUR^^N4#•J3::zÅç]ë7055UÐ1Š¢pìØ±%ûK$’ª¸ø)€ÃáØsÓM7ýÞk¯½FkkkW,ÛüzÞ÷T…³À\w@òbËëõ~)gm}FÑ~ðàÁP2™ „Ãaßj¬«Ù.]º@OOÀW€ÿYB´Õ­öþ …”—_)†÷Üsö`vv›ÍF2™\²¿üg’Ä­y@€C7ÝtÓøk¯½ÖØÞÞŽ®ë!Öìß¿?h9qõ óvpHÞÚúR(¢¯¯o÷ÈÈñxÜ;00ÎçÄÌjhÐ4Í®¼øþ¶5À3¥LR€îîn[ñÿëׯçÀÁý¨ªb™ªÕzúðé±¢hí¿¤Óéû÷îÝ›´‹/’H$þùÀë?]ü•%)Ò#¹IùÏ ëú}ƒƒƒ„B!m~~>´kWWeöÝPÏöíÛìv»ÿG?úQ0ç zsܱZ¯tOuu59g“½{÷º…ª¢\qƒâ/€ÞH$²ëë_ÿúR†Awñÿeee¬_×Åöm;Nœ:t*(„p !n^°,ëþÇÜŠF£Z ÈLNN²’ñ‘L¦H&S$R©¥hR>~ÇwàõzË~øÃÐß߯ç€o mmmû|>ÚÚÚ8qâ„3‘HÌæfñWÑÍßlÞ¼ùl>š|ôèÑP(”1M3’L&^¼xñKO?ý´÷Á4rÁCjkkyà”œg¾}±ÙÛÒÒ‚°Ä }}}ñt:Ðuýµ¾¾¾;yäsÿþý²eYÖþýû5àX‘* H"™mñdåš5k6MÊB(eÍš5œ={ÖB044tÕ¡’ÕŠ¬Àr¨¿ßµk×?àHgs³£˜Ž;—à´Ìç?ÿùº®Øl¶æÞÞ^>ûÙÏ–¼Ç™™SÓ4Åëõò¡}ˆÉÉÉmÛ¶uLNNn+>qïÞ½Üu×]¸\.~øáÅý(†a˜ûöíS‚Á ü×|¶8Dl_,²„$nÍ…tò¡”ljjú‡\ôV™ššÒ‰„|{¹Tqÿ«æI’v'– K’4û /lL&“sáp˜žžo)î$éàøÜÜ\É~zè!ýùçŸï<|øðáññqR©–ea‰D‚ÉÉI>Ì /¼ ôôôôD£Ñ´eYÄb±¶“'Ož6LcÛâpýëû_Ÿà“9Žòæ¾7 ô?þ¸xh.øª}kçfælŸüdַسgëÖ­CA8&‰ ë:B4MÃívSUU…¦i+¦­MÓÄãõ«û‘¹¹¹·ûï¿¿2N_ˆÇã5Ï=÷¦iÞ<¶ ÉtH—W‰ŸCÚ¹s§‡¿7™ÿØìÌ,Å€<üðC ËYÖ—¤BÈE%¤Ü#gß ¤½Èý#„à|_/?~æY,ë²T³L«9 .æ€Jà(Ì pâÍ<›úóÆúõëËãñø÷…,õ¹ÇëͶ2Ù£”&ÿ¾„T26–/ŠÀ-7ß‚ÓéäûOþ o–£iÚ¯ÿ{ÑeóÀº·êùÔŸ30$IzØjZ&Ý72;;»`09Œ$KȲ„,ÉH²Œ,ge9{̽¿TÀÒn·ÓÚÚJgÇZvïÞÍ“O=I*™BÓ´¿\·nÝ÷/\¸0~½žQùycãÆÝÀ+ÀzŸÏÇ'~c7ýå2zi”‰ñq.^fhhˆÎµíè™ ™ŒNFÏ\~É\nFÃÈÿ¯_Ñ’É$¡PŸÏ,Itu­§¿¯=£;œNç]çÉ`0˜¸Ïù®Ð!MMMβ²2¯¢¨P¥Â²,/P.Ër…â–|Bhçûvò‘{ïÅî°cš&##à óoý;Šªð ¿p[+Š9D–åËRôz9²ÛíTUù‘$™T*ÉSO~?Ûgöº€h‘e™¦yª¡¡á}ûöïJ@ÚÚÚ6›­MQ”v˲jFI’j$Iªµ,«^’¤ ¨X©/‡ÃÁÇ~å?³eËf$¢Ñ(‡‡Ðu©©)‚²”è·€D2EcCN‡ƒT:ÅÞ—Jd>²Ü%QàG’$}çüùóÿ˜o7 ÒºuëÖ(вÁ²¬²,w !6m¹Á^–\.U¾*¼^/^¯‡ŠŠ ÊËËq—»q98NŠ¢¢ª ÁÄÄSÓS½1œK]@LËdlt‚­Û¶á°ÛÉΞ9[(5’$ EQÈd2„Có„B¡âË'$Iz4“ÉüïÙëHgg§]Ó´Û…7ïvQ:á@EE ÔÕ×ᯮ¦Ê롼¼‚ò 7e®2TM[pÅ埋KAS©}z‰Æ®Œ8îxt3¶”>ÙqÃ6,aÑØÔLEEÅõT…!xãÄq"óó+EÂq:x=^¼^/RvS“Sì{åUææiI’n>þü‰«Dêêêú=I’Éãt:Ùuã.¶mßBSS3v›íŠ›N&“¤R©Üà§²¯S©UW¡Mp¡·Ÿ-[¶ðéOú ‘bY_þò— ƒ(ªDsKw|à—ÞKp>æǯ&o„Ï秦ºI–Èd2üø™g™œœønooï'VåvwwÛLÓ|¸[Amm ¾÷Ãlêî^Êd2DcQâ±ñD‚d2qMåÿ hNQú|>Ö­+íüVVVfe³€ŽŽµ…°ûðÅ‹ ] ëÖ­»¸W–e>óÇÿööö\X;ÌäÔ‰D|ÉŽÒi×ö:¡P—˵ °¬¬ŒÛn»o•7²i´¶¬Áï÷£ª*ó‘yŽ=Æé“§ ³+N/)ÏZZ[e™‰±q¦¦¦åëçR¥Ói.\ kãFêêꙜœ¸Ò¹ml&™L344L:•Æf³Q]íÇQå@qÙ»@$Iú<À7½ööv„ŒŽŽVìèÀƒAî½÷#”¹\;~œ úÈeöØ·ïU>úËAQ6uuuœ9s†Cr¾÷»äìïëëãÑG]Âr™E–eFGFÉè™ü’kÎ2ª›;pºÊ³\`˜YÄåÁ·dYäŽòå£b@¦i1K Gp:l´4ÖPæÊ373K zŽÖ¶5 ô_X0ð²"£§3X¦…¬ÈhšŠfSûÂWˆž5i ¦§§ini%‘L V…nKs3-Í͘–I2‘Än·/0•›ššinnæèÑ£ôõõ•˜ý Ó •Jå»”Ek;¸84˜5me™úÖNj›Ö`Z†‘ÉqAn‹q9ìȲÓ00M#“Á0LâÉdVñZÙ´¬Ë€Å)Î]¡Ú硹±E–éïëcç®]Œ_ºD²ÈÚTUUUJè®ÜŸ¸F@„ÄcqNž|ƒíÛw‡$‘Œ¯ÊTTdeA³Ã餹©…ÚÚZ†††øþ÷ŸZòÚd2ŽªªÈ²’K&I KÈH8]N>tï=;r!6‡‹Öõ›pWVeÍÛœC&d¡€¯ÜM¹ËŽËnC•¥l~DO£§Ó躾@1[–E,‘$8?Ïl D(.i˜&3s!"Ñk×4àt:è룭½óçzVkQ\½¹¬¤,‚Ñ‘QFGFùà]w²ó†H§ÓŒIé:–a,é)Š‚Ã᤾¾††ìš—ÓgÎðÝÿwôœ°] ZÓ4 bYB2B’Ù¼u ÑH„‰‰ \e´vmÁæp r•!r.tÑXíÇç)ËB×ut=M"ž  †H¥’¤Ó:–e¢**.§wYîlkkl ‘LrirŠáÑq¬t Ë4I¥Óœ»0B皬_篮Á]^Nl }Wºº&"÷"ÉÃÃÃ<¶g7Üð>¶nÝ–óMÚøi=kAiªŠ¦i¹pùeÖ ƒ<ÿÂóœ>uzAœJ\EˆOÓ4Ö®]ÇsÏýgY9k7"ÉJ É¡*lXÛI­Ïƒaèé==ç9ß{ž¡¡aæs+ˆËå¤Ú磮¦†úÚÖ·¯¡½¥™áú³•(¦eqaèm ôÓÞÙÉ…ìVo}N]!K’„aš ä]&“áàÁ=z„ÎÎN:::©««Ãívc·Û°ÛmP^¾0ö3?Ïèè('OäBï 3ó¦n´£³“~R© MÝ€”ã ‰¶–zvmß‚,Iæüäå—9pèà’‰$MSQ5«üM«0I‰$#‰1F.¡ª M ¬ëè`CgMuu¼q¦‡@v =Ãã(ŠŒ{zšªªªâPû2\rE¦i–TBùSr›§PV榼ÜÍnGQô´N<g>&åî2º×¯£³­•¾Á!Æ'§0 ““g{˜ ¸aÛVlšÆá'‰Å“LNÏÑ¢©H’TRO‰\ØD\ƒ•¥g5»T°ßiª¬¬d||·Ç‡eY¨’ÄŽÍÝØ4=ýgsOe[»7âÊ­/œ ¹xésåe»žÉ– ÍG¢ŒåbQu5Õt®i£ÜífËÆ Ô×Öpªç™ŒÁøäÑXœÛnº‘í›»yãL㓳Ty+°Û´õÈŠy”Rof cõ®åu¦2·›Ð|$KXlZÛŽÛiçño}«FS}ïß¹—ÓI<‘äØÉÓ=yª$BŒL=Â4K›íS3³¼~ø(ç.ôcš&Õ>Í …Ï#Ñ(¯¼¾Ÿ¿Ž¶VLËbrz9çY`]‹Èº«àz’,ËÌχqTÔ`Y-µ5ø}^~ðÃgÊFšé^ŸMfMLMÑÓwÃXèðÅ£Q±(ÉD‘ó‘ò9EQ°Ùí8\eØõò,¹4F(¦¾®–¡‘…k‰$¯:Ìí7½Ÿ`(Ìl Lc¿¤§~5,²X‡(ù‚@¼ã:DA:#°!¡©*-M1Lc…äRv–ç«GdY.øù¥fŠ¢°uëÖ+@È¿Øl¶(­­­òï©ªŠªªèºŽ¢(ùíj—ž(ƒ¶Ö6ü>?†i ̽9³—l¡ò;\4—DˆlÖPQ” Š¢066VHVå9Åï÷—#¿_o~à5MÃï÷LÞbP4MËæcTÇ¢µ1‹©¢¼‚-›²»ˆa˜Æª£ò’Ž!oCü}…îMciÑ),QIÅFH*•âìÙ³@ÊÊÊ'y0„?~|hš†ÍfÃét.x¿Xü©ªŠ¶B"ª²"ká]e|blÕ5Y¥bYZ6¸˜Yµ«ÿæYÚ¶Îî´ºTXE.˜²ù™Ÿ¡¿¿ŸŠŠ 6oÞL*•B×u¹âI’0M“#GŽÐ4­XÞC—e¹Ày‹+VV!5ôŒÎ‰“LjDæ¯:¿T ˆœ/ZmÙãõ¦¥ågz>·¡iÙ ù—e™sç·éêꢷ·—îînÒé4333œ={6[G–³Àòùw˲ˆF£…÷›¾v»UU$ÀJÑìÜ4²ä[29•ÿJ¬aW‹ŽÚ¹+ÞñࢡëË~®¨²”Ýq!¯GŠõD0äĉÌÏϳqãÆú Ïy‘”çœ`0XOÅû£H’DEEŠ¢O,¿«†Ãf/! è °“Ý(3³X‡h€ °-øIŸw§.W,.]žÏÊv¯×{÷'''‰äJtŠ#»Å¥( ‰D"»›õ™ÏçCQfW(Šs8¢j*B@ÆÈä9DäÇ<×$q†-w”³ÖuÝsꊢн±»`1å‰ì.=¹A@bdbŠ©éÙ%ƒª¢àóù¿"Ò?š¦IOOÛ¶m+l-¾8L‰D,dEQ0PAee%‡ƒX"A2¹´od³iÔÔø R*+þ@06:ž¿Ÿ“‹Èû€ê¢&’5×Ùʪ­©EU¤"+©PížSÚ’îel~‘ÐívãóùˆÅb ³¸¥ÓiNŸ>Mcc# TTT`励ó¿Ž×Š¢,ÜÞOZZZP…É©éeŸËï«ÂVYù aq®ç\~It:?Áåz±ŠŽ†šS.J±‚×S:Q(š6ßbÏÝï÷S[[‹iZL\+,_[¼É€µÂœ08U•öövN:µ`æç6ò{9æTÅx>Ô’¯ñýÝßý]&&&xì±Ç0M“ŠŠ |>¦e1|il½—¦çì¹ge2fgf‰D¢ùˆô_ÌÍÍ{^¹Ëy‘óM—$‰™ÙYÉ$N‡“ÎŽNF/.»ôjIUUêêêñx*±Ìl”uxxdå˜Vy%æܪ&änGG£££ ÌÕâ–ŸýŦrÞÌÍ išLOO Éd2H’ĺuëPU•ÞÞ¾ËØJÑøè¥%ªE ™L>2::úl¹TS‹´¾”ÉdöÚl¶]zZçð¡ÃÜ|óM8Öv®c~>ëÕÆâ±«æ›Í†§²oU²$c𣣣ŒŽ\ÊùzO&“¹´¤ƒˆÔât—o\êóxJÇïqÐÖÖF:& –£8X˜ç¤üûy±,‹G}4»(Ô4éîîÆív‹ÇZ~ }&šŽF£g%I2re¹†,Ëš®ëÇgff~jFla<¤Ð¼Y•3Ál€}ݺuÿ"Ëò­•žJvîÜNMmíåYd Ré$ÉdvûŒLÆ@×Ó…2!²1&YVsI;v›Kd ±#óóô÷÷Äò3tvddäKétzI[RÓ4g×Öí_“`ɨ^e¹_evÏ“¾¾>@É áâøT¤<‡ä9KÁúõë©©©Á4M^~õ5¢±eCèâbÿÃÑùÐÙGÓd7ÆÌó¯õ\+~]h àȱ‹ȉDâg7K’T“N¥É®˜5M;šMCS³‹rÜn7ååTzŸ!û%Zþ§@R‰ä‘ɱ‘Wr)5ع÷åšDv77­È&Ö\.WE}}ýƒš¦}¼8Þ%Iee.ü~?••”—»s³OC$Ëèéì.?‰x‚H4J(" ]±Ãa'&''ÿ6WUÅÖµyë#Šª¶-k½ù}Ôú}ȲL<gppp8¼€;òžøb}¢ª*---´µµ¡ii]çg­†"9Ôß÷ùx$È lf'¤rM_ôÙbàL på)öG4@óûýí•••¿­ªê$Iò½V"“ÉŠÅbÏLOOŸ¾ªÔ(µ kša…ÝT=å4Õ×ãr:e™D"Áìì,Á`°ðó«yqUVV†Çã¡®®ŽÚÚÚÂO¶ŽONqâôée}Ž<ÍƒŽ ¼˜³–ŒE ¤QÜôEG¤brM-:æ›V__¿ÕápܨªêfY–»W"jYV¿agR©Ô³³³' ÃЯ)W•³LÚ×oØí./ÿƒ/$êjªi®¯Çá°¸!oâ ! ÁÂâ÷¡§{έ¸Œár9} ï쩯‰#sÑ€ëEb¬¬ÅÀ´ØA,$ï›(‹Ì4 \.—Ãív·ªªZ%I’&˲H !2º®“ÉäT4¼…îK> §nؼíKšÝvÏj/ôTVPã÷S[íÇ Çç0-‹h4ÆøÔ“S+Æ©¦Œ¾þsgÔu=Q4¸Å:¡˜JéŒ+bYÒ¢×Åœ¡”C.º&?@o')€b³Ù´Î ›þVÕÔÿtÕ¨Ê26›†"ç¶ÓÐõkº˲FGúûþ(r^v~ðÍ¢Á΃b.gy.²J‰‚Rï-H±Ý¬p¿?ò€"Ûív{gW÷ʦþÊÛŸÉ4{†‡þ8>?Ì j~°ÓEƒmæÀ°ŠÎÉŸg-'›Wò» â«k˶?²ÙlÊÛ´mº‘1~82Ð÷ñx<‘`«hÖ§q‚µ¨­JY¾©™úFçóŠ^éÜ´éf—Ãù Ö]¯/Ô“©¿êë9ýTÑŒ_<óóºB¼™‡úy' Pü~¿³®¥í3²$ ï[ØÆ2ǧf¦¿27>X$‚JßôÃü¿B 7uwWzìÎß—%ù~$Ö¾ ž˜³LëÉHtþïFÆŠBä‹›ùf8âÿe@®§{ëΛ$Eº[’¥;$¤í€s9õ¢Ï2­× Ëxyjdä§áp8S”¯X ˆ¸^7þÿI€¼içÎ6Ù’Û,Iª²K’e ˆ˜™Ìxhffhzz:S4ТD{Þ£÷è=zÞ£w-ý_fwQ…âOû IEND®B`‚icons/MapiviIcon32.gif0000755000000000000000000000257012230036210013555 0ustar rootrootGIF89a ç              "#$%&),-/012456 8 !: ";!";!"<"#>"$>"$?#$?#$@%&B%&C%'D()G()H(*I)*K*,M+-N-.P-/Q.0T/1U02V02W02X12W13X14Z24[46^57_57`67a58a69b69c7:d8;f9;g9;h9k;>l=?m=?n>?o=@o=@p>@p@Bs@CtADvADwDF{DG{EH~FI~FIGJHJHK‚HKƒHK„IKƒIM…JM…JN‡KNˆLO‰MP‹NPŒNQOQPS‘QS‘QU“RU”RV•SV–TW—UX™WZW[žX[žY[ŸY\ Y\¡[_¥\_¥\_¦\`¦]`¦]`§]`¨^a©_c¬`c­`d­ad®be°bf±cg²dh´dhµehµei¶fj·fj¸gkºhkºhk»hl»il¼im½jm¾jn¿knÀkoÁlpÃmqÄnqÅosÇosÈpsÈptÊquËquÌrvÌsvÎswÎswÏtxÐtxÑuyÒvzÔw{Õw{Öx|×}؃†Ú„ˆÚˆ‹ÛˆŒÜŒÜ‘”Þ”—ßœžáœŸá â«««¥¨ä¦¨ä«­æ´·é¹»ê½¿ëÁÃìÌÍïÎÏðÒÓñÙÚóÛÜôÜÝôáâöÿÿÿ!þCreated with The GIMP!ù ÿ, þ8  Áƒ*(LÁ†!Øð¡°ˆ'^\-Ú †M¤G’&S¨¨£K_J¸°¦ÍšLðÀ"'©7®8"†O%X¶’*µ5è*«@I2Ô§š/PŒì‘ `„4¬’öR6l©-N*RÌ×RMf`p0PPA[¼šAÛe¯lsvÌmÒLcœ¤hp°‚­\Ï~Ù¢eö̈¤È’™åCa[¸˜Ý²ÊìŸ-I‹VªêˆB¶t-Kš©RIˆ”Ž2;«ŠB"±%}tg)ªÀ¶R12k«ŒÂ, ‰%åôcs[{01—ƒ !›èIQþ™ÃœÓUÌóHHؼ­YR:ð~å: ÙòµZ €@K-Æ×)"BB¤ì·œÀ.ˆq]$- 4eü)%H@’V°Èu›à€×9R6ØâÉËu¤ü€×iRd‡|v+L 4Ãu¦Ø@O@€×ÙR qÝ,O¤@AqDiË„E”g ô@'^ÚÁ€A$X%Œ@@©xiK,ZÀb!Øf–,nP@ J°qˆ#‘LrÉ&ŸˆRÊ)ª´R |pÄ`´A‡~Ò%›Xâ… °B 5Ü C?qD>P\ÑÅÀ T°A)РƒH¼ð@P­äPKÈRt¬².qtÒHЖDÒGѪijÉ.›¶4ë-;icons/MapiviIcon32.ico0000644000000000000000000000427612230036210013564 0ustar rootroot ¨( @€$ ¼ŽŒ”¬VLÔìÆÄ\ üRT즤Œ.ÌNL”>4„ Üz|䆄ôôæäd" ´jlD ´&¬6,ü¶´äžœü64l ¬F<ìJLüÖÔünl´^\Ì&übdüzt|& ì.,4”&ü®¬üúü¬B4\ ü†„t" D ¼2,ü–”Ü&$üÎÌüZ\üîì´zlÄ&ü¾¼ü><üFD´fdŒ&üŽŒüžœÔ¢œü¦¤tB<l üÞÜüvtü‚„ü*,< üNLÔ†„¤"ÄRLd ü&$üæä¼rt¼"´6$´NL¼^\Ô&ôjlü~„„&<¤&¼><t& T ü24,Ô"üÆÄüVTœNDŒ ül" ¼jdT üº¼ü:<t ´F4üÚÜürtü~|ü²´´><d üŠŒL üšœä.4üÒÔü^\üÂÄüBDüJLü’”ü¢¤üª¬„B<ü.,üêä¼&¬.$$„24ÜŽŒ¼ZT¼~|¤JLŒ>4´VT¬&d"ä&$¼fdlüjld¼’”쪬´bdüòôüâä´:,Ô*$üÊÌü"$´B4”Ô\ ´nlD ¬:,l ¬J<üfd4üþüŒ*d ¼b\„*Œ" L ü¶¼ü6<üÖÜüntÌ&$üz||&ü®´ü†Œt"ü–œüÎÔÄ&$ü¾Äü>DüFLüŽ”üž¤ü¦¬üv|üNT¤"< t&üV\ü^dü.4üêì¼&$ì¿sðr¾N ¨Àª·ÂÛ ÿÿ”ÂððysøwðÌð¼éw¯ìGý””§§TXbnn)nnb"T§§¤§¤§¤§§TXbnn)›nbX§§"+nYpppYnb"T§§¤§¤§§§"+nYpp©Y)bTT"›pUUÀp)?XT§§§¤§T§TXb›pU¢UYn§¨` Id¢ÀYnX"TT§T§§§¨` Ÿdd$Upn§aCf1m$UpnÁT"§"""T"a|v7wE€¢p›§\|v^ xO[U©n+X"X+X+XXšZvj*½[$,)§Q6j=‡[Up)++b?)nn?b6^B*q*[$p›§Q7tAk&ƒ[U©¥bnYYp©›n7¡ox=º€$,)§•w ¯Bl[$p)nnYpÀUÀpY®¹¡xqz.[$,›§•w S*[U,Yggp[$Up4E.;±.[$,)§•w¡B*eP[$p›JR$[[¶wq;l;$p›§•w¡ko5‘[¢|¸tA=[U~w.y;[$,)§•w¡B*u8[$UŠv1k*z [¢MEqye=¢,›§•w¡Bo]8[[fEj::qn[$Æwq=e¼[$p)§•w¡Bo–8[M¸toq::·$[ME­;=u&$,›§•wB:0cUZ7&]B;zp$ME;zu&[$,)§•w¡*:µœf¾:’¡u_¢Æ¾­;&]¢p›§•w¡*xµ„€rvtu“>vw;-jy&–e$,)§•w*x0Œ6wk2‚UGfvs>W^A=&0e[$,›§•w¡o.5Hwtl}†,›CfE± !1AQ2aqä"3¢¥±ÑÒ#'5br‘¡ÁãðÿÄÿÄ' 1A!2b¡ÑÒðñÿÚ ?¹3U^E)m©h¶´îT ßÔ}˜ªñ®Ó‡ÉbCRÌ©µŸ¹X$Δ¸Säƒ%ÇP¾è%%*µ·=9uÇfhl:ÚÝKÑÔbèå{ØmÏ¥G1V< ÈøN¬©VDHõ¶¿Ï£|I­,Ù©Äõ ÛëâíÄ–xy‘œ¨¡²º„å)ˆ ¤\% Ô}IçÿIѳ–fÌ©µiÕÏ —4’ó±ä)„ªä²okøvߨ¶ºˆP¹2Z}µ£vì é¼~!V\HQO ò)B÷úøèGÎÕ'¼x—> WæÇœ¼&ã&bÈù†;s&K©@ma+Šì•- lúZH²¶¿#ÎÛâëà}N‰Ä*[ZD媩U”b›jA¿PyÚãMõZÆê ‘€Îc Ÿ˜*O€¥3$ì•~8%Ž\‡6PTd%kEîÚÀô‡ÑƒFþm>Áp:¨±Èh;žBU)²!m¶§¡?†µ*;ÀëÔA%•X|­Àê0Š µà§≠¾²ÞˆóŽGKθKh:w;'ÈôG³ èU©i)n[©I賸¿† ²‚:¤uJÁ¥ÃI;v¼Í”ªàÄœã©qt†Ü •l^xtþS…aâF[Xœ|¸ƒq‡¿h¾ž)爹‡ô´Ò|–œˆ]Ç›»ýZ\qzõw¨·ÎZÖéÏ|ŽÇ râ/Ø~ñŽ]Ú¿*ŽV”zãïf•]‹Iö‹—¸mO¨Ôf™R_zzN—Š­M–±‘ ìõaO›rÕqU„ŦÆ"—áÝ<”·ç“mԳ̛•Z÷¶-‰ÆÃöö¼cct“„_°ýãYw–uÚj_§å•²œç2l’¸zÕ!çémÌ% ñaeJ<×Êö¹;oô᳕snA¡Á0éSÛ…¬¬¡¶£Ìú89“׈ Ÿ1{Æ3âl›ßá×ýÇï:õ>V³•¡sûóE>™Ÿâ&qcñ&¤ÝÌÊÚ}±Ýü¸®©êJà0´¥M$ƒâ-‰xv5öâB·éæ?÷⢂ϓAf>­]ËiF«Zö¾/Óê5·gÞP.8ÇôÆÑ@«8ŸÿÙicons/MiniPicMulti.jpg0000755000000000000000000000421512230036210013726 0ustar rootrootÿØÿàJFIFHHÿþ(c) 2003 HerrmanÿÛC    ÿÛC   ÿÀ<G"ÿÄ ÿÄ? !1AQÑ"a2Vq‘•±r¡Ò#RUb…²³ÁÓáðÿÄÿÄ*A!1Q"b¡ðDTqÑÿÚ ?vóE^l k-ºÊU´Yóµ'›þÖ*•\ó\iÃì«iHOR¦Aü±c͸Sê2Œ—B¥V·ÂŸž8ÖÂŒÍ ‡[[©z:‚¬]/{:àÔ¨ó`sààM¼jò•d.=¾m }ÔÀ³d;ŸûAjK:y‘œ¨¡²º„å)ˆ ¤\% Ü~IëÿŽèÙË3f ÔÚ´êç°KšIyØòÂUrá7µü‡[s‚Ô[]D(\™-?Ñ»vé„|õ_q!Eèä…-qŸ7UÜï"çÉ´úcºM¬™‹#æíÌ™.¥µ„®+²T´%³ô¶$‘ep-~‡­¹Ãסõ:&¡RتÒ'-PJ¬ ‹´Ûr üAëkŒ7Õh##ˆL.F9—º}b°ø Sà$žlÚ}0 í¢oª4ò±ZÿYì‡6PTd%kEîÚÀúC÷b›ÚKëyÛ9CªÓª”x­3Mn:‘6Bв ãЏ B¸²ÇÇ\#ê,r-x˜-³Ùç8=~æ·–Ü·]’Ý6ûšÄÀGC®l UJA¶Ö•mð#b}0×=O¥dZ#ÎfD?ݼÿtÈi;œWSp..8þ#ÝP­G¦ç'YyjL¡VV·ûaaíÏU®f”åÜ­—à¶âdwîwèŒú\;R~‚H&ö¹'oN w ÜdövÛ»uížcê>tb¡O‹1ˆÑc{3lÉdïC„¨’@$_ž£ÂØ«IfcqÛ}´9íHi%E Þ­É$—?»yÿ*Ô²¦vs/)×f¾ãMwˆm@8¥­ R“ÁUì¢Ss×i¶+ê†ß(B–öÅ€ÖÝÉX'û£ìéˆ5Ëç1µ ø™ÔÕ²è :¦Ü[ª q}⊛P=-~w~v·–Þº!éòéóBä:ãŒÎ—u–þ¹ÝП™Â¥’‰!˜iŽú· -<ÂTâV•,¡ÁøºqÓ ‡cý?¥T%œÏVöiÂC"wZ•àwî €’‚J¬>.@ñEÚðSÏ3—·¼w¢<ã‘Òó®ÚÞO Æ>uØsgáödÿ™X¦B­KIKrÝHBO‚Ï"þX%W‘dTE}]Û…°â\u¶61°÷‰ ,Bó(†Ò‡RÚyuÄÇ™B/³¥ÓÞoÜ;ÜLy¯²÷€nÖ²•BÕ4Dœã©qtöœ •pT±áû'C©m`nqò  Æ ý¢ôë(ê~k1³«yn˜–à·¸SÍ;r•-[·“ý~–ðÅNÙ~‰Q‰*D-_£ÉnBŸr4D:†A½ŠÔø¯[,f[«êªåi@WìšÍ*³0+EËÚmO¨Ôf™R_zzN׊­Í XÐxü°'͹j¸ªÂbÓcF‘KŒðîžJ[ŽóÉ·*YêMÊ­{Û èìÇA#c¢qåóã¯Ùß)SZBª:á•b-ۆĞ鰻Zö*ž¾^8–Ë:î JçÕ ÖÈ9X+OZ¤0?OKna(\‡‹ *Qê¾—µÉãŸß‚ÎU͹‡Ã¥Onr²²„FvÛSôqõ©h&M§–Ó?]²TgÉKòorxä]îz¼cŠéäµEí žsÁ Ïawù?ç‚MOU©»P¹üú¢ßNl3?™c¨y5&îfVÓöÇwùpÉ×gGERU¨m¬,ôX;¸·_/+4žÎÙ^ ·Q\r¬•2 —Sm¹ÝžlqÐõò89j…VŒ*ð[‹9©½ÄÛï£<•&à¨XM¾xÓÑ]­ÔêP(ã~L«B’´$eIlÉvBX%H@M”E‰½ñ1WÑ ˆš¹í¤Y-¡\ß©W¦&(uÚØ–TûÐ4Q0TÐé.BÑ­P˜Õ»Èô–EÅÅÒ‰~XàïØº:•77Óêš•Sq}‡‡Cà¥@ðA©÷Œ#1l“˜¸)\{yw÷Ľ@­IJ[–Ì7Ò“¹)r0U˜¹ëkó‡¬i>™™.ðÖ½1çº}2úƒ— kÓþ“©~àýäþ™=§=õ¦s4ʼXýÊ6„®; C‰ „…sÅÀã¦5ºnÔlü—)LÒ,¤Î݃© èEì/çaåŽ{§Ó/¨9wðÖ½1=Óé•ïúƒ— kÓt=@¶ãîÁvñXV„·\K¦¡O¯{¥1Ô Õæl¼lZÖã#â¥Ó8±-9üøv½Ôi—Ô»øk^˜žéôËê]ü5¯L0iú ýD_¤¯Ú » ç™ù¾©˜™™ tÄf:c¥@«qs­ÉòÄÁ¿*e­–}Ì»—©´¥É *T´\ö Úís÷âcGN¶­`ZÛ›Þ="àOÿÙicons/accessories-text-editor.png0000755000000000000000000000063612232326132016152 0ustar rootroot‰PNG  IHDR(-S±PLTEYÄ YˆŠ…ˆŠ…¡ƒULB&ccckW6p[9qqquuu†††ˆŠ…ŒŒŒY ƒV „W£…U¤‡W®®®±±±²²²···¸¸¸¼¼¼Â«ŠÄ ÄÄÄÈŸdÊÊÊËËËÌÌÌΦhϪiÏ­qÒ¬jÓÓÓÖÖÖÚÚÚÛÛÛéééêêêë±=ëëëìììíåÄíæÅîêÆîîîïëÇùèÆÿÿÿ Ô tRNS3D\f£üûlW “IDATxÚmŒÙ1Dc;Q¶!Œ`ìLdËÿ˜èvâEÕSݾ§ÅŸµ/í²~úúâ ”°:Ò3*²x¥œû óú“©Û! dð=ÛÜ&ƒî·ÍR來Åu¢åq»9¡×‘n“‘ Ö@ÿ8–‚Ä‹*êÝÕIÉ<֘ě*6îŠ3Q ùK Ä"œF(lOþIEND®B`‚icons/accessories-text-editor22.png0000664000000000000000000000174312431174262016323 0ustar rootroot‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs  šœtIMEÖ ´¢Ù‘tEXtCommentToolbar-sized icon ============ (c) 2003 Jakub 'jimmac' Steiner, http://jimmac.musichall.cz created with the GIMP, http://www.gimp.orgó¿ÓIDAT8Ë•KLAÇ»[‚ Z4{0Hˆ'ô¢W¢&œ|ÄDà•ƒ&$Þˆ'CB@Bð¢1*„òˆÐH|PzBD,S„ ¥Ø‡´ÝÐÝñ[Y*ðO6ßÌdó›ÿ|3ó Xäí@%æ”·!„G‰v<ÙÚI-oÅé'‡ŠIfÃÐØÔÐ5ø- \àM ¨Ý=ãwwÅžOw€Z^úïÑØäùŸïP×mHÙ›ĵ+7ÇårFq:¬¬¬ ª*±XŒââb‚Á ªª²¶¶†ªª„Ãa&&&p/5SÓæeq¤—÷}mÔu’ôîv»©¨¨À0 E¡¼¼I’b+eeeȲŒa!p/5sûÑ(˺˜œY×%› €ÂÂB4M@QÛ 6ÇE! ñõùÅh Btº_1S‘ËòÎ>ƒÁœ'h}}ÙÎ*jÚ¼ßu2æ‘ ¼Êæwت’’’}¡«««;з|™ûµº/x?ÇY§­[ÐéÅñ™´³ »žÿ‚íg¶zYéaj!JÄ߇óÒ BáŸ{þ?cÓiuË߇ڙù'âï§²~’ùùùƒƒ­Ž…Œ´œ£úáGCíÌ,ʼnÏPY? @$±M[NÇB<›òy¦|>W~ŸàtÍkÂá0Bt]?8Øt¬ë:ê†×Jééë£TãÔõ^R©Éd’¢¢"Òéô¾`ÙαMÓ¨ë6h½9ÇèæUN–]æŒË€ËåBÓ´ì-ÜæVð1;ÇB„Ù%ßÚ^yû$I"“Éàv»™&Øæü­2ê ì–"I’$Ù^iSyyyäçç›Ý@t@r˜E#‹áóù8¬,“[g ØH >}ö¸Š#*±‘ÒfÌzìNEÀqËI‘s°ÌMÊ) $·û»žÙòq°Ù6þã/žb£ÆN¡z™IEND®B`‚icons/applications-graphics.png0000755000000000000000000000077212232326130015664 0ustar rootroot‰PNG  IHDRóÿaÁIDATxcøÿÿ?E˜$Åy¶ìüdmÍn4+R¬èF½´Þƒ!F’ H·dwn÷9ñ`šû¼'íòIë„͆e@}EvtCqÜßí)?N°<þ¢Ilê‹¥‚sÅ‚þݾ\ê èéí¸²bå²ÿ+W.ýßÝÕüÿl»ÕÍ—Ó¬×Oå›t™^Íí=í¦½ý]ŸÞ¿ÿÿÍ›7ÿ×oX÷¿³³ùÿÂ\£wEö-½ÐÝÛ±f÷ž]ÿþýÿÔ©Sÿ—/_þá¢ùÿÚ›*~g:‹à5 µµU¼³»íûÇÿ?{öìÿæÍ›ÿ/_±ì?ÐÐ/-mõvc¡£«­që¶Í?@¶8pàÿüóÀšÛºÛ,ñ¦ `•’’jsrrüûãÆÿçÏkþÜÙÙb†®]3§°°ðÖŒŒŒÿMMMÿíííÿ+**þÏÉËú ÔlB0)Á ‰ÿ½½½ÿW¯^ýßÛÛû¿ººúqq±¹ó”ñ^^Þÿºººÿþÿ—””I¦à5ÜøÈæææþ/&&ö_PPÄÄb¸ h þÖŒÀÏø(Obq|i‘Kƒd[ÀøÈIEND®B`‚icons/applications-internet.png0000755000000000000000000000142512232326133015713 0ustar rootroot‰PNG  IHDRóÿaÜIDATxÚ­SiHÓa.ðS_ÊoFâ•4¡‹ìø`yì¨YXtyLAÌ<¦†˜–¥›ó(Ó6…Ì çQ.i3Å+‰W™j²•ymn5u©s{úoˆ©HŸzáá…÷}Ïû>¿çywý÷á@É8@¢r„¤‹YjGj†Ñ‰šit¡g©]©éudŽí?‹ÉéáNÔ ƒSŒàœwˆ*îE4°|9h Õ Ñ³ $ ;mÇb‚=ôð5îÚ9 d_ðf@ƒîÑy|SéñQù\É›päz>ÎÜÌËÞRœ+’»;Ó2õw‹;15¿„±I ” FÀd³ ȯê=¡·2A¼ÒääË¶Û  D¾¨ðfŠMla7>M¨1­3bf˜Y´z#û”Hr¸Mˆ{Þ³D™Õ¼Aàê—­ ÎiEbIºzLé€áYà³ x?¦Ã•{Õð‰/†Çm¥?.ôì…Í_aòûÀoÇøÜ2ºËèÿ±FÌ+HöÂ;ª çÂKq:´eˆ.éƒÙ¡¿Ý§pt ¥ýøµ´‚ñI-¤=ß!Ò¢¶_§’ø2E8K¸3Šp4€g! .5nv 3òYÛÚ¤jK¿WQ&D$·… àJÇÀªÆ vŽòZ$¤pT›%¤RãÊ—„­_‘!#„õɼf°*†$ìGLqâxíˆÏ«G2¿¾±b“³/K´%}„5:FN‹É?U†AE8SŽôÊA¤•À#a^µA¿b@Mû(.DÍùÅ ¶dÁÎëa„«.ÌnÐk@O‘âRê[ÐH’ÓŒÚÎ LÏ/b`D±ZZQ/ñññ9´=ŒûlN…±ÈéQ&šeãI î‹,îKm¥¤¾‡Ïç³­¬¬öï”h›=6n—=™õ$KCͰóLÒÚž ”û] Êc0ÖÖÖöÄÙÝ;þ‰õ½fUÜ_ŸíÍëÛ ÿ ºÈó¬A¹íIEND®B`‚icons/camera-photo.png0000755000000000000000000000131412232326132013752 0ustar rootroot‰PNG  IHDRóÿa“IDATxÚ’KOQÅ „Úç ´ÓÔ¶Sp˜VZ¦RkHhË£¥3my ­P(5¼¢,JPQLЉ;#ߨÉG€?„k·F\˜öxçü““›{çœß}Ì_s]†n«ÕzÂqÜŸbæØ`Ð WÌ׌§G_p±ö÷¿ü"ë'7œ½Û|‹Ý½síî`&Ÿ+ß Óé xx$ä8FFÓ˜Êeáñ 0™L¸b&w¶Ùl°X,0›Í`455"é¹dN¦ÔkëUõ‘\YÍj\.^®¾ÀÒò"qpp€ÂÓYôD‘ÂhfÙÜÌÌNam½ˆÜô$õ¯¿ZGMMÍ Ïó(ÌÐÜÂ!“É`nnÉ¡V7Þã͇XÛØÄôüsÄÓcÓIDû"hóy±¼²|àÜ$˜€»™C»èCŠ„WÖ^ãÙÒ RÃ#ˆÉ bƒ2¢±8Æ'§àˆ`m,æÉ¦àt:ñx|ŒR;1å0PÈ)†ájá¡ÕéÁÚˆôõC|D"5§ÓÂuQ¡ W à¶Þ€ÚÚZTTT ²²:½üý6úMðxÍf®‚BÄ áŽ•…™a V>ŸW_Z­¼ €oóCê "C·.ÂÑ5è&5DÃÛÛÛ(‹P}Ž´úÚá ȃO\´Üs£ëa4Z¬ôè¢(bkk ¥R ¡PÑÞ^ ðKH“Ç®ªª:ÖØívòÏû!+r9•Í­êêê@Œî‡±°¸€xbr*îžž“ÎÎNµµ¿ÑNT!dí>¾öoR¤!2m%Ó6p¹ßmý ŠN?¤b0q*•ÚOÖ¥æ~Y3Óˆw?IEND®B`‚icons/dialog-password-big.png0000644000000000000000000000214612230036210015223 0ustar rootroot‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“tIME× +Æ! wIDAT8­ÓKlTUÇñï}Og†2N…@´‰BÄ 5£-bbBB¢+wÆ@±thbã£)bØ”„•!°0øªÏD &Ô*b(‰2Ì0ú 3R(åÞ>澯KˆÌˆ&þV÷œò9ÿÿ¹÷B…ôõõÍ«´ÿ_"Ì=är¿?)Ëj·øÍ¶cI¦iz¦YÎåó¹ñÉI}½çyqI’&øÏy£­­søð…ßÎ_|(Uß%eI’×u1¦t.æ™_[KzU3¶cQ,ù½½½³¶i>ßÞ¾ïTU¸ÿçºëS ÷6,^*X–ÜÑ'MÃ0j‚À#‹£i!.å³D"R©zê)‚Ž9lÌøæêÎ]c•`ÑõƒW“‰”0V ò…KÓ½_~!ž~Ïó‰ÅbÁ±ãG§lß{¢cwÇ@E û@÷cŠ(}ßܼV‹ÔDÔkCWÐ Ë2ñ}?p]WX¸`¡3q{BÙøÔ&×¥aqCpôØGÓ–ënÊìÉœ½{÷&³'sÖ±¼åçÏÿª655‰DY¹|%¥R‰iãæ‚@7üQ*¾lÛ6Ë–=Œm[ä.ç„ÛwÎ kÚÉžž·¯d2™›ªªKÄH&“tu½7ÞÑÖq®½½óÀ¡ß§ïÇ3Ø–Å/çβcûÎ(¢p@®ö¹˜fMÕPå¾ÚÞ×÷ =ï¾lxt##£D£Ñ»õª°$‰Ÿ|v¢kåŠ&Y‘ï‡ç¢ªÚW>ÔzT”Óÿ–¦jžª*²,Ul×km›+íW¼cßó´¶lÖTEA”¤ªpµT…fffˆÅâDÂáÿ–d!_ØvâÓ¦ªª”Ëæ\-É^ø\*Í(¡ ·’‰Dvbò֖쥋ò…m®à?¾ïÏ»'*úëp0ûßt ð'Û÷¹½X¿HtIEND®B`‚icons/edit-clear.png0000755000000000000000000000140512230036210013376 0ustar rootroot‰PNG  IHDRóÿasBIT|dˆtEXtSoftwarewww.inkscape.org›î<—IDAT8•’Kh”g†Ÿï›™èL›ÛHB“H¼Õ¨+&‹ Ò‚‹JKv‚—…‹Ñ‚"ü ]R„ŽÆ…`Ku¡ é1-´i41Š1šqFI“ÉeÌ?óOþÿûŽ+Ec!ø¬ÏûpÎËQG¾`s8ĢƲ÷Ô ù‘÷@‡4߯mŽ­Ú°¢¼1¤¹zt›Úú^`qET³´*ÂÇ-±XHsöô~¥SIÕ™JªÚVè|Rt}#Ô×”]¬ê«éŽKˆð뙋·óîLɧ©Ê•­7ÄãM/€º…JDèÚ§NÄWrü“¶ÏUsK;=§»½éÆrìT“ÆÈ%Î'ÿ_A*©vi­¾ýtsG´®®¿þ›§W²þìvj×U‘í÷îßýCŒõ¾3FŽ%ÉÎìÑšdS}k9—$j&]¶toeQ…P5~PνÁ_ýþß âÿ` )àæëRIµ(rŽ;͵«:.iýB `ÆÀd±A϶àÍÕÉ Èã‘wj2[Ô¯VI8RòwÓU{°±¤#e`g!“ó^á³¹ëÌŒ#^ÚÔþU…VJÂo5RÁÏÙLZ[«Pþ">þœ‡Wš¥X˜¡P˜FDÐáJÒOz”ü¦ßÌ'yæÏM¤Ów™qcLLŒ’Ë¥™ÌepÝ)D„Xõ6 EÍЃþ’18o #‡úz)ú¬Æu=ŠÅ<Öèp •ñäݘܺy%oLðe‘ÌëߤûŒêYýQ{Ûò­/ÿ'e±5èÈ2 ÷CC}ãÖòY‘‡áwÒ€1t =øëvå’†?¨jÓ££÷üááŸk¼ëÖr áÈÄ;Ÿ8ŸTR-SŠ>¥àšµœL8òxþÜK~C(„ÚIEND®B`‚icons/edit-copy.png0000755000000000000000000000063012232326130013266 0ustar rootroot‰PNG  IHDR(-S¢PLTEЋޑ‰‰‹†ˆŠ…‰‹†ÄÄÃÆÆÅÈÈÇêêêëëëìììííìîîíïïîððïòòòóóòôôóùùùþþþÿÿÿŒŽ‰ŠˆŠ…ˆŠ…š›—‰‹†‰‹†ŠŒ‡ˆŠ…‰‹†ŒŽ‰˜š•˜š–ÃÄÃÇÇÆÔÔÔããâãäâîîîððïóóóôôôööõ÷÷öøø÷úúùúúúûûûüüûþþýÿÿÿJ7o°tRNSOPS}£«¿ÌÐûýý¼õëo†IDATxÚ]Ê×Â0 DQQl:6½&¢øÿ INÌ çmï,TTPVŸªÖ0k¡µÂ`ÎÁÆ.@yD.-ø`b—…¦ù÷è÷ʇ“n#<ò+…q§]>.ÙÂp4 À¶¡ªµŽ>òßå­Ÿ¼)œqéÃ-»¿_§}‚ ÄlƒbAû $ê^‘.6IEND®B`‚icons/edit-cut.png0000755000000000000000000000116112232326130013107 0ustar rootroot‰PNG  IHDRóÿa8IDATxQÝk’QwZºùîµ Ý‡o9÷q±› ‹®ºê*ˆþ†õwÁî‚róC·D ¡EË×5CñƒE4VË4ç´ˆÃ.\ƒØÆ–¾i;çé9Á›¼mœs~ÏïyŽª}Í:î]Çý}Þë.ãõ_ÌçòØ÷Tÿ[N÷l]’$‚ä”×77ÝŽ¹=Žjq³@Ñä•¢ i±T€ýƒ}2wßµ$¿ÏØï^—‚°ö6׌¤h€àQ<ñ$©NÑ쓜“5ßç×a9aéJŠXé `ëËg–‚zæ/íNÛƒd:¹|crkŠ)*‰d SHLpøhÁOÉ8¤WLS´t7­T¶¡V¯‘‡~„ÂO™™Ä°N¶rŠÇ+/Rdç[ØðpÛóÉøiRLÙlwàõZâ±(ø/]¬ºúòð°#28xÐjáÉk4 VÃÁ φ†³) Íæé°NGŠ££$?2BSç dñ¬z1tFs¼.°ïY«•„õz‚Ü›⨠L…´ZZ°Z…4šôê€ég « ¶ Wr– duÀØ@,•³XšA䢦õ“©Zž˜ H¸Íîy‹¥†•ŽeßÉ»óÂ!;Õê[, Ó´ x¾ñÙ9Áqîͱ1"öö–e\ìéÉ~Äiž÷²û&ô÷7[FãöÖä$ÁX„tº*Fü;uv»»wKãã ýá f·c¢Á°éëû? µ‰;9<Ÿ9n/Ìq_eÎo¶³7½§lxIEND®B`‚icons/edit-find.png0000755000000000000000000000077312232326133013247 0ustar rootroot‰PNG  IHDR(-SùPLTEmokstplnjŒŽ‰Šš›—lnjuvs‰‹†“¯Î€§Ñ‰‹†ŠŒ‡—¯Ë¦Ð~€{€¨Ñƒ€‚„€„„‚…©ÏˆŠ…‰‹†‰«ÐŠ¬Ò‹¬ÏŽŒŽ²Ø­Ë‘³Ö—´Ó™³ÐœºÙŸ¸Ñ ºÖ ¾ß¢¸Ð¢ºÔ£¤££®»£½Ú¥ÀߥÁ়ҧµÃ¨©¦¨·È¨¹Í©¿Ö®ºÈ¯Åß²Êã³ÈܳÊ常·¼Ðè¿ÓçÁÔéÁÔêÃ×ëÅ×êÇÇÆÈÙìÊÚìÍÜíÏÏÍÏÝîÐÞîÖÖÕÖ×ÕÙäñÚåòÜæòÝçòàêóäìõèî÷ððïúúùþþýÿÿÿ^| tRNS1\‰£«ÐØâûûýýýýþl`l IDATxÚUÏEÂ@EÑÆ5X‚·ÆÝ݆ì1T„@Þðž?¨"AÏNä|’ž '1-uÁVJ̰8þ-ZÐk”;`ù.NÐŽO—¹˜µT"×çc“ôƒ Jµóý6+dàÞÝúá²]O¼2¸_ÍØh1ßW= à‰;ȧûƒB\¶(~„a"”JDË ˜8¦”'ÿ¡Á$ŠüÖ²!äk\¸ZIEND®B`‚icons/edit-paste.png0000755000000000000000000000073112232326131013433 0ustar rootroot‰PNG  IHDRóÿa IDATxÚ•“K/a†g°°ôõ„ˆ–?¡jaâÒbÙj»·½X ‰µ^Ôt¡I-P;Ò˦ ô:½ÙXÐ×ù>FŒK3NòæÌœ™÷9ßLξ‡Õj§§m e|ÕGM~ §E”\±JK'&±³³…ÝÝm.ºf5þŒÞaRfaŠ›ícB™ŸÎÖúqàÁ’K‚ÍfƒßïE °ÊåóyxÍí”°ç…ìïƒÓ,ÖÉÛÁ=‹Ï›&,/Jh4ÈårPE'Vk6›pSƒÄ† ä{!uë³óXÔëµ_ÅbnAB|}ð' ±1 „"jµê¿_EfªZþàns‚`8Œ£`¥Rá:2u­ \.qs6›6¨VUT*e2Q,æ‘ɤp¼4 ó{×BáÉ›+ȱcœÈ‘6€q€wÍçNßA–¡…Q™ï‘JÝâ<~Šh4¬3Óý XrÍ@¥ïoµZšØÏ¤üª™5é]4×O1šïÃÕ!¬Ð8Ûó?´ïþTÈÓ ‡¶ ,èF"ßFbf•Ì|ßÛe[ª„á± IEND®B`‚icons/edit-redo.png0000755000000000000000000000076612232326131013260 0ustar rootroot‰PNG  IHDR(-SäPLTE¸¨·¨NšNšNšNšNšNšNšNšw„NšNšNšNšNšNšNšNšNšZ¨š™ÀÖ,NšNšNšNš› ¯Ç#Nš‘NšUŸ ©ÞIÂ0NšR ¸Ý:¬ÙE³Ù7Nš“¡ NšsÒuÓwЊâ4—Ý*™¦›Ö" Û$¡Ú#¡éP¤Þ$¥ß&¦ïa©ØF®ñj®òk®ól¯×¯ïg±éc±í`±ïe²ìd³õsºà1¾âFÈÐ ËÔÌÔÍÖ$ÙÞ#ÿÿÿ ʘ`+tRNS  +036;ACFHJ[^js†Œ–ž±ºÁÄÆÈÎÏÚÝêññõýýþþsâ"–IDATxÚ]Õ1 Eƒ»Ûâ®)Zœ÷ÿÿ Zv»L9/É=“Ì$`ƒÐAŒü †aÕ»¢F ‘Ù&˜/5¶»ËšX&^éîT@„ ø ›ÓrB)'„ ÍãŠóûˆÆr§~¸>fsÁÛÌÐ>¿(yšð6m¥ý¢Z°‡±oU·c§¯ÿR,WåÄd&—ukÆ›Jxœ•>«h(IEND®B`‚icons/emblem-favorite.png0000755000000000000000000000106012232326132014447 0ustar rootroot‰PNG  IHDRóÿa÷IDATx…’ËkQ‡'¶>‚Ššªh[MÐj’&©Fb„Ô„I5IcM­Ö ¢qQ©ºõ÷nƒt%u£¸)¥‰:%Ig¦4ƒÓÿÂMˆýyñ®b¦>fÎ÷ͽp%^%Ir|t¹ÄsœHø~!X {šæü4¬g ¬ç jw®¶DÈœÍÃzR€YLøÄbìlSD8ðjáè¾M£˜Cãn F> #ׯz4C¢‚_ó7`\9ÉŽa]‘ñ-øCîkTj3“ô2 }rŒAÏ0Aèé ´tz*¹< íÒ(Vã#P³ã`—´åâ¿~h¾6IÆ‹U™Hœ‚zÚ…ïC{P>°L}<v9€ÆÃê4T§rý"s’˜X Få B¨/JŽv`c9“ óG킇p£=Žjd?ŽííKrŒŽ2^|>´aÎÞDõܪáA¨#ûQpv•W&¢øìüMî}ø‰—ËZ*Š²È¨±–Q¼ïÛ½F^¿¸ ý¬§s¶2ó3+ãCŸ4ï–Ä·qÁçÃz!×U^S®m~‰„ñvG¯_xF>Ù!ëÊõÖ'ïvm‹yûÈôT‹EÕ{Æ­©Vi›ÔåÏ6óñƒfcî^³ä°•í#,¿éíé¶íž­#„ÝÌ_Fæ|zË ¸IEND®B`‚icons/folder-big.png0000644000000000000000000000152012230036210013372 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<ÍIDAT8µ”1o#E†ŸÙ]Ûg'·J)’4'N9‰2H‡!„ ˆ)mJ~åQñhȸšÂ „d¥! »&AJr’Ü%!Žïzwö›ù(âø6Ä1o3Í|Þygæ3Íf“ÿC@ÇáòòòýÛ6ÍÍÍõÛí¶Ÿ¼²²òÌóÐI›ò<אַ®>ÞÙÙéN‘w777qΡªxiNU1ư½½½´··÷åúúú·åâÃÃCŽŽŽ²ÛÀ*"¦Óé "xïñÞÁ‹‹‹õýýý¯»Ýî7W…ªJÇWãÏ»»»ŸÜ;çÔ{oDçÜ5°ªb­emm­>??ÏÌÌ Õjc Aà½gkkë£[*RàœÃoÀexn…þE‚1€ ÆðÜŠ‰~>{’6äÇ/l³Ýö€µE!œ Ow„ „£s¿q¢ÿ¸_UV>6ˆ9§ ï=:iµž,3vÎq|áùð½|öÁ#´T?29Q¥¥0µÂ“ï~Xh6?ÕqÆ"Bš+ õǽŒ—gÃÛit¯òöý A J‹ŠIïÌÔ@! ‚©À^!·ŽÀ½1¸(.//- q£†*DáÝÁI&Ô«!Ö & ;_FáH-Ì6*Dwà:¯¤¹Cœ§^­ -ÀiÙ1Î9²À`Åßxeô+/çV<^¯¿^’£^_—{ahë”AæHrÁŠŸ˜Gr|ž"ÞÿyÍq!‚uJwPÐ9âü¿ÃÊêWÎzY¡ÎÁ€OsG­r>dJ(@^(ÝAn1æ¯2˜AæY|+f®1{oº§C’ä¢^Ï¢V«mllèÞ«œÓ^ÆO¿L pê$iU’î‹+ǵ4M¿ÿí“/ŠF-ºHn´Ö;ȨªJÞõËó§_ü>ê` X ÍúÀa³ÙÌ"@€—Àk®õ”©¥#xð7ÂÎ µ ÊIEND®B`‚icons/folder-new.png0000755000000000000000000000107112232326131013434 0ustar rootroot‰PNG  IHDRóÿaIDATx­’ÑkRaÆO7]ô÷tƒºêºrDîb9[fÓ´-!hT«‹Ö,cH›š§5Ù†NF+[Ûê ²ÎÐO°ådkËá¢É tK»¨àéýNz èƒïÍóþÞçÀŽŸhpœ·ão¬= ó)“UÐ{<ÌkJ2ùÝ3èHÏþ,ÙCìå³AHÈðùG[B; põà÷Ã\ô¨D’6bŸ&Páp¸%Ó‘I¼x>ŒÂòT>ß„Zôà»jËü(N ü;ysû-SM˜„ñòææøTÂn¥€ZueõÒKÎ*ðZ8™W!e·³[`yÛ›ØQ'hù+¾Õj¨–%ÚsàO.˜}S„'–oàN,‡Ð¢ %ãAqã Ê_â¨î¬£Rò sA0™;yƒ–Îð“eŒ/Èx©ˆXË_ÅÖÇA¬®zaºîÿtÔÉáîSFm$D¤8âKY´ÙBš@¿As(»ÉwtèàžÍ!üªPð¬xH‚ÛWxH›·­`ˆÂ7fÞãÚô;ôOeá eàcÌhl`¼(B\\Gï†Þ±ß!ç}qNd°‹iØ‚ =÷Ò8KŒÄ×ê‚èLnºÆCö  -HãL  M‹?…Ó¾ºGS.l ½Ï‡Ã—Æ…ƒ]î7ý+‡ºß ÿãý.ô†5é ÌµIEND®B`‚icons/folder.png0000755000000000000000000000064512232326132012654 0ustar rootroot‰PNG  IHDR(-SÃPLTEUWSab_UWSUWSE]y>`‰?`†Fmœ4e¤9g¢Sq–UWSXv›_a]azšceac•Ìd–Ìe—Ìe˜Íf˜Ìgieh˜Íh™Íi™ÍjšÍjšÎkšÍk›Íl›ÎmokmœÎnœÎnÎpÏpžÎs Ïz¤Ò€¨Ó…«Õˆ®Ö‰®Ö‰¯×Н׋°×Œ±Ø”¶Û–—•˜¹ÜšºÞ›»Ý¦¦¦¦Âà§§§¨¨¨©«©­Çã®Çã¾ÒèÀÀ¿ÆÆÅÆØëÉÉÉÎÎΨëÞ tRNSYgŸÀÈØAyùbˆIDATxÚ}Êy@€ñ×™—E®%$rå,ÔÖöý?•]Ô4™ñûó™ †Di¾Œè-Psq`§"eà·˜{'"PÖÄ” PŽ¡ŸQèÙ ¡Á3°lø{ìyTŸ–m¶›õjižåQ¿.ææl:ÑGÚà(Âc¯‡Z¿×í´[—Z ʘ¦À¥¼Bt¨uIEND®B`‚icons/frame-bw.png0000644000000000000000000000027412230036210013065 0ustar rootroot‰PNG  IHDR(ѿˊsRGB®Îé pHYs  šœtIMEÚ  4<ùÒ^NIDATHÇíÖA DÑ&<ªÇò®v£•ÄŸ¥o!¢ÊÌñ"s< ðÿ°Õ’¤v¦îŽmûܽQf |ˆê‰¾óˆ¿ø;xà=5‚ºíìIEND®B`‚icons/frame-bwp.png0000644000000000000000000000024412230036210013242 0ustar rootroot‰PNG  IHDR(ѿˊsRGB®Îé pHYs  šœtIMEÚ  +6[ë"6IDATHÇíÖAA5+ý+` fô®À>øÐ¶+ÑT(àûp¬–ÄÆÀÀÀÀÀÀ_¿ø¼Z\ÇïùCIEND®B`‚icons/frame-wb.png0000644000000000000000000000026212230036210013062 0ustar rootroot‰PNG  IHDR(ѿˊsRGB®Îé pHYs  šœtIMEÚ  )-㸉iDIDATHÇíÖ± 0A YÕ©Öô1¥`ÿR„+Eˆ¦¥yõŒÞÐ~î™Y¡êîy¸ºN üÜöÍëfÑ %¹ >ŒIEND®B`‚icons/frame-wbp.png0000644000000000000000000000024312230036210013241 0ustar rootroot‰PNG  IHDR(ѿˊsRGB®Îé pHYs  šœtIMEÚ  *9ÒO×5IDATHÇíÖQ Q5+ý+` œ»+ð6¾¨"úVÛÎÀ’"𤦾ǎѿwÐHS‹”IIEND®B`‚icons/go-bottom-big.png0000755000000000000000000000161312230036210014034 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<IDAT8µ•QlU†¿sgfw0bÔ¢(h…h$ ±XšH‹šôEŒ6Æ}$è“Xãc5Ý'‰¾m‚šJ£QlRƒbDˆFCb+1‚‚Dl»ìιLJÙ.ÚíÊ7¹Éœ;ç|矓s抪r-–¹&TÀoõ¢o88ƒ²rÉhál4fo»*0ÊÊ×·¾Kâ,©KH\LêRgIœ%N«¼³{¤eâÖ຤ƒ³RK*Ô’ qZÍŸíÚ²däU×Xÿå׬( ]£šÙªŽvÝ´$xâP\v¢ŠMkmç5îßPÇà [<ªª.Tu8N³§—©&ó¹OßËA.] _zÓ>rX»î¸õîÞ­O¼¶Ì÷RMÂÔ%8MÀ8Œ¯8W¥Ê‚¢ÁxðâЫ¡*X[cÏôû•¿/žÝ•'i®Ußp0Ú}OïK›ûŸ š&vs$Ô¨¹9Î×NSM. ªàSä¡5ÏùòòÌï'ÞŠÆì+‹×XÙùãì·Ñ7?MÛ®Û{ÁK°2Ï¹Ú ‰™# …Є† hxpí ¿œü9ž=uâ0ÊÎfÔà¨dUƒû¿ûäÏÓÔ5+º9ÿŠ\ A脆 «˜/WõëcΨc0*Ym ®ÃËêøxß{eµpçëð ‚_X€®íèæf¯“Ï÷M–5e *Ùò¿9‹¶[T²3ÖÆC_ŒWÖÝ´‰ŽëVã„ hX±|÷ÞÐÏÄgTlE%;³£eG%;uiþÂÈÞý“•ž[6úËY\OOÇã|:5Y¹X>?•ìT«xi7A›v{z{6>¶áûŠÆŽÿCíÈÑÃ{ŽÚÁ¥ârpßpðʪÅ|ä™gŸ2b`÷ø„«Ïù‰ðÇ¡1{4ÿݔΆ²Ž1Fm¸²yÆ+«—®m× 왘2^}|[ÁˆAÄ  ¼üÞÓ"¸X:_ˆ>kŸ9wÙ==Mý#‡û kªÊî£`Ê«‚sx×ì5S´¢z0 æuܵp~gÃþÏù·2‚3Aª€CM„J†`5¨*YHëƒáÎ%ÝöªËÚì×Çvi‚5J ÀyR9÷ YV& ¢'RŸ¢âÜ‚f¤¡BêˈTP ˜X ŒŒD@ƒRñœöÃç0ÎyÒŠòe_o8úË‘Êâ9pyC •lo*`3\lˆ qÁ7 q£%*XŒ“ú`ªÒƒ?}÷Uß¾]í÷q}ËÍ+'ØH°±ªÀ¸É .LN[·ß¾íŸJžè_?vjÇ3w/ZU˜qE+?îÁˆÐ}C 댟)>UB6)ëXDÊõtÜæ·ßØýOÿ'å7/ïZÑT¼®G õðæû¯%>Ñø‚,ád^¬«6€=Ö¾˜X›¦Ÿ¾ÓqkWKqþƒ.xÅ'ï{+ë*¹—kVH=p<àOÖßÇþÊñɞׇ†OÞ´¬kE!ñ5r×3¹“àÕ ^¤fš$TŸ‹a|ˆtÿV¿.}ôȺÑÑá LäqiÍš©ªz‡0¸õ£7Î~ôp<¿’¢<ÉjÀÚî7%=ÇhžeNY 0&T5ógžˆD@œC`ó¾ÕJóÌÕ}­‰ª†IN½a*"¦js?êUÕ_,ÿ?ŽVìɵq³IEND®B`‚icons/go-first.png0000755000000000000000000000113712232326131013127 0ustar rootroot‰PNG  IHDR(-SAPLTEÿÿÿ:s:s:s:s;t;v;uIDAT8µ•]hUÇçÎÜìfÕ )lеP æ¡(hh³EQ° ˆŠÄBŸÁA$I±Rú¤­`Ä‘P¤Tm7EiQ &…jl*ZZc³6*mµ²ÚMµ5ÔØý˜™{|ØÍ:Ýd›¾tàp/wÎýsÿçÌ\QU®Åc® ð—rX·É~¡Ž·&ƒšùdì”[ê ÂÌ’`uÜÛ™^Ù°w£lž j§Üü܆͈CÙmíW%Å3mi½sUæ1Œeúí ÍüöN¾VŸ_Ø7>õlL®_Ó{r™~»rÉ=ó“Ì€- ´7:ˆG)Ò0y,?Êí+ºíòÓÃc;fú톉Áàó%Á(í¯>û õEQuI§!crf”m«åéÇû—íÙñqfÀ¾2ñz°ýÊ` t_þ«DÁ‹P¢Düü÷פS·òä£Ï§öüp˺M¶;–ÍâàÈ…”ÃK1⃈CM„J„jÈÅÌ–Ïòðý½©o&Ç<2uhú2p9¼D~ö'*QUG2™$•ºŽT2…õZ£@ˆJÀl˜çp>Ëê®õ‰öŽ6¤|øbé/þ)Ÿ¯&ëZ! ç(V ž6g|D<"qãP Áè‚þjÚnÆHÕ<Áóß ØgK„Þ×·.cmg/?LçÊŸŽ|âEnú剀©š'_ð¬ÁxÂM‰.nK¬å³Ñ}Å_ΜþR 4î÷«1=}ÕOÜñR5c¿š©çW¡cgßÄó…®¶ûh-uðÁÞ]¥ÂovÛ½ÝÓçÿ(MŠç熲ÛÒ $±T^|jk‹5IîZþ³þ«{ö¿SüýûÒË'GÜ8в؉çÁrøp’ñ±§Ï?‚šôF¦ŽO…ã‡^89 œËé)À«¿õy°.6: ªåJ™±ñ¥ééÓÇw‡[‹ç¹{])ã°9 Ú)R3سïÝRþ×Âðw»¢!uõ !P©íAÖëS«ª‘rCö`5âÛÓ¹Âû¹lt ,€ÂPv{G*ÌHã'"–jAl-°W;E¼çãr@¨¨ª«sš]¦"bbÐz‘ ‘ª.ªñ}èX;óÙ3–IEND®B`‚icons/go-last.png0000755000000000000000000000114112232326132012737 0ustar rootroot‰PNG  IHDR(-SGPLTEÿÿÿ:s:s:s:tE ÏöŠNcc (&$&§¦g¹{dÈð0âM-mìœ=<}’ä…@‘jÚF¶Žn®.Nq %eUM]}3+kß@ˆ€Šº–ž¡±¿¬X (4<*ÆÄÜ>Lš› f­D¬…_°;#Âa±!rüÌÈ.äEuº0+ªç8Þ, § 3ëÞIEND®B`‚icons/go-next.png0000755000000000000000000000114512232326131012755 0ustar rootroot‰PNG  IHDR(-SJPLTEÿÿÿ:s:s:s:t;u:t;v;t‚«Z«Z‹»]‹ÁXœÃz¤Æ„:sNšOœR T¤W§ Z¬ [® ^³ _²a·b¹dºd¼f¸f¿h©+k¬/lºl¿n¬3oº&t¯;x¶=Ë4€¾D‚¾I„ÂI…¹Tˆ»XˆÄM‰ÃPŠÂU‹¼[‹½\Œ¾\ŒÃXÁ\ÂZÆUÁa‘Ád’ÓR™Äo›ÊmœÒgÈsžÉv Õl£Ê~¤Ì~¥×s¦Ó{§Ñ¨ÚxªÎ‰ªÕ«Ö«×¯ÔŒ±Ò‘·Ôœ¹×œ»ÙŸ½Ù£ÀÛ§ÂܪÃÜ«ÅÝ­ÆÞ¯ÇÞ°Ëà¶Ëá·Îã»øå÷$tRNS -ee««¬¬Åááááèõõõõõõõööö÷÷øùùúþþþþ!œ‡Œ¦IDATxc.fT "ÎÆˆ*#ÇÍ„"›®,ÕÆ¯RÙþ ‰’Œ`ÉÔ´ŒÌ¬,_Ÿ dy&@Šƒ£«›»§—‹G¼’03P ÉÊÂÄÔÚÎÙÉÞ6 Z‚•A%TUUU]S[×ÈÌÒ;X " ¦¡¥£oh¨ Ò£gl%Ë˳V:ÖÀ/\”“á°¸Ed—FÈð¡:]„¦Ãû¾ϬÛIEND®B`‚icons/go-previous-big.png0000755000000000000000000000167312230036210014412 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<MIDAT8µ•Mh\U†ŸïœsïÍŸRû#4ˆ ‚Ƙ™,Ú(©ÁúŠ]Hé¦+k¡.¢ˆ„Š.L7­buQ"B-¶"*(ˆA”XÓˆ¡6ièdfî¹÷s1w’1¦c\ôÀË9‹ç{¿—{îUåJ,sE¨€û?Žûƒ„m¾ÿÔ¨úFµkvÜ7î½~Sç‡FlßZ ý'X^“voÞÜqû«{´XãÒµiØy`XZsâÆ»oÝš¿¯ûѦÀ…ka6矗vÁê¿{Gç]÷ßNNÏæAŒ£R^p‹¹ýÁ?êÅ0´úöZö«‚s#áÖ4ܶóšë6Ý`'¦>”$Mxa×[ÍF ‚ ²œäèØîõà°:¸÷9·½µ¹íèÃ[Ÿn3‘21õ F,Ö|1y##k,‚ADÈoy¼q¹á`ߺ«7¼¼£WËùÅ)~>{kœ H5%M¬q±øÔ`2ÇI£(²8?â¶oìxf ÿXËäùo˜žûi¨ ¤¤¶BE<‰Æ à‰ìUxYyƒ—Ú1ˆ’ûe_$‹¢ØÀcPŒÄT½iªh Šé…E±”þWÍÉçþüýÐG'ß½tÓ†nº®½‡²/’šØ .‚°ÙµXšZ-Qkõ4R©\¬£š~ýšß÷×ì…ácÇׇôt>DØÀàBC™*| jq‘ÁXA…s˼¥(D$¢‰7ü·=UœOÞ;rïö¶¾Î'¥0û1j*<¸åYœsØÀ ª¤^I|uG`æ -"«jì2¨ÂLÑï'ßݘç‘ã~üÜ]Û6æoÙéNÏCSxýÐ+år1ŽVŽ.07„"’ÔÛL®¦ß¾Lþ˜ÿ•¡ÄŸ<8q¾+×3j"Äe cþòE€r¦R¶×X®áOh~š…ÂÛÉÞBáôÉÏNŒ—‹¥Fåõ+­9N2ù:÷@| -Œ¥/V†~™¼T<²‡4ÄPÌ\ÆY_¡jªêE$‹ªÚ±®‘Ü÷G“±ÊàÌë6Ëî$f.Ý×Ác`(©ªJý‘*=¢lwuH&ÍT?e(«.¿*r¹ÇTD,ÕïÜÖ©6Mm¢HtÈßqæXº¼$έIEND®B`‚icons/go-previous.png0000755000000000000000000000106212232326132013652 0ustar rootroot‰PNG  IHDRóÿaùIDATxcøÿÿ?AlUÌ’b]Êr›!<Ö%,k"»5¿Ùÿ±©Á§Yת„åaýʰŸ'îoüßLÍ©ÎU_®û·÷æâÿ§lÅg¦“c{õ¾m¾4íÿªóÿ'Hùèë2–ï CPp Ë 4'³>¬]úkë•éÿ§Êúß¾+ü÷Þ˜ÿGî®ùôÞºÿÇ€Hƒù‡î¬‚p²kð·é{‹þ-?Ûò¿a›ïÿÚ­žÿ›¶ûÿoÝò¿cwÄÿÎ=‘ÿ»öDh0ÿ­e€Ñ37ªGçëÒ“Mÿ'ìOþ_°Öôé&›ÿÕÛœþ×ïtÿ_µÝéáÓÿ©+ÔÿÇ-–ýŸ°DáÆJíÿ;®ÍFÞ©þuî‘òÿí»Cþ§­QúŸ¶^áÒzñÿ1kùþG®áþ±’ëèÎÿóØÿûÍbãŧêP½àXÉ÷­}[ô¿i§Rÿ§n•üŸ°•÷âv¾ÿ‰ÛøþÇmæýµ–ûØ2¨!Pã蚇y‹l.½Zö¿æ„ñÿÌC‚ÿÓ þ_w»éÿšÿW^iø¿ì|=H3ÜÌh,cYÔ)÷uñÕ¢ÿÓo…üÏ;)úýæÿ¶•x£3!ÙWr}í=ùoé<°d%e›2–‡Ù‹Ì~¬¼^KŠ˜^òm‘üŠËг3”ÑÓ­´ IEND®B`‚icons/go-top-big.png0000755000000000000000000000162512230036210013335 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<'IDAT8¥”[heÇç»MººE‹m£”Џ’Ђ­xÁvŠSµAQ}+x{ð!OnÔhAA0Rûâ“}((údTÖHô%«éƒÚB¤´Òl âîÎÎ|>ìÌ2›[µ=ð1ßåœßùÏ™oŽxïíE<›¹f&Þ‰:LkÓ³ù…§^½*î{¿Õf–~2ùöAûw¾Ò¶n{ï9¸ãååQ‚4}ðà—»d%]ξi1ð`OŸøfì‹8c­àùgkº4ÓRÛó’½£{÷C]¥mAâœ9ûkíÔ™¿?=¶VœZë0´C[;oÛçö0øiúKNNŸ`G÷ÝÁ–M·ìíÐòí½îÚØódaòü·4|(®qrúszvõÖ_sýá°l{ÿ8,Û’1ÁHÿ¾g Ss§ø»6‡Ñ8âKœž§ïþ‚µn$,ÛÒ‡e[ÅØÁž§‹‹~Ž™¿þhB#*€ ‹¿0Ÿœ£÷¾G‹¢ ˶¸&8,[Åè¾{¿qã†N™šD+ƒs– p$RE[uŠßæ¾gý†¹wçÞNQŒ†e+«+†o/í ïê¾ßž®ü€QkÎ9©ÓÐUŒS'§ø¹r‚m]Û]ik÷n„á6T«W”í[6ÝúÑó ­3Úû˜$iðÝÔD,P©ŸE;6 QððÍ/’$ž$öT«5>ýêÃÅÙù™CG£Ï ÷ƒˆbà|å÷u¯®¹ÖTßxæx‡Ñ–Ê?íÑV¡­ ” Zx÷ý7«qÝw¤ˆ‚(€vðø‘hï’z{%ŠØGT“œS(-«PFPZˆë¾câhÔVÛUoE[D1_½ˆˆ”eãY3ò²`¡ÚXÈï4“ÈŠ"WKÓŒˆ)†(®à=øôc5ê >iÅD$‘¶Ni2 `ÓµIçxà@×Ê‚NëªtSµOZÍ«4€HDÙÜäèÔ—^;vhãeÞx6‘3Äâ½Ïç¡Ù3K¦Ò!i`’Žg*:P÷Þ'ËúqšDçFÕ©K’ƒÇ9¸÷9Ø¿\ÿ‚Ä^RIEND®B`‚icons/go-up-big.png0000755000000000000000000000155512230036210013161 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<ÿIDAT8µ”Mh\UÇç~¼y™1Fd,m1ŶBÐ…Œ¥NRl)± ¶©#¶.¥Y¸KlRAp¡+?@\Š‚uá"P âB‚HÀˆݸ° †2&L…ÖN;3ïã¸È¼ðRK’‚9p¸÷]îùÿ=÷¼+ªÊf˜Ùêf‚ÝF7îtïƒlß6OMiò¿€'ýKÛïè;†AjúÛ»Àèz1ë–b`½­T~ù`õhihÏ‘bÏ­åã{'‚Ö‹“µºbp,¨K¥™§ž(¯Ï’¤»Ê{øtúãÆ•VãÈ·oD_Þ´âGÆ¥×z7ýÄgK‹Wçøëòj—æX¸|žÇö=YòΜ wÝxpRºSggï{¦'µ-.\ügÖx~]úžFRg¨z¨¿ªŽÉ팈UÜçîéÝRÞj~Yúk|LjðÔn)ÊÀƒC[ŒwÓ•Qñë‚kwÛ÷ªýC»ûwTüOµ¯±bñÎS|`PÛÆxåÜÂYúî¼ÇÝïCý]=öôšàýø}wUŽí¯ ‡?×f1NÃaX =‘ià BP0à#Îýy†Ê‡}ÛvW'ü©z­·5ì Š8Vƒg_ö_W5Öâϕ־`0AU±VˆÛ~óf$××wøFfŒÐˆÿFmŒ ãASEì y‹¥™^B +8o0NˆÛ)²6wuWȲ9)dk͸ šBš(I¤hèJLQD "²J¤Ë€€ï|»ÎU8Ð;ŠJб ",ߤéÊÓ Ä@$"q6w¹6u@ýäÛÇËk˜z&"g $¢ª™â<4³d¦ã½¤$S ´¶ª¦ÿy6;IlÎ3¨ílIsð$WÍÁþíI â, èIEND®B`‚icons/help-browser.png0000755000000000000000000000142512232326132014007 0ustar rootroot‰PNG  IHDRóÿaÜIDATxÚuSÿ/Ôqoý%EÊZ²Òš¥Z­Z!}Aä˜|™ù–PíÈj]qÎáÈ!dçjçºÐ­ŽN¾U¤Øˆ­åË]wç>ÇøÜ«÷ûÃ5JÏöÚÞïÏçy=Ïûy=ϳ À&ì8+t%È$Иס_ÿæú·ÿFâv‚´}þyÖ\¹ŽÕöŒav~‘=ç>ѱäC|øÔ×ÁÛH~’®d¾PÛ:€hA=¼ÂK(¸s]Û & àÝV2Ô—r6H»z§Ž™™5#˜_Ó±Ñña ¦˜ |9ߤ*D T0ÌYšQc£Êåjv È·NÇ ¾䎄ìF°¬ÃFÌ›PÓö~…gø#Ä‹š0M¹på¸Ò™ŠN;}6%Sx_“q†Hœ^j+«,N%Êq"NM×(¤µ],åÒú7ýã´N޼ç¼n8Ä+Æ‘ˆ2ˆä føiwо©ÕÈ’w¢ëÓ78ûˆô4€ynÁŠÃ¼"¸øå¬‘C‹áYŽDq –WVa·Û!¨ÔÁ?½*$äi0OôÙémþÀ3Lн—òá"å2•q‚Q«jÀ•»»¯Fxf3nJµDà%8ÑŽâ²àTH„*ű˜J\¸¥D÷çIôO!"«‘ˆ·".¿ êAôLc×¹½CDVõj<)ŽF•ãd‚ùµÔtCªêG´HƒxI;®½Fj‰=#3¨jþÈ’2mdhkb…Ϲìg’ž"âKz¨E !ò+ô¨Ô cÎdƒûe ×FÇ ñ#ï=cŒ¿,H‘hàCÔŽ6Áa7J×ÈU­#0Y—Ÿ­¶9ùdóÿå(A½ÍH„kë‡XñÉ…íH+ÓA¦ù‚÷£d¨ˆpÉy-¶Ý~bn”·\¦ý¦¢¡ÏÞGÄ3-.ÁL2ŽPÝ:Ä*dHï7-Ó×ÙÙW¤'Ï4S¸ø‰õD°-×ù7B+žœÅçj(¯m¢¢¶)$…!?áPó§8ôûU¢»µ%º’'€ñXQÇCà@$j•ã‚y#¿ùðEq®¼­{£;é ”†¦I¤”!2mƒwWÎf|0‡w¢û»Êª÷ÆcEo< ¼Í¶ô5·¿îÛºç$'Î×1}üp^Z8ž !?9Ù${o·qèLûOV⸠¦XµdϯßÕžèJ.‹ÇŠŽõÓF¶n±O×5>ßuÓÐÙ¸êYV,*|¤¹?þz‰=±R×录3i¹ÛÎÇ_löpÅ3ó™Û`õÒI}à‹µ-Ø–‰eX¦A]ã] C~!¥˜×[ñD Q˜ï§²®¥4t]õÉ0Rß²vùd&?Àµ¦{ÄJê°-Û40Mú¦ûŒ•ƒçycÐH!R“(MCi=ý€¡+^˜bÚ˜T@ê›î³÷èUtCGH‰”èJ¥ZÑÙ[ñe@UÔ6æ!¥DÓRRš†RC¹}_P\z]×±-ËÔ1 ])FËæêµ„Tü}ä°ÁsItvÓÓãàº.žë±tVSê½ó•7*€Ã}àôè[ÿé·¿tÌ cJÁ`\×Åu<\×M½’^]Ý=$]tv%éNöP0¬?}ëpwEï¬þÇt .™Q8rÞ¶÷—ç¯Üâ§ã•MÃ2{R(¥!…@i‚ÅS‡ð÷cýæ}\jØX{`ÓŽ^Ö¿Çf¦b{V†ùò–dL7”óWnq­ù>7ZÚÑuQþŒÈË¢00ˆ3e Þ¦/t´wtð ô!0)Ã2ý³ÞŒØÙ#vÌ™ôÍŸ2ãóDþˆAT7üEyÍ-÷è™+ɳ¥5µ·«Žmh­>vhõ<¯í‘`!D60¤gä 阻ÐÈÌ ›YO 2rŽët¶6'Z«º[/·Ö? Þmàp¸éy^âq+À|@VZý€Ì´Ù Ri½ŸÞÛÓêô€ý Â"ç ÷>Œ@IEND®B`‚icons/image-x-generic-bw.png0000644000000000000000000000064012232326131014737 0ustar rootroot‰PNG  IHDRóÿagIDATxœÎ=«~Çñÿ{»g%¼ ¥”ÁCd <¤{‘’’Q ™Ùl±›ePøýϹ†Ó §{ø.WWŸ~ÿ‚ x…{¸®ë“!à±^e ¾K· ÿò_Ž»ä|ªÑIEND®B`‚icons/image-x-generic.png0000755000000000000000000000074212232326132014340 0ustar rootroot‰PNG  IHDRóÿa©IDATxÚ­ÓÝK“QÀñþ—þ€. d”ŠLȦ†"]ˆŠT†š›)*‡6Tz|Ű¡ ÝÚžùÂX£½)–ïøt-AW‚7ÁDqÏ×í[½X_8ÎùœsqÎ¥¾çj&.˜š0 ã" "@:æ‘{‚ëÖÆ?IÖ¦R©< Ú ¥†gs4¹¸×;K‡¤þé,wkÔv°9ýRµÝ'HÈß ÔöÇ`Œ¡@˜½ïI~|ÆŸÑÚå¾;BsOXjè^0Êjìt¿úÀªÆ0ŽƒoûK¸< ºF“8‡ã’]™ÖZƒ¾Ubko9<þÊIú}/‹éú¼+¸'—¥Ï’9Pž<óÛL…7ˆ~Œ³¦G˜Ž~bbn‹—¡MžôO1®m0X7*êhq-ñ%›Œƒ™17ïvQ÷ˆ™èŽ9Pfkãuèý™¼¡E¼Ú"“Ád\S@&-U­XnI2.ª|À•’F.ÔåÂbå| ØÖÉÍÛXª;(¬lãZy W­-²ñg¿\ê wC¿+üß§,‘šüÿòONÇËCáu IEND®B`‚icons/image-x-generic22.png0000664000000000000000000000160412430724565014515 0ustar rootroot‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÕ «1Â)IDAT8ËÕ•OˆE‡¿êêšÞžÞÍÎf1D#ŠˆèAÁ‹ÍÑ£¢g!G ˜ ’\<+xY‚ƒäLˆ ëuuE#ncŒ&êšÌ„=d3;{»ªž‡éig2h”äbCQÝýÞûúÕïU½†ÿÛ¥–N,½ ,Þcî±X<øòA±ÖªòþÝÃÙsg„Î9õÁGgøvígtú¾‚xA "x¤¸xtï~ëUB¥+ßüÈž'ž'ŒvıãfCcÇ:sëëÖú›µžõËQJýµ¢:pÿ})¯˜¤Z©òÙÊU¾^K @œƒÜm`çi6u!M‚€©$â™}){^@)Ãþ§›\ÙØ¡ÛS8ß;'8ŸÃz œNòwB¯¦ ­ÃðRTÊÛ-Mjk=ÍV£Ií" ÁÀ¾q6£Ñ¨Ónl2ùà¬óã+`2.Qßœã‹ÕKT§wøe£Êd¹„÷Rdâ½à½ðÇOßá¬åfý ÉìCÌ-<Âo¡×XPI"PŠv'¡ÝJ!˜$¯ü,Âfíw®^\åáÇ÷÷e´MæfLŒf,"Ø,£»½9º7ó-åEHÓ €^š²¾ú9ÆDdÝ[„¦Äîùvš7°YJëQd¼Ýlqâä†7²÷žV§K«Õ-Š…Žr˜¿ÖÏã¼pþä9<ŠÙJiTŠ¢’¥pΑ9‡µBh&Ø531ºïèµoÑjw0Q‚¦Cezúq°ÁñºÁîyl–ÒëlóÔcûˆ&ÊL•5õµQ)´ÖÌVgÿS§ÙÚÚ¢TŠyéÅ瘙Š1ưváZ‡ ð³OîåûK—ïÜïUF Þ{ÖXdé×ß|?ˆã˜·½6= Q¾üj…k×6¨]¿þáâ‘£‡BïýñÓŸž~çnðòòò{§>9õ1àQy>ˆòšMäÏ! ‡F˜Ù¡ÙÐz@: ,VÁGȵg•ÏÂI>|þ?ðQŒ)[@oÿeÈXGm#ö?ÂŒžØ½`žIEND®B`‚icons/input-keyboard.png0000755000000000000000000000061112232326131014326 0ustar rootroot‰PNG  IHDR(-S™PLTEˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ÕÕÔˆŠ…———¡¡¡···¸¸¸ÁÁÁÁÂÁÂÂÁÄÄÂÊÊÊËËaÍÍÍÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØéééêêêëëëìììÿÿÿùü>ÎtRNS "#+,@BDXgh‚œ£Ìþ'ÈZJ‡IDATxÚ]̉‚ €áµû.+ ¶ ʨ°ÞÿåZز±¨uûãA'áít½\LF½V,›ÛP›‰M\S[ï+_O"@¼ CELHö4U®ò쎪Ó1†p:Ð@­±yƒð_Zò'ÁBj-c?1<î7ç®ey±gk(¬DCÿÞ¿'#ÑeÈŠIEND®B`‚icons/internet-web-browser-big.png0000755000000000000000000000244712230036210016217 0ustar rootroot‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÕ+!{U͵´IDAT8˵•YlTeÇw™¥-ít›iéB§vJJË*”ˆ ¢†ø€ˆ‰ñU}‘D|2åʼnúà›"‹šˆD#hET JC™–¥¶èÐéL·™{ïܹsgîý|蠂Ŀ䟜/'ß/'ç; üOGº—cûž`°AÑ”š$IQà°ÿPߎëÿ üì«îó¨Ê‹+- =$XUŽÏ£’±rŒOê ÿ1ɯWcwß¡¾/ß¼}Ï:×qú—wµô<±¦“¼ãâutº:Ú8ÖãRF–Å¥Ô§pvàƒÃñs’$m=Ô·#q;K¾ý" Ιu«;z6õFHk)ÒӦŗßÿL}dŠkðËt†d*$ƒ «ÚX±8¼FñÙ+·Œm»ßoYWxãSw¢é:k–´“˜JÓܶˆÚÐ|dYÅÊêHÙL3K×ÂVB?¥%>fŒ\sc÷¦ªK§|5¼}ψǣ~°mó :ÔÒTWMÿwg¨onÃë÷£*2ŽȪpÐG[K#¥¥%ȲŒGäÀWÆàp¼wñÚ­/>2  äMë••«:ˆ4V!ËG¿þå½!I @–d¬©œ¹GöWBÞà‘£ª M•2íá:††ã»€×þα×ïyº­©Ë48zì[Z»B®(‚8®@U½<óäã¬íi¡¶ºŠó—o Iªk1?X °áV*TYUBµ•eôG *Cdr~¯ŠO•8Ž‹©¥XÙAA(XC(XCllœèµQb7ãB B,šSŽ‹âñªT8IláÅ0s$ÓY¦ ôÑ̦6‰bÎÏ×Tˆ%¦©¯©À£*s"– `dóªnæ¨Pƨb“PZÝ0›Ç!§ ÇEQþ©P˲hoòé7?±luư“/Ì$§´àx¡†Þ–r"á&¦Ò|~~¥,HÁqÉÙ~ÞúèÙ¼Ëúž:z:˜¦‰G‘¹>!k&HDç¤ÂÎZgGb“ÌóA$Ü@Ue€{ð£Ìh&)=‹®T£‰¿\G×u ÃàäilO¦$©صó¯G‡ã„küœ8}×ug²DÁÎ2£e™Ñ³¤u‹tÆ"6m“N§ÑuÓ´XÝÝÂÈŸI€ýsdhàx¼}é¦Gó.­rY =AY‰|>Oÿ¹Ë$s~2VžLÖÆÈæð»‘T:MZÏï±)R)ãÃ{w¼«¥Ïñq}Ǻ—*Êý>¯ÏG©’çÈñS\œ®ÂÌ0-›LvVI­@O½KÂ*!™v¸rudø“7žÐîÕv&sÒ†-¶PIEND®B`‚icons/internet-web-browser.png0000644000000000000000000000164012230036210015447 0ustar rootroot‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs  šœtIMEÕ,æÞ5tEXtComment(c) 2004 Jakub Steiner Created with The GIMPÙ‹oìIDAT8Ë­“]h[e‡Ÿ÷œ÷œc“4i›æ£mÖТ,¸u²ÈÄ«)¨0o”)ÝEe8o„¡ ·RÅû]ˆGAÄéL†7³0µ³–v+®ë\?³œ,Mr’s’œ× ÙÄíÖçúÿ{.þü~‚8þÞì)à-]Êý ,åû ¥XÐtíìÜÌÔÞ‹ûÁ÷¿œ4uílj±dâÈ4c©(Ë¢\o²¶i³¸²ÉÂe!Äɹ™©«ÿ¼öîç¹`¸ç·WŽ'â”ëMì d(=ýàØ¤QV¶9?¿r!ž½'‘Ñhè§çŸÉŠ'2”ŠyG³D‡pZmò›ëHßCë6y.—¡ë«è…K×ÏOho~øÕGÉxøhnCóéÈhCJLC°$‰h/Cñ±¾ ¹ì£¤‡޼ñÁÙi- œ<œ¥R.rÇ Þ‡Rà+@hßc·Ôàû‹Ë\˜_$b)ö¼ kN;–Nö³p£@f"ޝþýl½Râ©É,B”RlïÞ¡Õl2‹ÐZ^G³,-§Roa×<·KÍñðê6Bü£BˆE¹ºzËR×5¿ÚhiÁÖ!iQh R4L:]ŸV©Ãd»ëzô†‚8ŽÃ¾TŒBÓ(HCÓò·¶ŠÃ‡Æ“dF†¸½¹ÍüÚ&M/ȧß^Á]Þ~ùµZ¥k7¡7 ð;€¦ºÝsK«[x¾ =’âØ¡»…w« ª®àvÙǶmªÕ*ô§X]Ï| /ý|îüxî¥ÓB×ÍHРÇÔùeq™å¼¢ÖpÙk¸ì9.¹žÇVÙçÚÚίs3SïÜ/R¥PÃÿÍßpl7÷1Ú•IEND®B`‚icons/layout-nt.png0000644000000000000000000000262212230036210013320 0ustar rootroot‰PNG  IHDR có¨ pHYsÄÄ•+DIDATxœU;dGý¾¯^÷Þ¾ÝÓ=3½3=3;½¶5ÞE $ˆø›"âM,H%È–DjËb%lBˆ‡Ö둌vgØžžîÛÝ÷Ö}׃ VKDâ KU:çÔ9ç+üå¯~7Gœ ksŽCľë’$±ÖNöà ­¦éÊ,›AÍãØ¤C±Ý“ñ^U•q¬˜ŒSÐb0z¹ÚÔõ ʪ ±Šƒs΀÷€Dà½GD@@ĶkÆ“öhÔÿøvm%U䀄TÅ?øèÉWÒM›Ý4r½ºEDðÞ;kUEQd­UJYcˆˆªªêÚsŒÈ:×4ͱ¶m¾÷è=Î8;;>™îŸÞ¬3®Ò½ONNsñ†÷~OÙÉxâœóÞsÎ…}ß#b œ$ "v]'„€Àƒˆº®óÞÃfóòfqx8å›lwuõ”óA]‚Ö…‚ Þzï•RGGG‡‡‡UUm‹Ò–eeÚŠ3$¢º®çóy–eUUHç8ïOÏÎô` <¸Ï‰˜.ä씋£8©*MDÁ(kmÇççç/®ýß~mïÝ¿îÇ»mÆ«ëZ)5›Íú¾×ZK)Ã"ÄqE¶ï‰çtý¢ùê×M5Þn¤”bµZÕiš"âãÇ¿ôåw>ïß–¡w×Jrk­1ÆHcÒ4 VUUåœCĶm€"(%×KÛõëݶð¯ÓöÙååïŸ<¹¸¸Xß.8XÅ€ï £( ô‰Há½€ˆ¼÷ÖÚ ‰@ß"Üî²az$Æs¾Ýn­µóû÷ŒMs0ú÷§ŒqŽ^r$"cÌl6sÎåyî½ošcÞû$I¼÷··Kài:˜ÏçDœ‰íéÙ&3ZçDtzzZ–er8=ùÉÐÀ~øÆÃ¹VJüÓ>XÞ¼DĦiÚ¶õÞ3Æò<ã8¤¨i"RJu]¼išÕê€N“ËË+ðBJ^Eß÷Æ”òN¿Hñ'W÷DË7ëç±ä! á)&“ Çã°¹X,ˆ(„¸1¶m{DÅ1'æ|´ZÝö}ôbÛþà/ßoÊ"»IÓ4Q"Žã¢(ŽŽŽˆˆˆV«çüÙ³gAsîààÀ“$‰sŽ[kf§rµ¤çŸïÃÊà ¡þ÷§O¿óÓŸ=4=sðçMÖ‹àóû$™}y³È²,˜œ¦©"MÓ(ŠŒ1RJc …Z¬×«W)2†Ê²8¹{GÝÞ1¦cŒQY–u]·m›eYE½±ÿøO?iþU¹‚1¶^¯“$ -CÄÅbaŒ äî޽뜋¢èc”oHʺëÌn[1FA,cLk}}}]×µT‘Ö:IÎyÇZëããccLÈ‚Rj8j­ƒ2­u×uÛíöE„8FœÐe¼’R:€ÞYvæÞи8©t¾\.ÃN§œs­õåå%úÎ9"âœ'Iò LrަÑ<Ïsà\|ü§?þæýŸŸMö^ú軿QDÓ C¥Tx"!ç|ÿµgœó‘8Ž_¼xþÚƒÑívÚ¬ë²ï»P÷®kEß°ÒðrIúëUß·Ou± ³,³ÖH©ÊªdDeYö½‚{ï÷öö¼÷Œ‘1=z¯®Ëÿ÷ó½ûî;o¾ùÖë)€9ç}× )­µ„ˆˆÃÑ(ĺë:ð „ðàµÖY¶ù/¦J¦ sÙIEND®B`‚icons/layout-ntp.png0000644000000000000000000000303412230036210013476 0ustar rootroot‰PNG  IHDR có¨ pHYsÄÄ•+ÎIDATxœ•ÍowÆŸßËÌììÌî¬íõ®;rláÄNÛÄ4¡¢"TA´G¢¢  $AÊ‘êT ”@ÈATæ¥Ú¨Ž+Þ„šX‘“ØÉ:víõÌìÎî¼ÿ^88*áÂïðýèûò<ùÉë¿i×97¤’œ1!¤¥ãº¢,kusŽÿ«²¬ˆƒÀvx½NݺÑëõG8Nס†ã’Ì4­~ŸB‰“ÄqÛª:®Ã(%”*¥(¥J))%!„<ÑššçÉÈh£˜ðÒhŒ¯¾÷V‘'¦]Q’–åXìÏ×Þ=é„¢cÌE½€R @IE„õF£ŒchÍcŽSµ+iš=I „ö{½Ñ±±8^¸ð gœM·ŒMíù¯¸žM‘æéÏß^¿Ýª¹G§¦(¥ŽãdY€RZ ‘íïÎæç¯^¿¾ºú>*h@* Í ‘ŒJBÈÆ}ÄgÎ<Ï࿽ý!çNc8†1Œ¢¯_ºxùòå¹Ù™²(‚^´»×“¤HcÎ9cLj]«Ú_úæ·®¬¬·8ý¹S'ngbígo~—ë‹%~ \pœÓGl錓%^hJ)µÖ~ð÷ÎÕåW€×ó¯~ƒovBŸRêºî`0Ȳ Ð:z\ùÅòé—_¼ðòó—×l¥~ –eúû²(ý~/ív»J©gΞ™ûÚ¥2Õ©Ý»u»â:žçÕjµ(ŠšÍfPš °Œôâ¯ÌOtZc_^yûðÔ9E)ÊRPŠ^?¨¹cÕªãyžR0AOÎ+Ë’›Ökï¦É|ß§”ލ  PX¦fíoÕ[znþúü€Gí¶z¸ë:³³Ç 3{­ñé0yžB6ïm,ÿ¶g×jq†/ž@1sîÞVgwwçðý-ËäÜ!L#Ðù¨öûÕÑ¿ÜÁÎÞѱ¢ã¿“Šñ&À³,ëv÷X³åܽ» m!!YQ2fu:;íd3Õήï¸®Ë ‚@Jõz Ĥ¯4»ñfÚ7'Û“Qùs –óm BEY”eÅÖŽÍl»âû> fgçÆ[í“ ÇODaÐjµšÂ4MÓ4äyBkû¥Ž5æG½³Aº+é†?¶9âü_÷S\J19eìÓ‡›}Â)#‰B)¥(=qúTžåŽ]]}ï†[¯EQÄ+ŠB¡µJ¢‘03’wÔÙÁàO ]Æëe9Êßÿ(mMXŽã‰\–e@ͦw¾J¾ð‰–÷ÔÄ O·_|饩©©C×!„T«UÃ4À@a Xµî”å±0ùéT…¤ÐøXBÐ89Ú²º-!Š<Ïz÷ÞÃ7®üzC—ßùÞ÷×}ÚÞØÃÐu]Ã0ʲ„TPZó-ÓÃ_5¿ú™K)yÀBjšiQˆ~/ ÀÝðs?ü7 Ž￿Ɖ¶m;Ïs˲LÓ$”€@ 8¯ñ‡öãXé7=K<¸ƒ~  ‡B#¶ Û6½º§5”’I?ü̧>=?»hJIµªT*Y–¥iAY–eQJ! °¼]a›_JË•§Ú·¶"—ý—}s®€¦ZI·æŒŒ@£î:ÏžÿìØÓ²lþîêÕééé­­-Ó4å”÷)¸c]ð“kGjkÛ=&¥eq ý1ƒJ‚ÐWŠHÝ—‚$ÉvõC? ”tÃÒä¤õÖ×oÅI¬µ6 c8ú¾ïyžR¦é®BÇO®ØvC P•‹ IEND®B`‚icons/layout-p.png0000644000000000000000000000237312230036210013141 0ustar rootroot‰PNG  IHDR có¨ pHYsÄÄ•+­IDATxœµUIoœE}Õ›ç›ÕŽÇKxp7í¥I3cÂ(.rÝ|N-.,!À`°sŽÙ !½TŠÁ9礔MßÇŽ‚Èo¶ …Üúºõê5”JûŸ»ÿi7nü"þ?vDBårÙ_:µº¼ÅA_qo½^ ‚ \››±6JC(¥% A`’ÄÌpŽ%3YË &À:v¼Å^­Ö(ëâ‡÷­óy¯Z_Èzé(nEIÄF«ÃGÆ&ÿ¸].œœHçöÔëKÖ±‘ŠX ©3CHg­c"3³s`@lÕF!¤”RËh­éâÄÚÐ]]šqd£ÇO*˜œš­Öæ/¼;N.96tæÜ‰±³'F^>QI)$‘´–\’Šy«íŠ™IPFʈQ_Ù²›Œé†I’0:±Ð’`dLrÊo—Ž—‡‡K屺)N*­»Ú| Â­JfÓšHY©TJˆ‚d9H6.WðÌÙáÂDîÕÑþ|õ¿Ò»£'×N~~á¼ñ;KMÑ……Ìh‘«ëáœ@}0_kw-¯mø–¡J)ŒŒŒl5Åèk{e\éYéÏúoâp¿sˆ_7üå|\÷l‡>æÑ"Ü-øx‰S༸ßí}Ú×  R™PJIòäÉŽýÕ‹nÑ!ÖºÌTœ$@$ðV—ùøIXøs&G·¾GŒÀCÍÉUa/j9ÛWæÚý½]ß‹ ‡Æ<€÷3ÙÃþƲJ¶{à¸XlDíׄhÔ$É•ù*r©Ü±²ÐñÉg«@l‘Êg–6ZOc,Y{ËÓ·£86ò“ÀúµµË§ß8S”æúÍÊOõÀ¢Ö@¼yMHœ/uÎë’œSZcny´¿pinþÊ¡Ò ¦Ê¡¾¤§°ïä }Ŭ˜.ù}íÎh'<ìžS'®ßžÀüâþ›÷>Br«ÐÛØhý8<Øð»¬µÚõR¤Óf@D̼ùymvúÿ½ Ýjåò…¿B oA ARIEND®B`‚icons/layout-t.png0000644000000000000000000000251312230036210013141 0ustar rootroot‰PNG  IHDR có¨ pHYsÄÄ•+ýIDATxœ•»%GÆOÕ©ê÷½sïö»ã]%{Áà`‘A€ cKdDĈ¿ÁD–ì€ÉNpB`!$K,Èk£]cÖ³;ãÙÙ{ûýª®Ç!(y„ºÂêîSç|¿ï«f¿ÿß¶‡K!¤uV cÑi–­Ë!|«5Ms_qq!–K~¸]6m»Y­‡a‚Ô1»^Êð¢n$gY? išÆa’e©#""àœãœcìº.`Œ©yZåkýÂj¬sñÁŸß{ðà)%g CΕ¡;ò)gð˜Žëªðû@äœsD°Zm²ÅB)Õuµ–s_éœSÓ”!*5½ùë߈ nß>FÄ üc`hØ"ºÍàÎ+¯h­áÿÚ$"km’$Œ±yžQJéœcŒùw8çû²Œ‡áâòéáá¡È²l³Ykmª¶ßívÓ8Z­„Œ1DŒ¶Û¦i´ÖDt­ƒµöøø¸( k-ùö§irÎÀf³I“—˹mONNZ3.£ƒõé—‡?{~ùåÕÕ•s®ïû4MÃ0¬ëÚÏáki­q½^;çÆqBpÎã8æœK)«ªRJ9çÔ8!Äãÿ?üð5€²DýòÍéჶ*‚ ‚ÀZ;M- ¯Œ1Æ BDQí÷ûÕj5 """c¬,KÃ9gŒñÝ»?˜NN4Á‚—OŸEÅz†aß÷›Í†1¦µÇ1Š"cŒµ¶( çÜÑÑÑ8ŽˆX–e×u~2ιÇ@Þ΀£èÎñmÎXÙtÿøûGRJÉÁ3°ÖÞºuk­µÖÚä;­ëÚeŒåyNDMÓøÒQ !¤”ÃЀ8=ýâÓOîãš‚_½ŽgùOžœ>ºººòŽŠãXJiŒñÌ1J)cL–eˆØ4RjG)eERJD¬ªŠsnŒ©ª Ä8åóç»Už¤_U7Uupp`­­ëzžg­µR* æicMÓQ’$ˆè=ã% Èêºö*y*JM⥗^Þïwß{5ϲÅù—gBˆårÙ÷ýb±hÛÖ9W×µ”üNš¦Ã0À<ÏŒ1¥Ô~¿÷ä…ÞÍhmDš&›ï"°±þyï^œ¥MÓ!æy&"cL†]×u]ç[fŒ9ç|×^±<Ï ®kO5Š¢ qžˆ< œ0º¹ù‹R¿¨^vÛ×>ÿìán·ó™Ê²,Žã4M}b•RÓ4Yk³,órù)eLjè•ô¡ó–Oýí·s€ïÿî·÷KÄú?mÛz>2ÖÚ¾ï“$9??÷ø«9ç7oÞôûGEQ\ûÕÛL㛽õÖ¾.å />¿÷¯,I¢(RJ]wí$I✓R2ƈèÚZ]×ív;ïÝëå¾Z ’$ùáÝ“‹«²>ÿŒA0Ž£RÊ3ð›¦i¿ßo6:(Ë’ˆ|¹££#Ï…ι¯3Èó×úóïÌðß¿¾ÿðóGQ={öÌãõnó,‹ªªæyöR¤iêãæCàM霦É3(Š\ìúí:­Ëòþý·Û­16ŠÂº®Ú¶«ªBÄ<ÏOOO}ËÖZÆ@J9 Ã7.//½Áñìì‰#"ú^dï¾ûLJÛ0 Æ®íûžˆ|bQk-¥$""BÎÕ<_ߨAø ÇZûŸeÅB†¾®ÛÿC¸Uê—Œ1 IEND®B`‚icons/layout-tp.png0000644000000000000000000000255112230036210013323 0ustar rootroot‰PNG  IHDR có¨ pHYsÄÄ•+IDATxœ­UYˆ•uÿý—o»ß]Ô;ãrgÒS+ÉÑÔAsÀzÉE* ª—R(*‚–‡ ’ê¥7_z¨ 1Èl—È‚ Ͳs¹cƒ³ÝÑ;:Ëݾï~÷[ÿ§‡1 #‚è<‡Ãùßïlì­·ÎoÏJ©)•AJéû­L&†ÁÜyyüWóý°Y©X•²Ìfy[{¦Ñpòíyß÷R©´¦·“_Og 3uG%JJîz-ÛNYf*mÛ`L¡’„s®”J”bcìjjÆX´Rv»ëçJ^þò³ÁÁ³šÔuÓ€ç]¬VkµÆBQÓs 2'ÊeÎ9RJ)Î…eš˜RJá:þÀs\/—ËÔëξ}û¥iš×éºQ­×ûÖõ¾º{7ˆ&”KÿD¿ÑpÿU¢zÝÐÕÕÅMÓêéY½qãÆŒmßûðC úϺÿÝãrQaÑœ\z|ü¦¾>Ç ssÓÝÝ+ªÕéÀó—t/`œ3Æ„`Bpà Ơ8)$IÂ8%Š8cÉŸÅA)U.Oç~ûí×õë{‡††6¬ß‡ÑÈpѲ,…$޽Xóó…L&7Y-®/OY™¹ÕêTœ¨LÊb¤À˜”ÌkE‚³„ˆŒˆˆ©?IÈB¡0ÿ†Œs£ZI¯¤s–®Ë(Â… ÃRŠJm²V›Ô!ÉmL½´ã¾×Þù`]o·ÅNK‘nž4-‹sÆ•J¢0B\H¥ì_v½¼8ùü –›Ã©·¤¦'†îqŠÃŠàGÐçLêZE?NW­êXºj­j_©-^ÎŒžî¶)8GÓ‡ÂÜðÃ0$¦Ó¶[®€êtcõÒù·Ù*€`ÀòÞ.x!˘(ƒ ¨¶ñQà ›M7³sç#\(Ç©µZBµ?„Ëq*—:«U½ˆBíÒ´mçZ¹‘ñt).‘„›âeMŒšñ3#±zñĨv˜hÎJ·Ûªo²³³sûö­D(‹Y]Pâ¦8YLøtôòéž5=¥óïŽLm¦„€ìÈ•ËõÑ@MC}ÁÀUܧ‰ûÃlã±í[ÆŠçŽ _t¼ìl“…®ÛÌçóº®çL»e®`žm£4yO>óäÑãO/éè7ô3Kæ$sç-ÛÜk: 9V^Ö¬-öüµ^à‹róR«Wž8ü­üÌ'‡ú$ëOÏr¼+SÔpœñññR©466vsÏ-¥†WR`u`NÞÞcvPÃC3xnè²%.³Ÿ$ :CDüR|¢8 ¥ö­Y¾°­öáM“Qï«$ãŒsÉ….E&'¾ÿáüÕ¥¿¨ èœ Æ™R†©s‘B‚8ðµ0޼ð«oN~ÍB¢kW ¿÷ùǺeòˆvîØ±ë©‡·ß±)ˆãñK“3Ó•j£QoÔ¢0æI"éR›=Š)åQ”¨ 8 0ÉlC/ä3#—ëa‚_N€¶Þ³xèû/€þþS²gÃ&q×V€=Z›i8Õÿ Žc¶gÏÞöö6)d…ÇŽ#¢ŽÎΖç]*„°R)"š}^³¸Æ¹à5›™lîØÖ¬“7”ÊIEND®B`‚icons/list-add.png0000755000000000000000000000033012232326132013071 0ustar rootroot‰PNG  IHDR(-SääãåßaæåâèßCéçâëèãëëëììëðïíðððòðìóóòõîUõõõ÷ð_÷õñ÷õò÷õó÷öë÷÷÷øï;øöòù÷óùøôúøõúùöûú÷ûûøüúÓÿ÷gÿøsÿûÁÿüÎÿÿÿwƒ xtRNS,-0:XfgmwšÍßåïõ„Yº¢IDATxcÀ˜Ø¹¹Ø lVaaa%>~^f(?,,,4ÈQHŸÊ×WWS5 à€ò¥U4uÍ<ƒùX€¦Šâ†6Na!fÎ2Œ 9§0 °6Ðqf „™ËZ[éiK9„AŒÅ”%Üà¶’¢Þö0y ßÇGÙ& àâçåëj¥° ƒ;°0ð`úÞßë,2ºyIEND®B`‚icons/media-floppy.png0000755000000000000000000000142310757353421013775 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<¥IDAT8¥•ÁkTWÆgæ½ £FÓ.,DRpcە躡«‚¿ àÆÖ•.ãF¡PpQ RÜÄUìºàJ©h7Ý̦B –@&š‘{óîùº¸ïÍÌ3V¦õÀðî»ï½ß|÷œûkç¾ýùš»9ÄÁxsî_ß1pÌ®î~íê…%+‹.gïÿ@’ážïÝAÊ 5÷¿~uסâνõ[`3~ûc›Onß|‹éãÉ©Ë|¾x  [–ÜIÉ9ñý­÷WƒÌ(RÊKžûúÒ{ùiä“`w¼žˆ1²µµ5kqqq4–;>©Ø]ÈÇ…ž™™i}0mHN•2§P%!ù;?š ìj¥Âܽ¥8¥ÄæææT°É•¹œTµŠç-ÅÝn÷ÿ¥ÂEJ ¨S‘’ãþ63ýW°“jN]– —þõ˜ååefgg)Ë’¢((Ër4.Š‚#ý~Ÿµµ5zs§i–éR{§*7“æŸççç)Š‚ÕÕÕ‘º••B ‡CbŒ˜åfè)åqÎE+¤”pMÒëõ(˲µìÁ`@Œ‘#!bŒM1 WnMލ47Û¦‰ÝÝÝ´gÅ$á@•&W•c“ìííM(zز «{²1în/Ÿ÷GOïñ¼ÿj !OBGÏåHõᣱA Á—Ï^<üìäGG?þðOB,--±¿¿OÁ`Чºhs³3˜uႸŸd’8ûÍÝ3ǶéŽN¿7Bj»Ó06?8Ö­OLËP¸a^6ë%p8 ™¸ ½‰ßk JjUû¯é냴øËIEND®B`‚icons/media-playback-start.png0000755000000000000000000000102712232326131015371 0ustar rootroot‰PNG  IHDRóÿaÞIDATxc@, y bf² psw½!!!a d ’¬[LTTÎÇ×믯Ÿ÷o/o÷+222Æ¢¢¢\D ¥¥%çíãùç÷ïßÿ7lZÿÈþåîîºWQEQI[[›°·ìäüü}À|úüáÿ‹Ïþ/^²èW@ ß7×¥ººº2öŒ8  • ðíÛ×ÿ¿~ÿúÿóçÏÿ÷îÝý?w¡á!oÝ<ÜÜÜܰ‡OvV–\dt8Ø€7ï^ý÷þ ¿}÷úÿë7/ÿ?{þôßÔiSþääeîŽÒªç@1 ¾¾A..>lÀÃ'÷ÿ?||ïÿ ~úüñÿ¯_¿üýúÕ¿)S'þ­o¨ÙVSSkÙP_/ŠbÀÔ©SåRRÁ\¿uùÿû·þûþíÿçÏŸþO:éO~aÎëþ‰ÝÁS§L•œ:e +†V­Z%›™4à$ ~ýü¿lù’?•Õåßç/š]¶fÍ9 vœ¸ÿY -¿5nÞºñ_k{ãïISû7íܹS (ÇM0¯\½"[QUü«³«õO]CÕƒö8]¹zUˆè„ôêåK ^îÙ¿+ñÕ«—B¯^½b$))ÿÿÿ_ˆÅ˜¼ÌÔÈDŽ>ô§ç®%iVIEND®B`‚icons/media-playback-stop-big.png0000755000000000000000000000100112230036210015742 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<“IDAT8Õ•ËŠÔ@†¿sª’Žm_è}c±Áq'ˆ;_OÄWPñe¼4"³”A˜™Ôq‘IÒIÚ˜{amRuR|õÕ_¹ˆ™qЦ'¡žìEDî ²F¦ÒÌXDVÀô¾`àÊÌöÅ eœ$Ƀç/ž½Su»¾D ¶ÿðþãÓÃZ ¼ÙlLÕ½yý–(ŠzÏ/^žM§ÓÐ žÏçe6_÷ŸÈB>¿z^x´} Àz½®eÚ/—Ëre¡ï9¦iZ3n=n‹Åâ¯Þ˜ÃÂãÉdb?/¯ç86¦y1qeÜoqFÁ†Å(> GtS©¬3÷èμŽlÌ<{áQÕ§ê«×nMêŸ7 È—9vs5WÀqN÷h˜þøµš~~ÿ†‘ˆ=سD"ºÔP×w,¬çÊ0+UXÜe×`[¸YÂLjNõ{¼!ív³+œíŽhžÅ/?QXK¢”I£ZþƒšU„Ã+pl¼˜‡QävôjáQ­ñ Ç„²É)¸v “=•Cð¶óúáò ~üê‚7¤;DÔ ÿ¯!ÄLfÇÖÎúH’¡V)ÂÌ­¡´±‚ÒÖ70Ê[«1)Õ¸ƒé|zžëýÈX½ ÉÆd€1(mÝð{$”ïËÛŸÞð=#{½½ðj–‰ÈÉ(ÀXR§Ã¯¶£oè–cO¸[µ'þçƒYQ?^ Gºdäñ-øTቩg<·2{×X|;¹ŸÝSںǃ}®7ØJ¥Lš•2éLÕ,^Î.Åßíkå]iýg$Ù×r@D±]ŠçšÕÑAßù/±"þÐgÁøeIEND®B`‚icons/media-record16-green.png0000644000000000000000000000127112230036210015163 0ustar rootroot‰PNG  IHDRóÿasRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÛ :7ÑjÛÂ9IDAT8Ë¥SKka=ß73éLkžû iµ >(¨U4 ®ýV±âNн¸Ð…K7âÖ¥¸p%.lA¤ ÑZ«¢}ÑNpBk_“I;Íkf¾|Ÿ‹R¡!vÓ³¼÷r¸÷œs‰Å!7*&SºF(¹à<€€ÏÆÌ´5S?KêOèˆ_Q£Êë w{c'»úäCr„Î4ÿ9²ègG×_x`¦­rC‚îøÕÄÅØ»›w†Ô*« ﮢÊÊÐät¶ôàËú‡Úû‡?&›BrjþíŠØCLé!Mdï=Ö g[ž Áwzœqp&pº³“¿2þÄ ã¾™¶žï‘PrýÜ­#¡ÕÊÖókp·}øe¯Äàn3¸ŽïÆ.õ]VÔ¨ò¨‘ mí­ÊÜÂ,6fØFU‡Á+20—ïrTlvu ú±`,™Òcõ.ôrÁ‘K[`•@ ¤@MAjX¬J`Õ–·L¨¥F(I°ÿm ¸ÈØ[ÛÕÕ¤ºWdp–+È/agK 2Åæï’‚¹z Æþ|-xýQ€‰‚@R(¨D@NhPe ›¹ò´9ny{Ì´5bŒ®}"G[A@%²KŽæx'“˜|µàÕ\~û9è婳C=‡ã]Q©°±V©A‹E0õfÞ[š°›ãÖ“ý’Ø ‚g¡„6¤r5¬;[¢£¸â:þ\&ÿqß(ï¢ãLD ´È§´ .¾å2ù|£9rÐwþ Á,¸*ÒP\IEND®B`‚icons/media-record16-red.png0000644000000000000000000000121512230036210014633 0ustar rootroot‰PNG  IHDRóÿasBIT|dˆtEXtSoftwarewww.inkscape.org›î<IDAT8¥’¿OSQÇ¿çÞ¾öµéúÄôÛE)a± ˜HÁ‰Uÿ »ƒn,.2;+sâÔt¨Ml C,–­%@mJÞ{¼¶÷‡Öšb1&|“³ÜœóÉ=ßó%fÆu$®5 À7î±@Ô€ÇXP@@Qù9毗{éò ;DE4úajmÍMOûD8Lîþ¾÷3—ë¶ …÷ ¼¼Çl”ˆÅfg³©õu½çºè5›ð\2„ß4á”Jªº¹ùe¾Û]Ä`p(E±Ø÷»7œƒ¨v`î—çJ!˜Nã<ŸïÖ·¶^Ì3¿1QOn­®F»§§èÕë€eŽØ6`Y`Ë‚].#¾¼¬iÑè«¿LÀ=™Ô.vw¡ ßi ¢á¼^½V z*eˆŒEææŸ+Ý‘J¡S.ƒB°”‘(°ëBÕjðÇb*пNs¸‚b.tNNX›œ„ ‚‚dY@£¯Z…wtA„‹ZMêÀ·È·÷ö:úÌ ˆ4€H)û0! ™&H×a7•4sg°Àüé¬XüÌDÊd ¤ ABKK¨g³¸îó«r0¥…ÃåäÊÊMÝ4¥:>\41£¾½Ý9¯T^Ï)õæÊ$–ˆB$Ä[=‘xJ¥<_8LN­&œÃîm?½Ïœûg”ë#‘¼ ¤HxÀN†ùl\ß•€ÿÕ/ÎôÅqk.IEND®B`‚icons/office-calendar-big.png0000755000000000000000000000144712230036210015134 0ustar rootroot‰PNG  IHDRÄ´l;bKGDˆŠ… ¥©Õ pHYs  ÒÝ~ütIMEÖ Ä#³ì>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3ïXjIDAT8˵•ÏKaÇ?óÎŽ²²›æÆ:"4œéf×í0b‰È“Õ› ià¡ Réb#BÄ:xºŠ¤n%b DPb`ÔÝÙ;´ï°ÎŽ?Zéá™÷™÷ý¼ßyžçÿd ÀýÔ½×Àð%YO–žÈA¤ê‡çf_Q,¦>ž´&:0𦱷·×4ÖÅ|p¡P ­­­!°ªª§ƒÈd2ÿV E!‘H ëz8Ø4 º»»‰Çãxžç/’^^Á¸La,Ã4 –|°*ƒ^ôyh*jŸ8K}­…*N¥Rô÷÷ÓÛÛËèè(žç¡( ÉdÃ0èèèð;èøøøb`!étšL&ÃÈÈ+++~///3==}b¾ÜôÜTd³YÆÆÆ°m›¡¡!Z[[Q…–––:H`¬¿C»®‹i𬝝cYóóó8ŽšKEQÐ4 !FŸ pýTÅûûû P*•(•J ÒÞÞ@OO®ëL&ÉårضÑw›wïßf>xt ø^ÖuÕÕÕÐVÛÚÚ"ù»¤¹¹€¦¦&4Mãðð®Îöó—ϾYãJ$øj²Ê*Aªªú1!®ëúðŠÅ"¹_?¯Yãwe*D­ZYåJ¥àûr¹ìwªªxž‡ã8ìîî’ÏçÙüúk|bøt<;7s©ñÆÆ&À›ºâuuÞäèèèÜS'}¹\Fã8looS©x?‚}ì­­}ž¦U{5q…/>Þ¼¿¦ê1 ^õjØÉ¬1 ¨ _õn,!";,ï½@Œ?ÐÎdù¬ÛIEND®B`‚icons/preferences-system.png0000755000000000000000000000062412232326131015220 0ustar rootroot‰PNG  IHDR(-S¢PLTE J‡ J‡ J‡ˆŠ… J‡ˆŠ…îî숊…ïïîììêîîíóóóööõ J‡r‹®tŠ¢x¤z•´z•µ|“³}ƒ…}‘§†œ¸ˆŠ…ŠŒ‡’¥¾ ¡ž¤¤£«¬©­­ª­»Ì¸·µ¹¸·º¹¶½ÈÔ¿¾½ÄÃÂÆÄÂÊÉÈÒÑÐÔÓÒÔÓÓäãâåãâìëéððîððïððð÷÷÷úúúÿÿÿ:ðótRNS0DLLVÓüþþþþ²Œã…‘IDATxmWÂ0 ƒ4”-(* 4Œ²7÷?‰ÞЗýŠ-WþÉШ‰‚áF¹CýáC _Œü¼{Êæ<òñ`Å÷­hi“äýg¯»¥²&N§%cÅ 574Õ5ÓâXÒüØÚ1/¥ÎÔ±K’®êzØd4NáËÏ{J0› Ñs ò,óì·ýJPúËf ¡ž^ðIEND®B`‚icons/printer.png0000755000000000000000000000063412232326130013060 0ustar rootroot‰PNG  IHDRóÿacIDATxÚ­’ÉjÂP†ûÆ¥VÅ–¶ ¨ ÛwE$àBÑ(‚+‡…v'pJ &Pþz5) |ÜÜù_Nþäá_/Q¼pÞKl·Û«l6–\ …KÜ'ÐuÝÂ0 Æ4Mf½^_Pƒõ´R©dQ.— šà¶ ßïCÓ4  ‡CŒF#ŒÇcL&“Û¬V+„ÃaD"&BUU‹³‚÷7ïø>H’Ah=ê¢(rõÓ¹£»ÝŽ\.—·à×™N§,±ûýŠ¢`>Ÿ#sâ½^r Ñ)ΦÝn# á»Z± ‹ª• :ÓívÐ!«ÞjµðõD±X´ ø[çóyü>¼¾x·Ë Û·ÓÇÓ#œÏÞ'‰ó‚\.‡l6Ëd2™_d¤Ói$“I¤R)ȲLuú'ì‚Ùl†F£f³IÐý µZ×z½NyÙ1ÚüØ{Ìyù›xªþIEND®B`‚icons/process-stop.png0000644000000000000000000000237012230036210014025 0ustar rootroot‰PNG  IHDRÄ´l;bKGDùC» pHYs  šœtIMEÕ +C+Gû…IDAT8Ë•[hTGÇsÎnö’lâ µµjH[M ÜfÅ •R ¾DÄ”¶ØB)¶T‹Ñ•ÄzúЇBû¡PA©­¶ ¥(Ú‚¦sÑ´I›Ûº»IÜÝìÙë9s¦»®ÆDéÀÇ<ÌÌo¾ù.ÿß®öû›·ïÞí‰Þ¾M:!êv#ªªÈ˜&v6[„'„ áóá]»–x8Œ#ÃJ¥xÅïwF‰µµ ;àŠ Ž÷¶77{¢]]dB!rJ‘µ,ìØÁÒ}ûHù|L ¡iee¬jmeÅÞ½˜BV #áÁà [·lñhÏ'€®ëhB¾ O÷Z¡3gB°ìÀRå奥¼ÜÚŠ³´”¿Âmš¸¥F8Œ®i¨B.źT*Ÿy!@)*l›øÔÁÓ§Yqð /?Ž®ihJ1pø0ƒør9”R(¥°•)‹IÖU¼*–ÓÃ’*Ïå`r’© pz<8½^"/b¡¶mc?„ËG½äxŒmç=×ò÷%tév³¸±M4à¹ÆFâ·naI‰êîFš&¶i¢4 JJòÎÍòXÊ|3HI0<^:v gYÿœ:EßþýhBPsô(SããÄ¥$ÙÓCòÞ=Œþ~äð0¶iÎ?„*)ÉHIrÞ^ﯯwæ’I²ñx¦æ˜Ë*+YY[ËïÙþ±±_mx³²³Ô­®Õe2æh0¸É8s™ Ùtt¥ë i¨‚•WU±ÜïçÚ/¿d†b±mx»¬§é±Xò1|Vãó}øÖ† %Îùóeeùâ/h²’åõÒyþ¼Ùÿö%²!„ô“àR`9ð<°¬ v¼ oBå÷ B䣠¨èo‡sÀ8þSÌñUyP,‹€y…@xP°(0 Ä(Äà?˜—dð¢yóIEND®B`‚icons/rating-0.png0000644000000000000000000000137112230036210013005 0ustar rootroot‰PNG  IHDRK¬‡2sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÙ »Ú01yIDATXÃíØOKAðgßÝA6 IA%OVÁK¨½ (ôÚ/!ñÔ«¥½ö”Ô€éÕx(~‚% ä = ‹ùsP<äp7°ÉÎô´6…öÚíg÷÷,ó0flWÆÞÞžAD !Dr{{»‰¿ÿª%ÈårÑ\.õ'MÓ$ÆØ‘a’1vdš&ùÏž¿$«ù“Œ±‡b±x>ò­V+iÆÒÆÆ†R.——šÍæn¡Ph¨ªšB¬ ‡ÃØøšE±X<“RÊ““§T*ÉÁ` oooe¯×“²R©@,)Šr³°°°²¾¾®sÎÁ{ªª ]×±¼¼¬sÎWE¹ šU777mÛ~ÛívãóóóLUUôz=H)õD˜šš‚®ë¸¼¼tºÝnMñÀ· YÅÏp«ÕÚ ‡ÃŸÒé4,Ë‚ëºp]011ÙÙYT«U¸®û9‘H|Éd2"h– “É!DczzÚ€¹¹9D"@<ÇÌÌ „ˆF£}!DÃ_4h–Æ2šM¥R¡‹‹‹ÇJ¥‚X,Î9Âá0ªÕ*,Ëê§Óée_Èw ¬â·ôŒ±":÷0ŽZ7ŒdIU4g sM ù®ïyµb¶ AÉ\ŠaKxF1‚"iun⮪hF³ó—ŒQ¦Y‹ÊbœÜÑ!·x>ä; íh本F¡ žÅÄa(»˜!ˆ$ºsêΛ¾¾x tÝU·‚:¡g9xÖðÀëµáš…¸»Ä®¬àE¡êÜKDØÍ¹…g «·›¾¼y‹ON´2 ¸9ÇÚÕ°ìbQšà°Õ´óéTâÿŒvG¢7ñ!DcŒù=º.K`¸ÎáN8 yˆDÄ~gc;—ˆØÏ{VìÖ³µ" …œ¡P¨¸VU•H’Ôïóù¸$Iýªª’õl…Âa%ˆ(Žšy#L(ñ»ê_>¾­óãw/¿ÿ`!6p’™L# rà ,s¸v]Fƪl(rJ’´@)5 #Lñû|¾ãííí£ÑèƒD"q’1¦QJƒŒ±@>ŸwuwwgŠ.öÓñ7 ž¼ð†>Þ]Ås‰oùòð‡<ÿš¿»'ÏXâ¦ÁÇŽÐOº§ÙsçÎÝàœók×®é‘H„çr9žL&ùââ"¿té\âœó³gÏY±01óFøîÅÃçݯ½'»Ûøð*Ø£½gßþm¶dß^åÍð“?¶"kFxhhè|KK‹ÜÜÜŒh4ZÜÛ¿?DQ´ 覹ʒB¼Jü÷c×e,¥@n~ôØÁŸÙþú%f#”ø×æ+²ªªBˆzzZÖuãã㱓““Ðu³³³6Bˆ¿Ã„[oÂ/Jâ×s/nwoeWŸ^2¶5#u/—Ó“?O&?Vc‡_8Q¯x½Þíeeer*•z*ª( Òét.NOš¦yˆÀ”cûž¦Š×ËRu»c}rÃFP¥Ï´î“7”{›`Ê’¬ Ly<ž¦ÖÖVYQH’´.J)…,ËhhhEiaоSËÆÜÌî‡÷´R[Ý+•E°?€i¬’ÒPO†ÝƒÙï¯êÒw&ǧ­ÆÎVî;ÉdvÏÏÏ—VWWK”R,..‚s¾ª§Ayy9dYF,Óççç'c{ÈŽ´z ,LŸJ|Ѹ›@7¹t5þ›À7U#ñÍWXºw§·¾†vD Y‘íêêÒ<O`ff攪ªP²,£¤¤dõž% v»###˜››ë­©© tuui+‰«Œ™LsVùtÎ^w–ê•[®¨ƒPõ`æaß\–e&4¡§Xt,Évtt0Ƙ¶uëVªªªàp¬„rii)***ÀƒÓéÌ2Æ´ŽŽV¬†@Dܼ÷Ûïß}¹ïÿ ¦»¤²FYâÑ«HÝʺ~â Á´d)¥Á––ÛØØØÒàà \.E)zT<϶µµ9!EöqO…QÓàaBá·o©Ò üùKÄBxâÉ%fYEQ!Ä3==-iš†ñññGìää$4MÃì쬅âÉCëµÜ7áDá²ýÙ¼Žm‚Ä¢ÊÚ)cg=w3-þó¤n°×À ;ô|7A¸ìr¹¼ÅÅÅR"‘X“ʲŒd2™I&““†a¬Ì»NK8à–Õ»¿®ôµ÷%±¢ \¡õÉrËSàå<ÝxPÚRâªã€[¦YŽ»åt:ë%Y–!Šâ)Ïó$ 555’,ËuÇmÈòïTã’>7³ïþ]µÈRõ²ÈKè1ÀÐóRÇðNôB'f¸¢ý•¼=Aö8c†-;x&•J훟Ÿ/ª¨¨yžÇââ"ck"BPRRI’‰D´ùùù Jé~ë¶dwjµ“ú&‡NǾìuàe8©èaì°nÛZØ·_céîíÎêJæÛ‚j–mkkSN§offæ´¢(e’$¡   ¿4@aa!†‡‡177×YYYékkkÛ] \ Ô ª­Ü­1 °ª#à¶W¬¬ri¸ò—#‹ÂmÅij@å:r Ë4ÛÒÒB)¥êŽ;4(//‡Õºr”‹ŠŠPZZ J)l6[šRª¶´´lØæ2xÿ¶Z~ûþ«¥hïç05 eÕЋ« _AâÆ`ÚqäS+!ðÿã`’åyÞßÐÐ`]êïï‡Ýn‡,˹]FÓMMMVBȦXnõ*QÀað܈¡³ áá)Ü^v¢lÏs–_‡Ô2÷’Ô€JüŒÁ·Ì`ß{ )3m °‰¢¸@1 #HñȲ|b×®]–›7oj©Tª“RªBüŒ1_6›µ···oÈæŠÒeûÞ ,µZÿD¦fÏ\½_»œú=Zë©G ”?3Ûæ/€¢(dzzúðÄÄDm:Žº\®SŽO(ÿ¸Ëýÿ×áßÛ¿3Z%CIEND®B`‚icons/rating-4.png0000644000000000000000000000245212230036210013012 0ustar rootroot‰PNG  IHDRK¬‡2sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÙ «wM[ªIDATXÃíØoLeðï=w7¼[¸ i›Aÿ 3ɘšŠÍ†Yb²ÄÌÜ}…勯üÙ_L_´ƒ8¶C`/&oLL–œM0D7(0!‚ÝJq–Õ¥*¥\Ýõîy|Q‹Ý‰®&Ý ïÝÝó|îòVQBq¬­­Iªªbqqñ»²²UU±¾¾n"„8r³¤–»þ&‚(\¶>û‚³|¯ ±²û–QÙ‚ØTJþ°¢ìux\;ýüA¸l³Ùœ¥¥¥R,ەʲŒx<žŠÇã+†adž[K8àf±óHsÅkïKbM=8sñ£eÑSàå2<Ó~L**³5sÀͼ,Çݬ­­mnoo—dY†(Ф<ÏC’$466J²,7sW0˿ӀKúo·ß¿.1Õ¿,ò’úë*`è9[GøÚVèæZ¬{Eý=þãe8àÜãÚõªcç‰Äá’šš‘çylmm1ö×w !(++ƒ$IƒêÆÆÆ¥ô€‚X.›; Ò+<]Þw «lîSÐ{ °Ô½Lî”îgsã†òèýÄ@“ g¸þL†çcE!‘H¤×l6÷¹\.„B!hšMÓ2 ²¨•••Ð4mÀf³q»Ý³™àê¥ [ªí*£«?n_Mf–+êÁU¿i˜÷–&©pv°ùZ·ÛM)¥áýû÷«P]]ââÌ«\RR‚ŠŠ PJa±X’”Òpv°…²;;xÏÞ£š~þæ‹íÐøg0ÊAª —Ö#ä¿‚Øõ©dù‰‹ çoe@–çyO[[›i~~~{rrV«²,Ãl6#  %].W1!¤à–Ë–!{8lž›5tæ#<æ}U§«>gú)ðšº?K „ ‡1´j ÖC—È×z½^‹(Š›„YÃ0|„‡,˧ëêêLËËËj"‘8K) B<Œ±Öt:míéé)˜Ýù(Õ¬‡FX"›CÁÈúñ[×î7i‰_B993–[Ûý6wŠ¢µµµãKKKMÉd2”“3c¹µ\¡,÷ÿ_‡nÿÓ@K»©QûIEND®B`‚icons/rating-5.png0000644000000000000000000000241112230036210013006 0ustar rootroot‰PNG  IHDRK¬‡2sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÙ Èq!‚‰IDATXÃí˜_L[UÇ¿÷wïÞ[¸,Ò6MÌø“a&SS±ÙX–˜,Y fîEŸfyqÑøÀØ^|˜>P!Ž-Æ,c““%×&¢6!…»•â,«KU6J¹u·÷žãC-v $úXãïíœóûœ{òËï|çwla7ß‚›,ĘÏÞKXÄ¿°ÁÁA7Åcž“'OþgX€É8&{à(Lò~LÒ¨£¥‹Ë$òþ¼ßf¾ޱ¦i$Ëò¨Ûíæ²,jšF[ù–+&· X=!M[938'sÏËÞÚž„ûW>ðÎFÆúnõ°Éb€›V»Áá,ޏ,Ë«ÃÃÃÓ¦iãñ¸Çív{»ºº„P(ä]\\ìЉ¢`Œµçr¹’g1{Bšâ–ÉßÐçzëyvñnLžæÙèW|îÝZž:²Î-“Ïœ§žNÏááá)Î9¿~ýº>22³Ù,O$|mm_¾|™¯sÎùùóçKšÝÈ,+gï_:~¡úð{Jµï8Ì‹¯ýµ¶ëí/ÉfK ÔYÎ >ýaÓ4ƒ:::”¶¶6„B¡µîînH’dÓ-«´Y*h‰äy¹¡`= ºy扅>2I<º±‘Hžb Ó4ˆÈ³´´¤èºŽ¹¹¹'Ø……躎ååeyŠõ ÔXáÖ›ðH²tÕùüKÞêí’£ÚÖ%£¶ ÉÙ¬žøqÁ´ø±É‡ IÒU—Ëå­¬¬T’Éä–¨ªªH¥RÙT*µ`YÖ1%Ç’Ü-÷h­yý}Enh‚`/ßœ,{¢Z…ç:)eU®V¸+ÂÝÆÆÆÖÎÎNEUUȲ¼)*Š"EAss³¢ªj« %ÉŠïìÁó÷{û?ˆUØš^•EEûm°Ì¢ÒQ±±¦½Ëß]ÓÿHý4Ï8,×ú4Nï_YY©hhhEQÄÚÚ8ç¿MˆPUUEQ‰Dô•••yÆØ%Ç ÍšSŸôlõé]=½à7?{”Ï>ÊkVå.?îh_ƒ=NŸiqá¬ÐŸ×MÓ(÷ÙíöÓ>ŸÑh†aÀ0Œ|B–•¡¶¶áp†aœq¹\gý~I²yáêc‹9êÝ:go: aGC>Ê5Mê_¬ìÛ+3ÌB¬(ðûýŒ1Û¹s§õõõ(/Ï_劊 ÔÔÔ€1‡Ã‘aŒÅ .Ev£B$¶üÐöË·_®GG?‡UÝ ªÛ³² ÑÐ5$oMdª~\N„À&÷;ÐÑÑa›™™Y‡Ó鄪ª°Ûí‡ÃˆF£ŸÏWND%Í …f›€U…iËäAá±ï¨;U·÷ÛÏáïõìÃÔ9f!F„çh78œû® ]hdY^%¢i˲‚DäQUõÔîÝ»m·oßÖÓéô9ÆXŒˆœóö\.çìíí-IvãQjp8÷]â邆EâËGîÝxÜb¤iÔHq_X°âƒhšFKKKGæçç[2™L´H+FŠû±Rd…ÿÿ:üsöOñ¾dïkÿ}ÞIEND®B`‚icons/rating-6.png0000644000000000000000000000240412230036210013011 0ustar rootroot‰PNG  IHDRK¬‡2sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ 8ƒÕ„IDATXÃí˜_hSWÇ¿÷Üܶ7 I{µËJkšå^Öu“õ–Ag}XV*uC÷‡±Áž„a Œ * {ò)™Eëí ®•Mpƒkœ,h‘X\™M­iÓf̶®µKÍŸÞØ›ÜsöPS£´°=Þ±ßÛ9ç÷9çÇs¾çü‡-ì»w qÊC9ôfð/ìôéÓ2!$N)Uzzzþ3,€Án8»á(vF€à‡ê˜~(zdÝo3ß@ àmUU‰ C²,3A†TU%[ùšµ;Ëx¤.äG5‚÷æ‰òÒËÞFùÝO92üM㽩™Þáý4ŽøÁh›nÀYšqARýýý£…B!8;;«È²ÜØÙÙÉ…B¡Æ™™™Þ¾¾¾8Ïó~Ji[>Ÿ7=‹‹ù[ÙÈe6Ýûšv飶rõK–»èc®~ƾÿÐŦ¿W³‘Ëlø¹õüöìïï¿Åc###ÚÀÀËårìþýû,“ɰóçϳp8¼ÊcgÏž55»±³5‚^ùü\õÞÅ·Ö"6øÕ“‘ŸÑùþ'_aýý’_£Áç. Á›7ožkoo[[[ …6ÆöíÛ‹Åb½víšf†©YRÔ(pDŸZùÜ”ß <3±>1.·ˆéDÊ Ž(¥¦ª*!„(‰DBÔ4 ãããϰ“““Ð4 sssVBˆRªfc¹áýPxž¿àQ<Êö´›y¸åá®¶cì¯m¹ìƒ?&c‡÷öÁb±\ðz½UUUâ–¬$IXZZÊ---M†q€éX`ºNö¶È݇Ų:ˆX±)ȉå°mñæ®bŶºÓÇM{<ž–ŽŽQ’$‚°)Ëó `OVéþ:NïN&“•õõõÏóÈd2`Œ=}›‚êêjˆ¢ˆ;wîhÉd2J)ÝÀt,WÔ¬{ó¤×b—ŽwîËw¯"µÂA_{ "êÝ;påúèZöÄ+u8¹ó hñüÏÎÎöÚl¶ã>Ÿ±X º®C×u@yy9jkk‰D ëú ¯×{²««Ë”,€g@Áh¼Îe×8HÞ]xQZ×ÃZ—ˆúZ@ °V:²âÅD@WW¥”Æ].—n·v»PYY‰ššPJáp8²”Òx1`3²ä©(¿»ã=ëØÈøjèÇßP&yà®±¢ÂùÔ‹¸Õ²Ío²ðor¾ýíííÖ±±±Õp8 §Ó I’`³Ù‰D‹Å²>ŸÏN15ËK˜2)ŽãFcAŠXå:¶çõ2kø×Uíq&y @üI¢ÚtÎ~BºX‚"„Œ†$„(’$khh°NLLhétú¥4Nñ3ÆÚòù¼óèÑ£¦d7¥ë `领MÍ/øåvUs~u%V¢Q¥uaÑJQU•$‰Ñh´9›ÍÆJ´b ´3#ËýÿëðÏÙ¿;Q&ihIEND®B`‚icons/rating-7.png0000644000000000000000000000237612230036210013022 0ustar rootroot‰PNG  IHDRK¬‡2sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ &6V­q~IDATXÃí˜_lSUÇ¿çÜÞu·­mw±Ì1Yg{#if262c–më²è”!¼«ñi)O$Ƈx †%$«a$úÂ4!ÃøÿO3EŠR2…a©t«acbÑÆþÙ­´½ççv²%úx¿·sÎïsÎ/¿œó=çw6°7ûáçIÁ¡½~ ø6>>îçœ'…Úððð†åp¸ÎÃ=pV;Oï—™4Ýäõ’̤éÓ{îú­ç;66æG".Ëò´ßï'Y–§#‘ßÈ×l¬¥ÚiáÈŽöK³$Œp*Í´--[Á]mìëE ucyäH%Áx$º*®ÚŒË²œœœœ­T*áÅÅEÍï÷ØÌÌL`aaadbb")IRHÑU.—MÏb´_º˜>6LŸ½æÕ=L¿¼ÝK…qýúÎ:úœJ½²u5}l˜Ž<Í/>¸='''/?^Ÿšš¢b±HKKK”ÏçéäÉ“FW‰ˆŽ?njvmg‘0—æ?9±cg›òTé6¾øðÛ¿G.áå} Éjû*öá®T*á .œèîîV:;;133³6644‹Åb;sæŒn†©Y^Õ(Ƙ¿QTêJ·°Ÿ»oâKàw2¸q³`cŒiµ‰D8ç\K¥RŠ®ë¸råÊ}l<‡®ëX^^¶qεZ=0ËŽôA“$éTóÖ¦€æÎ(×–ô oŒGlHd]Eý·[q"Úkß;‹ÅrÊçó”••• YUU‘N§‹ét:nÆ^¦c9€ë6w<»³Ey¬Ù€E©[dVjƒÀKmy¥Îµ¹ÀuÆØõÖÖÖŽÞÞ^EUUȲ¼.+IEA{{»¢ªjcÌ”¬4àÅ»z>×só¶pk[ÝrCÝ*Vò 0ÄH\†Ï#á!ÅŠO¬×ïüq{@ŸÜöü[¹\®'“ɸ½^¯,Iòù<ˆèÞÛ„sx<(Š‚Ë—/ë™Lf^ÑÀt,«jV*ÍF$ÅuàÕg€s?éXÍ3”KwV»O4Yðùœ•‹[7ãÐîiˆêù_\\±Ûí‚Á ‰J¥J¥Ò]ÖjEss3b±J¥ÒAŸÏwhppД,€ÝÓD”ܼɦ ptzëáq1@ã&+uGA’ÕDÀàà B$uhiiÃá¸Ýn455A§ÓYB$«›‘å÷D‰‡zÛý¶÷/YWß‹VàqZ±ÅS·£_~/p6Y_xáIŸƒ3„Ö9ß¡îînÛÜÜÜj4…Ë傪ª°ÛíˆÅbH$…`0èàœ›šeÕÆÂ‘eŒÍQ˜3h²cÓþþÇÿ´½fÑËzvT’œ!$]×ß!W-dYÎrÎg ÃsÎ5UU÷oÛ¶ÍvõêU=—Ë !’œóu•Ëe×¾}ûLÉ®=Jï&€rU û9ýû®oâÎí•b6Q£QSµuaÕj‰D"<•J횟Ÿß^(5Z1U[™‘eÿÿ:üsö/1ÔS2? IEND®B`‚icons/rating-8.png0000644000000000000000000000235712230036210013022 0ustar rootroot‰PNG  IHDRK¬‡2sRGB®ÎébKGDÿÿÿ ½§“ pHYs  šœtIMEÚ 8ÌYoIDATXÃí˜ML[GÇÿ;ÏçÙÆÆˆC£b[B )­„BÒŠF¨I”Þšc.Q>¤Ô9EU/„¤‡r R@=5ôqª”‹…”âCP>„\ŒI MR7…àg°ývz &NR{|Uç¶»óÛ]fÿ»³»ØÅ6 ‘!84…Eü »sçN€ˆRÊàÕ«Wÿ3,À…Cp_8w¹³ïˆ@#µ>h¤ïÄ–ßN¾ýýýîþþþív$!UUG«ª:‰Dh7_«±¶í¨ ¬_n£ )eøù+¬Ý[×òé‡$~«kyžú£÷âaN!BÌÜ!žÊˆ«ªº>888Q*•ÂÉd2ZNž<)FGG[{Š¢„¤”ÅbÑò,.·Ñxò|}Æg\ùÀÃc_ìãŸïá±/ëøÊûÕüÕ'õ¹äù&¾Ô*ÆßNÏÁÁÁqfæGÃÃÜÏçyyy™3™ ß»w£ÑhŽ™ùîÝ»–f·3KJ¾¿–úè=E“%Æ^ü=²ÏŽUƒ¨àønŽ f¿½p©T ?~üx¨³³S;räFGG·ÇNŸ> ›Íæxøð¡aš¦¥Y*k”"¸œÚÔÌb?'×ߘxòI›%üþ2ïB+5,‰—––4Ã0033ó;??Ã0°²²â ¢`¥X#HD÷ë÷Ö¶4¸2ÚJjc×£¶ÚŽ_s®üæúŸó’q®õÒl6Û}¿ßßâõzµgÏžíÊ꺎T*•O¥Ró¦iž`9–,èõuí§ŽJ­¡Ö„¨Rw&Uª«Ç› ÍæÔÛ,!šššÚ»ºº4]סª;³Š¢@Ó4´¶¶jº®· !,É*G}ø~Ó0Ž=YsÔø÷ªªÃVÄKCRnƒ‚¼ãU°GUñÓ“*£”[›eà¸ïè©oÒéô±ÕÕÕšÆÆFUQd20óë· êëë¡i¦§§ÕÕÕY)åq–cEY³ž¿½ŠÝyãL»ÄÔryCÀ,·’jŠƒµ b 7oîóâV_²|þ“Éd¯Óé¼ÑÝÝx<ŽB¡€B¡°ÛíØ¿?b± …ÂM¿ß«§§Ç’ì–ÀG!™9áuÛ †@ÐWS¼nºÉ€ªiY $Ê€žž)¥Lø|>8—˨©©ACC¤”p»ÝY)e¢¼a+²Û7„"ÔuPsD±ç~œ6áu©Ð=UpiUOHLýV•ý¸Ùå" ´Ãùuvv:&''sÑhº®Ãét"‹!g»»»]DdiV”KX' É´9Ü×ÛÞ-8&Ÿ*†¹‘»-! tH†çÛ¤Ë倪ªëD4ašf˜ˆ‚º®_onnvÌÍÍétú¶”2AD!fî(‹žk×®Y’}ý(ex†¦·Ðwôb-}vê©£ÍÜÈÅ+4j¸².,[åF"‘---mËf³ñ ­®¬Ç¬ÈŠÿþ9ûz'I¡Y7[fIEND®B`‚icons/system-log-out.png0000644000000000000000000000143710562753754014330 0ustar rootroot‰PNG  IHDRóÿasBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<œIDAT8u“OhIƯªº»z²ƒ{ˆŒ êÎa/âEA…Ñ«hPêä( ,’».öàŒ WEP/žDP$j…ÝwB&l@g¢Éôd:î©=Äÿ?ø¨z—¯~_=žÜ2I’~P"‚ïûØÀbŒ²,ÅöϳgΟ0I’ŒôïÚC¹\&Š"Íf“8ŽIÓ” Σ”BkƒR €fÜäì¹3ÃÀiA$ išÒjµpΡµ@)1æc/hmèYÙWù|ž0 Éår„aع[kAD¥Ñ‹<ؾÁÚÀ˜vNk-ιNnA)…¸,ãÙÑ!²j•Å"õ¿_ÃÓü3:Úm–Zk´Öø¾O–e³´^çÉþƒt ¬ .—ß»eÉOË÷:m¥TG6ŠxØÛÇÚ;X¾néô4ëÄî>¼¡MÑ–ÖšÜÔ«nÞ¤°s'?Ctÿ>(µ$ò}}ˆRt Ú¯·Ïî[·X](`ã˜èùsZqL«Ñ 5?îê"X³DPŸ|áíà o^½bºXD…!‹• ÙÜ.MÉêu’J—eŸÚÿÐ6JV¬`êÈr7®7›¬Ü°†,.,ÐRŠùjœ“Á÷¦‘†!··lÁ~¡\©Dóïä$/Š/ñººž~a`Œéhbb‚Àú ã×Ë—XÖ¿›×¥ÚZ6>còø0û¢¨ÿ‹žçQzS†>¿ ŸÏ£µ&jÔYþû²ª‡Ù»w™û@µö`ÞxžG­V%Y\`óÖMxžs-æê³€|ÚÌm½ü¼­—÷ïkÌÌÌ,ø¾êêµ+#µZaìÑØ7ëüu9ç‚à ý?=ý]“¥eIEND®B`‚icons/system-search-big.png0000755000000000000000000000236312230036210014717 0ustar rootroot‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÕ Šƒúu€IDAT8˵”mlSUÇÿç¾´½·´kG»¶lD'¨c"st@ Q$!qFð&F3Ad‚|¡N!pQÂ?ø2!‹Æ ââ?(”±ÂdPC–•m]{Wú~{ï=ÇvI!cŸäÉÉIÎù=ÿó?Oà>±bÅ ‚ÿ^ 4mkSsÙÕ„‰2J!”RšµÙì­{w7¿õ `¡xónÓ¶g2Ùì¯k׬5•””’¼f0ƒŠ<á`M&ÆÞL$’¯Ùí¶¥{w7Ÿy Å;?Ü1/w5nz‡œ¸p+ß«9‹,K…Àé©T*ëŸËÒÚ ÓÁCh™»¬nÇö¡û*ŽÅ”Ž-oo%íç"Ú°j×-©H•O•oSÆôŒjèW í÷Þч’ å77n5<ôi'Û½À<ìøàý€nݲŠ`ÜHËâKgŒøœ±˜8‘RPÊ Û$QϨúå>…VšiFó8-dNÍ“©Ó]¹ÕF·ùçúųýš¾àQ×u'Z^§ù\ÞHQÆÒK°´’Ô !§z" óç-°D"#M“Z!‚ƒãEXd«¹¼Ô¢©e çt¦QŠÜ’ Çjéh‹%IÏ ®IÁ„^7 ð‚ÀÇ3zÚ,rˆ Œå9BrcéüÈ©K‘ó„ÃF^xà8ÂO ¦”êfQEޱkáä0Glœs 5o %²zgOä²È<#³èšf¨ªÊSJµIÁç|8®+1™ÈÕ›ÑÒ›ÑÌÆ!B7Â3D(C‚Ü ÌªŒFž®z¨”(JLõz}gïæ ì89´Úïá%«´È`ÔF80&0FÍŒP;%0år¹2§ÛýìʧÜ\Ëá¯ñøXpÒv;y"šU]°šuöc•òh¾nthÐ-IÒuðZ.¶8]®WV×8 ¨1êšêN\¹Ò»¤rÆÃ‡/÷ô¦&ÀKk^üíÏžK›XflxݪEv ‚7gÏŒ¼ ™„U53˦Õû]8òùÇtùòç¹þþ~›Ý^¢ÝNÄ™QùíÝð;†Pó¾=snþâv¹sG©É?×ï˜>}º­¯¯ïö¹îsÃ×þþKR³Ú.‡ÓþUã¦Íƒày^½p±;£(ÊGÛ~štº5ïÛó:€µƒƒƒ‹ !c,[QQqÀѦÀöo÷޽·aýFtuuRj\¸Ø‰ÅbUÇ~8~ëžà¢å gH‘ ¯J²ù‹ ë7’P(³ÙÌ::ƒ¹Hd´êø±ŸÈ] ¡à;€¯­­5íß¿–Çãñ¢Ñh¬µµõjKKK­¯¯_X^áûyÃú$™LÂétâ£=»n}ÿ][9WÔÏ&ba5˲,Ù>Ÿo¾Ïç{Ùår­´Z­5ÕÕÕ3 SmJ{{{è|whÑÏ>×ë…ÍfƒÃáÈàŠŸ( Œ«¾C}‘- `Œç‚…ó§U=^uD–e¯[ÖÖÖv‰L0øÉ@¾Èçqðxê…tÜÿÿ5þÀbÿÐݘ²«IEND®B`‚icons/system-search.png0000755000000000000000000000143212232326132014163 0ustar rootroot‰PNG  IHDRóÿaáIDATxÚ¥‘íORqÇ]ëáoèŸhõ¢7½è_èE³VË6Ÿ¸+ 3Ÿ0ÐÐ-ŸJ* $–µò¹iLD/"àr/xA T† ÜÓï¶bZn½èlßí·óû}?çœß)ú3J¹YR…§ª¤"V\…H"<¨­SêŠþ5Jùu‘?´/‘,íóÚ‡bÝëlxi ÉEciºvª¹QUwU"őѽæ/‘Κ”Y° üK²oÄ’ €:Ê«5ª+p±0EÓ4´ Y‘ÉÉÚ¼±]&–Š£û¹w á埘¬*€‰û'Ìu µŠaÓPVÿ~.ƒ²&¹³ó=‹ï&±TÎJøuS>‹T¿ÌŽ|"3ƒÆt£ª^R0Ð4¼Îx;Ïd¨Hò(=8 Dz¡è>,ÝM4 ¯mzç–j€Šòç)Ð\¬—öªcKÛv»/îXÝLÌoD’³ëLbKit‹« ÎZâµ³{Ã0 pžcó ²Á`ý¶ã5:áØ·Qñ³«Ño>¥ç]™Þ…#H³øåbŽûäÉ4M¶¹¹¹#Í™<µZ¹JÈ$'úÝ÷«õ.B¦wÞœ•¥mÓ3Z³=¿¸¸p¨niž-Zµê²fµŠqS›À­MòÊñ\ªwÕW¡_V¡­ã:ÛCîn… ÁAå7™BÚpbBœ¿c2›âÓ¤ÿ'äžfÔUÕG*$}äÀ¦G5ÊY—V çY7ÛÒªIñ¼½âÛ7/Û4—Ð\é®î®°‰Bïè (–ƒéÆVaÝ!ÆÏOMM‚¦U 2‘çñ+¢¸Hxùuilª÷wtv0‹eû\m›Ö[Á+Û,)))‰1˜œmûc°Ùly(¬ó8¨iu”BîÌå¸; ÃZ8“ÝN‚ÇãS(Î E:‡tþ—.ü>———W #‹H·Šþ7~?´î„½Û!IEND®B`‚icons/transform-rotate.png0000755000000000000000000000122212232326131014677 0ustar rootroot‰PNG  IHDRÄ´l;YIDATxíÒëK“aÇq§3¦´BŠ™T¸ŒJËh l¥MÍ,›¬ì…ƒ9ÍrëPj %z#Dt¨è Í³5µD! „FЛèE½½ú=×=®Ý­Uï»áƒðà¾×vÝOÚÿ“ô8«¯ÑßúçpÇýì#…ÀÃOѨðöÐSúS†Ûî.PÝ¥ª½h˜¦:¨Úï½OóIçpèæ;‚)€ €¿-·d¸>C<þÛp ï Õ ’(ßmðÆ[þ ßeXù%Ütý5ÕtÇ$3$÷«¯B?2ltã‡zæèpï5öÌScï|<Úpe–^M¸<ާŒZÁ¦¿RÅ*Œ]ÏàR_RýåWÉãúŽ9j‡\ÈlQÑ}çcTÝ ]1òwM‘¿{šü’,®ßY&ä€6GE+NMPå™ISÕ¹ç zAû; 1׎öWðÔ(Vñ²ð(•EÆ©*NOÐ0í=kJçk{ZkT¼ôÄ0•ž!_Ç(ùÂcTnˆŒáƒdÜÂRÞ¬\*îm‹Ò®ãCTÒ>lÚm‘Ñe`+Xþô.n…*/n¤­ÏÈÛÅ !õÁ:p€¬fXQAwó“îæÇßðÛàTÜ ϱ B%ÂZŽÛ m1ä'„ƒ¾½3ˆça'ÔCˆã2€pÃzpÊðpm>Ò7ÉqýWC”¨µx  a%äð*p²!65ÝŠÊÕðÐl°ÃräA.8ø™q,3~y8KÁΡ,y•e0+¤«×VHû 1bÜâhIEND®B`‚icons/transform-scale.png0000755000000000000000000000140612232326130014473 0ustar rootroot‰PNG  IHDRÄ´l;ÍIDATxÚµ’ÙK”Q†Ûh£¬2ˆèª«¨ (S#”Ñv-Ó É%55•²R3Í­l\Jè¦ÔlÛgGg\F­ˆˆ®Š,­!ë6è/x{ßw¦qFë…‡o†sxÎ{–9ÿ-A1M˜ŠY‰ïÂòæ;loÇa÷Cþ ÿF\ÑþqEÏQ~ïJ­ø:þÁÑu`Š×jŠ}}g$®}üI“JRªÓ6¦|fbãÓaiªI%Ew†41>¹ÙöR¾~q‹Ù¾|5)㨗ÿ>É1:ýyÄ 9SÏoJ%;Rša(´ ºÐŠý—¬8xùÛ`îÿ‚Ø+8vÕŽø²n$\ëÆÉ ‡Gì÷…È€L©$$­(R5@T5@S6AQ6AU7AP8BN:CL?HU@GDAGJCLYDLRDRjGPWIQWKUdLO4LUWMV\Ug„s}Œv„œ€‚f‚…e‚†e„Œ˜‡Ši‡Šjuœ¯’˜¢œŸ|Ÿ¡Ÿ¢¡¤‚¢¤‚¢¥‚£¦„£§…¤¦…¤§„¥¯¾§©ˆ§ªˆ©«Š©¬‹ª«‹ª¬‹«­Œ«­«®¬®Ž­°®°®±¯±‘°²‘°²’°³’±³’³µ•´¶–´¶—µ·—µ·˜µ¸—¶¸—¶¸˜¶¸™¶¾Ê·¹™·¹š¸ºš¸º›¹»›¹»œº¼»¼»¼¬»½ž¼¾ŸÌ͹Í͹ÍлÏмÑÓ¿ÓÔÀÖׯÿÿÿØ7»>tRNS M‚„§©«ÁÔX£ ‹àIDATxc`dceef6fF àâää`±D$¢ìì4UA@<ÊÈHS‚AÂHCQ.;;;+=MÆNC$ ¨ A¾®2`M0?)64ÐßS( , ä'ÆÇ„ø{¹iK 00¨f¦&ÄEÇDzû¸Xé¨r2R#ƒ½\¬ÍuTy„¤rRƒ‚ý¼]l-Ìt¤ù$´dR¼=lÍMMÔŒ¶hh){»;ÛÛXš™¨Gi€4´ä ôuut”ì4 *’ §Ëêi€Ä@^äãáá…z4¡-\Iä¿IEND®B`‚icons/user-trash.png0000755000000000000000000000117312232326133013474 0ustar rootroot‰PNG  IHDRóÿaBIDATx­“ÛK“qÇ«‹ú/ ƒˆtÑEtPÑ43eºMçÍÃÔ\ó 4™ˆæ!q:E‡:µJ;÷2Ú¿11õ¬U°§þü¸1O8‹IEND®B`‚icons/user-trash22.png0000664000000000000000000000217512431174262013647 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆ pHYs × ×B(›xtEXtSoftwarewww.inkscape.org›î<úIDAT8µ•ËkÜUÇ?÷wùÍL^Mj„ZE‹.Ä*ºÁ m‚Z7nÜèª@ Å]èF•ZWŠu'î ­`¶ Å4iZ33Édf~ù½î¹×E¦1VñÀá^ÎïùžsÏ9W9çø?Ę:W«C­¯¯=¨í)åIU?ȲÞ[_}‘—>@¼ÿØ£‡'^yù„ªD5¶frgVçKÓ¶>qjúØÂÂOÏÔä‘p,¢Å÷Ný<ù.í•e<Ïû“jíÝaÓñ=}èÕºpúÃWçÏ~ÚÜïií{äà„ítr.^úa;Uè3îÞwá¡]£SG£ç¼0Œßxö™×ãóçÏ#b¶ ¬µ¦ÝnðÂÔÛõzmä¸/"Õ••/NO399‰¯5›tnã~ûü¶N§‹XÙ¡¦ŽD/yZŸù/l·Šï‡Ö˜â°rÎñÚ›õæÄÄS£Q!â0â±”¹%/„<Š\XO ë=C𖤩 ŒX¬8ÄBiRâê+Ÿœî=àoÄQ«eÁhk”'øOùˆ“~ÙγŽZu/ÆXv> ø\þå[jAS6 C­cj£ô’*­öì€ ÆÝ* Aëp³Vε­«ÔâÝ„AëK‚:=8Â'kÔkw1<´‡z} kÆØë›Œ±7òÜ ½èøƒÄÑŠ íÎÌYV|€o¾,*“GüæÜììž{î=€é¶ípˆl¨µ°üqÄZnï{crf~¾D‘«kÀ>¥”òoÄÍÅò3ŸŸ;."CÛù­ÕW®Î÷‡.WÎ9”R ê@ú¯Q7$  ãœ+~1cZW‰z4IEND®B`‚icons/utilities-system-monitor.png0000755000000000000000000000107712232326131016422 0ustar rootroot‰PNG  IHDRóÿaIDATxÚ¥“ßKSa‡ûSºêjƒAˆ Ân¤›.‚𢋈ˆF:ÇÔ­ãšÍ©sº´•¦[˜éVû¡ ´‹04™ÙØæÜVþØš Û’Üñœ ûô¾ïÅ@N æ ÏÕû|Ÿ÷œÃ{.éõ­nþ‡¶¶7 @’Dˆ„ÓVµZEå¸RƒÎÖÚ¾.7ðg¹éLª e¶qåÅm+Mvt · cDåýa(ï='8¡¼ë {}ÌÍå³ò€âŽHƒ0ºt(ì{a3àzókƵGcP=tAEbÔÝÞÙ”®’aÕ‚Ÿ(Š-:añ˜Àé&Áµ¾§u“Ð(slfd2üüx;¢É „×¶Ï`2£Î胺s7ôoÁ=ö07•NʦQÌFÄw / ‘+¡×Û_ša›±°˜eò sɘs¿}ȜֵáêùÐXçàö"¿ëÅàl?4ÏBô{0weuù_7 L¡Ž÷¡ÈôÔ› h´ÏCÓFýÓÔÆæ.­,ž¼‰íÎð¹n¢?è;ŽH ‡å?( ØÛßE6·Ÿ[l¤×KDÙ{,ÁòQXàâãE×_Õ–rùÐIEND®B`‚icons/view-fullscreen.png0000755000000000000000000000077312232326132014515 0ustar rootroot‰PNG  IHDR(-SÿPLTEˆŠ…„†‚ƒ4e¤5f¤6f¤6g¥7g¥8h¥;j§@n©CpªFr«Kv®Lw®Nx®W²\‚³_„´g‹¹o‘½z˜ÀƒŸÃˆŠ…‹¦ÉªÊ™°ÌžµÑŸ´Ï ´Ï¢¶Ï¦ºÔ·ÇÛ¹ÇØ½ËÜÇÒßÉÓàÊÖäÌ×ãÍ×âÎ×âÓÛæÙáêÛâêîîìïîìïïíïïîððîððïññïññðñòðòñðòòðóóñóôóôóòôôòôõôõôóõõóõõôöõôöõõööôööõ÷öö÷÷öøø÷øùøùøøùù÷ùùøùùùùúùúúùúúúûúúûûúûûûüûûüüýýüüÿÿÿ§“QtRNS™ ¨IDATxc`G@~  0ŽŽ¾ˆ† °éèè°Áظ…ôõù¹8 ,z‚br&R¢æì`CC#YEu%+ +°€‘±©‰²™¥†­5XÀÖÖÎ^ÍÁEËÕÍÃ,àäè,© ©­*ïéã1Ô…OXÂÕKZ„×j-+'›§¯7;ÜaÌ>^Þp—¿pÄ阞cDõ>¡*UþêIEND®B`‚icons/view-refresh-big.png0000755000000000000000000000252412230036210014535 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<æIDAT8µ•iLgÇÿï;3;»³K9–[äk‘ E¨hš&­1iÚ‚š˜¦ÖO¶M“&ýÒ415iÒZ$5¶€¤4iµ±Gbý¦5ØÖ3LNUÂÐxO&Öç§3Eæ°,ÙžÃkÓÌã]žzC×M/ >€›5Þöúж+5åå†nmüýJ_û‰;‚ñŽÌ$ —nB‘8,!Xd¡1 ¨šCÊÌH®Õì `¼Ï"›wáÄ´ÕUté†yh* K1Cã!v…òˆ)B0ÛcKˆ`¢ËñeŒÃþ³®«)ÂNÅ,MžZ.ªj\¥ÈÒÙê·ŠÔ¤x i‰Nl.H†ªH01›µìHKÏ9‹ËâÌÞ7 rÅ¢ù.ªjHcij,ÒŽ¶´GHˆÂô^²éV@s>⪦1¸ÑÁǸØþÄæîqùFO«… ¶ŸÚ;J’çkød‡l“9Æ€°nâãºÖÈý‘É÷¯ßÝ­‹LÛ×)@¼VZPvÇèñÝÿ>,}\G„Äy­³Ûd\¸5Bi^šÎ߉ Œú޶ßÕ°ÐNÎIÇT‡ï§É°Hšðû«l²lÍœŸ3nÓ–†)‘èÐÝ?Fœ±UÅï4/]ôÜž³,±aÒÜ}ûÞà‡¾@8›qܽñ¬0ˆ1krâ08Ä—ãÈ-ö¦ó=ÛÎ]éï)©núJXÔb\±d"éˆCUâzîm¶)¹c5Ã;ª_ž–f†tä/‰EÄ´ÐyÏ‹W 3¥âüt­ãÎðžÇ¾°gÌÇÆ'‚NÉÆâÖ—“ŒÄ8'kü¥+¬™zã"0gŒÝ4…C5—&8ß±i¹m˺,»7`Â" 0/M.°R]ŒM÷ÏÓ˜ðGÐøë ¿e/žÜž•$"0Ƥõ• F¬ÓèﺸÃÓqæQî–Êî%¹Õ«ŸOËÈÏMQ2Sc™ÓaCÄ0L “]½qõæC=81r¸ûôÁaa"ÒgÀöuû¼~Ïí²ÞsŸýÀ=}íÎ5)+·•+ZüZp¦:UÅFL‰ߟ£}­Ç®6uE$¢à ˜­Þ}ìÍ› û/°PçD‰F’œn%!£ !èöFû¼ôè\G¢P€Ñ¢¥‰1&ÍÍD aú7oEcD£0‰ÈšËù)i\šrÜß`IEND®B`‚icons/view-refresh.png0000755000000000000000000000136112232326130014001 0ustar rootroot‰PNG  IHDRóÿa¸IDATxÚmR]H“a®H¨Ì º©È«º©nÓ©ÙEDt‘ý€EDu¡nþ@I¥FX”‘D¥aRR’Æöm¦æ,s ÿŲ™ZmŠ3ÝfsN¬ùm9·ïô¼ß'ctñÀyÏÏsžsÞ³ŠˆV°?U Ò¾JÈÐNĦk¼ñ*­v üÉ@DXn>°eÅ“ª>§ä¼Ê’±ÞBº®Ÿ¤ëœ Ò†Aºp߸¨H×, àœ˜›¦NƒMÀf™mO¼ŠãÖY¨¢uŒªÚÆ©ñ‹Ú-N꟧¡©j7ÏбÜzÊ:_©ñ$€ºM"¤¾P•tsž¤ÄL!H8Ïà©j¬¶ß4êøCã¿RLEÕ×öhÏnÎ W)”rQ®ËØÝÑèç_ný*°2a¢(<íÖüÏ?îúFÙo}·¬öÂÚÿÊöC];ºb®r¾jššš„Ö:Ð44$­ŽŽíí=]=ÂU.ׯ_ûdìî˜&4gŸù$RÊÝ]Ý1˲À@wW·H§2óå Œ‰°m›x];{ûÄ¢_vs¶Î¶åÀé‘CŠùåŠùDEi§V£!‘ qÍlÛFû>Zk|í#¥Íúõͤ’i­ÑZã*—Jµ‚‰¢´šÓõ‰d\­,űΎíÉT2I*•ÂuˆL„¯çÅ_zñyª³U®\¹Ìã‰2hlldÃÆÖ®[×h¢ÇýÇ[/ž|RXJùQoO¯ð<ß÷ßð}íûLMMóõůØÕ·›={öâyã¥qnÞü‰ŽömöôÔô ê¬3Á…ÀÌg¬•§À@¥R!ŒB\åR~ôˆ‘‘;@Z»úv“ÍnÁU ËÒ4¯oÂS.wFnc0ÍŸÞ>\ ðœÈ^Rrÿ}žã8¯'“I9W«Q­V(•Æ‘–ÍýÙ´i““tvnC1­”r ‚ˆÄs Þ8ü¦µ%Û–qç`¥2SÎ=wÃÎÝ“¥ßÆODQÔ -É‘#ïc @ÐÛ³“t*ƒç©”ÄèèÛÅBgÒÖ¶µîþï÷€³KW(æ‰Çãxž—–õàÝwÞË4$Äëë—(q]å),K’N§©9Îü™ÖÔæj”˸v}xÒÓ²4„NžÂ÷}€cíÛ’©d’L&ƒ‰ÌS”4f2` a2S™¡:[¥æ8 ëÖ¾úí'Ù3Æð,Jô»‹”(å¡õü fggñ}o1Z[[cÓL .›ÙK,Râº.ÌTf(—⪹¥†yðð>ÕÙ Žó—èÊ©i¯Ü Ãphøê÷Ÿµm}9®}åÍ7”ëÎ!íZûh0W«Ë?ƒ `tlÔÃpH¬.óqà¨eYK”¬Ö¤”÷Â0¾XU@¡˜'¸#8ŸûG¿ÿÜþÝbìgjIEND®B`‚icons/weather-overcast.png0000755000000000000000000000153612232326133014665 0ustar rootroot‰PNG  IHDR(-SÈPLTEˆŠ…‰‹†ŠŒ‡‹Œˆ‹ˆŒŽ‰ŠŽ‹‘Œ’‘“Ž“•‘«¬©±³¯²´°µ·´¹»·ÑÒÏÒÓÐããâñòðñòñûûûûüûˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…ˆŠ…‰‰‚…‹…ˆŠ…ˆŠ…†ˆƒˆŠ…ˆŠ…ˆŠ…ŠŒ‡‹ˆ‹ˆ‹ˆŒŠ‹ˆ‹ˆ‹ˆŽ‰‹ˆ‹ˆŒŽ‰‹ˆ‹ˆ‹ˆ‹ˆŠŒˆ‰‹‡ŠŒ‡‰‹†‰‹†ŠŒ‡ŠŒˆ¶·´­¯«ˆ‹…‰‹†‹ˆŒŽˆ’‘“޲³°ÈÉÇÉÉÇˆŠ…‹Œˆ‹ˆŠŽ‹“•‘—™”™š—¤¦¢®¯¬±³¯²³°´µ±·¸µ·º¶¹º¶¼½»½¾½¾¿¼Á¿ÁÿÂÃÀÃÄÁÃÄÂÃÅÂÄÅÂÅÅÂÆÇÄÇÇÄÈÉÇËÌÉÍÎËÍÎÌÎÏÌÒÓÐÓÓÐÓÓÑÓÔÑÓÔÒÓÕÒÔÕÓר֨Ù×ÙÙ×ÙÚØÚÚÙÛÜÚÜÝÛÝÞÜßßÝßßÞßàÞßàßààÞááàáâàããâãäãææåççæççççèæééèéêèêêèììëííìððïðððññðñòñòòñôôôôõôõõôõõõ÷÷ö÷÷÷ÿÿÿÐPF¾ItRNS !#((,,.0Yil‘­®¯¯±³´ÀÁÆÆÉÙåëíñòõööøùúüüþþþþþþþZº1‡üIDATxcF6.VVnvF Ìe•R11U•æ‚1²ó©ÙµTÙ«‹Jr€D” ,Ì{ºêól-õøÙ€ Œ+[";›K2ÂZŒÄy”\&LœÜÝPÞÞјë,/ÄÌ S6aÚ¤¶¢h¿ÐØ`m1N«ÞÆÚÆòðÖê À8qV üˆß¾®ú|oo3EV-¯Ê–¨Îæâ¬°–xCF&e× 'BŒÍq’c`Ð-ëŸ:¥­´¦±´0)D“UÒº·±®©";½ %))ÞšUDÃ=?953-1)))ÆM“‘WFËÑ õdY˜¡€™•‘ÞBu«aqƒIEND®B`‚icons/x-office-calendar.png0000755000000000000000000000073512232326131014647 0ustar rootroot‰PNG  IHDR(-SÕPLTE‰Œ†ˆŠ…ˆŠ…ˆŠ…dfbcea.4628:8>@9>@;@B@FHAGHJPRLQSMRTUWSac_ceadfbhjfnpl|~y„…ˆŠ…“•‘•—˜›œš ¢£ª«¨°±®°²³¸¹·ÂÃÁÂÄÄÃÄÂÅÆÆÈÈÈÈÉÊÉÉÊÊÊÉÊËÊÍÎÍÎÏÎÒÒÒÖÖÖÙÙÙßßßàààáááâââããâãããäääæææçççéééìììîîîñññóóóûûúýýýÿÿÿí¢=8 tRNS"t¡¼Íáïfb®ªIDATxMÈçv‚@GñIIHɦ—!“^‹âŠ(üQDæýÉ]Qäžs¿üˆèø„舫M7sˆXøO„uYš…Wð0Ǫ@­S³¶Òöóøîú+Ë …÷*zmÝá"ýÞ¼] ¾ÀÂyçýñk¸º¼Xƒ+H’QÇñ (.‘'˜Îò9ÂÞwñùæYØöñdàì±Ù)ÑŽâ‡:µ·KûªÙáÁSz%H·ŽúJIEND®B`‚icons/list-add-big.png0000664000000000000000000000060212702723576013650 0ustar rootroot‰PNG  IHDRÄ´l;bKGDÿÿÿ ½§“ pHYs × ×B(›xtIMEÖ ,ôû+¼IDAT8Ëí•1KA„¿;OPl´¬,¬ìïJ‚þŒ4!… þI ±ñÛü[-,lr½…µ…Šâí s!1»Á Xd`afç=v†·°À¼…²V¤Ãc>¸jf¾;q`i·Ý Ón0V á™ñ?„57ñ5)ªçw)C“*£.Où2"Ç1o\P˜@ú‰ÕYï&õv ðúéxz~G3agÂΉ„ìl­†=wz·Sœ6÷É_¸»¨ÒɽÂUáÏZ}E“œ¨¶T¬¯-cšG܆fÕ%¼Tn¶7WØÛÝž5‰GØ•æ_§Fé/Ó-[¥˜->†`|ð^4lée'IEND®B`‚icons/media-floppy-big.png0000664000000000000000000000142312702723576014537 0ustar rootroot‰PNG  IHDRÄ´l;sBIT|dˆtEXtSoftwarewww.inkscape.org›î<¥IDAT8¥•ÁkTWÆgæ½ £FÓ.,DRpcە躡«‚¿ àÆÖ•.ãF¡PpQ RÜÄUìºàJ©h7Ý̦B –@&š‘{óîùº¸ïÍÌ3V¦õÀðî»ï½ß|÷œûkç¾ýùš»9ÄÁxsî_ß1pÌ®î~íê…%+‹.gïÿ@’ážïÝAÊ 5÷¿~uסâνõ[`3~ûc›Onß|‹éãÉ©Ë|¾x  [–ÜIÉ9ñý­÷WƒÌ(RÊKžûúÒ{ùiä“`w¼žˆ1²µµ5kqqq4–;>©Ø]ÈÇ…ž™™i}0mHN•2§P%!ù;?š ìj¥Âܽ¥8¥ÄæææT°É•¹œTµŠç-ÅÝn÷ÿ¥ÂEJ ¨S‘’ãþ63ýW°“jN]– —þõ˜ååefgg)Ë’¢((Ër4.Š‚#ý~Ÿµµ5zs§i–éR{§*7“æŸççç)Š‚ÕÕÕ‘º••B ‡CbŒ˜åfè)åqÎE+¤”pMÒëõ(˲µìÁ`@Œ‘#!bŒM1 WnMލ47Û¦‰ÝÝÝ´gÅ$á@•&W•c“ìííM(zز «{²1în/Ÿ÷GOïñ¼ÿj !OBGÏåHõᣱA Á—Ï^<üìäGG?þðOB,--±¿¿OÁ`Чºhs³3˜uႸŸd’8ûÍÝ3ǶéŽN¿7Bj»Ó06?8Ö­OLËP¸a^6ë%p8 ™¸ ½‰ßk JjUû¯é냴øËIEND®B`‚icons/IconsReadMe0000664000000000000000000000133012754672046012757 0ustar rootrootMost of the icons in this directory are from the Tango Icon Library Thanks a lot to the artists! http://tango.freedesktop.org/Tango_Icon_Library License: Creative Commons Attribution-ShareAlike 2.5 (http://creativecommons.org/licenses/by-sa/2.0%5C5/) You are free to: copy, distribute, display, or perform the work make derivitive works make commercial use of the work As long as: You give proper attribution to the author(s) All derivitive works adhere to the Creative Commons Attribution-ShareAlike License The terms of this license are made clear in any distributions of the work Any of the requirements may be waived by the copyright holder of the work in question. docs/0000775000000000000000000000000012771201200010476 5ustar rootrootdocs/Tips.txt0000775000000000000000000001405312754672046012211 0ustar rootroot Here you will find various tips about mapivi and picture processing (last update: 2016-08-16) Functions/Menues All functions of mapivi may be accessed via the menues. Some have also shortcuts (keys). All menus start with a dotted line. If you select this line you are able to place and use the menu anywhere on the desktop. A lot of the functions are also accessable via context menus: press the right mouse button while the mouse is e.g. on a thumbnail, picture or directory to activate it. Context menu are also available in other windows and frames, e.g. in the right directory frame. It contains a list of the most used directories (hotlist) and a list of the last used directories (history list). Selection To select one picture, simply click on it in the thumbnail frame. Double click to select a thumbnail in the picture frame. To select several pictures, try one of this: 1. Click on one thumbnail, scroll up or down and press SHIFT and click again on a thumbnail -> all thumbnails between the two will also be selected. 2. Click on one thumbnail, press and hold the CTRL key and click on several other thumbnails, just the clicked thumbnails are selected. 3. Press CTRL-A to select all thumbnails and remove some of this selection by pressing CTRL and left mouse button together. 4. Use "select all backups" or "invert selection" from the context sub menu "Selection". Most of the functions of Mapivi will work with a single selection as well with a selection set. Layout You can move the adjusters (the vertical lines with a blob at the lower end between the frames) with your mouse. If drag is done with Shift button down, then the changes are made in "real time" so that text-flow effects can be seen. Try the l-Key to toggle between some predefined layouts. The keys F1 - F4 toggle some bars and info boxes. The keys F6 - F10 can be used to switch to a certain predefined layout (I recommend F7 to choose a new directory, F8 to view the thumbnails with meta info and F9 to view the pictures with a small thumbnail column at the left. Picture processing Unlike other picture editors (like e.g. The GIMP) there is no "Save" or "Save as" function. That's because Mapivi manipulates the files directly, not just the copy of the picture in the RAM. So be careful and always use the "Create backup" option. So you have at least one Undo-Level. If you delete a picture and a backup file is available, Mapivi will ask to rename the backup file. Picture compression and size To take a picture with a digital camera I recommend to use this camera settings: JPEG higest resolution and best quality or RAW/TIFF format (RAW can not be displayed by mapivi, sorry!). While processing the image with e.g. The GIMP I recomment using the lossless file format like XCF (which preserves layers and even undo information!) or TIFF if you have to save the results in-between. If your Input is JPEG, your Output is JPEG and you are going to edit the picture in one step there is no need to save it in a lossless format (like TIFF or PNG). To archive a picture in JPEG format I am using 95% Quality (more just increases the file size, not the quality). To present a picture on a web site or to send it via email use a resolution eqal or less than 2000x1400 and a quality of about 80%. This should end up in a handy file size of about 400kB. PlugIns Since mapivi V0.1.48 there is a simple PlugIns interface. A PlugIn is any executable program (written in C or Perl or ...) which can handle command line options. Mapivi will call the plugin with a list of all selected file names (all with full path) as arguments (hint: Mapivi versions < 0.7.5 used other arguments). The PlugIn will be accessible within mapivi if the executable is copied to the PlugIns directory in the mapivi config directory (usually: ~/.maprogs/mapivi/PlugIns). In this directory there also has to be another file with the same file name but the suffix ".txt". This is a simple ASCII file containing 4 informations needed from mapivi: 1. the file name of the PlugIn (string) 2. the menu entry text (string) 3. does mapivi need to update the displayed pictures after the execution (boolean) 4. a short description of the PlugIn (about 10-15 words) (string). The informations have to be seperated with this string: " + " (space plus space). This is an example: filelist-plugin + write file list + 0 + this plugin will write the names of all selected pictures to a file Table display The symbol [s] in the EXIF column means, that a backup of the picture EXIF data is stored. The symbol [t] in the EXIF column means, that an EXIF thumbnail is available The symbol [bak] in the size column means, that a backup of the picture exists. The symbol [x:y] in the size column means, that the aspect ratio of the picture is x/y. MP means mega pixels - the resolution of the picture in mega pixel(width x height) b/p means bit per pixel - picture compression: a higher value means better quality (optinal) Working fast Some examples: Add keywords to several pictures: Select all pictures in a folder showing your dog (see Selection above) - open keyword tab, select "Add" mode and double click on your dogs name Search for all pictures of your dog: - open keyword tab, select "Seach" mode and double click on your dogs name Add a copyright notice to the IPTC of all pictures in the current directory: - selects all pictures - open IPTC dialog, add copyright info in "CopyrightNotice" entry - close window and write the copyright info Rename the selected pictures according to their EXIF date picture P2011.JPG will be renamed to e.g. 20030612-120303.jpg - open rename to EXIF date dialog - accept the name format Fullscreen mode To toggle the fullscreen mode on and off use key . To the get rid of the window border, open the options dialog , go to the Advanced pad and enable "Remove the window border in fullscreen mode". Try it, it may not work on all operating systems and all window managers. If you discover some problems disable the option. Drop me an email if something is not working as expected. Martin Herrmann docs/README0000755000000000000000000002260012230036207011362 0ustar rootroot ########################################################################## # # Mapivi - Martin's Picture Viewer # ########################################################################## File README last modified: 2010-07-08 Picture Viewer and Organizer for Linux, UNIX, Mac OS X, Windows written in Perl/Tk. Mapivi supports adding, viewing and editing of JPEG meta informations like: EXIF, IPTC/IIM and JPEG comments. Mapivi is an open-source and cross-platform (UNIX, Mac OS X and Windows) picture manager and organizer written in Perl/Tk. Mapivi is a photo management tool with focus on JPEG pictures. Mapivi supports adding, editing and searching of IPTC/IIM, EXIF data and JPEG comments. The pictures are managed in place (in your folder structure), so you are able to use other tools like file managers or other viewers together with Mapivi. Mapivi can be found here: http://mapivi.de.vu/ http://mapivi.sourceforge.net http://sourceforge.net/projects/mapivi I am always happy to receive some feedback about Mapivi. Copyright (c) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Martin Herrmann. All rights reserved. Feel free to redistribute. Enjoy! Of course I give no warranty at all, so if you accidently delete all you files, corrupt your harddisk or burn down your computer it's your own problem. ########################################################################## # # Copyright # ########################################################################## Copyright © 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Martin Herrmann. All rights reserved. ########################################################################## # # License # ########################################################################## Mapivi Martin's Picture Viewer Mapivi is free software, if you want you may make a donation, see http://herrmanns-stern.de/software/donations.shtml Your donation of any amount will encourage me to continue the development. A Picture Viewer for Unix/X11, Mac OS X, Windows written in Perl/Tk Copyright (C) 2002 - 2010 Martin Herrmann. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ########################################################################## # # Systems # ########################################################################## Mapivi is known to run under: - Linux (Ubuntu, Mandrake, RedHat, Gentoo,...) - Sun Solaris - Windows 2000 (with Activeperl) - Windows XP (with Activeperl) - Apple Mac OS X Please write me, if you know more ... ########################################################################## # # Features # ########################################################################## see also feature list on the Mapivi homepage: http://mapivi.sourceforge.net/features.shtml Image Management * Mapivi supports any existing folder structure * Mapivi stores just picture meta information (IPTC, EXIF, comments, ...) in his database * Mapivi supports pictures on removable media (CD, DVD, USB-HD, ...) * IPTC/IIM information with keywords, categories, location, headline, caption/abstract, writer, ...: o display IPTC o add, edit, remove IPTC (also multiple pictures at once) o hierarchical IPTC keywords and categories o apply or save IPTC templates o rating of pictures using the IPTC urgency flag * EXIF (EXchangeable Image File) information (date, time, aperture, exposure time, ISO speed, ...): o display EXIF o copy EXIF o save and restore EXIF o remove EXIF o alter EXIF date/time absolut or relative o (re)build, copy, rotate EXIF thumbnail o reset/clear EXIF orientation flag * JPEG comments (single and multiple comments are supported): o display JPEG comments o add, edit, copy, join or remove comments o add comments to multiple pictures without overwriting the existing comments (e.g. to add a copyright notice to many pictures at once (batch processing) Image Manipulation * lossless JPEG rotation (90, 180, 270 degrees or automatic rotation using the EXIF orientation flag) * lossless JPEG cropping to any format (useful when printing and framing pictures) * rotate pictures at any angle (this is not a lossless operation) * resize pictures, change the quality (file size) of pictures * filter a picture (normalize, equalize, saturation, gamma, unsharp mask, ...) Image Browsing Nowadays many programs are able to display meta data like IPTC, EXIF or JPEG comments, but you have to press several buttons to see them for just one picture. Mapivi is able to show and edit (batch processing) this informations for a number of pictures at once. The unique feature (as far as I know :) of Mapivi is the compact tabular display of all pictures in a directory including: * thumbnail * filename, file size, size in pixels, aspect ratio (4:3, 3:2, ...) * IPTC/IIM infos in a short readable format * EXIF infos (date, time, aperture, exposure time, ISO, mode, camera type) in a short readable format * comments Other Features * generate and display high quality thumbnails * store generated thumbnail pictures in a sub directory (.thumbs) or a central thumbnail database for further access * search for pictures * import pictures from a digital camera, memory card or other device * file operations: rename, copy, move, email and delete pictures * directory operations: rename, create, delete * compare/diff two picture directories and show all files which differ by: file name, file size, pixel size, comment, EXIF info, IPTC info * search for duplicate pictures (by file name or by file size) * export a static html web gallery of the selected pictures with comments and EXIF and IPTC infos * add a border/drop shadow and/or a copyright info (text or picture) to images * build index prints * compare details between two or more pictures by switching between these pictures zoomed to the same picture area * batch rename pictures * show pictures in a slideshow * fix camera CCD defects by interpolating dead and hot pixels (also possible while importing pictures) * show the histogram of a picture * make screenshots (single window or complete desktop) * set a picture as desktop background ########################################################################## # # Not available: # ########################################################################## o Add or edit meta information of non-JPEG pictures o Edit other EXIF data, but the EXIF date and time (like e.g. Exifer on Windows) o Print pictures (there are better tools for that task) ########################################################################## # # Bugs: # ########################################################################## o Bugs can be reported using Sourceforge (http://sourceforge.net/tracker/?group_id=98810&atid=622115) or by email to me o See also Waranty ########################################################################## # # AVAILABILITY # ########################################################################## The latest version of Mapivi should always be available from: http://mapivi.de.vu/ http://mapivi.sourceforge.net http://sourceforge.net/projects/mapivi http://herrmanns-stern.de ########################################################################## # # THANKS # ########################################################################## Thanks to all users giving feedback, reporting bugs, supplying bugfixes, patches or suggestions. Many thanks to the developers of Perl and Perl/Tk and to the newsgroup news:comp.lang.perl.tk where I learnd so much about Perl/Tk. Thanks also to the Tango Desktop Project (tango.freedesktop.org) for supplying such nice icons. Spezial thanks to Slaven Rezic, Gisle Aas, Guido Ostkamp, Hans-Peter Rangol, Holger Stephan, Uwe Steffen, Russell Adams, Michael Waldor, Hans-Peter Kunzle, Kish Shen, Josh Buhl, Eric-Olivier Le Bigot, Alexey Semenov, Chris Drexler, Eric Piel, Matthias Bobzien, Mister Fred, Antti Kaihola, Steffen Rehn, Bruce McKenzie, Keith G. Robertson-Turner, Andreas Rath, Christian Franke, Stefano Bettelli, Dan Eble, Martin Sarsale, Stephan Helma, Kevin Moloney, Milan Knizek, Piet, Martin Brachtl, Andres Kuusk, Jack D., Nicolas Heuser and a lot of other users who send comments, code, money, CDs and/or a lot of good ideas and tips. ########################################################################## # # AUTHOR # ########################################################################## Martin Herrmann Send me an email if it you have problems, questions, feature requests, patches or comments: Martin-Herrmann@gmx.de Share and Enjoy! docs/mapivi-changelog-history0000644000000000000000000017224012230036207015341 0ustar rootroot RCS file: RCS/mapivi,v Working file: mapivi head: 9.7 branch: locks: strict herrmann: 9.7 access list: symbolic names: keyword substitution: kv total revisions: 109; selected revisions: 109 description: mapivi - martins picture viewer ---------------------------- revision 9.7 locked by: herrmann; date: 2008/02/21 ; author: herrmann; state: Exp; lines: +12 -12 changed 2007 to 2008 added 2008 copyright ---------------------------- revision 9.6 date: 2008/02/21 ; author: herrmann; state: Exp; lines: +514 -196 + added location window to search for and to add location informations (based on IPTC tags Country, Province/State, City, Sublocation) + revised the import dialog: removed mount/unmount + revised the import dialog: moved some options to a foldable frame + revised the import dialog: added an option to add a high rating to locked (write-protected) pictures + embedded a new Mapivi icon + added common key bindings to the dirtree and the picture frame + included patch to correct timezone calculation in case of migration over 24 hour border (Thanks Rene!) + included patch adding a third IPTC dialog layout (without categories) (Thanks Rene!) + show changed IPTC caption in main window if edited in IPTC dialog + added an ignore filter in search for duplicates + improved search for duplicates window ---------------------------- revision 9.5 date: 2008/02/06 ; author: herrmann; state: Exp; lines: +942 -760 + improved usability in crop dialog (better mouse handling, 1/3-grid) + added evolution and mozilla-thunderbird as possible email clients + IPTC caption is now editable in main window (key: F4) + added support for RAW files (files are moved, copied rename along with their JPEG file) + added searching for duplicates by same creation date + improved and reordered several menus and added all layouts as menu entries + improved usability in change EXIF date/time dialog + usage of a proportional font in the keyword search (cloud tag) for better font sizing + increased number of maximal shown thumbnails from 1000 to 10000 + bug fix in IPTC dialog when editing several pictures with different settings + bug fix in search duplicated by file size + usage of nstore instead of store enables usage of search database across different OS ---------------------------- revision 9.4 date: 2007/07/25 ; author: herrmann; state: Exp; lines: +521 -291 Support for XMP sidecar files and WAV files (copy, move, rename them with JPEG file) some experiments with encoded file and folder names (see Encode::encode) replace some non-printable chars in IPTC data Adaptations for keywords added from Picas improved user feedback (progressbar) when editing IPTC of several pictures some work on fullscreen mode in main window and when pic is displayed in own window (key: F11) new key (m) in search window to show selected picture in main window the options window is smaller to fit on small screens, it may be closed with Ctrl-x (OK) or ESC (Cancel) ---------------------------- revision 9.3 date: 2007/05/13 ; author: herrmann; state: Exp; lines: +2266 -1006 When editing IPTC info of multiple pictures the dialog shows all common tags and keywords added several lossless JPEG operations: add border, add relative border, add border aspect ratio, add watermark, ... added icons to all menues (they are stored in configdir/icons) added Image::ExifTool as an optional module if ExifTool is available some XMP operations are supported (see Menu:Edit->XMP info ...) more search options in the cloud tag: date range and rating range the keyword window may now be docked to the left or right side of the main window TOP50 of most popular pictures has been replaced by TOP100 of best rated pictures (see special searches) The number of digits in the filename in the light box are calculated by the number of pictures (thanks to Yann Michel) Some parts of the old filename (e.g. the number) can now be reused in Smart rename (thanks to Thierry Daucourt) better and more checks when adding new keywords to the hierarchy The decoration dialog offers the ImageMagick fonts now the progress dialog calculates the estimated total time for an operation some experiments with encoded file and folder names (see Encode::encode) renamed directory to folder ---------------------------- revision 9.2 date: 2006/12/31 ; author: herrmann; state: Exp; lines: +273 -198 new command line option -i to start with import wizard new option to start import wizard at startup when a memory card is inserted modified some default options Mapivi now saves the last selected picture (optional) code cleanup support of three different date formats (yyyy-mm-dd, dd.mm.yyyy, mm/dd/yyyy) see line 6000 check new keywords and categories for unsupported chars (slash and backslash) support to delete multiple keywords/categories from the catalog at once better progress info for tasks with undefined length smart rename adds now a 3 digit number instead of a 2 digit number when needed added some balloon help in the option window some experiments with ExifTool (not yet active) ---------------------------- revision 9.1 date: 2006/10/31 ; author: herrmann; state: Exp; lines: +320 -174 several improvements for adding new keywords, e.g. new keywords may now also be ignored - the ignore list is saved in file keywords_ignore the number of keywords in the keyword browser can be limited to the 100 most popular more information in the keyword browser, e.g. number of displayed and available keywords the warning after changing an existing rating/urgency can be switched off some more speed improvements when reading in a directory a second time by using file and pixel size information from the database bug fix: when using the IPTC edit dialog the rating/urgency is now handled correct improved folder selection dialog for "add to database ..." and "build thumbnail database ..." new popup menu in thumbnail preview window (also used when showing pictures from keyword browser): open picture folder in main window show at least the complete path and file name in balloon if picture is not available ---------------------------- revision 9.0 date: 2006/10/21 ; author: herrmann; state: Exp; lines: +836 -234 + new feature to browse the pictures by keyword/tag clouds (menu: Search->browse database by keyword ...) + mapivi detects new IPTC keywords in pictures and asks to add them to the keyword catalog (try Menu: Search->add to database ...) + integrated a new color chooser (Tk integrated a new color chooser (Tk::ColorChooser is no longer needed) + added a color picker (color pipette) to choose a color of a picture, the picked color may be used to e.g. add an adequate frame color + search for duplicate pictures may now be restricted to selected directories + search for pictures with a certain pixel size is now supported + new function to add an inner frame when using the montage/index print function + add join button and mode in category window + new preset when changing picture size (PAL: 720x576 pixel) + added Mapivi icon image to all windows + removed unused function exportIPTC() to XML + small bug fixes and improvements ---------------------------- revision 8.3 date: 2006/09/17 ; author: herrmann; state: Exp; lines: +510 -87 new feature: timeline; showing distribution of pictures over month and year sort by EXIF date, file date, file size and urgency are much faster now, as they use info from database it is now possible to restore pictures from the trash as the original folder info is saved montage/index print is now also available in the light table bug fix: when a folder is removed all pictures are now also removed from the database search duplicates: it is now possible to ignore links search window: new menu item to remove items from the database changed key bindings: key-F5: smart update, key-u: update thumbnail list pressing the middle mouse button to view a picture no longer changes the selection search window: added dummy frame for further extensions several GUI improvements ---------------------------- revision 8.2 date: 2006/08/20 ; author: herrmann; state: Exp; lines: +1222 -608 speed improvement up to 10 times when reading in a directory a second time smart update of directory (much faster especially in big directories) added basic support for PNG pictures improved IPTC dialog (simple and professional layout) improved directory diff: more functions, other layout keyword window and light table save their size and position on the desktop new feature to remove image meta info when resizing a picture better logical ordering and consistency when displaying IPTC infos show pic in own window is able to display several pictures more and better infos when importing slideshow files new aspect ratio 5:4 (for PAL video) better layout of overlay meta info improved grayscale function new additional builtin histogram function added more variables in the smart rename function add pictures from a thumbnail window to the light table improved search dialog: entering of date ranges fixed bug in light table resize under windows fixed bug in light table move thumb and scroll ---------------------------- revision 8.1 date: 2006/01/14 ; author: herrmann; state: Exp; lines: +22 -10 fixed a bug with the default thumbnail EmptyThumb.jpg (thanks to Christian Franke) ---------------------------- revision 8.0 date: 2006/01/04 ; author: herrmann; state: Exp; lines: +1511 -578 Mapivi has a nicer and more modern look improved menu layout more color options plus presets keywords and categories may now be added in join mode: e.g. Family.Herrmann.Martin grayscaled (black/white) pics using a channel mixer with a lot of useful filter presets more functions and menues in the light table window added options when saving xnview slideshows (e.g. support relative paths) more menu items and key shortcuts in the search window (e.g. keywords and rating) save and restore size of search window reduced thumbnail generation options TOP50 displays thumbnails now a lot of bug fixes and improvements ---------------------------- revision 7.5 date: 2005/11/14 ; author: herrmann; state: Exp; lines: +1207 -510 + new: added a light table, to sort out and rearrange picture collections, albums, slideshows it is possible to save these slideshows as XnView compatible *.sld file lists + improved the search window (cleanup, more help, better layout) + made more file operations work in the search window (e.g. backup, rename, build web pages) + added possibility to sort and search pictures by popularity (number of views) + added TOP50 of most popular pictures (see menu: Extra->show TOP50) + added -size to convert arguments which speeds up the resizing (idea: Dan Eble) + experimental: menu: Extra->show picture view list (to show slide show lists in main window) + changed the PlugIn arguments from (dir file, file, ..) to (file, file, ..) (each with full path) + reworked all PlugIns accordingly + show file size difference in Bytes in the directory difference window + removed gimmicks like background thumbnails + improved thumbnail options window + new key bindings (e.g. Ctrl-k for categories) + code cleanup and minor bug fixes ---------------------------- revision 7.4 date: 2005/11/02 ; author: herrmann; state: Exp; lines: +527 -273 + build web pages: escape special HTML characters + build web pages: first page is called index.html (easier access) + new feature: track popularity of pictures (count how often a picture has been viewed within mapivi) + improved handling of linked files and modification time checking + better EXIF support for Canon cmaeras + cleanup and rearrangement of the IPTC dialog (not finished) + picture balloon messages are in better shape now + code cleanup and minor bug fixes ---------------------------- revision 7.3 date: 2005/09/16 ; author: herrmann; state: Exp; lines: +727 -344 + import EXIF date, time, owner in IPTC + search just in IPTC keywords + add asymmetric borders around pictures + show or edit IPTC and copy to print also in search window available + more options in directory checklist + better? folder selection for windows + backup is shown immediately + adjustable separator when joining comments + user is asked before a backup file is renamed + easier search patterns ((,),[,],{,},+,... may be used now) + support for new gimp-win-remote (GIMP >= 2.2), old one is no longer supported, but code still available + slideshow displays just the selected pictures + exiftool experiments + code cleanup and bug fixes ---------------------------- revision 7.2 date: 2005/03/16 ; author: herrmann; state: Exp; lines: +83 -19 + new feature to add comments while importing pictures + new feature to add IPTC templates while importing pictures + swapped IPTC and EXIF column in search window too ---------------------------- revision 7.1 date: 2005/03/16 ; author: herrmann; state: Exp; lines: +1751 -664 + mapivi supports and needs module Image::MetaData::JPEG in version 0.14 or better => no more memory leaks when cataloging pictures + autocompletion in most of the entry fields (e.g. in the IPTC editor) to add a new value you must leave the entry with tab or enter key! see also menu:Extra->edit entry history + new feature: add lossless border to JPEG pictures (see menu: Extra->add lossless border) + crop dialog nearly completly rewritten, frame is now drawn using the mouse directly + fixed bug when adding IPTC info to a single picture + improved font selection (new dialog) + new option to overlay the picture with it's meta data + better keyboard support + simply add/remove keywords to comments of one or many JPEG pictures + the setting all/last in keyword and category dialog will be saved + removed the Stop button from the main window (subs: stopButStart, -Stop, -End, -Check) + swapped IPTC and EXIF column in thumbnail table + middle mouse button click in dir tree opens thumbnail preview window of selected directory + bug fixes in: checkCachedPics() and selectThumb() + added more support for non-JPEG pictures, but still a lot work to do + improved display in directory checklist (red for unavailable directories) + resizable columns in search dialog + improved empty-trash functionality + renamed the String, And and Or-Search to exactly, all and any + several improvements and bug fixes ---------------------------- revision 7.0 date: 2005/01/20 ; author: herrmann; state: Exp; lines: +1752 -756 + directory checklist, helps to keep track in which dir the pics are sorted, commented, rated, ... + hierarchical IPTC keywords and categories with easy edit functionality (also for comments) please rename your existing keywords and categories files in the config dir to see the new examples + IPTC keywords and categories are also available as extra dialogs, simply double click on an item to insert it in all selected pictures (use right button to open the edit menu in these dialogs) + copy, save and restore EXIF data stuff is working again in this version + more data in the search database: file size, pixel size, modification time + improved search dialog (show file attributes, urgency search, display EXIF and histogram, sort found pics) + bug fix in search: only the first line of multiline comments and IPTC infos was searched + better memory management (remove thumbs objects after searching) + new keys: to show histogram jump to next selected pic + faster histogram for those with limited ImageMagick installations (no LZW) + show [raw] in the size coloumn if there is a raw (*.nef, *.crw) pic available + show directory sizes including all subdirs in a grphical way + improved comment dialog + new function: empty trash + show file date in balloon popups + removed warning for thumb caption none + corrections, improvements and bug fixes ---------------------------- revision 6.2 date: 2004/12/21 ; author: herrmann; state: Exp; lines: +1101 -726 + Warning: copy, save and restore EXIF data stuff is not working in this version! + Warning: old IPTC templates can no longer be used! + Warning: existing stored EXIF files can no longer be used! + Warning: non-JPEGS are no longer copied, renamed or moved with their JPEG pic! + copy and paste IPTC infos between pictures + new HTML template tags: file-no-suffix, iptc-headline, iptc-caption, comment + edit IPTC info now also in search dialog window + move pics now also in search dialog window and in duplicates window + selectable font family, color and shadow and better layout in add copyright info + editing IPTC infos of multiple files is better supported now (working with Image::MetaData::JPEG) + show the transfer rate when importing pictures + bug fix: flash usage in EXIF data was sometimes wrong + libexif and exif are no longer needed thanks to Image::MetaData::JPEG + replaced jhead by Image::MetaData::JPEG where ever possible + Show ISO setting also for Nikon D70 (it's hidden in the MakerNotes) + startet to support other picture file formats than JPEG (see e.g. is_a_JPEG) + solved some focus problems when clicking on dialogs + added version check for module Image::MetaData::JPEG ---------------------------- revision 6.1 date: 2004/10/20 ; author: herrmann; state: Exp; lines: +339 -107 + new function to add fuzzy borders to pictures (see menu Extra) + indexPrint() now uses a lossless file format when more than one convertion is done + new option when importing pics: delete Canons *.ctg files thanks to Dan Eble + better Mac OS X support thanks to Dan Eble + improved user info for not available external programs + corrections, improvements and bug fixes (thanks to Martin Sarsale) ---------------------------- revision 6.0 date: 2004/10/13 ; author: herrmann; state: Exp; lines: +3 -3 510 should have been 60 ---------------------------- revision 5.10 date: 2004/10/13 ; author: herrmann; state: Exp; lines: +1424 -997 + new more compact layout (filename below thumbnail; optional) thanks to Dan Eble + big internal change of the hlist usage: the entrypath now represents the path and filename + better layout, balloon infos and a delete function in display directory (showThumbList()) + better auto rename function: the first picture of a set will now also have an number (-00) + bug fix: mapivi did not work if Image::IPTCInfo was not available + the offset to GMT is now added in the IPTC dialog when using the EXIF time + default settings for Windows and Mac OS X for the external viewer + better user info in case of errors: the errors are collected and displayed after a batch job + added mapivi icons to most of the windows + enhanced the myButtonDialog to replace the myEntryDialog in many cases + added a test suite for regression test of the non-GUI parts + more function prototypes + some first researches to support UTF8 encoding in comments + bug fixes, code cleanup und minor improvements ---------------------------- revision 5.9 date: 2004/09/28 ; author: herrmann; state: Exp; lines: +588 -209 + new function to convert JPEGs to other formats (GIF, PNG, TIFF) (feature request from Stephan Helma) + new and very fast function to convert pictures to grayscale using jpegtran + better layout of the IPTC dialog thanks to Dan Eble + the section color is configurable thanks to Dan Eble + new possibility when dragging pictures to the dir tree: link file thanks to Dan Eble + do not link to a link bug fix by Dan Eble + cleaned-up the directory hot list management (btw, the file ~/.maprogs/mapivi/dirs is no longer needed) + transparent box around the crop frame in crop window + lossless cropping is done in 8 or 16 picxel steps + the urgency scale is now well aligned for all Tk versions and has more colors + new key: Ctrl-F10 to remove the IPTC urgency flag + select and show the actual directory in the dir tree should work now + bug fix in the short IPTC output and the IPTC urgency setting + the picture in the image processing window may be dragged now (panning) + some bug fixes and some first steps to support more image formats than just JPEG ---------------------------- revision 5.8 date: 2004/09/09 ; author: herrmann; state: Exp; lines: +351 -324 + interface of deletePics(), crop() and copyPics() and other functions changed, they are now usabel from any window + improved web gallery generation (e.g. better use of IPTC infos) thanks to Dan Eble + the search database is saved per default now + it's now possible to edit comments of pics in the search window + minor changes and bugfixes ---------------------------- revision 5.7 date: 2004/09/07 ; author: herrmann; state: Exp; lines: +447 -379 + show memory usage of mapivi in Help->About if Proc::ProcessTable is available + removed preload pic function - it was not very helpfull + improved the handling of cached pictures (they are now adressed by name, not by index) + showPics() argument also changed from the index to the path and file name + new function in dirtree context menu: calculate directory size + new option in build thumbs in all subdirs: update or rebuild + better update of the search database when moving, renaming, ... pics ---------------------------- revision 5.6 date: 2004/09/01 ; author: herrmann; state: Exp; lines: +207 -166 + all (?) functions use the new progresswin dialog + bugfix: show [s] in EXIF column, even if there is no EXIF in the pic + show changed comments and EXIF infos above the actual picture + windows only: show an info when there is no directory requester + enabled search function to search for pictures without comments, EXIF or IPTC infos + added hint for thie empty search as balloon info in the exclude pattern entry ---------------------------- revision 5.5 date: 2004/08/30 ; author: herrmann; state: Exp; lines: +713 -430 + new window to show just thumbnails in columns and rows (try key in dirtree) + a lot of functions use the new progresswin dialog + showThumbs() new approach: use the information of the search database + speed up of many functions (by trying to load the MetaData only once and using the FASTREADONLY option) + corrected the wrong layout of the crop dialog under windows + the name of Image::MetaInfo has changed to Image::MetaData (V0.11) + use Image::MetaData for all EXIF functions + no more use of Win32::FileOp, because it simply didn't work + bugfix: the delete to trash function in the search meta dialog removed the pics instead of moving them to the trash + take OS X into account (OS name of Mac OS X is darwin) ---------------------------- revision 5.4 date: 2004/07/15 ; author: herrmann; state: Exp; lines: +319 -113 + search: added the creation time to the database and search win + search: searching of dirs is possible + search: local search (below a given dir) is possible + search: speed-up and better messages if nothing was found + To use the new features a database rebuild is recommended + improved progressWin with a stop watch and estimated time to go + more use of progressWin + better info in buildDatabase() + started a sort function in the search window ---------------------------- revision 5.3 date: 2004/07/08 ; author: herrmann; state: Exp; lines: +266 -105 + started to use function prototypes + new functions progressWin, to show a progress bar while working + cutString will accept negative length values + mouse wheel works in some more widgets even for Tk older as 804 + buildThumbsRecursive works on the selected directory + new function centerWindow + buildDatabase has a new fast update mode, where only new pics are added (no update of all existing pics) + some improvements in searchMetaInfo ---------------------------- revision 5.2 date: 2004/07/05 ; author: herrmann; state: Exp; lines: +246 -224 + added a lot of constants to replace the cryptic 1 or 0 arguments + changed pixels per byte to bits per pixel (which is more common) ---------------------------- revision 5.1 date: 2004/07/05 ; author: herrmann; state: Exp; lines: +2198 -2069 + new column in main table: directory + getPics() may add the path to the filelist + sortPics() works on a array ref now + checkLinks() works listbox specific now + the IPTC urgency is handled as a seperate tag in the search DB (more to follow) + the EXIF functions (remove, copy, ...) no longer destroy the comments + removed more than 2000 useless double quotes + removed useless braces + replaced double quotes with single quotes (still not finished) + use of constants + double quotes in comments will be replaced by single quotes + new sub: trimComment() + sendTo(): do not remove the compressed pics afer 5 secs ---------------------------- revision 5.0 date: 2004/06/30 ; author: herrmann; state: Exp; lines: +1075 -607 + more than 300 changes in this version + using new module Image::MetaInfo::JPEG from Stefano Bettelli - wrjpgcom and temp file are no more needed - easiers and faster comment handling - pixel size available for all pics + using Text::Wrap to format the text in the listbox + pictures may be compressed (in size and quality) befor sending via email + more perl like for loops e.g. (0 .. 10) + seperated getPics() and sortPics() function + the checkDialog(9 is applied in more places + time measurment in the showThumbs() sub + started to redesign functions to be listbox independent + new subs: cutString, getMetaInfo, updateOneRow, replaceComment, myReplaceDialog, showSegments, checkDatabase + search and replace comments (works in dirs and search findings) + check database: search Database (comments and IPTC) for strange chars + solved a win32 grab problem + better and more configurable EXIF output + cleaned up the different getIPTC subs + change font without need to restart + bug fixes, code cleanup + mouse wheel works in some more widgets even for Tk older as 804 + windows like serach patterns in edit database (added new search patterns) ---------------------------- revision 4.1 date: 2004/05/05 ; author: herrmann; state: Exp; lines: +901 -230 + added use bytes and use locale for better locale support + send pics via email (works currently only with thunderbird mail client) + the EXIF date may also be changed in year steps + compare two directories containing JPEG pics by file name, size, pixels, comments, EXIF, IPTC + some little layout improvements and new key bindings + go to pic / select pics (try key Ctrl-g) + removed the use of KDEsite (drag and drop) for Tk >= 804 + some addaptions (e.g. dirtree) to Tk >= 804 + sort pics by EXIF tag: artist + display of EXIF tag: artist in thumbnail table + sort pics by IPTC tag: byline + ask before opening of more than 10 windows (for EXIF, comments, thumbs or IPTC info) + fixed some zoom bugs + more menu functions and key in the search and duplicates windows + edit a pic in GIMP even without gimp-remote ---------------------------- revision 4.0 date: 2004/03/22 ; author: herrmann; state: Exp; lines: +1643 -493 + thumbnails may also be stored in a central place (thumbnail database) -> viewing read only dirs (CDROM) is possible now + new function to clean thumbnail database + improved edit database (it is searchable now) + improved duplicate search (by file name, by file size, popup menu) + improved search: exclude pattern, urgency (lower, bigger), popup menu, just count option + show histogram of a picture + improved import wizard: log window, import pictures from subdirs, save original name ... + improved IPTC editing (e.g. more and better ballon help, correct syntax) + middle mouse button selects and open picture + do not start in fullscreen mode + show the thumbnail when editing comments + the coordinates are now correct when pics are centered (introduced in 0.3.6) + option for external viewer handling picture list ---------------------------- revision 3.9 date: 2004/02/17 ; author: herrmann; state: Exp; lines: +447 -175 + added IPTC template support (save, merge) + it possible to remove the IPTC urgency flag via menu or dialog + sorting of the pictures for meta data is much faster now + smarter reselection after delete + removed a little bug in the search database (dot dirs) + some more windows are decorated with icons e.g. Help->About + some litte bug fixes and improvements ---------------------------- revision 3.8 date: 2004/02/09 ; author: herrmann; state: Exp; lines: +118 -42 + show image infos (using identify from ImageMagick) + several dialogs also show the thumbnail now (e.g. EXIF info and add comment) + changed the order of the fullscreen mode calls + some litte bug fixes and improvements ---------------------------- revision 3.7 date: 2004/01/30 ; author: herrmann; state: Exp; lines: +93 -61 + some editoral changes in the perldoc part of mapivi + little layout changes (enlarged the canvas some pixels) + removed bug with big pictures centering (introduced in 0.3.6) + removed bug not storing IPTC infos in the database (introduced in 0.3.1!) + improved handling and display of exotic EXIF data ---------------------------- revision 3.6 date: 2004/01/28 ; author: herrmann; state: Exp; lines: +805 -165 + the config directory of the windows version has moved to $ENV{APPDATA}/maprogs/mapivi + improved program execution in windows (no more dos-box pop-ups) + open a picture in gimp even in windows (needs gimp-win-remote) + EXIF thumbnail actions: set, (re)build, rotate (needs libexif/exif) + the adjuster settings (frame width) are saved now + a lot of new small features (e.g. beep when loop to the first pic) + more zoom levels + adjust EXIF date relative (e.g. +/- 1 hour) + better fullscreen support (try key F11) + picture is centered in picture frame (canvas) + clear the EXIF rotation tag + small bug fixes + more infos in menu Help->About ---------------------------- revision 3.5 date: 2003/12/18 ; author: herrmann; state: Exp; lines: +599 -171 + splash screen while loading + new modern look with small widget borders + copy to print directory support (help burning CDs/uploading pics to print) + the open pic in ext viewer should now work on windows too + assigned key: v + redo last selection + new function to add drop shadows to pictures + the app to set the desktop background is now configurable + mapivi will clean up dirs, when they are empty + it's possible to see if a picture has an ebbeded EXIF thumb (optional) + more intelligent display of picture comments + better fallback solution, when Tk::ColorChooser is not available + find duplicates started a new improved interface (not finished!) ---------------------------- revision 3.4 date: 2003/11/11 ; author: herrmann; state: Exp; lines: +1304 -350 + import wizard: mount, copy, interpolate, rotate, rename pictures in one step + support for jpegpixi to remove dead pixels nearly lossless + optional display of the mouse coordinates in a picture (e.g. to locate dead pixels) + build a difference picture of two pictures + complex borders (up to four frames) + display the rating/urgency of the current pic in the status bar + improved user interface and more key bindings + the current dir is also displayed in the balloon of the status bar label + the middle mouse button will open the selected pic in a new window (also in the search window!) + new menu: Search + the directory specific menu commands try to be more intelligent when choosing the right dir + code cleanup and bug fixes + better support for Windows (when using file dialogs) + new function to delete a directory (a summary will be shown first) + improved crop dialog (portrait/landscape switch, more infos) + when building web galleries it's now possible to leave the pics untouched + new variables for the web templates: mapivi-date and mapivi-time ---------------------------- revision 3.3 date: 2003/10/09 ; author: herrmann; state: Exp; lines: +461 -112 + mouse wheel support in the dirtree and thumbnail listboxes + a make-new-directory-button was added to the directory browser window + better handling of write protected pictures + keyword support when adding JPEG comments (just one click to insert often used words) + new function to create symbolic links (not available in the Windows version) + complex borders (up to three frames) + umlaut convertion is optional now + improved handling of the tab key (to switch focus between widgets, scrollbars are ignored now) + a logo may now also be added into the picture border + improved error handling when writing IPTC infos + faster reaction, when deleting all pics of a directory + renamed filter to image processing ---------------------------- revision 3.2 date: 2003/09/09 ; author: herrmann; state: Exp; lines: +455 -611 + new function labeledscale saved about 470 lines and 14kB + adjustable font size in index prints + interface to external picture viewer + Middle mouse button: display selected picture in a new window + the file size of the selected pictures is displayed in the status bar + it's now possible to add a copyright info in the new border of a picture + improved overwrite requester with thumbnails, file size and date + bug fixes and improvements ---------------------------- revision 3.1 date: 2003/08/28 ; author: herrmann; state: Exp; lines: +845 -423 + drag-and-drop support: - copy/move pictures by drag/drop them from the listbox into the dirtree (this works at least on solaris) - open pictures or directories by dragging them from e.g. the desktop into the mapivi window (this works at least on MS windows) + import of Postscript (*.ps) files + the font family used by mapivi is configurable + build web pages: new element HTMLFooter (holds e.g. your email adress) + build web pages: improved dialog + improved search: Comments, EXIF, IPTC and filename may be joined now + the number of lines and the line length of comments and IPTC infos in the thumbnail table are adjustable + the dir hot list is now also available in the file menu and via the key + the search data base file is renamed from dirInfo to SearchDataBase + the extended selection via the anchor and Shift-B1 in the thumbnail table was a litte bit broken, this is fixed now + the actual thumb in the thumbnail table is moved, so that the next (and the prvious) thumb is also visible + added feeback when deleting pictures ---------------------------- revision 3.0 date: 2003/08/18 ; author: herrmann; state: Exp; lines: +927 -571 + import of PPM files + thumbnail limit for directories with a huge number of pictures + new in filter dialog: Level + now optional: check and warn for linked files + index prints with adjustable border size + the actual dir label is now clickable (-> a simple dir requester will pop up) + new button ".." to jump to the parent directory + clean up in the status bar: there is just the stop button left + new function to show the backup file key: + bugfix in deletePreloadList + new function: invert selection + bugfix: when working with linked files (update of thumbs works now) + improved string formating (for thumbnail table) + code cleanup and improvements + the actual selection of pictures is no longer lost, after an action (e.g. rotation) + added an unsharp mask option in rotateAny() + menu rework finished: nearly no more double entries, all single source now + new function: rename actual directory + bigger preview thumb in filter dialog + improved the layout of the crop dialog window + the buildDatabase function may be stopped now ---------------------------- revision 2.9 date: 2003/08/11 ; author: herrmann; state: Exp; lines: +1263 -668 + added a FAQ (also visible in the help menu) + IPTC urgency support, set and diplay the priority of a picture - via menu or Ctrl-Fx keys - the search dialog also supprots the urgency now - is possible to sort by urgency + all IPTC attributes are now saved in the database + new function to remove the IPTC info of pictures + bugfix in the edit IPTC of multiple pictures sub + the file date is displayed + auto wizard when removing a certain comment in multiple pics + support of lossless flip transformation (horizontal and vertical) + improved rotate dialog with artificial horizont to ease the alignment of a picture + an unsharp mask filter is now implemented in the filter dialog + the resize filter is choosable + improved batch renaming, key + improved crop dialog + the file name is used as a second sort criteria + mapivi icon also when running under windows + new update, only for the actual picture: key + improved handling of the preloaded pictures + code cleanup ---------------------------- revision 2.8 date: 2003/07/30 ; author: herrmann; state: Exp; lines: +487 -232 + completly new zoom concept: 23 sinnfull (and fast) zoom levels with smaller steps than before for autozoom and normal zoom (keys: + and - or new picture menu Zoom) + added Gamma correction (see menu->options->advanced) + improvements in buildDatabase(): better user info and working break button + new: cleanDir() cleans a directory from all the stuff added by mapivi (thumbnails, ...) works recursive, but will ask first + allow big comments in html export (1000 chars) + mySelListBoxDialog: Double clicking on items is supported + the ShowHiddenDir flag is now also used in the dirDialog() + bugfix: removed a unconditioned chdir (dirtree) in dirDialog() (thanks to Kish!) + bugfix in filter: the gamma value was only applied when the Sharpness value was != 1.0 + some menu reorganisation + corrected the titles of a lot of dialogs + increased sliderlength from 10 to 30 + added a new layout (20% dirs 80% picture) + code cleanup ---------------------------- revision 2.7 date: 2003/07/23 ; author: herrmann; state: Exp; lines: +314 -206 + the layout of the mapivi window is completly configurable now try the F1-F4, the F6-F11 and the l-key + the exact layout of the two adjusters (width in letters) is no longer saved, but the overall layout + fixed a bu with different itemStyle versions (-bg -> -background) + removed warning, when there is no plugin directory + the rotate dialog is able to do individual or common rotation (doforall-flag) + start of menu reorganisation/reuse of common parts + the height of the comment text box is configurable (options->advanced) + improved the installation process (some text files will be copied to the config dir) + code improvement and bug fixes ---------------------------- revision 2.6 date: 2003/07/17 ; author: herrmann; state: Exp; lines: +539 -264 + picture rotation by any angle (and with preview) is now possible + the problems with preloading pictures and fast cycling should be solved now + mapivi now handles different DirTree.pm versions correctly (chdir vs. set_dir method problem) + the comment and the EXIF frame above the picture frame may now be enables and disabled without restarting mapivi + now all buttons are in the aqua look + new keys and + added a restart mapivi function (File-menu) + some bugfixes ---------------------------- revision 2.5 date: 2003/07/10 ; author: herrmann; state: Exp; lines: +213 -148 + jpegEXIForient is no longer needed, because the actual jhead version is able to do automatic lossless rotation + the preloading of pics works better now, because we block the space-key until the (pre)load is done viewing pics forward and backward (with or without preload) seems to be stable now. + more balloon help in the options dialog + comments will be added to pictures created/processed by mapivi (e.g. the complete filter setting so it's possible to reproduce a certain result) - this feature is optional (see options dialog) ---------------------------- revision 2.4 date: 2003/07/03 ; author: herrmann; state: Exp; lines: +1043 -344 + big redesign of the IPTC dialog (much easier to work with now + added balloon help) + user predefined IPTC keywords and categories + it is possible to search for duplicate pictures in the database (by file name) + the crop preview size is 75 percent of the screensize + the search dialog has a stop-button now + it possible to copy the EXIF date to the IPTC date created field + all (at least I hope) batch actions can be canceled using the stop button + the canvas has a popup menu now (to reload the actual picture from file) + the UNIX commando touch is no longer needed (using perl replacement utime) + adding a decoration is also possible in the filter dialog + the preview size in the filter dialog is adjustable + it is possible to hide some (mostly unreadable) informations in the EXIF info display + or menu: Extra->window list shows a window list of all mapivi windows + using execute (Tk::IO) instead of system + minor bug fixes ---------------------------- revision 2.3 date: 2003/06/11 ; author: herrmann; state: Exp; lines: +706 -267 + the thumb table columns are now resize- and clickable (optional module Tk::ResizeButton) + there is a color chooser (optional module Tk::ColorChooser) + using better dir dialog for windows getOpenFile (choose a file to select a dir!) + using better dir dialog for windows (optional Win32::FileOp) not tested yet! + try to determine the home dir under windows not really tested yet! + there is a stop button (currently only in sub changeSizeQuality) to quit from batch processing + more options: Set the colors of the thumb table, the background of the index prints, the aspect factor + nicer layout of the status bar + display the last pic until the next pic is loaded (before there was a big gap between) + fixed bug in cleanSubDirs: the thumbs of delete pictures were not removed!!! + added aspect ratios: 7:5 and 16:9 and X:Y + fixed bug with disapearing mapivi icon + menu clean up + more key bindings in the dialogs (ESC and Ctrl-q) + new sub execute() (uses Tk::IO if possible, mapivi stays responsive) + replace german umlaute in search pattern ---------------------------- revision 2.2 date: 2003/05/28 ; author: herrmann; state: Exp; lines: +441 -361 + make screenshot function (single window or desktop), needs xwd, which should be available in nearly all UNIX enviroments + gamma correction is available in the filter window + lossless cropping in any aspect ratio, size and offset, needs jpregtran with crop patch, trys to be intelligent when cropping several pictures of different sizes + removed the old crop functions + use a picture as root background (only with X11 at the moment, needs wmsetbg) + resorted some menu entries + using two more (optional) backend progs: xwd and wmsetbg + bug fixes ---------------------------- revision 2.1 date: 2003/05/26 ; author: herrmann; state: Exp; lines: +88 -50 + minor improvements (rename backup- and non-JPEG-pics) + some bug fixes + code cleanup + menu cleanup + new key bindings (e.g. Key-R) ---------------------------- revision 2.0 date: 2003/05/20 ; author: herrmann; state: Exp; lines: +3 -2 Increased version number ---------------------------- revision 1.55 date: 2003/05/20 ; author: herrmann; state: Exp; lines: +893 -204 + new: slideshow + new default colors (gray) + display actual date in a balloon over the clock + more key bindings + display backup info [bak] in size column + add a decoration to pics: border and/or a copyright info (text or picture), also available in the make HTML window + join multiple JPEG comments to one (some progs are only able to display the first one) + trim option for jpegtran to avoid border strip (optional) + copy, move, delete, rename non-JPEG and backup files + more screen friendly layout of the filter window + added a JPEG quality scale in the make index window ---------------------------- revision 1.54 date: 2003/05/08 ; author: herrmann; state: Exp; lines: +287 -70 + ask only once per session and directory to convert non JPEGS + bugfixes (delete hash keys instead of undef ing them) + replacing newlines with spaces in the database for better search results + lossless automatic image rotation using the EXIF Orientation Tag + added a dialog to sub cleanDatabase() to select the ignore paths + new option in the search window: search only complete words ---------------------------- revision 1.53 date: 2003/05/07 ; author: herrmann; state: Exp; lines: +395 -18 + display the aspect ratio of an image (currently only 4:3 and 3:2) + select all backup files (menu) + new sort type: random + show the new size after rotating a picture + lossless cropping to 3:2 format with preview (needs patched jpegtran) + improved change size/quality with pic-to-email preset and backup option ---------------------------- revision 1.52 date: 2003/04/30 ; author: herrmann; state: Exp; lines: +328 -144 + code cleanup + improved search functionality: - search in comments, EXIF and IPTC info - different search modes (String, And, Or) - windows like search pattern (with ? and *) - read in meta infos of several directories at once (build database) - better memory handling - better dialog, more balloon help ---------------------------- revision 1.51 date: 2003/04/29 ; author: herrmann; state: Exp; lines: +98 -59 + code cleanup + more comments + improved clearDirInfo(): ignore certain paths (for removable media) + improved search dialog ---------------------------- revision 1.50 date: 2003/04/17 ; author: herrmann; state: Exp; lines: +85 -40 + case sensitive/insensitive search + improved list of search results with thumbnails, name, comment + search dialog: open dir, jump to picture on left button press ---------------------------- revision 1.49 date: 2003/04/17 ; author: herrmann; state: Exp; lines: +405 -73 + show a balloon info with comment, EXIF, etc over the actual picture (optional) + search meta info searches the file names too (optional) + new key Ctrl-s (search meta info) + automatic PlugIn installation + removing of outdated saved EXIF infos + nicer, more colorful thumbnail table using item styles + improved handling of non-JPEGs + a picture with saved EXIF info will be marked with [s] in the exif column + better info when restoring EXIF infos + function to remove the saved EXIF info files + function to clean the dir info (comments of all pictures, cleanDirInfo()) + minor improvements ---------------------------- revision 1.48 date: 2003/04/09 ; author: herrmann; state: Exp; lines: +366 -64 + new popup menu in the directory tree frame showing a two list: - the most often visited directories (quick list) - the history of the last visited directories + new option to check for non JPEG pictures with an automatic conversion to JPEG + the number of non JPEG files is displayed on the right side of the actual direcrory there is a button labeled i to display infos (name and size) about them + more and changed key bindings + after an update, delete or resort the actual picture is displayed again + better navigation in the dir tree (trying to show all sub dirs when opening a dir) + slightly improved IPTC display + possibilty to insert a complete file in the JPEG comment + simple plugin interface see menu PlugIns and Help->Tips ---------------------------- revision 1.47 date: 2003/03/13 ; author: herrmann; state: Exp; lines: +705 -135 + internal change: using Adjuster instead of packAdjust + crop picture (this is experimental stuff, still problems when zooming) + some new keys: f, F5 see menu Help->Keys + do not show a picture if the canvas is very small + improved IPTC info display (more infos in the thumbnail view) + user info when sorting pictures + sorting pictures by camera model + improved EXIF info display, better and more infos (contrast, white balance, metering, ...) + export IPTC info to a XML file + editing IPTC infos of multiple pictures + new menu Help->Tips + improved zooming ---------------------------- revision 1.46 date: 2003/02/27 ; author: herrmann; state: Exp; lines: +140 -169 + removed the program combine from exprogs hash + new option: add or remove the EXIF infos in the html pictures + new option: ask before making a dir + new option: warn before resize + new function: removeFile() + display the picture size in megapixel (mp) + code cleanup + EXIF: display the focal length in 35mm film if available + EXIF: shorter display of various cameras and program modes + some experimental stuff with the layout: try key-l ---------------------------- revision 1.45 date: 2003/02/25 ; author: herrmann; state: Exp; lines: +509 -130 + new key bindings a, z, Ctrl-e + EXIF data display: remove unprintable data (e.g in Makernote) + new functions: EXIF save, EXIF restore + new function: edit in GIMP (EXIF data will be saved first) + new function: make index print (see Extra menu) + add IPTC caption to web pages + the add EXIF and Comment switches in the web page dialog really work + new page in the options dialog + some bug fixes ---------------------------- revision 1.44 date: 2003/02/11 ; author: herrmann; state: Exp; lines: +315 -216 + sort reverse + export a file list of the selected pictures in the displayed order + code cleanup + build thumbs recursive works again + better display of IPTC infos + works for multiple pictures now + make dir with (sinnfull) permissions + deletePics ask to rename the backup file (if available) + filterPic is able to process multiple files (with the first as preview) so batch conversion is possible ---------------------------- revision 1.43 date: 2003/02/06 ; author: herrmann; state: Exp; lines: +758 -380 + more information is saved (directories, filters, comments) + added some options in the options dialog (colors for mapivi window, show clock, ...) + added a thumbnail preview and some options in the options->thumbnail dialog + german umlaute in picture comments are replaced for better portability + optional clock in the status bar + the filter function has much more options now (sharpness, brightness, saturation, hue, filters) and a double preview (a part of the picture in 100% zoom and the thumbnail) + code cleanup, using new subs: getAllSizeInfo, showText, mycopy + the showEXIFthumb function now accepts more than one picture at once + removed seperation of comments in showComments + added License and History in the Help-menu + better user information while executing background commands (= show a busy mouse pointer :) ---------------------------- revision 1.42 date: 2003/01/31 ; author: herrmann; state: Exp; lines: +305 -502 + insert the file name in the comment (usefull bevore renaming) + code cleanup + some new subs for common used functions (e.g. getRealFile, checkLinks) + removed setJPEGComment now using addComment + jump to the right picture after deleting and making a backup file ---------------------------- revision 1.41 date: 2003/01/29 ; author: herrmann; state: Exp; lines: +137 -76 + adjustments to run under Windows ---------------------------- revision 1.40 date: 2003/01/28 ; author: herrmann; state: Exp; lines: +258 -128 Max trash size - show a warning if trash is full pixel per byte as a indicator of the compressing ratio Picture buttons in the main window user info while loadingthumbnails every 0.5 seconds new sort criterias: Number of pixels and pixel per byte the text dialog can be closed with Control-x menu adjustments remove unknown keys in the config file better info when building web pages ---------------------------- revision 1.39 date: 2003/01/23 ; author: herrmann; state: Exp; lines: +653 -227 + search in picture comments of all dirs visited + copy comments from one picture to another + more info (comments and thumbnail) when copying EXIF info + better support when using EXIF thumbnails + more options for the generation of web albums + the web dialogs now open on the right place + more configuration options + new menu help-keys to show all key bindings + popup help for nearly all configs in the options dialog + more logical layout in the options dialog + code cleanup ---------------------------- revision 1.38 date: 2003/01/21 ; author: herrmann; state: Exp; lines: +1049 -236 + myEntryDialog can display a optional Picture (e.g. the picture to rename) + mapivi is now able to export a html web page of the selected pictures the web pages can contain the JPEG comments and the EXIF data most of the subs are taken from mapiwe (Martins pictures to Web) + option dialog has a new layout + autozoom (21.01.2003) + use embedded EXIF thumbnails as mapivi thumbnails (21.01.2003) ---------------------------- revision 1.37 date: 2002/12/09 ; author: herrmann; state: Exp; lines: +558 -264 + configurable file name format when renaming to EXIF date + display the number of selected pictures + mapivi is much faster now because we alter the thumbnail list direct and do not reread all infos + piclist is only used at startup, using infos from the thumbnail list instead + displaying the picture width and height in the thumbnail list + sort pictures by: EXIF-date, -aperture, -exposure time + code cleanup ---------------------------- revision 1.36 date: 2002/12/04 ; author: herrmann; state: Exp; lines: +370 -71 + key binding r for renamePic + subs getSize and getFileSize + intelligent file name selection in myEntryDialog + sub applyFilter to do some image manipulation + rebuildThumbs, renamePic and deletePics are much faster, because update thumbs is not called anymore + renamePic checks for the right suffix + using Tk methodes FullScreen and packPropagate instead of geometry ---------------------------- revision 1.35 date: 2002/11/06 ; author: herrmann; state: Exp; lines: +136 -61 + improved fullscreen mode in showPicInOwnWin c bugfixes + minor improvements ---------------------------- revision 1.34 date: 2002/10/31 ; author: herrmann; state: Exp; lines: +295 -94 + delete to trash + show/hide hidden directories c ignore empty files + check always before using a extern program + make backup c move thumbs only if move was successfull ---------------------------- revision 1.33 date: 2002/10/30 ; author: herrmann; state: Exp; lines: +122 -19 c using DateTimeOriginal instead of DateTime because this seems to be more accurate + remove of EXIF thumbs possible + change the EXIF date/time stamp ---------------------------- revision 1.32 date: 2002/10/30 ; author: herrmann; state: Exp; lines: +160 -20 + copyright notice at startup c rework in getEXIFThumb + sub showEXIFThumb + key t to show embedded thumbnail + sub copyEXIFData ---------------------------- revision 1.31 date: 2002/10/30 ; author: herrmann; state: Exp; lines: +845 -145 + show which external progs are available + adjustable font size c default number of background jobs limited to 1 + show thumbs or default thumbs + show-hide EXIF and Comment frame in picture view + sub checkGeometry + sub checkAdjusterGeometry + information about the number of thumbnails to generate + more key bindings + mapivi now installs itself c better handling of long time exposures 1 sec c better display of Bias value + remove EXIF date + extract EXIF thumbnail + open picture in own new window c sub toggleHeaders c rebuild thumbs + change size and quality ---------------------------- revision 1.30 date: 2002/10/16 ; author: herrmann; state: Exp; lines: +215 -114 solved newline probleme in JPEG comment (sub formatString) EXIF data: using ApertureValue if FNumber is not available EXIF data: corrct display of exposure times longer than 1 second EXIF data: using ShutterSpeedValue if ExposureTime is not available sub displayEXIFData works for multiple pictures now new: sub showComment to show JPEG comments of (multiple) pictures ---------------------------- revision 1.29 date: 2002/10/14 ; author: herrmann; state: Exp; lines: +213 -33 mapivi saves the last opened dir for the next start possibilty to select a font (in the perl file) for all widgets When the window is closed, quitMain gets called before exit support of JPEG suffix .jpg and .jpeg (added by Hans-Peter Rangol) layout changes in some windows (e.g. close button moved up) for a better support of small displays new: function change Quality (added by Hans-Peter Rangol) new: showPicInOwnWin shows the next picture of a dir by pressing the space key (q and ESC to close) ---------------------------- revision 1.28 date: 2002/10/10 ; author: herrmann; state: Exp; lines: +308 -142 It is now possible to sort the pictures by name, size or date Added maker and model of the photo to the EXIF description edit and remove JPEG comment now work for multiple pictures at once new function: make dir more concise popup menu (with cascade menues) ---------------------------- revision 1.27 date: 2002/10/04 ; author: herrmann; state: Exp; lines: +31 -28 progress bar moved to another place minor code cleanup ---------------------------- revision 1.26 date: 2002/09/11 ; author: herrmann; state: Exp; lines: +71 -25 MaxPreloadPics is now configurable use of DateTimeOriginal if the other EXIF dates are not valid rename to EXIF date uses the file date if no EXIF dates available formate the comments in the balloon to a line length of 70 chars more infos in the help->About ---------------------------- revision 1.25 date: 2002/09/05 ; author: herrmann; state: Exp; lines: +522 -124 added a menu bar added a Help->About added a configuration dialog (Options) removed the Time::Hires stuff all functions can now be activiated via the menues it possible to add/edit the IPTC/IIM information of a JPEG pic added a confirmation dialog to rebuildThumbs() the JPEG comment is cutted down to 800 chars in the balloon output ---------------------------- revision 1.24 date: 2002/09/04 ; author: herrmann; state: Exp; lines: +147 -59 The IPTC comments now have a nicer layout The thumbnail view is now formated in about 4 lines to suppress a too wide layout new function formatString for a short 4 line output of comments in the thumbnail view The EXIF data is now displayed in more detail, mapivi can now handle arrays and arrays of arrays and arrays of hashes comments can now contain double quotes bugfix: it was possible to overwrite an existing file, when renaming an other file some small bugfixes and improvements ---------------------------- revision 1.23 date: 2002/09/03 ; author: herrmann; state: Exp; lines: +82 -21 added a dirtree to the main window handling (save, load) of multi coloumn comments some little bugfixes and improvements ---------------------------- revision 1.22 date: 2002/09/03 ; author: herrmann; state: Exp; lines: +165 -11 it is now possible to view the IPTC comments of a picture the caption/abstract is displayed in the thumbnail view and the full IPTC comment can be displayed in a window ---------------------------- revision 1.21 date: 2002/08/28 ; author: herrmann; state: Exp; lines: +99 -39 the comment and EXIF coloumns are now optional (see menu) Delete key to remove the selected pictures Bugfix: no wrong display of thumbnails when background processes are still running when changing to another dir new: the EXIF display and rename is now more robust (if now DateTime available, it will use DateTimeDigitized, ...) removed the eval calls in getShortEXIFData, which makes everything a bit saver new sub touch() bugfix: after deleting a picture the next pic is shown (not the first in the dir like before) ---------------------------- revision 1.20 date: 2002/08/27 ; author: herrmann; state: Exp; lines: +3 -14 chnaged the default config (showPic and preloadPic are now on) removed a print statement ---------------------------- revision 1.19 date: 2002/08/27 ; author: herrmann; state: Exp; lines: +63 -10 moved the gifs to /home/herrmann/.maprogs/mapivi switched from DirSelect to Tk::DirTree ---------------------------- revision 1.18 date: 2002/08/27 ; author: herrmann; state: Exp; lines: +72 -13 some little changes in the description new sub firstStart() ---------------------------- revision 1.17 date: 2002/08/20 ; author: herrmann; state: Exp; lines: +39 -6 bugfix: added hash to store all thumb photo object they are now all deleted when we open a new dir ---------------------------- revision 1.16 date: 2002/08/20 ; author: herrmann; state: Exp; lines: +92 -12 added lossless rotation with jpegtran ---------------------------- revision 1.15 date: 2002/08/19 ; author: herrmann; state: Exp; lines: +85 -14 show multiple pictures each in one window with balloon info (double-click) Some more key shortcuts Display if files are links in the thumbnail view new menu order ---------------------------- revision 1.14 date: 2002/08/14 ; author: herrmann; state: Exp; lines: +156 -53 snapshot a lot of nice things are working now edit multiple comments save geometry and adjuster position etc. ---------------------------- revision 1.13 date: 2002/08/14 ; author: herrmann; state: Exp; lines: +232 -48 added global config hash with save and load to file function it is now possible to start mapivi without a file or dir ---------------------------- revision 1.12 date: 2002/08/08 ; author: herrmann; state: Exp; lines: +140 -11 remove all comments rename to EXIF date ---------------------------- revision 1.11 date: 2002/08/08 ; author: herrmann; state: Exp; lines: +124 -46 new colors and thumb background pic looks pretty nice now! :) some bug fixes some new features ---------------------------- revision 1.10 date: 2002/08/07 ; author: herrmann; state: Exp; lines: +184 -43 zoom functionality (first test) rebuild thumbs add comment to multiple pictures etc ---------------------------- revision 1.9 date: 2002/07/31 ; author: herrmann; state: Exp; lines: +193 -35 handling of links in some functions not finished yet ---------------------------- revision 1.8 date: 2002/07/01 ; author: herrmann; state: Exp; lines: +21 -16 lalala ---------------------------- revision 1.7 date: 2002/06/27 ; author: herrmann; state: Exp; lines: +286 -58 icon buttons show/do not show pics more comments in the code some clean up ---------------------------- revision 1.6 date: 2002/06/26 ; author: herrmann; state: Exp; lines: +83 -4 added a pod for perldoc ---------------------------- revision 1.5 date: 2002/06/26 ; author: herrmann; state: Exp; lines: +28 -9 works with time measurement and memory freeing for pics (not for thumbs by now) ---------------------------- revision 1.4 date: 2002/06/26 ; author: herrmann; state: Exp; lines: +832 -146 just a checkin ---------------------------- revision 1.3 date: 2002/06/07 ; author: herrmann; state: Exp; lines: +352 -94 a lot of changes ---------------------------- revision 1.2 date: 2002/05/27 ; author: herrmann; state: Exp; lines: +184 -11 with thumbnail display list ---------------------------- revision 1.1 date: 2002/05/27 ; author: herrmann; state: Exp; Initial revision ============================================================================= docs/ToDo-List.txt0000664000000000000000000002161612734656017013046 0ustar rootrootToDo list for Mapivi starting with version 1.0 legend: Line Pattern: State Date (yyyy-mm) Text State: o = open point - = finished x = canceled - 2009-07 check buttons should be disabled when not in folder mode (they make only sense in folder view) - 2009-07 adding locations needs a progress bar x 2009-07 Mapivi should start with last navigation tab opened (e.g. keywords or location) x 2009-08 Add navigation history (see also navigation_history_save()) - 2009-07 Add search to navigation tab - 2009-07 Add advanced search to navigation tab - 2009-07 Add keyword cloud to navigation tab - 2009-07 Add rating and date constraint to navigation tabs (at least where it makes sense) o 2009-07 (Old ToDo-Point) Support UTF8 (non-ASCII) folder and file names o 2009-07 (Old ToDo-Point) Clean up options dialog, move all options to new dialog - 2009-07 Use a hash of hash for all configuration options to generate the options dialog (like in BB) - 2009-08 Add a donation info to the help menu - 2009-08 Show i-Button (show non-JPEG files ...) only in folder mode - 2009-08 Fix rating_frame o 2009-08 Fix fullscreen mode - 2009-08 Decide what to do with copyConfigPics, copyOtherStuff, copyPlugIns -> deleted! - 2009-12 use chooseDirectory for import Wizard "Source Folder", and use the next existing parent dir, if not existing o 2009-08 Support display of non-JPEG pictures, see http://groups.google.de/group/comp.lang.perl.tk/browse_frm/thread/46597bdea9e5ad71?hl=de# o 2009-09 change checkDataFormat() to ISO date (2009-10-22) - 2009-09 add item kind argument to all calls of checkSelection() - 2010-03 Bug: Moving pictures to a folder where these pictures already exist and pressing cancel deletes the pictures instead of canceling the move operation o 2010-03 add logo.jpg and thumbExample.jpg to distribution - 2010-03 idea: animated folder preview by cycling through all thumbnails of a folder when mouse hovers above a folder (see line 1633 "$dirtree->bind(''" or Balloon.pm) o 2010-04 When the rating of a picture is changed a text box with a warning pops-up "Info: Urgency changed from ..." this should be replaced with the stars notation (as graphics) + the thumbnail of the picture should be shown + an undo button would be nice to reset the rating to the old value - 2010-08 generate a dynamic menu (only valid in a session) with a "copy to" and "move to" entry for each folder the user copied or moved pictures to - 2010-10 get the available image magick fonts using: "convert -list type" for IM older than v6.3.5-7 or "convert -list font" for newer versions - 2010-10 draw labels/text on pictures using: convert -size 320x100 xc:white -font Book-Antiqua -pointsize 72 -annotate +30+70 "Anthony" -blur 0x4 -fill black -annotate +25+65 "Anthony" font_shadow_fuzzy.jpg, use ' instead of " under Unix - 2010-10 "mouse coordinates: ..." should not be recorded in the Mapivi log o 2010-10 add function to avoid Mapivi log growing too big o 2010-10 Navigation frame: keywords and location: either split up in a search and an edit tab or at least hide the edit buttons (Add + remove) when in search mode or talk to UI guys to find a better solution - 2010-10 get the available image magick fonts using: "convert -list type" for IM older than v6.3.5-7 or "convert -list font" for newer versions - 2010-10 draw labels/text on pictures using: convert -size 320x100 xc:white -font Book-Antiqua -pointsize 72 -annotate +30+70 "Anthony" -blur 0x4 -fill black -annotate +25+65 "Anthony" font_shadow_fuzzy.jpg, use ' instead of " under Unix o 2010-11 Show all drive letters in folder navigation tree under Windows - 2011-02 enable drag-and-drop from keywords and locations to picture(s?) - 2011-02 rename keyword hotlist to keyword clipboard - 2011-02 Ctrl+scrollwheel-up/down to zoom in/out the actual picture in the canvas (see GNOME Human Interface Guidelines 2.2.1) -> Key + and - are now usable - 2011-02 +/- to zoom out do not work properly: Zooming out stops at certain level, zoom display (text in %) and canvas scrollbars behave sometimes wrong - 2011-04 thumbnails of small pics should not be bigger than the originals - 2011-04 thumbnails of animated gifs should not be bigger than other thumbnails - 2011-04 check if it is possible and usefull to shift the focus from the navigation frame to the thumbnail frame after opening a folder; the user should be able to scroll thru a folder using the mouse wheel without first clicking on a thumb - 2011-05 Folder statistics: percent of folders with first, second, third, all checkmarks - 2011-05 new option to save database statistics and compare old and new values at mapivi exit. To show how many pictures have been deleted, tagged, etc. o 2011-06 Search picture database can be started several times (while still running), but only be stopped once; Search button should be disabled during search (or maybe exchanged by the stop button and vise versa) - 2011-08 bug: After Ctrl-m no picture is selected o 2011-08 After creating a new folder (during move or copy operation) the new folder should be selected as default target. - 2011-08 bug: Copy AND Move to originals folder (Ctrl-m) does not update searchDB! thumbnails are also not copied /moved o 2011-08 bug: seems like Mapivi can't delete empty folders o 2011-08 new feature: make selected pictures writable (see checkWriteable()) - 2011-08 jpegpixi source is offline -> removed jpegpixi from mapivi o 2011-08 when deleting a JPEG and its NEF file an error occurs because the NEF file is already deleted when the JPEG is deleted. o 2011-08 search for pictures with red,green and/or blue flags (see also get_rating_and_size()) - 2011-08 search for pictures with same EXIF date/time to also find renamed pictures, see 'Search for date/time ...') o 2011-09 calling edit on RAW pictures should open RAW-Converter e.g. RawTherapee or darktable o 2011-09 context menu in location tree (collapse/expand) - 2011-09 remove "Filter" label under location tree (and same layout as in keyword tree) - 2011-09 add thumb context menu to light table (see also feature request Enhance Lighttable - ID: 3392323) o 2011-09 rough idea: new function: create slideshow from any selection, search and expert search (e.g. via light table) - 2011-09 Slideshow filter options: Add include keywords, translate dialog - 2011-09 light table: add menu entry "show slideshow" (with rev 221 we have to select all pics and then press MMB or "Show selected pictures") - 2011-09 light table: add d-key to show pics in new window - 2011-09 slideshow: add user feedback about slideshow started/stopped, show time in seconds when +/- is pressed o 2011-09 light table: allow doubles? (a pictures may be used several times in a slideshow); maybe show a warning/question when adding - 2011-10 after zooming a picture to e.g. 100% it should been shown centered (at the moment it is aligned to the top left corner) o 2011-11 Autosave (database and config) save on focus out (saving laptop batteries)? - 2012-01 light table: file name of selected thumbnail should be displayed in status line - 2012-01 light table: @light_table_list should be an elements of $ltw, not a global array o 2012-04 show also PDFs, imagemagick is able to convert, convert PDFs to JPGs - 2012-08 light table: only ask to save on close when something has changed! o 2012-08 Icon for drag-and-drop of several pictures: image stack generated with imagemagick, see http://www.imagemagick.org/Usage/montage/#index montage -size 100x100 null: *.jpg null: -auto-orient -thumbnail 100x100 -bordercolor white -background none +polaroid -gravity center -geometry -50+2 -tile x1 result.jpg o 2014-10 when do_other_files() renames or moves e.g. a RAW picture, the list box is not updated (thumb removed or renamed). Find an intelligent way to handle this situation. o 2014-10 use EXIFTool to extract thumbnails from RAW files? o 2014-10 replace Image::MetaData::JPEG with Image::EXIFTool everywhere (<- this is a big one!) - 2014-10 show_pic_in_own_window: Show short IPTC data (just values, no keys) and date (if needed change from button to canvas?) o 2014-10 check function extract_jpeg() where is it used? does it work? - 2014-11 improve tag cloud navigation (show pictures button is not always visible) - 2014-11 search for date/time: add time range, e.g. +/- n min, n hours, n days, n weeks, ... - 2014-11 show_multiple_pics: new args to start fullscreen and to start slideshow directly o 2014-11 do we still need the stand alone keyword (tagcloud) browser? It is now embedded in the navigation bar. o 2014-11 drag and drop keywords to clipboard see existing but deactivated functions drag_keyword() and drop_keyword() o 2015-02 showNonJPEGS should be renamed to show_hidden_files and show the diff between the files in $actdir and the displayed files in listbox - 2015-02 showNonJPEGS/show_hidden_files should offer a dialog to display the files e.g. in a video player like vlc o 2015-02 showNonJPEGS/show_hidden_files add option to select video tool docs/FAQ.txt0000775000000000000000000003101012754672046011671 0ustar rootrootFAQ for Mapivi (last update: 2016-08-16) 1. What should I do, if a dialog pops up saying: "Error in mapivi" "image "imageXX" doesn't exist at ..."? Press OK, and update the thumbnails by pressing . If there are still problems, exit Mapivi with . This problem may occure, when you are working with Mapivi while it's still busy e.g. loading the thumbnails. 2. What's about the memory size of pictures, when adding comments and IPTC infos to pictures? It's like you would expect it: about one byte per char. Example: adding a comment of 30 chars to a 1MB picture will increase the file size about 0.03%. 3. How many comments are allowed in a JPEG picture? From the wrjpgcom man page: The JPEG standard allows "comment" (COM) blocks to occur within a JPEG file. Although the standard doesn't actually define what COM blocks are for, they are widely used to hold user-supplied text strings. This lets you add annotations, titles, index terms, etc to your JPEG files, and later retrieve them as text. COM blocks do not interfere with the image stored in the JPEG file. The maximum size of a COM block is 64K, but you can have as many of them as you like in one JPEG file. 4. How do I work with comments? Simply select a picture to comment and press , enter the comment an press Ctrl-x to close the dialog. Example: you have three pictures to comment, one with Tom and Tim, one with Tom and Peter and one with Tom and Linda. They were taken at Lucy's party. Here is what I would do: select all three pics, press , enter "Lucy's Party Tom", press Ctrl-x. Now all pics have the comment "Lucy's Party Tom", now select the first one and add "Tim", the second and add "Peter", and so on ... 5. I've added several comments to my picture, I can see them with Mapivi, but not with my other tool. Why? Some viewer tools only support the first comment of a JPEG picture. You can use the join comments function of Mapivi to join all comments to one. 6. I can manipulate pictures with Mapivi, but why is there no "Save" or "Save as"? When manipulating pictures (image-processing), Mapivi serves as a frontend to the ImageMagick tools. The manipulation is done directly on the file, so there is no need to save. But there is also no undo, so better keep the option "Create backup of original picture" enabled. You can also force a backup with the menu File->Make backup. If there is already a backup, Mapivi will ask you to overwrite it. The backup of file.jpg will always be named: file-bak.jpg. If you delete file.jpg and there is a backup named file-bak.jpg, Mapivi will ask to rename the backup file to the old file name. This is also done when renaming the original file. If a file has a backup file the string "[bak]" is shown in the size column of the thumnail table. 7. What's the picture rating (IPTC urgency tag) for? The urgency is an IPTC field to specify the importance of a picture. The urgency may be used as a search or sort criteria. I use it to seperate the really good and important pictures from the rest. That's my reflection: My grandma has about 5 photo albums of her whole life, that number of pictures can be viewed in some hours. Since I own a digital camera the number of photos I take is exploding (even if I really try hard to sort out about the half of them). So when I keep on taking so much photos I never will have the time to look at them, when I am older. That's where the urgency tag comes into play, I simply mark the best or most important pictures with a high priority. Later I can choose a view to see only the best pictures of e.g. my children. This can be done in the search dialog by selecting the proper urgency and e.g. the Name of the person to search for in the pattern field. 8. How can I get the Mapivi Icon under MS Windows? Use the MapiviIcon32.ico (this is located in the directory pics of your mapivi packet) in the properties dialog of the windows explorer. 9. How do I open a directory not accessible with the dirtree on the left? (This may happen when running Mapivi under MS Windows) Press , or use the menu File->open directory. 10.Is it recommended to do image manipulation with pictures in JPEG format? Usually image manipulations should be done with a image manipulation programm, like The GIMP or Photoshop. They support lossless image formats (like XCF or PNG). The disadvantage of JPEG is the fact, that every time you save a JPEG picture it has to be compressed again. This compression is NOT lossless, so with every step your picture quality gets worser and worser. It may be appropriate to use JPEG for some basic image manipulation like the functions provided by Mapivi if the source and the target format is JPEG anyway. Mapivi tries to support the manipulation of JPEG files as much as possible by lossless operations (rotation, cropping, adding of comments and IPTC infos) and lossy one-step-manipulations. It's e.g. possible to apply several filters and color adjustments to a picture while adding a border. So there is just one recompression to the picture. This should produce the same results like manipulating the picture with the GIMP and then saving it as a JPEG file. If it's necessary to do several steps, set the quality to 95% (more just increases the file size, not the quality). 11. How to compare details on two similar pictures Switch the autozoom option of (menu: Options->Window->auto zoom) or open the pictures and press to zoom to 100% for each. Click on the thumbnail of the first and than on the thumbnail of the second picture. Now you are able to move the picture in the canvas, when you switch back to the other picture you will see the same part (e.g. the lower right corner) of this picture. Another possibilty is to build a difference picture of two pictures. This can be done using the menu "Extra->build difference picture". You can toggle the display between selected pictures using . 12. What the meaning of all the special info in the thumbnail table? Let's start with the EXIF column: [s] the EXIF info has been saved to an extra file use the menu to restore it back [t] there is an EXIF thumbnail embedded in the picture this is optional (press to view the thumbnail) date the date and time the picture was taken 18mm the focal length of the lens (27mm) the focal length of the lens in 35mm film equivalent F2.8 the aperture 1/60s the exposure time ISO100 the ISO speed rating +0.3 the exposure bias value the size coloum: [bak] the file has a backup (e.g. file-bak.jpg) 1143kB the file size in kB (1kB = 1KiB = 1024 Byte) 2048x1536 the picture size in pixels 3.15MP the amount of pixels in mega pixels (1MP = 1000000 pixel) 2.69 b/p the bits per pixel value (kind of quality value) [3:4] the aspect ratio date the file date the rest should be obvious, I hope. 13. Panic! Where is my backup file? A backup file (named e.g. pic-bak.jpg) is only created, when the "Create backup" button was selected. If the backup is not shown directly after a conversation, please press to update the thumbnail table. Hint: If a picture has a backup this string: [bak] is shown in the size coloum. 14. I've rotated my pictures manually, but the rotate flag is wrong now. What should I do? Clear the rotate flag (see menu Edit->Rotate->Clear rotate flag). 15. I've rotated my pictures, but the EXIF thumbnail is still in the old wrong possision. What should I do? There are two possibilities: a) lossless Press to open the options dialog, go to the Thumbnails pad and deselect "Rotate EXIF thumbnail when rotating picture". Now rotate the picture to match the EXIF thumbnail (press to see the EXIF thumbnail). Now select "Rotate EXIF thumbnail when rotating picture" in the dialog noted above and rotate the picture again. b) fast Just build a new EXIF thumbnail (see menu Edit->EXIF info->(re)build thumbnail) 16. I want to save a copy of a picture in the same directory, with a different name (Save As ...). How to do this with Mapivi? Select the picture (e.g. pic1.jpg), use the menu: File->make backup. A new file named pic1-bak.jpg will be created, select this picture and press (or use the menu: File->rename) and rename the picture to the new name. 17. If I resize the Mapivi window the pictures do not resize. What's wrong? Nothing is wrong, zooming is slow, so Mapivi just rezooms on user request. Press (Shift-u) above the picture and it will be reloaded and zoomed to the new canvas size. This is also needed to update the display after you processed a picture in e.g. GIMP. You may also try (fit picture in canvas) or (display in original size = 100%). 18. Mapivi is great! Why is it free? Because I like and use a lot of free open source software myself and Mapivi is my contribution back to the community. 19. How can I reuse the saved EXIF infos or IPTC templates from the pre 0.7.0 Mapivi versions? The only way is to use an old Mapivi version, restore the EXIF infos to the pictures and apply the IPTC templates to certain pictures. Quit the old Mapivi version. Then start the new Mapivi version (>= 0.7.0) and save the EXIF infos (if needed) and the IPTC templates again. 20. How to use hierachical keywords and categories? If there is no tree visible in the "edit keywords" or "edit categories" dialog, follow these steps. Quit Mapivi, use a file manager or the shell, change to directory ~/.maprogs/mapivi/ and rename file keywords to keywords.old and file categories to categories.old Then restart Mapivi, open menu: Edit->IPTC Info->edit keywords ... and you will see an example keyword tree. You may edit this tree using the context menu (press the right mouse button to open it). A double click on a keyword will insert it in the IPTC info of all selected pictures when "Add" mode is selected. 21. Is it possible to search for pictures stored on an external media (e.g. CD, DVD, USB-Stick, external HD)? Yes, of course, if Mapivi knows about them. Follow this procedure: 1. Insert the media 2. Press F7 in main window (directory tree will show up) 3. Select media root folder (e.g. /media/dvd for UNIX or D:\ for Windows) in Mapivi directory tree 4. Select Mapivi menu: Search->build database ... to add the pictures stored on the external media to the Mapivi search database Hints: - As Mapivi will just show the path to the picture (e.g. /media/dvd/pic1.jpg or D:\pic1.jpg) it is recommended to use a CD/DVD folder structure with unambiguous naming e.g. dates like 2005/200510/20051026_Party/ this will help you to find the right CD/DVD - If you didn't use Mapivi for deleting the pictures you should select Mapivi menu: Search->clean database ... to update the Mapivi search database 22. When I find pictures stored on external media (e.g. CD, DVD, USB-Stick, external HD no thumbnails are shown. Can this be changed? or: Is it possible to show the thumbnails of pictures stored on an external media in the search dialog? Yes, it is possible. Follow these steps: 1. Insert the media 2. Press F7 in main window (directory tree will show up) 3. Select media root folder (e.g. /media/dvd for UNIX or D:\ for Windows) in Mapivi directory tree 4. Select Mapivi menu: Extra->build thumbs in all sub directories ... Mapivi will store these thumbnails in a central folder if the media is not writable. 23. Does Mapivi store the thumbnails? Where are they stored? Yes, Mapivi tries to store all produced thumbnails. They are stored depending on the configuration in a) a sub folder of the current folder named .thumbs or b) in a central folder (for UNIX: ~/.maprogs/mapivi/thumbDB/) 24. What is the dotted line in the menu for? When you select the dotted line you will get a so called tear-of menu. The menu will become a new window and you may place it anywhere on your desktop. This is very handy if you need some functions several times. docs/INSTALL0000755000000000000000000003175612771201200011544 0ustar rootroot ########################################################################## # # Mapivi - Martin's Picture Viewer # ########################################################################## Mapivi is an open-source and cross-platform picture manager. Mapivi is written in Perl/Tk and runs under all important operating systems, like UNIX/Linux, Mac OS X and Windows. Mapivi supports adding, viewing and editing of JPEG meta informations like: EXIF, IPTC/IIM and JPEG comments. File INSTALL last modified: 2016-08-16 Mapivi latest version can be found at: http://mapivi.de.vu and http://mapivi.sourceforge.net/mapivi.shtml ########################################################################## # # Requirements # ########################################################################## You need: o a computer running UNIX (Linux, Solaris, Mac OS X) and X11 or Windows (Mapivi also works at least with Windows 2000, WinXP and Vista) and for the Perl variant of Mapivi: o Perl 5.005 or better o Perl/Tk 800.015 or better o jpegtran to do the loss-less rotation. They are included e.g. in the The Independent JPEG Group's JPEG software release 6b (this program is part of most Linux distributions as part of the libjpg package) o jhead - for auto rotation of pictures homepage: http://www.sentex.net/~mwandel/jhead/ o the command line tools convert, mogrify and composite (or combine) from Image Magick (this is also part of most Linux distributions) see http://www.imagemagick.org o Perl module Image::MetaData::JPEG o Perl module Image::Info o only for old Perl/Tk before version 804.025: Perl module Tk::JPEG o optional: Perl module Proc::ProcessTable You will find all the perl modules at http://search.cpan.org/ for the Windows distribution (mapiviXXX_Win32.zip) you need: o jpegtran to do the loss-less rotation. They are included e.g. in the The Independent JPEG Group's JPEG software release 6b (this program is part of most Linux distributions as part of the libjpg package) o jhead - for auto rotation of pictures homepage: http://www.sentex.net/~mwandel/jhead/ o the command line tools convert, mogrify and composite (or combine) from Image Magick (this is also part of most Linux distributions) see http://www.imagemagick.org ########################################################################## # # Install Mapivi version 1.0 and newer # ########################################################################## First check if Mapivi is available as a package for your operating system. Linux Ubuntu e.g. offers Mapivi as a package which may be installed using the package manager. This is the easiest and safest way to install Mapivi. If this is not possible or you want to try the newsest version, follow these instructions: Download Mapivi from the sourceforge subversion repository: http://mapivi.svn.sourceforge.net/viewvc/mapivi/ as a GNU tarball. Unzip and unpack the tarball mapivi.tar.gz Windows: Move all extracted files and folders to any folder e.g. "C:\Program Files\Mapivi\" UNIX/Linux or Mac OS X: Move mapivi.pl to e.g. "/usr/local/bin" and all other files and folders to "/usr/share/mapivi" Open a DOS box or shell and start Mapivi: Windows: "C:\Program Files\Mapivi\mapivi.pl" or Linux: "/usr/local/bin/mapivi.pl". If Mapivi doesn't start read chapter "Install Perl and Perl Modules" ########################################################################## # # Install/Upgrade Mapivi version 1.0 and newer for Ubuntu Linux # ########################################################################## Install older version of Mapivi as available in the Ubuntu package manager. Download newest Mapivi version from http://sourceforge.net/p/mapivi/code/HEAD/tree/ Use "Download Snapshot" to download all files as Zip archive. Unzip archive into any folder in your home directory. Or do a SVN checkout (in this example to /home/user/foldername): svn checkout https://svn.code.sf.net/p/mapivi/code /home/user/foldername Change to this folder: "cd foldername" Update icons: "sudo cp icons/* /usr/share/mapivi/icons" (Alternative way for icons if you e.g. have a SVN snapshot: "sudo rm -r /usr/share/mapivi/icons/" "sudo ln -s /home/user/foldername/icons/ /usr/share/mapivi" the same can be done for languages and docs folder) Start Mapivi with "./mapivi.pl" To install optional perl modules (the missing modules are shown in Mapivi in menu Help->System Information), use CPAN: "cpan Tk::MatcheEntry" "cpan Tk::ResizeButton" "cpan Tk::Splash" ... ########################################################################## # # Install Perl and Perl Modules # ########################################################################## Install Perl: See http://www.perl.org/get.html Note: Under Windows Mapivi has only be tested using ActivePerl. Install Perl Modules: If Mapivi doesn't start, read the errors in the DOS box or shell. If the error looks like "Can't locate Tk.pm in @INC ..." then a Perl module, in this case the Perl module (pm) named "Tk", is missing. Other errors in the same category may be: "Can't locate Image/MetaData/JPEG.pm in @INC ..." or "Can't locate Image/Info.pm in @INC ..." The installation process for Perl modules depends on your Perl distribution. If you are under Windows and use ActivePerl you just need to type: "ppm install Image-Info" to install the module Image/Info.pm. Under UNIX/Linux you may use CPAN: "cpan Image::Info" to install the same module (see also http://www.cpan.org/modules/INSTALL.html). Some Linux distributions like Ubuntu offer Perl modules as a package (e.g. the Perl module Tk as package perl-tk), use the Ubuntu package manager to install the modules. ########################################################################## # # WARNING: Outdated documentation!!!!!! # # The installation hints below are only partly valid for Mapivi Version # 1.0 and newer! # # WARNING: Outdated documentation!!!!!! # ########################################################################## ########################################################################## # # Installation of Perl distribution for UNIX # ########################################################################## File name mapiviXXX.tgz (or mapiviXXX.tar.bz2) in short: unzip, unpack, change to the mapivi directory and run mapivi with: perl mapivi in long: ("-> " is the command line prompt) unzip the archive (XXXX is the version number): -> gunzip mapiviXXXX.tgz or -> bunzip mapiviXXXX.tar.bz extract the tar-ball: -> tar xvf mapiviXXXX.tar go into the directory -> cd mapiviXXXX/ (where ever you extracted mapivi to ...) run mapivi -> perl mapivi (you only have to do this the first time for the mapivi install process) later Mapivi is started like this: -> mapivi ########################################################################## # # Installation of Linux executable distribution # ########################################################################## File name mapiviXXX_Linux.exe.bz2 1. Unzip the package to a new folder anywhere using bunzip2 (e.g. to ~/progs/mapivi) 2. Download and install jhead and jpegtran, if not already installed. 3. Start MapiviXXX_Linux.exe with a double click in your file manager or in a shell (if this doesn't work you may need to adjust the file permissions: chmod a+x ~/mapivi/MapiviXXX_Linux.exe). That's all. ########################################################################## # # Installation of Perl distribution for Mac OS X # ########################################################################## File name mapiviXXX.tgz (or mapiviXXX.tar.bz2) [Unfortunately, I did not keep track of all the things I had to install to make mapivi work. If you are the next person to install mapivi on Mac OS X, please make a list and tell Martin. -- Dan Eble] Follow installation instructions for UNIX. macosx-preview Mapivi comes with a shell script, macosx-preview, which opens the Preview application to serve the "open picture in external viewer" feature. To use this feature, make sure the script is in a directory that is in the PATH, or instead open the mapivi options (Ctrl-o), go to the Advanced tab, and change the setting to include the full path to macosx-preview. The macosx-preview script attempts to use full-screen view, but this requires the feature "access for assistive devices". To enable this feature, run System Preferences, select the Universal Access pane, and check the box labeled "Enable access for assistive devices". ########################################################################## # # Installation of Windows executable distribution # ########################################################################## File name mapiviXXX_Win32.zip 1. Unzip the package to a new folder anywhere (e.g. C:\Program files\Mapivi) 2. Download and install ImageMagick, jhead and jpegtran. 3. Start MapiviXXX_Win32.exe with a double click or in a dos box. That's all. ########################################################################## # # Installation of Perl distribution for Windows # ########################################################################## File name: mapiviXXX.tgz (or mapiviXXX.tar.bz2) Installation order: 1. Active State Perl 2. Perl module String::IO 3. Perl module Image::Info 4. Perl module Image::MetaData::JPEG 5. Perl module Tk::JPEG (needed only for Perl/Tk versions < 804.025) 6. ImageMagick, jhead, jpegpixi, ... 7. Mapivi Install perl e.g. the Active State Perl (www.activestate.com) Try also to get the needed perl modules from there. Install Image magick, jhead etc. Suggestion: Create a new directory e.g. C:\Programs\Graphics and install all backend tools in this directory. Don't forget to add this directory to the PATH variable (System->Environment variables->Path). unzip, unpack mapivi (e.g. with winzip) change to the mapivi directory in a DOS-box and run mapivi with: perl mapivi I recommend renaming mapivi to mapivi.pl (or mapivi.wpl), so the windows explorer is able to recognize it as a perl file. After renaming it's possible to start it with a double click, or via the start menu or the quick launch bar. You may also change the default perl icon to the MapiviIcon32.ico icon (see directory .../mapiviXXXX/pics/). If you have problems with Win Xp and Tk::JPEG, maybe this link is a help for you: http://perlmonks.thepen.com/195691.html Hint: If you have problems with convert from ImageMagick, this may be due to the fact, that there is a windows program with the same name. Workaround: the path to the ImageMagick convert tool must be in front of the windows convert tool in the PATH variable. ################################# Perl Installation Update for Win32 (Nov 2004): Install perl e.g. the Active State Perl (www.activestate.com download here: http://www.activestate.com/Products/Download/Download.plex?id=ActivePerl) e.g. ActivePerl 5.8.3 build 809 (I usually use the file with the MSI suffix) Open a DOS box, start ppm (enter ppm and press return) and download the actual Perl/Tk 804.027: ppm> repository add BdP http://www.bribes.org/perl/ppm ppm> install http://www.bribes.org/perl/ppm/Tk.ppd -force This new version already includes Tk::JPEG Now you just need to install Image-Info and Image-MetaData-JPEG: ppm> install Image-Info ppm> install Image-MetaData-JPEG ########################################################################## # # Installation of Mapivi PlugIns # ########################################################################## Plug-Ins are executable applications which are stored in the Mapivi config sub folder PlugIns (on UNIX: ~/.maprogs/mapivi/PlugIns). They are called by Mapivi with the selected pictures (each with complete path) as arguments. The example PlugIns contained in the Mapivi distributions are written in Perl. To get them running on your system it may be necessary to adjust the first line of each PlugIn (e.g. filelist-plugin). The first line must contain the chars #! and the full path to your perl executable. Example: #!/usr/bin/perl (To get the full path of perl try: which perl in your shell.) ########################################################################## # # Help # ########################################################################## You will find a lot of help to Perl and Perl/Tk in this newsgroup: news:comp.lang.perl.tk The newsgroup may be searched using this link at google.com: http://groups.google.com/groups?hl=en&group=comp.lang.perl.tk If this won't help, try to start mapivi from perl and read the output carefully -> perl mapivi You may also set the $verbose variable to 1 to get more information. To do this, press Ctrl-v while running mapivi or by opening mapivi in a text editor and searching for this line: my $verbose = 0; # boolean (1 = print debug infos, 0 = be quiet) and changing it to: my $verbose = 1; # boolean (1 = print debug infos, 0 = be quiet) then save mapivi and restart it: -> perl mapivi Send me an email if it you have problems, questions, feature requests, patches or comments: Martin-Herrmann@gmx.de (german or english spoken)