# Copyright 2005 Jean-Michel Fayard jmfayard_at_gmail.com # Put into the public domain. # package Image::Kimdaba; use strict; use warnings; use XML::Parser; use Carp; =head1 NAME Image::Kimdaba - Parser for the KDE Image Database See here : http://ktown.kde.org/kimdaba =head1 SYNOPSIS use Image::Kimdaba; use English qw( -no_match_vars ) ; my @ListOfPictures; my $folder=getRootFolder(); parseDB( "$folder" ); print "Your actual Kimdaba settings are :\n"; while( my ($attr, $value) = each %kimdabaconfig) { print "\t$attr => $value\n"; } print "\n"; my $nb1= scalar keys %imageattributes; my $nb2= scalar keys %imageoptions; print "Following options were present in your $nb1 pictures :\n"; while( my ($option,$r_values) = each %alloptions ) { my $nb = scalar @$r_values; print "\t$nb $option\n"; } print "\n"; local $, = "\n" ; # print bla,bla prints "bla\nbla" print "\n\n== NO Keywords (ten first) ==\n"; @ListOfPictures=matchAnyOption( "Keywords" => [] ); print sort(@ListOfPictures[0..9]); print "\n\n== Holiday ==\n"; @ListOfPictures=matchAnyOption( "Keywords" => [ "holiday" ] ); print sort(@ListOfPictures); print "\n\n== ANNE HELENE ==\n"; @ListOfPictures=matchAnyOption( "Persons" => [ "Anne Helene" ] ); print sort(@ListOfPictures); print "\n\n== ANY OF (JESPER, ANNE HELEN) ==\n"; @ListOfPictures=matchAnyOption( "Persons" => [ "Jesper" , "Anne Helene" ] ); print sort(@ListOfPictures); print "\n\n== ALL OF (JESPER, ANNE HELEN) ==\n"; @ListOfPictures=matchAllOptions( "Persons" => [ "Jesper" , "Anne Helene" ] ); print sort(@ListOfPictures); print "\n\n== PERSONS=Jesper, Locations=Mallorca ==\n"; @ListOfPictures=matchAllOptions( "Persons" => [ "Jesper" ], "Locations" => [ "Mallorca" ] ); print sort(@ListOfPictures); $, = "" ; # print bla,bla prints "blabla" print "\n\n==Print all infos known about specific pictures\n"; print "\n\n== Drag&Drop pictures from Kimdaba ==\n"; @ListOfPictures=letMeDraganddropPictures(); printImage( $_ ) foreach @ListOfPictures; =head1 DESCRIPTION From the website : http://ktown.kde.org/kimdaba KimDaBa or KDE Image Database is a tool which you can use to easily sort your images. It provides many functionnalities to sort them and find them easily. =head2 Datastructures The infos available in the database are directly translated in following perl datastructures. (See the index.xml file to see how it looks like) note : the reading of man perllol is highly recommended =head3 C<%imageattributes> HASH OF (url of the picture, REF. HASH OF (attribute, value) ) Now and in the rest of the document, B is given locally from the root directory, such as "Folder1/Subfolder/img001.jpg", it's neither file:/home/user/Images/Folder1/Subfolder1/img001.jpg nor http://www.google.com/images/logo.gif An B corresponding to this B could be ( monthFrom=>"1", dayFrom=>"18", hourFrom=>"19", yearTo=>"0", monthTo=>"0", md5sum=>"7f120e3cfb698ce0d7bb6e4e454c1a8b", minuteFrom=>"29", file=>"2005-01-09-Gif/img_0290.jpg", label=>"img_0290", angle=>"0", dayTo=>"0", secondFrom=>"46", yearFrom=>"2005", description=>"" ) =head3 C<%imageoptions> HASH of (url, REF. HASH OF (optoin, REF. LIST OF value) ) C is given locally from the root directory, such as "Folder1/Subfolder/img001.jpg" An C corresponding to this C could be ( Keywords => [ "holiday" ], Locations => [ "Mallorca" ], Persons => [ "Anne Helene", "Jesper" ] ) =head3 C<%alloptions> HASH of (option, REF. LIST of values) Could be something like : ( Keywords => [ "beers", "holiday", "new wave", "silo falls over", "Anne Helene's 30 years birthday" ]m Locations => ..., Persons => ..., OtherCategory => ... ) =head3 C<%membergroups> membergroups are called categories depending on your version of Kimdaba. HASH : (Locations => REF (HASH : USA => [ Chicago, Los Angeles ] ) ) Beware, you can have loops between membergroups. =head3 C<%kimdabaconfig> HASH of (attributes, values) Fast all KimDaBa settings are stored in the index.xml file, as attribute of the "KimDaBA/config" XML element. So using this hash you can access many of the user preferences, for example it could be something like : ( viewSortTye=>>"0", passwd=>"", ensureImageWindowsOnScreen=>"1", viewerCacheSize=>"25", albumCategory=>"", showDrawings=>"1", htmlBaseURL=>"file:///home2/jmfayard/public_html", previewSize=>"256", thumbSize=>"64", displayLabels=>"1", launchViewerFullScreen=>"0", windowWidth-0=>"800", autoShowThumbnailView=>"0", showInfoBox=>"1", windowWidth-1=>"800", slideShowWidth_1280=>"600", viewerHeight_1280=>"450", fromDate=>"2005-01-01", htmlDestURL=>"file:///home2/jmfayard/public_html", trustTimeStamps=>"0", windowHeight-0=>"600", thumbNailBackgroundColor=>"#000000", windowHeight-1=>"600", slideShowInterval=>"5", toDate=>"2006-01-01", exclude=>"1", infoBoxPosition=>"6", locked=>"0", showDate=>"1", imageDirectory=>"/tmp/kimdaba-demo-jmfayard", searchForImagesOnStartup=>"1", autoSave=>"5", version=>"1", viewerWidth_1280=>"600", launchSlideShowFullScreen=>"0", showDescription=>"1", maxImages=>"100", useEXIFComments=>"1", useEXIFRotate=>"1", showTime=>"1", htmlBaseDir=>"/home2/jmfayard/public_html", slideShowHeight_1280=>"450", ) =head2 Fonctions =cut BEGIN { use Exporter (); our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS); $VERSION = 0.5; @ISA = qw(Exporter); @EXPORT = qw( %alloptions %kimdabaconfig %membergroups %imageoptions %imageattributes &printImage &getRootFolder &parseDB &matchAllOptions &matchAnyOption &letMeDraganddropPictures &askForPictures &makeKimFile ); %EXPORT_TAGS = ( ); # eg: TAG => [ qw!name1 name2! ], # your exported package globals go here, # as well as any optionally exported functions #@EXPORT_OK = qw(%imageoptions %imageattributes); } our @EXPORT_OK; # exported package globals go here our %imageattributes; our %imageoptions; our %alloptions; our %kimdabaconfig; our %membergroups; # non-exported package globals go here # initialize package globals, first exported ones %imageattributes=(); %imageoptions=(); %alloptions=(); %kimdabaconfig=(); %membergroups=(); # then the others (which are still accessible as $Some::Module::stuff) # all file-scoped lexicals must be created before # the functions below that use them. # file-private lexicals go here my $option="" ; my $image=""; # image element that we currently handle my $optionname=""; # currently handle option with name $option for image $image my @values=(); # currently found values my %alloptionshashed; my $folder; # I need it for askForPictures =head3 C<&letMeDraganddropPictures()> print "\n\n== Drag&Drop pictures from Kimdaba ==\n"; @ListOfPictures = &letMeDraganddropPictures(); Wait until the user drag and drop pictures from Kimdaba and Konqueror and return a list of url. =cut sub letMeDraganddropPictures { my @res=(); my $line=; chomp $line; # for pictures having a "'" in their filename $line=~s#'\\''#\\#g; my @a = ( $line =~ m/'[^']+'/g ); my $folder2 = $kimdabaconfig{"imageDirectory"}; foreach (@a) { # Change '/autre/Photos/USA/2004-08-09_Monument_Valley/Monument_Valley_05.JPG' # in USA/2004-08-09_Monument_Valley/Monument_Valley_05.JPG s#$folder##; s#$folder2##; # in case where $folder="." or similar s#(file:)?/+##; s#^'## ; s#'$## ; s#\\#'#g; push @res, $_; } # Now check which urls are really correct : @res =grep { exists $imageoptions{$_} } @res; return @res; } ########## =head3 C<< &matchAllOptions(HASH of (option => REF List of values)) >> Returns a list of urls. See the example in the synopsis. =head3 C<< &matchAnyOption(HASH of (option => REF List of values)) >> Returns a list of urls. See the example in the synopsis. =cut sub matchAllOptions { return @{ matchOptions( 1, @_) }; } sub matchAnyOption { return @{ matchOptions( 0, @_) }; } sub matchOptions { my ($matchall, %request)=@_; my @urlsfound=(); my @checkoptions=keys %request; URL: for my $url (keys %imageattributes) { my %options = (); %options= %{ $imageoptions{$url} } if (exists $imageoptions{$url} ); OPTION: for my $option ( @checkoptions ) { unless (exists $options{$option} ) { if ( scalar @{ $request{$option} } == 0 ) { next OPTION; } else { next URL; } } my @values_image =@{ $options{$option} }; # (Anne Helen) my @values_searched=@{ $request{$option} }; # (Jesper, Anne Helen) for my $req (@values_searched) { my $res = scalar grep { $_ eq $req } @values_image; if ( ($res == 0) && ($matchall) ) { next URL; } elsif ( ($res!=0) && (!$matchall) ) { # if trouvé, here next OPTION; } } # If we went this far, this means that we... if ($matchall) { next OPTION; # ... found a value corresponding to each of the options } else { next URL; # ... never found a value corresponding to one of the options } } push @urlsfound, $url; } return [ @urlsfound ]; } =head3 C<&getRootFolder()> my $folder=getRootFolder(); parseDB( "$folder" ); Returns the absolute path of the root directory. You should run the demo, keep the files, and use /tmp/kimdaba-demo-$USER when you are experimenting. Thanks to this function, the root directory can - passed as first argument on the command line ($ kim_script /tmp/kimdaba-demo-$USER ) - or will be asked to the user =head3 C<&parseDB( $folder )> Readonly access to most information available in the index.xml file. To modify a database, see B<&makeKimFile()> =cut sub getRootFolder { no warnings; ($folder) = grep { -d } @main::ARGV; $folder = "~/Images" unless (-d $folder ); $folder = $ENV{PWD} unless (-d $folder ); until ( (-d "$folder") && (-r "$folder/index.xml") ) { print "In which folder are your pictures stored ?\n"; chomp( $folder=); } return $folder; } =head3 C<&printImage( url )> printImage( $url ); Interesting to debug. Its code also shows how to access the hashes %imageattributes and %imageoptions : sub printImage { my ($file)= @_; print "=== $file ===\n" ; print "Attributes : "; my %attributes = %{ $imageattributes{$file} } ; while( my ($attr, $value) = each( %attributes ) ) { print " $attr=>$value ; "; } print "\n"; my %options = %{ $imageoptions{$file} }; print "Options: \n" ; while( my ($key, $r_values) = each( %options ) ) { print "\t$key ==> ", join('; ', @$r_values ) , "\n"; } print "\n"; } =cut sub printImage { my ($file)= @_; print "=== $file ===\n" ; print "Attributes : "; my %attributes = %{ $imageattributes{$file} } ; while( my ($attr, $value) = each( %attributes ) ) # for my $attr ( keys %{ $imageattributes{$file} } ) { print " $attr=>$value ; "; } print "\n"; my %options = %{ $imageoptions{$file} }; print "Options: \n" ; # for my $key ( sort keys %options ) while( my ($key, $r_values) = each( %options ) ) { print "\t$key ==> ", join('; ', @$r_values ) , "\n"; } print "\n"; } ##### ###### sub parseDB ($) { my ($folder)=@_; my $p1 = new XML::Parser( Style => 'Subs' ); croak "Can not find KimDaBa's database" unless (-r "$folder/index.xml"); $p1->parsefile( "$folder/index.xml"); } sub member { my ( $p, $el, %attrs ) = @_ ; my ($groupname,$member) = ( $attrs{"group-name"}, $attrs{"member"} ); # index.xml format has changed at Fri Dec 3 my $category=$attrs{"option-group"} if ( exists $attrs{"option-group"} ); $category=$attrs{"category"} if ( exists $attrs{"category"} ); if (! exists( $membergroups{$category} ) ) { $membergroups{$category} = { $groupname => [ $member ] }; } elsif (! exists( $membergroups{$category}{$groupname} ) ) { $membergroups{$category}{$groupname} = [ $member ] ; } else { my $r_list = $membergroups{$category}{$groupname}; push @$r_list, $member; } } sub config { my ( $p, $el, %attrs ) = @_ ; %kimdabaconfig=%attrs; } sub image { my ( $p, $el, %attrs ) = @_ ; $image = $attrs{"file"} ; $imageattributes{$image} = \%attrs; } sub image_ { my ( $p, $el ) = @_; $image = ""; } sub options { my ( $p, $el, %attrs ) = @_ ; return if ($image eq "") ; # We are in KimDaBa>config>SearchInfo>Options>Option # or in KimDaBa>Options $imageoptions{$image} = {} ; } sub option { my ( $p, $el, %attrs ) = @_ ; return if ($image eq "") ; # We are in KimDaBa>config>SearchInfo>Options>Option # or in KimDaBa>Options $optionname=$attrs{"name"}; @values=(); } sub option_ { my ( $p, $el ) = @_; if ($image eq "") { $optionname=""; return; } $imageoptions{$image}->{$optionname} = [ @values ]; $alloptionshashed{$optionname} = {} unless( exists $alloptionshashed{$optionname} ); for my $value (@values) { $alloptionshashed{$optionname}{$value}=1; } $optionname=""; } sub value { my ( $p, $el, %attrs ) = @_ ; return if ( $optionname eq "" ) ; push @values, $attrs{"value"}; } sub KimDaBa_ { # %alloptionshashed is hash of hash for efficiency reasons, but we want to return # a more clean hash of list. my $nb= scalar %alloptionshashed; for (keys %alloptionshashed) { $alloptions{ $_ } = [ keys %{ $alloptionshashed{$_} } ]; } } ##### ###### =head3 C<&makeKimFile( $destdir, $name, @list )> Instead of modifying directly the database (which could easily be dangerous for your data), you write a kimdaba export file (*.kim) then you use the import fonction in kimdaba (no dangerous, you are in control) A .kim file is a zip archive containning an index.xml file, and a Thumbnail directory. You just have to create the index.xml file (say in '/tmp') then you call : C where /tmp/index.xml is the file created by you /tmp/perl_output.kim is the resulting kimdaba import ile @ListOfPictures is a list of urls present in /tmp/index.xml Not that the KimDaBa import feature has some limitations. Example : use Image::Kimdaba; my @ListOfPictures; my $folder=getRootFolder(); parseDB( "$folder" ); print "\n\n== Drag&Drop pictures from Kimdaba ==\n"; @ListOfPictures=letMeDraganddropPictures(); print join("\n", sort(@ListOfPictures)); print "--\n"; my $destdir="/tmp"; open( EXPORT, "> ${destdir}/index.xml"); print EXPORT < FIN ; for my $url (@ListOfPictures) { my $description="yeah! I changed the description"; my $md5sum=""; if ( (exists $imageattributes{$url}) && (exists $imageattributes{$url}{'md5sum'}) && (! $imageattributes{$url}{'md5sum'} eq "") ) { $md5sum="md5sum=\"$imageattributes{$url}{'md5sum'}\" "; } my $value="Test Add Another Keyword"; print EXPORT < FIN ; } print EXPORT < FIN ; close( EXPORT ); makeKimFile( $destdir, "perl_export.kim", @ListOfPictures); =cut sub makeKimFile { my ($destdir,$name,@ListOfPictures)=@_; system( "rm -rf ${destdir}/Thumbnails" ); system( "mkdir -p ${destdir}/Thumbnails" ); for my $url (@ListOfPictures) { next unless( -e "${folder}/${url}" ); # ( my $dest = $url) =~ s#(.*)/(.*)#\2#; my ($dirname,$basename) = ( $url =~ m#(.*)/(.*)# ); my $thumb="${folder}/${dirname}/ThumbNails/" . "$kimdabaconfig{'thumbSize'}x$kimdabaconfig{'thumbSize'}" . "-$imageattributes{$url}{'angle'}" . "-$basename"; if (-e $thumb) { my $a=symlink $thumb, "${destdir}/Thumbnails/$basename"; next; } print "Creating thumbnail for $url...\n"; $url=~s/'/'\\''/g; $basename=~s/'/'\\''/g; system( "convert -size 128x128 '$folder/$url' -resize 128x128 '${destdir}/Thumbnails/$basename'" ); } chdir $destdir or croak "$!"; unlink $name; system( "zip", "-r", $name, "index.xml", "Thumbnails" ); print "KimDaBa export file created : ${destdir}/${name}\n"; } 1; # don't forget to return a true value from the file # Desactived for now, because it's rather pointless and adds a dependcy on Term::Readline ##sub askForPictures ##{ ## my @res; ## print < [ "Jesper" , "Anne Helene" ] ); ## \@res=matchAllOptions( "Persons" => [ "Jesper" ], "Locations" => [ "Mallorca" ]); ## \@res=matchAnyOption( "Keywords" => [ "ForMyScript" ] ); ## ##EOF ##; ## use Term::ReadLine; ## my $term = new Term::ReadLine 'Kimdaba Query'; ## my $prompt = "Kim> "; ## my $OUT = $term->OUT || \*STDOUT; ## ## while(1) ## { ## while ( defined (my $perlcode = $term->readline($prompt)) ) { ## my $res = eval ($perlcode); ## warn $@ if $@; ## print $OUT $res, "\n" unless $@; ## $term->addhistory($_) if /\S/; ## } ## ### Now check which urls are really correct : ## @res=grep { exists $imageoptions{$_} } @res; ## print "Following pictures were found\n"; ## print join("\n",@res),"\n\n"; ## print "Continue with those picutres? [yes]/no : "; ## $_=; ## /no/ or last; ## } ## return @res; ##} =head1 BUGS/CAVEATS/etc A lot ;-) =head1 AUTHOR Jean-Michel Fayard ; jmfayard{at}moufrei.de =head1 SEE ALSO B