# HG changeset patch # User Peter Gervai # Date 1224106136 -7200 # Node ID a84c32f131df8e73ba1f7ad15217ca068a88ec15 Import vendor version diff -r 000000000000 -r a84c32f131df CREDITS --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CREDITS Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,48 @@ +BINS Credits + +BINS is a modified version of SWIGS 0.1.1 +created by Jérôme SAUTRET + +bins-edit-gui was written by Mark W. Eichin . +joi template was writen by Joachim Kohlhammer +satyap template was writen by Satya . +petrus template was writen by Thus0 , based on +joi template. +marc template was writen by Marc Menem , based on +joi template. +mwolson template was writen by Michael Olson . +martin template was writen by Martin Pohlack , +based on marc template. + +German translation by Colin Marquardt +Polish translation by Grzegorz Borek +Italian translation by Lele Gaifax +Russian translation by Andrei Emeltchenko +Spanish translation by David Barroso +Traditional Chinese translation by Chris Chau +Esperanto translation by Pier Luigi Cinquantini +Finish translation by Ville Pohjanheimo +Japanese translation by Yoshinori Okuji +Dutch translation by Eelco Maljaars +Hungarian translation by Aurel Gabris +Catalan translation by Joan Antoja Sabin + +Thanks also to all the other contributors for their patchs, comments or +other contributions. + +Image & Exif tag descritions are from +- the _Description of Exif file format_ document + of TsuruZoh Tachibanaya (tsuruzoh @ ba.wakwak.com) + you can find here it here: + http://www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html +- _EXIF MakerNote of Canon_ from David Burren (db061 @ burren.cx) + you can find here it here: + http://www.burren.cx/david/canon.html +- and from the Image::Info documentation. + +SWIGS 0.1.1 Credits + +* David Guichard for bug fixes and cleanup for 0.1.1 release +* Brent Bryan (bryanba@whitman.edu) for design advice, html templates, + and debugging. And kicking my butt so I keep working on this. +* Initial code based on IDS 0.21 by John Moose (moosejc@muohio.edu) diff -r 000000000000 -r a84c32f131df ChangeLog --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ChangeLog Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,980 @@ +BINS Change Log -*- text -*- + +BINS 1.1.29 +----------- + +- A search engine has been added. It only woks on web browser +supporting javascript and DOM. It can be deactivated via the new +searchEngine parameter. It allows search on image description fields +set in the new searchFields parameter. Maximum results returned by the +search engine is set by the searchLimit parameter. +This adds a new dependency on Text::Unaccent. + +- Michael Olson's mwolson templates have been added. Michael can be +joined at . + +- Martin Pohlack's martin templates have been added. Martin can be +joined at . These templates are based +upon marc ones, in turn based on joi. Here are the modifications with +marc: + - fixed some bugs in the css + - more layout stuff done in css + - changed colors to grey-levels, which allows the viewer to + concentrate on the important parts, the images (if you don't like + it, you only have to change some lines in the css). + - some layout changes, links (next, prev, ...) have a fixed + position now, so you don't have to move the mouse if you want to + cycle through many images. + - use transparent pngs for the slide background in browser which + support it -> smoother slide corners (round corners are + oversampled, compare the gif and the png) + - Display the content of the jpeg-comment filed below the image + +- Fixed a bug when javaScriptPreloadImage was set to 1 : the next image +preloaded was always at maximum size. +Patch from Malcolm Parsons + +- Add support for jpegtran with MMX (libjpeg-mmx-progs). +Patch by Ludovic Rousseau . + +- Fix -f option so it can work with files given with a relative path. +Patch by Ludovic Rousseau . + +- Default template has been renamed to swigs, has it may not be the +default in the future (it uses tables and don't use CSS). + +- A tools directory has been added in the archive, containing the +small BINS related utilities. The new tools add_num_prefix, +remove_num_prefix and bins_addtext have been added. All is documented +on the web site. + +- A FAQ has been added. + +- BINS now has a page on gna!, see https://gna.org/projects/bins/ + +BINS 1.1.28 +----------- + +- Replaced parameter "enlarge" with "whenSrcSmaller" to dictate what to +do if the source image is smaller than the size of the generated +image. Fixed a bug that prevented enlarged image from actually being +generated. +Patch from Alexander Blazej . + +- Added new linkRelative parameter allow to use relative links if +linkInsteadOfCopy is set to 1. +Patch from Dan McMahill . + +- Transform functionality now allow perl code. A new dateString +parameter allow to specify the date string to be used (following +date(1)), introducing a dependency on Date::Parse. +Patch from Martin Michlmayr . + +- Handle buggy EXIF information in DateTimeOriginal. +Patch from Martin Michlmayr . + +- Fix on deExifyImages option. +Patch from Martin Michlmayr . + +- Fix encoding problem whith ISO 646 used by Solaris. +Patch from Martin Michlmayr . + +- Fix bug when source directories is a prefix of the destination one. +Patch from Pizza . + +- Make JPEG Comments available as image subtext. +Patch from Martin Pohlack . + +- Catalan translation (ca) has been added. +Thanks to Joan Antoja Sabin . + +- A CSS bug in marc template has been fixed. +Correction from Martin Pohlack . + +- The image details page, using the Joi template, now respects use of a +background image. +Fix by Alexander Blazej . + +- The image details page's "Album Tree" link is fixed. +Fix by Alexander Blazej . + +- Standardized indentation (4 columns). +Done by Alexander Blazej . + +- Default value of borderOnThumbnails has been set to 0. + +BINS 1.1.27 +----------- + +- Added new parameter borderOnThumbnails to change or remove the +border of the thumbnail's image in the thumbnails page, in pixels. + +- Exit if target_dir is in source_dir since it will generate an +infinite recursion. +Correction from Ludovic Rousseau . + +- Bug on tips popup when text contained double quotes has been corrected. +Patch from Arthur de Jong . + +- bugs on -e and -o custom options has been fixed. +Patch from Adam Lackorzynski . + +- Adding a check for Perl < 5.8 in bins_edit for utf-8 handling. +Correction from Robert Funnell . + +- Hungarian translation (hu) has been added. +Thanks to Aurel Gabris . + +- zh language code has been changed to zh_TW. + +- Correction in comments of binsrc. +Patch from Robert Funnell . + +- Added RCS directories in excludeDirs parameter of binsrc to shown +regexp use. +Suggestion from Ambrose Li . + +The following changes have been made in bins-edit-gui by Mark W. +Eichin : + +- Primitive "current album" editor. +- Force LATIN1 if 7-bit ANSI is selected. Add charmap override box if +either conversion fails. +- Free server-side pixmap correctly (oops.) +- Fix grey-out of filename box. +- more error checks. +- Update man page. + +- Also, shortcut for autofill has been corrected (now CTRL A). +Correction from Ivan Daou + +BINS 1.1.26 +----------- + +- Added new marc template from Marc Menem . +It is based on the joi template, so it has all of it's features. It +don't use any table for the positioning but only css: this allows the +number of images displayed on one line to scale according to the width +of the browser window. +http://quentinlepingouin.free.fr/album/apples-gal/ + +- Added new petrus template from Petrus . +It is just a modified version of the joi template with a different look : +http://www.campus.ecp.fr/~petrus/bins/index.html + +- Added support for more Exif tags. + +- Fix a divide by zero error crash, due to a corrupt image. +Patch from Phillip Cole . + +- Fix javascript errors occured when a singe quote was included in a filename. +Patch from Phillip Cole . + +- Fix bug on joi template : accesskey="t" was showing in the pages. +Patch from Gerard Gerritsen +and Phillip Cole . + +- Fix missing /IF directive in images pages on satyap template. +Patch from Guillaume Rousse . + + + +BINS 1.1.25 +----------- + +- The images in each directory can now be displayed in an arbitrary, +custom order. This is done via an optional file album.list (a slibling +file to album.xml) that lists the files in the order you want them +displayed. If album.list doesn't exist in a directory the sorting by +image name is done as usual. +Patch from Stefan Rueger . + +- Added hot keys (use with Alt key) : (N)ext, (P)revious, (U)p, +(H)ome, images (L)ist, (T)ree. + +- Added first, last, index, parent, up and index relation links in +thumbnail and image pages. They are used by Mozilla and Opera +navigation bar. + +- Added first and last links on thumbnail and image pages in default +templates. + +- Templates are now installed under /usr/local/share/bins, as data file, +instead of /etc as configuration files +Patch from Guillaume Rousse . + +- Fix bug on images list page for custom sizes (defined in description +image file). + +- Added BINS completion for bash. +Send by Guillaume Rousse . + +- Updated Spanish translation. +Send by David Barroso . + +- Updated Esperanto translation. +Send by Pier Luigi Cinquantini + +BINS 1.1.24 +----------- + +- Added anti_bins program, to create a clean image files tree for each +image size from a BINS album. + +- Added jpegProgressify config option. Now, generated jpeg can be +progressive (instead of baseline) using jpegtran. Usually progressive +jpegs save space, but by default it only makes them progressive if it +does save space. See bins man page for usage detail. +Patch from Bill Clarke . + +- When backgroundImage was empty, the background attribute on body was +not correct and thus produced 404 HTTP error on web server. +patch from David Pfitzner . + +- Use the more generic AddDefaultCharset directive (instead of +AddType) in the .htaccess file for setting HTTP headers encoding. + +- Corrected wrong orientation for value 3 (bot_right, 180° rotation). +Patch from Guillaume Rousse . + +- Added prev/next link element (to enable Mozilla's link prefetching +feature and site navigation toolbar) in joi templates . + +- Added JavaScript image pre-loading in satyap templates. + +- Esperanto translation (eo) has been added. +Thanks to Revuo + +- Finish translation (fi) has been added. +Thanks to Ville Pohjanheimo . + +- Russian translation (ru) has been updated. +Thanks to Andrei Emeltchenko + +- Italian translation (it) has been updated. +Thanks to Lele Gaifax . + +- Added BINS CVS Web interface : +http://kashmir.sautret.org/cgi-bin/viewcvs.cgi/bins + +BINS 1.1.23 +----------- + +- new parameter 'deExifyImages' which performs a Profile("*") call +just before the resizing. This removes all Exif information in resized +pictures and thus, saves some disk spaces and network +consumption. This is specially noticeable on thumbnails an images list +pages. +Patch from Leo Breebaart . + +- the "Unknown discipline ':utf8'" bug in Perl versions older than +5.8.0 has been corrected. + +- images were sometimes copied even if they were identical. This has +been corrected and thus, runtime speed has been improved (about 15-20% on my +album without regenerating images). +Patch from Stuffed Crust . + +- the ' (quote) character was not correctly encoded in URL. +Reported by Benjamin Wilbur . + +- corrects a bug where descriptions containing a ' (quote) were used +in javascript (like information popup window in joi templates). + +- added icons for custom huge picture size in joi templates +(see panoramas on +http://album.sautret.org/200_vacances/700_Irlande_2003/thumb5.html). + +- japanese translation (ja) has been added. +Thanks to Yoshinori Okuji + +- german translation (de) has been updated. +Thanks to Christian Bang . + +BINS 1.1.22 +----------- + +- Perl 5.8 UTF-8 file writting problem has been corrected. + +- htmldefaults option of HTML::Clean has been removed, because it +breaks UTF-8 strings in Perl 5.8 + +- added satyap style templates from Satya . + +- added Dutch translation (nl). +Thanks to Eelco Maljaars + + +BINS 1.1.21 +----------- + +- PNG images can now be used directly in the album (even if then are +still renamed to .jpg...). + +- File timestamps are now preserved when they are copied in the gallery. + +- bins_cleanupgallery script has been added. Use it to remove any unused +file in your HTML galleries. Run it without argument for usage +information. Note that this script is still experimental, so if it +performs wrong, just re-run bins to recreate erased files. +This program was written by Jochen Schaeuble . + +- default templates has been updated as follow : + + * link element was added in default templates to enable Mozilla's link + prefetching feature and site navigation toolbar. + (http://www.mozilla.org/projects/netlib/Link_Prefetching_FAQ.html) + Patch from Chris Croome. + + * a width problem was corrected in the "In this album" column and + the overall display now uses 90% of the screen. + Patch from Migrec. + + * imagePageCycling option now works in default templates. + Bug reported by Migrec. + +- joi templates have been updated by Joachim Kohlhammer as follow : + + * the full width of the browserwindow is used; the width of the + navigationbar is set to a fixed width + + * more colors and mouse-over-highlights + + * usage of custom stylesheets per subalbum (config option + customStyleSheet, works like the backgroundImage-option) + + * initial support for video, the number of media files is displayed + along with the number of images and subalbums + + * the Mozilla next/previous-buttons-navigation-collapse-bug is + fixed + + Note there are still some known problems with this version of the + joi templates : + * Konqueror displays the lower info-popup wrong + * Opera has problems with all (?) mouseover-popups + +- A mailing-list about BINS has been set up, thanks to Chris Croome. +See http://www.email-lists.org/mailman/listinfo/bins for information. + +BINS 1.1.20 +----------- + +- corrects bug with UTF-8 local encoding + +BINS 1.1.19 +----------- + +- added Traditional Chinese translation (zh). +Thanks to Chris Chau + +- es.mo (spanish messages file) was not included in the archive. + +BINS 1.1.18 +----------- + +- joi templates has been updated by Joachim Kohlhammer : + * The Albumtree works now correctly, even the thumbnail for the + root-album works, i.e. no more broken images. + * New icon for the albumtree. + * Redesign of the image-view. The icons moved to the left border. + * Leave was renamed to Home; the variable in the template is now called + HOME_LINK. The icon was changed to a little house. The old icon is + still included. + * If the option pathImgNum is set, the image-view shows + (Image/Imagecount) in the path. + * The path contains a icon for each level (corresponding to the type of + the level (albumtree, album, image)) if the variable pathShowIcon is + set to 1. + * In the Thumbnailview the bar with the next/prev-buttons shows only if + there are two or more pages. If the bar shows, it now has a grey + background. + * The "cycling" of the thumbnailpages and the imageviews can be disabled + by setting the options thumbnailPageCycling and imagePageCycling to 1. + +- If only one Thumbnailpage exists, the navigation bar shows +"Thumbnail Page" instead of "Thumbnail Page 1". +(from Joachim Kohlhammer's patch) + +- select LATIN1 as default encoding on systems lacking the locale +command (like *BSD systems) +(patch from Dan ). + +- added new parameter emptyAlbumDesc to get rid of the "No long/short +description available" message if no description was set. + +- added Spanish translation. +(thanks to David Barroso). + +BINS 1.1.17 +----------- + +- new parameter feedbackMail to add a link "Send Feedback" in the +pages (only used in the joi templates for now). + +- new parameter treePreview to add a the thumbnail album in the tree +page (only used in the joi templates for now). + +- new parameters backgroundImage & excludeBackgroundImage to use an +image as a wallpaper (only used in the joi templates for now). + +- joi templates have been updated, using above features. + +(templates and patch by Joachim Kohlhammer). + +- Russian translation has been updated. +(thanks to Andrei Emeltchenko). + +BINS 1.1.16 +----------- + +- static elements (icons, css, javascript, etc.) can now be used by +the templates, by using a static subdir in the templates directory +(see the joi templates). + +- joi templates has been added. It uses icons, css and javascript. See +http://album.sautret.org/300_lieux/500_Paris/index.html for an example +applied on some of the sub-albums of my main album. You can use it +with the templateStyle parameter in the binsrc or album.xml, or with +the -s command line parameter (see bins(1) man page). +(templates and patch by Joachim Kohlhammer). + +- new parameter homeURL has been added to link your home page to the +Leave button of the joi template. + +- javaScriptPreloadImage parameter has been renamed to +javaScriptPreloadThumbs. New javaScriptPreloadImage parameter can be +used to add some javascript code in image pages to preload the next +image of the same size when current one is loaded, to speed up the +album browsing. +(patch from David Panofsky). + +- added Russian translation. +(thanks to Andrei Emeltchenko). + +- Mandrake 9.0 and NetBSD packages are now available. Check the +download page. +(mdk rpm by Cédric Thevenet, +NetBSD package by Thomas Klausner <(wiz at netbsd dot org>) + +- install.sh script can now install BINS in specified directories. For +example, to install it in /opt/bins, use the following command : +PREFIX=/opt/bins install.sh + +BINS 1.1.15 +----------- + +- New parameter linkInsteadOfCopy has been added, to create a link to +the image in the destination directory instead of copying it, when +it's possible. +Patch from Vincent Bernat. + +- Correct a bug that crashed bins with Perl 5.8.0 +Patch from Marty Leisner + +- Include links for movie files (avi, mpeg and mov) in the navigation +bar of albums ("In this album" upper left box). +Patch from Vincent Cautaer. + +- Scale method (to created scaled pictures and thumbnails) can now be +chose with the new scaleMethod parameter. It can be either scale or +sample. sample is faster, scale is better. +Idea from Mark W. Eichin. + +- Don't perform rotation on files matching the regexp defined by the +new noRotation parameter (default to _Orig suffix). This can be used +in conjunction with scaleIfSameSize=0 and a scaled size of 100%x100% +to keep original pictures in your album. +Patch from Vincent Cautaer. + +- Correct a bad behavior with some little pictures when scaled sizes +uses mixed pixels and percentages. +Patch from Vincent Cautaer. + +- jpegtran can now be used with image names containing spaces. +Patch from Vincent Bernat. + +- Define $verbose earlier to avoid warning. +Patch from Vincent Bernat. + +- Chop local encoding to avoid carrier return. +Patch from Vincent Bernat. + +- A sample album.xml file is provided in the doc directory. Take a +look at it to see how you can customize a album. + +BINS 1.1.14 +----------- + +- Some image files and directories can now be excluded by setting some +regexp to excludeFiles and excludeDirs new parameters. excludeDirs is +set to ^CVS$ in default config, and thus, CVS subdirs aren't processed +by bins now. + +- HTML generation performances have been increased by using the +blind_cache parameter of HTML::Template. +Thanks to Mark Eichin for this one. + +- Corrected a bug that wrongly set width and height of thumbnails and +prevented Internet Explorer (at least version 5) to display them. + +- Changed the image template so that Internet Explorer can display the +title tooltip on the prev/next thumbnails (when thumbPrevNext is 1). + +- bins now process .thm (THuMbnail) files. Accroding to Mark Eichin, +Canon cameras that do movies generate mvi*.thm files which are really +small JPEGs with exif data. + +BINS 1.1.13 +----------- + +- It is now possible to use the parameter in picture +description files to have different scaled images number and scaled +sizes for pictures in the same album (for example, one can have three +scaled pictures, small, medium and big, for most of the images of an +album, and a fourth one, huge, for big panoramas). Some other +parameters, such as titleOnThumbnail, defaultSize or +thumbnailBackground, can now also be used on a per image basis. + +- A bug introduced in 1.1.10 version that caused scaleIfSameSize +parameter to be always 1 has been corrected. +Thanks to Mark Eichin for pointing out the problem +and to Dan (mcmahill @ mtl.mit.edu) and Kamil Iskra for the correction path. + +- jpegtran can now be used even if it cannot handle the same file in +input and output (this is the case for the jpegtran shipped with most +GNU/Linux distribution, except Debian). +Patch from Kamil Iskra. + +- Corrected encoding problem on creation date. + +BINS 1.1.12 +----------- + +- Sorting order for directories and/or pictures can now be reversed, +using the -r command line option or the reverseOrder parameter. +Patch from Christian Hoenig for the -r option. + +- A bug on automatic rotation of destination image when -o was used +has been corrected (width and height were inversed). + +- French translation has been corrected. + +BINS 1.1.11 +----------- + +- Some javascript code is now added in thumbnails pages to preload +thumbnails of the next page when current one is loaded, to speed up +the album browsing. This can be deactivated with the new +javaScriptPreloadImages parameter. + +- Generated HTML code is now cleaned up to reduce the size of pages +and thus, speed up browsing. This reduces the size of HTML BINS files +by about 30%. This uses the HTML::Clean(3) library (new +dependency). This can be deactivated with the new compactHTML +parameter. + +- Use of the jpegtran program is now deactivated in default config +(some versions fail to perform rotation correctly). A new parameter +rotateWithJpegtran has been added. Set it to 1 in binsrc to continue +to use jpegtran. + +- Added some non breakable spaces in HTML code. + +- Strip . (dots) in small size names when creating file names (this +caused problem with italian i18n). You may have to delete all your +generated HTML files before running bins on a old italian album to +clean it up. + +- Some minor bugs have been corrected. + +- French translation has been corrected. + +BINS 1.1.10 +----------- + +- Most of the formats that ImageMagick can handle are now supported by +BINS. See the @knownImageExtentions variable in the configuration +section in bins. + +- Italian i18n has been added. Thanks to Lele Gaifax (lele @ seldati.it). + +- added 'ignore' parameter, that works like the -i command line +options. + +- added -n command line option and 'hidden' parameter to prevent some +sub-album from being linked, in the same way -i and 'ignore' +works. This can be used to create private albums. + +- Remove link to details page if there is no data about the picture. + +- new parameter stripDirPrefix to strip numeric prefix in directories, +like the -p command line option. + +- Typos corrections (patch from Leo Breebaart). + +BINS 1.1.9 +---------- + +- Corrected bug which broke album thumbnails when using +include_images. +Patch from Mark W. Eichin. + +- Corrected bug that crashed bins when run on some particular images, +in particular grayscaled JPEG. + +- Corrected bug that crashed bins when run on some particular exif +structures images. +Patch from Klaus Ethgen. + +- Corrected problem with -v option that appeared in 1.1.8 version. + +- New 0.7 version of Mark W. Eichin's bins-edit-gui which corrects +some miscellaneous bugs. See RELEASE.gui file for more details. + +BINS 1.1.8 +---------- + +- Use of color styles to choose a set of colors via the colorStyle +parameter in the config/desc files or using the new -c command line +option. Color styles provided in the binsrc are blue (default one), +green, ivory and pink. + +- Get charset encoding from the system's locales, instead of the +ISO-8859-1 hardcoded value. + +- new -f command line option to use an alternate configuration file +instead of ~/.bins/binsrc. +(patch from Bill Carlson). + +- Correct a bug with read only images and scaleIfSameSize=0 (set write +permission on destination image after copying it). +Thanks to Mark W. Eichin for the bug report. + +- Correct some bugs with the -e option (editable albums). +(patch from Christian Hoenig). + +- Correct bugs with command lines parameters (command line options now +override config file parameters). + +- Use of verbose functions to display command line messages +(patch from Christian Hoenig). + +- Updated man page, thanks to Mark W. Eichin. + +- New 0.6 version of Mark W. Eichin's bins-edit-gui which adds +user-specified rotation to interface plus some other +improvements. See RELEASE.gui file for more details. + + +BINS 1.1.7 +---------- + +- Use of HTML:Template to manage HTML creation from template +files. Thus, the HTML code is totally separated from the Perl code. + +- new version 0.5 of bins-edit-gui by Mark W. Eichin, supporting i18n +via gettext (French translation is provided). See RELEASE.gui file +for more details. + + +BINS 1.1.6 +---------- + +- rotation can now be performed on destination images, thus, original +images can be preserved. This is controlled by the rotateImages +parameter. See the comment on it in the binsrc for more +information. Note that you may have to manually add this attribute in +the desc file of picture if the original image was already rotated by +previous version of BINS (or else, the original image will be rotated +each time you run BINS). + +- the priority attribute is added in the exif tag in XML desc files to +use value from desc file instead of exif data from image file +(normally, data from image file takes precedence on values from XML +desc file). In particular, this is used to not read the Orientation +tag in image file when the original is already rotated. + +- use of the title HTML attribute to show tooltips on prev +and next links (display title of picture), sizes (display long +names and resolution) and other links (misc help). + +- display the current album thumbnail in sub-albums page if it has +pictures, with links to the first thumbnail page. This is controlled +by the albumThumbInSubAlbumPage parameter (see binsrc file). + +- a bug on sample images with special characters in the file name has +been corrected. + +- man pages have been added. Thanks to Mark W. Eichin for writing +them. + +- new version 0.4 of bins-edit-gui by Mark W. Eichin, see RELEASE.gui +file for more details. + +BINS 1.1.5 +---------- + +- new bins-edit-gui program to set or edit description fields using a +GTK+ graphical interface. Thanks to Mark W. Eichin who wrote this +program. + +- an "ignore" directive for the album.xml has been added with a new -i +command line option, so that entire albums can be ignored (not indexed +and published) (patch from Rene Weber) + +- new -e option to add link to .xml description files in HTML album, +to ease the edition of description fields. (patch from Christian +Hoenig) + +- number, sizes and size names of scaled images can now be +personalized in config files and desc files (see comment in binsrc). + +- the default scaled image size (which is displayed when user clicks +directly on the thumbnail in the thumbnails page instead of one of the +size name) is configurable in the config files using the defaultSize +parameter. + +- check explicitly for -t directory existing (catches typos) (patch +from Mark W. Eichin). + +- clearer -h explanation of what -t does, and clarification of what -p +does. (patch from Mark W. Eichin) + +- use of generic exif tag "Software" and "Owner" instead of Canon +specific ones. (patch from Christian Hoenig) + +- transform the Apex Value to the exposure time in seconds for +ShutterSpeedValue. (patch from Christian Hoenig) + +- add empty description fields in the section when the +image description file is created to ease later edition with an text +editor (this controlled by the createEmptyDescFields parameter in the +configuration files). (patch from Christian Hoenig) + +- corrected a bug with the "-o copied" command line option. (patch +from Rene Weber) + +- the addition of "all thumbnails" pages for the parent directory and +sub-directories that have thumbnails that take more than page for +off-line browsing. This is controlled by the allThumbnailsPage +parameter and is an alpha feature and doesn't seems to works +properly. It is deactivated in the default config. (patch from Rene +Weber) + + +BINS 1.1.4 +---------- + +- strip control code characters in Exif tag name. Some DigiCam (at +least Kodak ones) puts some 0 code in their Exif tag names, which +crashed bins or bin_edit during the XML parsing. + + +BINS 1.1.3 +---------- + +- add a parameter to display thumbnails close to the previous and next +link at the bottom of the image page. Thanks to Rene Weber for the +patch. + +- new parameter templateStyle and command line switch -s to choose a +style of template. The only style provided for now is "default". If +anyone want to make some new ones, don't hesitate to contact me, I'm +so bad at HTML design... + +- new template footer.html to add a footer on all generated pages. +Thanks to Rene Weber for the patch. + +- add a parameter thumbnailBackground that allow to add a background +colour to the thumbnail's cell in the thumbnails page so that if the +top and bottom borders are wider than the image (for example, if it is +in portrait mode), instead of spilling over, there is a border around +the whole picture. Thanks to Rene Weber for the patch. + +- center the list of image size names under thumbnail. Thanks to Rene +Weber for the patch. + + +BINS 1.1.2 +---------- + +- use of configuration files /etc/bins/binsrc and ~/.bins/binsrc to +personalize BINS. + +- personalization parameters can be used in album or image description +files too. + +- new Exif tags for ISO, Exposure Program, Original Image Width and +Length, and Compression Quality (tested with Olympus camera). Thanks +to Rene Weber for the patch. + +- new install script. + +- BINS is now in Sid (Debian unstable version), thanks to Mark +W. Eichin. + +- some typos corrections (thanks to Mark W. Eichin and Rene Weber). + + +BINS 1.1.1 +---------- + +- automatically rotate picture if the Orientation EXIF tag is found +and the orientation isn't correct. This tag is set when you rotate +pictures on the DigiCam. For JPEG files, BINS use jpegtran if found, +otherwise use mogrify (form ImageMagick). + +- corrected bug in reading EXIF fields from picture file. + +- corrected bug on date format (gettext doesn't seems to work with +perl string containing backslash, if anyone has an idea...). For now, +default date format is set to French. You have to set it manually if +you want to change it : just edit the Transform field of the date +record in the %fields hash in the configuration of bins file (near the +beginning, read comment). + +- removed forgotten debug output. + + +BINS 1.1.0 +---------- + +- new details pages which displays all information available on file +and DigiCam settings for each picture, with tooltip explaining the +meaning of some fields. Additional information for Canon DigiCams are +provided. + +- customizable charset encoding, including full support for UTF-8 +(Unicode), with Generation of the Apache .htaccess file for correct +encoding charset in HTTP headers. + +- use XML as desc file format (with a utility to convert old .txt +format to new .xml format called bins_txt2xml). + +- all Exif information is now saved in the XML description file, +preventing they disappear when the image is modified. + +- ability to change background color (thanks to Christian Hoenig). + +- center links under thumbnails. + +- don't center decription fields under picture. + +- corrected bug on URL that wasn't correctly escaped in tree page and +for album thumbnails (thanks for Andrew Ruthven for this last one). + + +BINS 1.0.4 +---------- + +- use of the tags to show tooltips on short size names. + +- added a sample album.txt and album_fr.txt in the templates +directory. + +- complete the doc with use of album.txt. + +- complete the doc with names of the mandrake 8.1 & 8.2 packages to +install. (thanks to Cédric Thevenet) + +- display BINS version at bottom of each page. + +BINS 1.0.3 +---------- + +- Correct a bug with path of thumbnails in album page when an +album.txt is used + +- new command line option -d to control addition of EXIF tags in +description files + +- New parameter $defaultSize to turn thumbnails into a link + +- Use more appropriate alt on thumbnails + +- Work around for corrupt images that return a zero size (thanks to +Bill Carlson) + +BINS 1.0.2 +---------- + +- Correctly escape space and other odd characters in directory & file +names using URI:Escape. + +- correct a random bug that caused problems in HTML generation when +I18N is used. + +- added date of generation in HTML + +- added BINS version and CVS Id tag in generated HTML + +- some tables where empty when no description field exists (this is +not allowed by HTML 4.0) + +- fix some minor problems + +- some code clean up + +BINS 1.0.1 +---------- + +- added German (thanks to Colin Marquardt +and Polish translations (thanks to Grzegorz Borek +). + +BINS 1.0.0 (differences with SWIGS 0.1.1) +---------- + +- Possibility to display the title of each image on top of its +thumbnail. (see the $titleOnThumbnail parameter at the beginning of +the bins file) + +- Possibility to display the thumbnails in the image list page, if +$thumbnailInImageList parameter is set. Be warned that this may +produce a really loaded page if the album contains lots of pictures. +(see the $thumbnailInImageList parameter at the beginning of the bins +file) + +- Template modified & some little bugs corrected to produce some valid +HTML 4.0 Transitional pages, as checked by the WDG HTML Validator +(http://www.htmlhelp.com/tools/validator/source.html). + +- I18N, using Locale::gettext, to produce fully translated album +(French translation is provided). If Locale::gettext is not installed +on the running system, album will be generated in English. + +- Verbose command line option, with different levels, to see work +progression. + +- Use of a generic parameter to define the list of fields describing a +picture, with the possibility of reading the EXIF data structure (see +below). See the @fields structures in the bins file for details. + +- Possibility of reading the EXIF data structure found on some images +(usually, those produced by digital cameras) to fill automatically the +desired fields (date and time, for example). See the @fields +structures in the bins file for details. + +- bins_edit utility: it can be used to set or edit description fields +on several pictures in one command. + +- Size of scaled pictures can now be expressed in percentage of the +original picture, i.e. you can write + my @scaledWidths = ("640", "66%","100%"); + my @scaledHeights = ("480", "66%", "100%"); +and SWIGS generate 640 x 480, 1024 x 768 and 1600 x 1200 scaled +pictures from an 1600 x 1200 original picture. It is useful if your +pictures are not all of the same size. + +- Don't convert an image if it has already the right size, if +$scaleIfSameSize parameter is 0. In this case, the original picture is +just copied, instead of being rescaled to the same size with a new +JPEG quality. Thus, you can keep the original picture in the album +(otherwise, you loose conpression level for jpeg). + +- Don't generate scaled and thumbnails images if they already exists +and the original is older. This speed up regeneration of albums. + +- Use Image::Size insteed of loading an image to get its size. This +speed up regeneration of albums. diff -r 000000000000 -r a84c32f131df LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + 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. diff -r 000000000000 -r a84c32f131df Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,198 @@ + +# version of the package +VERSION=1.1.29 +# name of the tarball +TARBALL = bins-$(VERSION).tar +# files to put in the tarball +DIST = bins bins_edit bins-edit-gui bins-edit-gui.glade \ + install.sh binsrc tools/*_prefix tools/bins_cleanupgallery \ + tools/anti_bins tools/bins_txt2xml tools/bins_addtext \ + LICENSE README README.gui RELEASE.gui TODO ChangeLog CREDITS \ + doc/*.html doc/*.sgml doc/album.xml \ + doc/bins.1 doc/bins_edit.1 doc/bins-edit-gui.1 \ + doc/bins_man.html doc/bins_edit_man.html doc/bins-edit-gui_man.html \ + templates*/*.html templates*/*.js templates*/*.txt \ + templates*/static/*.* \ + bash_completion \ + intl/*.po \ + intl/generate_mo.sh \ + intl/fr.mo intl/pl.mo intl/de.mo intl/it.mo intl/ru.mo intl/es.mo \ + intl/zh_TW.mo intl/nl.mo intl/ja.mo intl/eo.mo intl/fi.mo intl/hu.mo \ + intl/ca.mo intl/gui-fr.mo + +# name of the dir in the tarball +DIR-DIST = bins-$(VERSION) + +# files to publish on the website +PUBLISHWWW = $(wildcard doc/*.html) +#PUBLISH = $(TARBALL).gz $(TARBALL).bz2 ChangeLog doc/index_fr.html doc/index.html + +# Files to put on donwload area +PUBLISHFILE = $(TARBALL).gz $(TARBALL).gz.sig $(TARBALL).bz2 $(TARBALL).bz2.sig ChangeLog + +# where to publish webfiles for plain website +PUBLISH-DEST = $(HOME)/public_html/perso/BINS + +# where to publish webfiles for plain gna +PUBLISHGNA-DEST = $(HOME)/src/gna/web/bins + +# where to publish download files +PUBLISHFILE-DEST = $(HOME)/src/gna/bins-download + + +PROC=SGML_CATALOG_FILES=/etc/sgml/catalog xsltproc --catalogs +STYLEDIR=www/xsl + +# End of configuration +######################################################################## + +# File to publish on plain web site (not gna) +PUBLISH = $(PUBLISHFILE) $(PUBLISHWWW) + +PUBLISH-TMP = $(PUBLISH:doc/%=%) +PUBLISH-TARGET = $(PUBLISH-TMP:%=$(PUBLISH-DEST)/%) + +PUBLISHGNA-TMP = $(PUBLISHWWW:doc/%=%) +PUBLISHGNA-TARGET = $(PUBLISHGNA:%=$(PUBLISHGNA-DEST)/%) + +PUBLISHFILE-TARGET = $(PUBLISHFILE:%=$(PUBLISHFILE-DEST)/%) + +.PHONY: all install tar publish filepublish wwwpublish upload test album demo famille clean cvs arch website + +all: + @echo "see README file to install bins" + @echo "publish upload clean" + +install: .install + +.install: $(TARBALL).gz + rm -Rf /tmp/bins* + tar -C /tmp -xzvf $(TARBALL).gz + sudo mount -o rw,remount /usr + yes|sudo sh /tmp/$(DIR-DIST)/install.sh + sudo mount -o ro,remount /usr || true + touch .install + +tar: $(TARBALL).gz + +#sudo cp /tmp/$(DIR-DIST)/intl/fr.mo /usr/local/share/locale/fr/LC_MESSAGES/bins.mo + +publish: filepublish wwwpublish gnapublish + +filepublish: $(PUBLISHFILE-TARGET) + +gnapublish: $(PUBLISHGNA-TARGET) www/xml/depends.tabular + cd www/xml && make publishgna + +wwwpublish: $(PUBLISH-TARGET) www/xml/depends.tabular + cd www/xml && make publish + +website: www/xml/depends.tabular + cd www/xml && make all + +www/xml/depends.tabular: + touch www/xml/depends.tabular + +release: upload repo fileupload gnaupload + +upload: publish + @if [ `hostname` != "ketama" ]; then\ + cp -afuv ~/public_html/perso/BINS/* ~/kashmir/public_html/perso/BINS ;\ + ssh kashmir 'PATH=$$PATH:~/bin update_www free' ;\ + ssh kashmir 'PATH=$$PATH:~/bin update_www libertysurf' ;\ + else\ + update_www tiscali ;\ + fi +# update_www free ;\ + +gnaupload: gnapublish + cd $(PUBLISHGNA-DEST) && cvs commit + +fileupload: filepublish + cd $(PUBLISHFILE-DEST) && rsync --delete -avr --rsh="ssh" . zubro@download.gna.org:/upload/bins || true + +test: install + time bins -p ~/share/pics/test ~/public_html/test + +album: install + time nice -14 bins -p ~/share/pics/album ~/public_html/album + +demo: + rsync --stats --progress -vac -e ssh ~/public_html/album/ kashmir:public_html/album/ +# time cp -afuv ~/public_html/album/.??* ~/public_html/album/* ~/kashmir/public_html/album +# time rsync -avu ~/public_html/album/ ~/kashmir/public_html/album + +dormans: .install + time nice -14 bins -p ~/share/pics/Dormans ~/kashmir/public_html/photos/Dormans + +famille: .install + time nice -14 bins -p ~/share/pics/famille ~/kashmir/public_html/photos/famille + +repo: cvs arch + +cvs: + rsync --stats --progress -vac -e ssh /home/jerome/share/cvs/ kashmir:/var/lib/cvs + +arch: + baz archive-mirror + +backup_album: + rsync --stats --progress -vac -e ssh /home/jerome/share/pics/album/ kashmir:share/pics/album + +clean: + rm -f *~ *.old *.tar */*~ .install *.sign *.tar *.tar.bz2 *.tar.gz + +#cp Makefile $(DIR-DIST) +#cp bins $(DIR-DIST)/bins.src +$(TARBALL): $(DIST) Makefile + make website + rm -Rf bins-$(VERSION) || true + mkdir $(DIR-DIST) + cp --parents $^ $(DIR-DIST) + cp www/xml/*.html www/xml/*.xml $(DIR-DIST)/doc + $(HOME)/bin/change 1.1.29 $(VERSION) $(DIR-DIST)/* + tar \ + --exclude=*~ \ + --exclude=*.old \ + --exclude=*.bak \ + --exclude=\#* \ + -cvf $(TARBALL) bins-$(VERSION) + rm -Rf $(DIR-DIST) + + +$(PUBLISH-DEST)/%.html : doc/%.html + cp $< $@ + +$(PUBLISH-DEST)/%: % + cp $< $@ + +$(PUBLISHGNA-DEST)/%.html : doc/%.html + cp $< $@ + +$(PUBLISHGNA-DEST)/%: % + cp $< $@ + +$(PUBLISHFILE-DEST)/%: % + cp $< $@ + +%.sig: % + gpg --yes --use-agent --detach-sign $< + +%_man.html: %.sgml + docbook2html -o /tmp/$(notdir $@) $< + mv /tmp/$(notdir $@)/index.html $@ + rmdir /tmp/$(notdir $@) + +%.1: %.sgml + docbook-to-man $< >$@ + +%.mo: %.po bins + ./intl/generate_mo.sh $< +# msgfmt -o $@ $< + +%.gz: % + gzip -c --best $< >$@ + +%.bz2: % + bzip2 -c $< >$@ + diff -r 000000000000 -r a84c32f131df README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,211 @@ +========================================================== +BINS Photo Album +Version 1.1.29 +Mar. 17, 2002 +http://bins.sautret.org/ +Jerome@Sautret.org +BINS is free software, licensed under the GNU GPL. See the +COPYING file for details. +========================================================== + + (Une documentation en français est disponible dans + doc/index_fr.html + ou en-ligne sur + http://bins.sautret.org/index_fr.html ) + +A complete documention in HTML format can be found in doc/index.html +or on-line on http://bins.sautret.org. See it for BINS installation +and usage. + +See also the bins(1), bins_edit(1) and bins-edit-gui(1) man pages. + +The aim of BINS is to generate HTML photo albums. +Some of the functionalities of BINS are: +- album can contains other albums (sub albums): the album + can have a tree structure ; +- generation of a thumbnail and of scaled images of each picture ; +- number and size of scaled pictures can be personalized, in pixels + or percentage of the original image ; +- several description fields (date, location, etc...) can be associated with + the pictures ; +- usethe EXIF data structure found on some JPEG (usually, those + produced by digital cameras) to fill automatically some fields (date and + time for example). + +You can see an example of an album generated by BINS at +http://album.sautret.org/ +This my personal album with french I18N. + +BINS (BINS Is Not SWIGS) is a modified version of SWIGS (Structured +Web Image Gallery System). I've made these modifications because I need +them. I tried to contact SWIGS author, but I've got no response, so I +decided to publish my modified version in case someone is interested. + +See ChangeLog file for differences between SWIGS and BINS. + +Migrating from BINS version earlier than 1.1.0 +============================================== + +A new XML format is use for pictures and albums description files in +BINS 1.1.0. There is an utility bins_txt2xml to convert from old +format to new one: + +WARNING: make a backup of your album before proceding to the +migration, just in case something goes wrong. + +Install the new bins and bins_edit program in tour path as explained +in the installation documentation. + +Run the bins_txt2xml with the source directory of the album as a parameter : +bins_txt2xml ~/album + +This will create .xml files for each .txt in your album and its +subalbums. This can take some time... + +You can then run the new bins to check if the new desc files are OK. + +When you are sure all is OK. You can delete the old .txt files: +find ~/album -name \*.txt -exec rm -f {} \; + + +License +======= + +BINS is free software, licensed under the GNU GPL. See COPYING file +for details. + +BINS is Copyright (c) 2001-2004 Jérôme Sautret (Jerome @ Sautret.org). + +Original SWIGS code is Copyright (c) 2000 Brendan McMahan + (mcmahahb @ whitman.edu). + +Initial code based on IDS 0.21 is Copyright (c) John Moose + (moosejc @ muohio.edu). + + +Original SWIGS README: +====================== +_______________________________________________________________________________ +SWIGS README, INSTALL, and DOCUMENTATION +version 0.1.1 +Dec. 31, 2000 +Brendan McMahan (mcmahahb@whitman.edu) +http://people.whitman.edu/~mcmahahb/projects/swigs.html +_______________________________________________________________________________ + +Document Contents: + Requirements + Installation and Usage Overview + SWIGS command line options + File Formats for Image Text Files + File Formats for Album Text Files + +Requirements: + * ImageMagick with PerlMagick (http://www.imagemagick.org/) + * the "Image::Size" Perl module (http://www.cpan.org/). + +Installation and Usage Overview: + 1 Decompress swigs.tar.gz into a directory, say ~/swigs/ + 2 To immediately test your installation, run + ./swigs.pl -t templates/ sample_input/ sample_output/ + 3 The default search location for templates is $HOME/.swigs_templates + You can copy the templates to that location so you don't have to + specificy the -t option everytime you run swigs. + 4 Put the images you want into a directory + (possibly including subdirectories), say ~/pics/ + 5 Edit the HTML templates in ~/swigs/templates/ if desired + 6 Create text files with annotations for each image and directory + if desired. For a description of the file formats, see below. + 7 run ./swigs.pl ~/pics/ ~/swigs-output/ + +SWIGS command line options: + +swigs.pl [-o [src]] [-t template_dir] source_dir target_dir + + -o Tells script to use only one copy of image, using html size specs + (height, width specs in the image tag) for scaled versions. The + default src of the single image is scaled. The possible values are: + "scaled" (make scaled copy of orig in target_dir hierarchy, + sized to max size). Default. + "copied" (copy orig to web dir) + "custom" (use copy if filesize < 1meg + resize, resave, if bigger than 1 meg) + + -t template_dir + directory where html templates are stored. If nothing is + specified, then the direcory is assumed to be templates/ + + -p If this option is given, then prefix ordering numbers on + directories are removed. For example, if one has directories + may, june, and august, they can be renamed 0_may, 1_june, and + 2_august and they will appear in the album in the correct order. + Procceeding numbers followed by an underscore are stripped. + + +File Formats for Image Text Files: + + * If the image is named foo.jpg (or foo.someothersupportedformat) + then the associated description file should be foo.jpg.txt. The + older format, foo.txt, is also still recognized, but should be + avoid do to possible conflicts if you hava a foo.jpg and a foo.png + in the same directory (for example). + + * Each starting or ending tag goes on its own line. Each block of + data between tags may take any number of lines. + + +Type a short title for the image. + + + +The event where the image was taken. + + + +The location where the image was taken. + + + +The names of the people in the image. Should be a comma separated +list without the word "and" to allow for easy parsing in future +versions that allow searching. For example: +Colin, Mike, Steph, Jeff, Marc + + + +The date and time the image was taken. + + + +A description of the image and any other information. + + +File Formats for Album Text Files: + * Each starting or ending tag goes on its own line. Each block of + data between tags may take any number of lines. + * The text file must be called album.txt + * One album.txt file may be included in each directory. + + +A short title for the album. If none is specified, the directory name +is used with underscores replaced by spaces. + + + +The name of an image (without any path information) in the directory +to use as a representative for the album. Only specified if there are +actually images in the directory. For example: +Image003.jpg + + + +A short description of the contents of the album. + + + +A longer description of the contents of the album. If none is +specified, the short description is used. + + + + diff -r 000000000000 -r a84c32f131df README.gui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.gui Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,45 @@ +bins-edit-gui -- graphical editor for BINS-format XML tag files + +Copyright 2002 Mark W. Eichin +The Herd of Kittens + +Licensed under the GPL; see COPYING. +-------------------------------------------------------------- +Perl packages used: + +Getopt::Long Gtk Gtk::GladeXML XML::DOM XML::XQL XML::XQL::DOM +XML::Writer Gtk::Gdk::ImlibImage Gnome Glade::PerlRun Text::Iconv +Image::Info + +-------------------------------------------------------------- +Debian packages used, beyond what bins already uses: + +libglade-perl +libgladexml-perl +libgnome-perl +libgtk-imlib-perl +libimage-info-perl +libparse-yapp-perl +libtext-iconv-perl +libxml-writer-perl +libxml-xql-perl +-------------------------------------------------------------- +I18N: +domain is "bins-edit-gui" +Don't bother marking or translating anything under $debug. +-------------------------------------------------------------- +Open bugs that bins-edit-gui works around: + +debian #147051: libglade0: i18n: CList titles not properly handled +debian #147341: gettext: xgettext -D breaks -j + +Bugs fixed/features added as a direct result of bins development: + +debian #147516: libjpeg-progs: output error message unclear +-------------------------------------------------------------- +Thanks to: + +Jérôme Sautret [for BINS itself, and for + encouraging my feedback, and for cleaning up the French] +Rene Weber [for being the first hint that anyone else cared + about the debian package] diff -r 000000000000 -r a84c32f131df RELEASE.gui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RELEASE.gui Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,31 @@ +# Release notes/log for bins-edit-gui +# cvs log -rrelease_thok_0_8:: for logs, or +# cvs diff -u -rrelease_thok_0_8 for full diffs + +20020611 - 0.9 - Fix grey-out of filename box. Force LATIN1 if 7-bit + ANSI is selected. Add charmap override box if either + conversion fails. + +20020607 - 0.8 - more error checks. Primitive "current album" editor. + Free server-side pixmap correctly (oops.) Update man page. + +20020602 - 0.7 - workaround non-debian LC_MESSAGES bug. Guess + encoding from locale. Add slider between tags and image + (which cleans up some other visual sizing bugs automatically.) + +20020521 - 0.6 - handle bad/missing filenames, filter out .xml files + from argument list, updated french translation. Add + user-specified rotation to interface; cleaned up "revert" as a + side effect. + +20020516 - 0.5 - i18n support, including an approximate french + translation. Added more list navigation, moved them all to a + separate menu. + +20020512 - 0.4 - Support EXIF rotation tag. Stopped using GtkFixed + and improved the space usage significantly, especially in full + screen mode. + +20020510 - 0.3 - First thok release. "real looking", including getopt + and about boxes; also has proper focus-row preservation and + properly frees previous images, and handles XML UTF correctly. diff -r 000000000000 -r a84c32f131df TODO --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/TODO Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,116 @@ +TODO file for BINS (in no particular order) +------------------ + +- fix date encoding + +- use chmod() and symlink() instead of system() + +- add author email in default templates. + +- include bins_cleanupgallery in bins program. + +- copy the origninal image to the destination album, with a link in +the HTML, if it is not already there (same source & destination size, +scaleIfSameSize=0 and no rotation). + +- link .avi files to .thm when possible. + +- rotate JPEG images with a size that is not multiple of the iMCU with +mogrify instead of jpegtran (and perhaps remove border on scaled +images to avoid such size problems). + +- display numbers of images for each album in tree page. + +- don't process empty dirs. + +- possibilty to have images and sub-album in top level album +(currently, the thumbnail of the top level album is not displayed). + +- possibility to put XML files in another location (so images from +CD-ROM can be used). + +- more sophisticated install procedure to not erase HTML templates +and config files + +- command line option to select XML encoding. + +- handle correctly date/time format using plxgettext from Mandrake +package: lacks -k option for now. + +- add long option names with getopt + +- select images to include in album by using contents of desc fields +(eg: album with all images where 'people' contains 'joe'). + +- add possibility to exclude directory (like CVS ones). + +- generic description tag in the album.xml, to be used with all image +in this album missing this tag. + +- blue bar on the left is too long if lot of thumbs pages + +- possibility to have albums in different langages + +- gzip .xml files (controlled by an option) + +- use of CSS + +- possibility to have parameter by album (in the album.xml) or by +image (in its .xml file) to permit different image sizes, for +example : almost done for some parameters + +- use of the JIT compiler for HTML:Template: impossible for now, due +to bugs in HTML::Template:JIT. Some characters ($, \, %, @) must be +escaped (see http://rt.cpan.org/NoAuth/Bug.html?id=349 for bug +report). + +**************************************************************************** + +- hotkeys : DONE + +- don't overwrite .htaccess file if it exists, but edit it : DONE + +- preserve timestamp on files using cp -p when possible : DONE + +- possiblity to have different number of scaled images in a album: DONE. + +- width and height are not inversed on rotation with -o copied: CORRECTED + +- parameter to choose to rotate the original or the destination image: DONE + +- attribute (in exif section) to tell BINS to not read EXIF +fields in picture but only in desc file: DONE + +- use the previous attibute to set the new orientation when bins +rotate the original image and get rid of the BinsRotated parameter in +Exif section: DONE + +- use of TITLE attibute on some links or image to give help as +tooltips (for image title, sizes, etc...): DONE + +- thumb of current album (if any) in subalbum page: DONE. + +- add date of generation in the HTML code: DONE. + +- use of a config file to store parameters (instead of putting them +directly at the beginning og the code): DONE + +- chmod u+w on destination image after a cp: done + +- guess local encoding from locales setting: done + +- correct bug with grayscales jpeg: done + +- use of color styles (to select a set of colors): done + +- process .tiff files, as well as other formats: done + +- screenshot of bins-edit-gui: done + +- strip any . in the short size name when creating file names + +- handle broken jpegtran + +- use of javascript to preload images of next thumnails page when +current one is loaded. + diff -r 000000000000 -r a84c32f131df bash_completion --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bash_completion Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,63 @@ +# bins completion for bash +#-*- mode: shell-script;-*- +_bins() +{ + local cur prev tempdir + + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + case "$prev" in + -f) + _filedir + return 0 + ;; + -o) + COMPREPLY=( $( compgen -W 'scaled copied custom' -- $cur ) ) + return 0 + ;; + -c) + conffile=$HOME/.bins/binsrc + for (( i=1; i < COMP_CWORD; i++ )); do + if [[ "${COMP_WORDS[i]}" == -f ]]; then + conffile=${COMP_WORDS[i+1]} + break + fi + done + COMPREPLY=( \ + $( awk -F'"' '/ section of the +# album or image description files (i.e. album.xml or +# .jpg.xml) when it makes sense. + +# note that all of these options are now documented in bins.sgml; any +# new options need corresponding new documentation. + +my %defaultConfig = + ( + homeURL => "/", # Set this to your home page. This is + # used for the leave button in some + # templates. + + feedbackMail => "", # Put here the mailaddress of the + # album-maintainer. If this is set, you + # will get a mail-icon in your views that + # links to this address. + + treePreview => 1, # If set to 1, preview-thumbnails will be + # showed in the album-tree-page. + + backgroundImage => "", # Set this to the image that should be displayed + # as the background of the album-pages. + # The Image will be copied to the static-files + # directory. + # The name should be unique for the entire album. + + customStyleSheet => "", # Set this to the name that should be used + # for the current album and its subalbums + # The Stylesheet will be copied to the + # static-files directory. + # The name should be unique for the entire album. + + dateString => "%c", # Specify the format of date strings; this + # accepts all formats supported by date(1). + + excludeBackgroundImage => 1, # If set to 1, the image with the name given + # in backgroundImage will be excluded from + # the current directory. + + addExifToDescFile => 1, # If set to 1, write exif data found in + # the image file to the image desc file. + + deExifyImages => 1, # If set to 1, do NOT copy exif data found + # in the source images to any of the generated + # resized images. Setting this option can yield + # significant space savings, especially for + # thumbnail and imagelist pages. + + createEmptyDescFields => 1,# If set to 1, add empty description + # fields in the section + # when the image description file is + # created to ease later edition with an + # text editor + + jpegQuality => 75, # Quality of scaled jpegs (lower number = more + # compression, lower quality) in 1-100 range. + + jpegProgressify => "smaller", # values: never, always, smaller. whether + # to make jpegs progressive using jpegtran + # (if available). smaller means only if + # the progressive file is smaller than the + # original + + titleOnThumbnail => 1, # Should the title be displayed on top on the + # thumbnail in the thumbnails page ? + + emptyAlbumDesc => 0, # If set to 1, and album desciption is + # not set, no message will be displayed + # (instead of the "No long/short + # description available" one). + + reverseOrder => 0, # are we reversing sorting order (see -r) + # (0=none,1=dirs,2=pix,3=both) + + defaultSize => 1, # Size to use when user clicks directly + # on the thumbnail in the thumbnails + # page instead of one of the size + # name. 0 is the first size (Small in + # the default config), 1 the second + # (Medium), and so on. Set this variable + # to -1 if you don't want the thumbnail + # to be clickable. + + thumbnailPageCycling => 1, # If set to 0 next/prev-Links will be hidden if + # the actual page is the last/first Thumbnailpage + + imagePageCycling => 1, # If set to 0 next/prev-Links will be hidden if + # the actual page is the last/first Imagepage + + pathImgNum => 1, # If set to 1 the path in the imageview contains + # the number of the current image + + pathShowIcon => 1, # If set to 1 the path contains icons + + thumbnailInImageList => 1, # Display thumbnails on the Image List page ? + + albumThumbInSubAlbumPage =>1, # If set to 1, display the current + # album thumbnail in sub-albums page + # if it has pictures, with links to + # the thumbnails page. + + allThumbnailsPage => 0, # If set to 1, generate a page with all + # thumbnails in the album and + # sub-albums. This is deactivated + # because it is an alpha feature which + # seems to not work properly. + + borderOnThumbnails =>0, # Width of the border of the thumbnail's + # image in the thumbnails page, in + # pixels. 0 means no border. + + thumbnailBackground => 0, # If 1, add a background colour to the + # thumbnail's cell in the thumbnails + # page so that if the top and bottom + # borders are wider than the image (for + # example, if it is in portrait mode), + # instead of spilling over, there is a + # border around the whole picture. + + thumbsPerRow => 4, # Number of thumbnails displayed in each + # row in an album. + + numThumbsPerPage => 16, # Number of thumbnails displayed + # in each page in an album. + + previewMaxHeight => 150, # Max Thumbnail Height. + + previewMaxWidth => 150, # Max Thumbnail Width. + + thumbPrevNext => 1, # If set to 1, display thumbnails close + # to the previous and next link at the + # bottom of the image page. + + rotateImages => "destination",# Do we rotate images if the Orientation + # Exif tag is found ? If set to + # 'original', the original image is + # rotated the first time, and then it is + # left untouched. If set to + # 'destination', this is all the scaled + # images and thumbnails that are + # rotated. This is less efficient, but + # the original images are preserved. If + # set to 'none', no rotation is + # performed. + + rotateWithJpegtran => 0, # If set to 1, bins try to use the + # jpegtran program to rotate JPEG images + # if it is available. jpegtran is faster + # and lossless, but some versions fail + # to perform rotation correctly, so it + # is deactivated in default config. If + # set to 0 or if jpegtran is not found, + # mogrify (from ImageMagick) is used. + + scaleIfSameSize => 0, # If set to 1, we scale the picture even + # if destination size is the same as the + # original picture, if set to 0, the + # original image is just copied if the + # size is correct. + + scaleMethod => "scale", # What method should be used to create + # scaled pictures and thumbnails ? Can + # be either scale or sample. sample is + # faster, scale is better. + + whenSrcSmaller => "enlarge", # What to do if the source image is smaller + # than the size of the generated image. + # If set to "enlarge" the generated image is + # enlarged to the requested size. + # If set to "original" the generate image is + # the same size as the original image. + # If set to "skip" do not generate any image. + + linkInsteadOfCopy => 0, # If set to 1, we link the picture instead + # of copying it if possible + # (i.e. scaleIfSameSize is set to 0 and + # destination image doesn't have to be + # rotated : rotateImages is set to + # original or none, or orientation is + # already correct). + + linkRelative => 1, # If set to 1, we use a relative path for the + # link + + updateOriginalPerms => 1, # attempt to update source image permissions + + maxAlbumsForLongSubAlbum => 20, # If the number of sub albums is greater, + # generate a short sub album page + # instead of the long one. + + stripDirPrefix => 0, # If set to 1, Numbers preceding + # the album title, followed by an + # underscore, are stripped. If this + # parameter is set, then prefix + # ordering numbers on directories + # are removed. For example, if one + # has directories may, june, and + # august, they can be renamed + # 0_may, 1_june, and 2_august and + # they will appear in the album in + # the correct order. This can be + # overridden by the-p command line + # option. + + compactHTML => 1, # If set to 1, generated + # HTML code is cleaned up to reduce + # the size of pages and thus, speed + # up browsing This reduces the size + # of HTML BINS files by about + # 30%. See HTML::Clean(3) to know + # how optimizations are performed. + + javaScriptPreloadImage => 1, # If set to 1, add some javascript + # code in image pages to preload + # the next image of the same size + # when current one is loaded, to + # speed up the album browsing. + + javaScriptPreloadThumbs => 1, # If set to 1, add some javascript + # code in thumbnails pages to + # preload thumbnails of the next + # page when current one is loaded, + # to speed up the album browsing. + + searchEngine => 1, # If 1, generate a search page. + # Images can be searched on + # description fields set in the + # searchFields parameter. + + searchFields => "title description people location event comment", + # Space separated list of + # description field names used by + # the search engine, if + # searchEngine is set to 1. + + searchLimit => 50, # Maximum results returned by the + # search engine. Note that if + # this number is too high, it can + # hang the browser. + + createHtaccess => 1, # If 1, create an Apache .htaccess file + # in the root dir of the album with the + # encoding charset bound to html and + # htm files + + noRotation => '_Orig$' , # Don't perform rotation on files + # matching this regexp + + excludeFiles => "" , # exclude image files that match + # this regexp (if set). + + excludeDirs => '^CVS$' , # exclude directories that match + # this regexp (if set). + + ignore => "", # Put here a comma separated list of keyword. If + # one on this keyword is found in the "ignore" + # field in the section of an + # sub-album.xml, then this sub-album will be + # ignored, i.e. it will not be processed. You can + # also use the -i command line option. + + hidden => "", # Put here a comma separated list of keyword. If + # one on this keyword is found in the "ignore" + # field in the section of an + # sub-album.xml, then this sub-album will be + # hidden, i.e. it will be generated but not linked + # anywhere. You can also use the -n command line + # option. + + colorStyle => "blue", # name of the color style to use + + templateStyle => "swigs", # name of the template style to use + + # The following parameters cannot be set in config files for now : + globalConfigDir => "/etc/bins", # System wide configuration directory + globalDataDir => "/usr/local/share/bins", # System wide data directory + userConfigDir => "~/.bins", # User configuration directory + configFileName => "binsrc", # Configuration file. + + htmlEncoding => "UTF-8", # HTML pages charset encoding + xmlEncoding => "UTF-8", # XML files charset encoding + defaultEncoding => "ISO-8859-1", # Charset encoding of your + # environment. This value + # is overridden by your + # real local encoding as + # reported by the 'locale + # charmap' unix command. + # This is used to display + # strings on console and to + # convert strings from .po + # files. + ); + +my $codeset; +eval { + require I18N::Langinfo; + I18N::Langinfo->import(qw(langinfo CODESET)); + $codeset = langinfo(CODESET()); +}; +# ANSI is unspeakably primitive, keep LATIN1 instead +# Solaris refers to ISO 646 as "646" but that is not a valid codeset +if (!$@ && $codeset && $codeset ne "ANSI_X3.4-1968" && $codeset ne "646") { + $defaultConfig{defaultEncoding} = $codeset; + beVerboseN("Forcing encoding to $codeset", 2); +} +my $local2htmlConverter; +$local2htmlConverter = Text::Iconv->new($defaultConfig{defaultEncoding}, + $defaultConfig{htmlEncoding}); + +# Here are set number, name and size of scaled images. +# This can be changed in the binsrc or album.xml files. +# By default, there is three sizes, but you can remove or add some new +# by editing the @scaledWidths, @scaledHeights, @sizeNames and +# @longSizeNames lists. + +# This is this size of each scaled picture : +my @scaledWidths = ("40%", "64%", "100%"); # Can be either a resolution or +my @scaledHeights = ("40%", "64%", "100%"); # a % of the original picture + +#my @scaledWidths = (200, 400, 600, 3000); +#my @scaledHeights = (200, 400, 600, 3000); + +#This is the name of each scaled picture. Remember that the _("") +#function is used for I18N only and is not mandatory. +my @sizeNames = (_("Sm"), _("Med"), _("Lg")); +my @longSizeNames = (_("Small"), _("Medium"), _("Large")); +my @fileSizeNames = ("small.png", "medium.png", "large.png", "huge.png"); +my @fileActiveSizeNames = ("smallActive.png", "mediumActive.png", + "largeActive.png", "hugeActive.png"); + +#my @sizeNames = (_("Sm"), _("Med"), _("Lg"), _("Hg")); +#my @longSizeNames = (_("Small"), _("Medium"), _("Large"), _("Huge")); + +$defaultConfig{scaledWidths} = \@scaledWidths; +$defaultConfig{scaledHeights} = \@scaledHeights; +$defaultConfig{sizeNames} = \@sizeNames; +$defaultConfig{longSizeNames} = \@longSizeNames; +$defaultConfig{fileSizeNames} = \@fileSizeNames; +$defaultConfig{fileActiveSizeNames} = \@fileActiveSizeNames; + +# Fields to display (in the list order) under the picture. These +# fields are defined in the getFields function below. +my @mainFields = ("description", "people", "location", "date", + "event", "comment"); + + +# Fields to display (in the list order) in the details page. These +# fields are in the getFields function below. +my @secondaryFields = ( + # DigiCam + _("BINS-SECTION DigiCam Info"), + "make", "model", "owner", "firmware", + # DigiCam settings for the image + _("BINS-SECTION DigiCam settings for the image"), + "artist", + "copyright", + "imagedescription", + "usercomment", + "canon_quality", + "canon_image_size", + "CanonContrast", + "CanonSaturation", + "CanonSharpness", + # DigiCam settings for the photo + _("BINS-SECTION DigiCam settings for the photo"), + "canon_easy_shooting_mode", + "canon_macro", + "lightsource", + "flash", + "canon_flash_mode", + "flashenergy", + "CanonISO", + "iso", + "exposure_time", + "exposure_prog", + "canon_digital_zoom", + "canon_focus_mode", + "CanonFocusType", + "subject_distance", + "metering_mode", + "focal_length", + "shutter_speed_value", + "brightness", + "aperture_value", + "max_aperture_value", + "fnumber", + "canon_timer_length", + "canon_continuous_drive_mode", + "focal_plane_x_resolution", + "focal_plane_y_resolution", + "orientation", + # Image characteristics + _("BINS-SECTION Image Characteristics"), + "date", + "comment", + "file_media_type", + "jpeg_type", + "interlace", + "color_type", + "samples_per_pixel", + "bits_per_sample", + "resolution", + "compression", + "usercomment", + "exif_version", + "image_width", + "image_length", + "compressed_bits", + "BINS-SECTION end", # close the last section + ); + +sub getFields { + # The key is the string used as the name in the picture + # description file. + # Name corresponds to the string displayed under the picture. + # EXIF is the name of the field in the EXIF structure + # found in some JPEG images. + # The value of the EXIF structure is only used if no value + # is present in the picture description file. + # Transform is a Perl operator used to convert an exif value + # to the desired format to display. It is evaluated as + # normal Perl code and the result has to be in $_. + my %fields = + ( + "title" => + { Name => _("Title"), + }, + "description" => + { Name => _("Description"), + }, + "people" => + { Name => _("People"), + }, + "location" => + { Name => _("Location"), + }, + "date" => + { Name => _("Date"), + EXIF => "DateTimeOriginal", + Transform => '$_ = local2html(strftime $configHash->{dateString}, gmtime str2time "$_") if str2time "$_"', + # Alternatively, you could use regex substitution: + # English version is yyyy:mm:dd hh:mm:ss to yyyy/mm/dd hh:mm:ss : + # Transform => 's%^(\\d+):(\\d+):(\\d+) (.*)$%\$1/\$2/\$3 \$4%', + # French version is yyyy:mm:dd hh:mm:ss to dd/mm/yyyy hh:mm:ss : + # Transform => 's%^(\d+):(\d+):(\d+) (.*)$%$3/$2/$1 $4%', + }, + + "event" => + { Name => _("Event"), + }, + "comment" => + { Name => _("Comment"), + EXIF => "Comment", + }, + "model" => + { Name => _("Equipment Model"), + EXIF => "Model", + }, + "make" => + { Name => _("Equipment Make"), + EXIF => "Make", + }, + "firmware" => + { Name => _("Software"), +# EXIF => "Canon-Tag-0x0007", + EXIF => "Software", + Tip => _("Firmware (internal software of device) version number."), + }, + "owner" => + { Name => _("Owner name"), +# EXIF => "Canon-Tag-0x0009", + EXIF => "Owner", + Tip => _("Name of the owner of the digicam."), + }, + "artist" => + { Name => _("Artist name"), + EXIF => "Artist", + Tip => _("Name of the camera owner, photographer or image creator."), + }, + "copyright" => + { Name => _("Copyright"), + EXIF => "Copyright", + Tip => _("Copyright information."), + }, + "flash" => + { Name => _("Flash"), + EXIF => "Flash", + }, + "usercomment" => + { Name => _("User comment"), + EXIF => "UserComment", + }, + "file_media_type" => + { Name => _("File Media Type"), + EXIF => "file_media_type", + Tip =>_("This is the MIME type that is appropriate for the given file format."), + }, + "color_type" => + { Name => _("Color Type"), + EXIF => "color_type", + }, + "jpeg_type" => + { Name => _("JPEG Type"), + EXIF => "JPEG_Type", + }, + "interlace" => + { Name => _("Interlace method"), + EXIF => "Interlace", + Tip => _("Interlace method used."), + }, + "metering_mode" => + { Name => _("Metering Mode"), + EXIF => "MeteringMode", + Tip => _("Exposure metering method."), + }, + "samples_per_pixel" => + { Name => _("Samples Per Pixel"), + EXIF => "SamplesPerPixel", + Tip => _("This says how many channels there are in the image. For some image formats this number might be higher than the number implied from the \"Color Type\""), + }, + "resolution" => + { Name => _("Physical Resolution"), + EXIF => "resolution", + Tip => _("The value of this field normally gives the physical size of the original image on screen or paper. When there is no unit then this field denotes the squareness of pixels in the image."), + }, + "compression" => + { Name => _("Compression Algorithm"), + EXIF => "Compression", + }, + "exif_version" => + { Name => _("Exif Version"), + EXIF => "ExifVersion", + }, + "subject_distance" => + { Name => _("Subject Distance"), + EXIF => "SubjectDistance", + Tip => _("Distance to focus point."), + }, + "bits_per_sample" => + { Name => _("Bits Per Sample"), + EXIF => "BitsPerSample", + Tip => _("This says how many bits are used to encode each of samples."), + }, + "exposure_time" => + { Name => _("Exposure Time"), + EXIF => "ExposureTime", + Tip => _("Exposure time (reciprocal of shutter speed)."), + }, + "shutter_speed_value" => + { Name => _("Shutter Speed Value"), + EXIF => "ShutterSpeedValue", + Tip => _("Shutter speed. The unit is the APEX (Additive System of Photographic Exposure) setting"), + }, + "brightness" => + { Name => _("Brightness"), + EXIF => "BrightnessValue", + Tip => _("The value of brightness. The unit is the APEX (Additive System of Photographic Exposure) value."), + }, + "focal_length" => + { Name => _("Focal Length"), + EXIF => "FocalLength", + Tip => _("Focal length of lens used to take image."), + }, + "aperture_value" => + { Name => _("Aperture Value"), + EXIF => "ApertureValue", + Tip => _("The actual aperture value of lens when the image was taken."), + }, + "max_aperture_value" => + { Name => _("Maximum Aperture Value"), + EXIF => "MaxApertureValue", + Tip => _("Maximum aperture value of lens."), + }, + "fnumber" => + { Name => _("F-Number"), + EXIF => "FNumber", + Tip => _("The actual F-number (F-stop) of lens when the image was taken."), + }, + "focal_plane_y_resolution" => + { Name => _("Focal Plane Y Resolution"), + EXIF => "FocalPlaneYResolution", + Tip => _("Pixel density at CCD's position. If you have MegaPixel digicam and take a picture by lower resolution (e.g.VGA mode), this value is re-sampled by picture resolution. In such case, Focal Plane Y Resolution is not same as CCD's actual resolution."), + }, + "focal_plane_x_resolution" => + { Name => _("Focal Plane X Resolution"), + EXIF => "FocalPlaneXResolution", + Tip => _("Pixel density at CCD's position. If you have MegaPixel digicam and take a picture by lower resolution (e.g.VGA mode), this value is re-sampled by picture resolution. In such case, Focal Plane X Resolution is not same as CCD's actual resolution."), + }, + "canon_macro" => + { Name => _("Macro"), + EXIF => "CanonMacro", + #Tip => _(""), + }, + "canon_timer_length" => + { Name => _("Timer Length"), + EXIF => "CanonTimerLength", + #Tip => _(""), + }, + "canon_quality" => + { Name => _("Quality"), + EXIF => "CanonQuality", + #Tip => _(""), + }, + "canon_continuous_drive_mode" => + { Name => _("Continuous Drive Mode"), + EXIF => "CanonContinuousDriveMode", + #Tip => _(""), + }, + "canon_flash_mode" => + { Name => _("Flash Mode"), + EXIF => "CanonFlashMode", + #Tip => _(""), + }, + "canon_focus_mode" => + { Name => _("Focus Mode"), + EXIF => "CanonFocusMode", + #Tip => _(""), + }, + "canon_image_size" => + { Name => _("Image Size"), + EXIF => "CanonImageSize", + #Tip => _(""), + }, + "canon_digital_zoom" => + { Name => _("Digital Zoom"), + EXIF => "CanonDigitalZoom", + #Tip => _(""), + }, + "canon_easy_shooting_mode" => + { Name => _("Easy Shooting Mode"), + EXIF => "CanonEasyShootingMode", + #Tip => _(""), + }, + "CanonContrast" => + { Name => _("Contrast"), + EXIF => "CanonContrast", + #Tip => _(""), + }, + "CanonSaturation" => + { Name => _("Saturation"), + EXIF => "CanonSaturation", + #Tip => _(""), + }, + "CanonSharpness" => + { Name => _("Sharpness"), + EXIF => "CanonSharpness", + #Tip => _(""), + }, + "CanonISO" => + { Name => _("ISO"), + EXIF => "CanonISO", + #Tip => _(""), + }, + "iso" => + { Name => _("ISO"), + EXIF => "ISOSpeedRatings", + Tip => _("ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232."), + }, + "CanonFocusType" => + { Name => _("Focus Type"), + EXIF => "CanonFocusType", + #Tip => _(""), + }, + "exposure_prog" => + { Name => _("Exposure Program"), + EXIF => "ExposureProgram", + Tip => _("The class of the program used by the camera to set exposure when the picture is taken."), + }, + "image_width" => + { Name => _("Original Image Width"), + EXIF => "ExifImageWidth", + #Tip => _(""), + }, + "image_length" => + { Name => _("Original Image Length"), + EXIF => "ExifImageLength", + #Tip => _(""), + }, + "compressed_bits" => + { Name => _("Compression Quality"), + EXIF => "CompressedBitsPerPixel", + #Tip => _(""), + }, + "Orientation" => + { Name => _("Orientation"), + EXIF => "Orientation", + #Tip => _(""), + }, + "lightsource" => + { Name => _("Light Source"), + EXIF => "LightSource", + Tip => _("The kind of light source."), + }, + "usercomment" => + { Name => _("User Comment"), + EXIF => "UserComment", + Tip => _("Keywords or comments on the image."), + }, + "imagedescription" => + { Name => _("Image Description"), + EXIF => "ImageDescription", + #Tip => _(""), + }, + "flashenergy" => + { Name => _("Flash Energy"), + EXIF => "FlashEnergy", + Tip => _("Indicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds (BCPS)."), + }, + "sensingmethod" => + { Name => _("Sensing Method"), + EXIF => "SensingMethod", + Tip => _("Indicates the image sensor type on the camera or input device."), + }, + ); + return \%fields; +} + +my @priorityExifTags = (); # the field in this list are taken from +# the desc file, even if they are present +# in image file (normally, image field +# takes precedence on desc file + +# these substitutions are made in all templates, useful for doing easy +# color assignments, etc. The colors can be set in the bins/colors +# section of config files or album/pictures desc files. +my %colorsSubs = (blue => + { PAGE_BACKCOLOR => "#FFFFFF", + PAGE_TITLECOLOR => "#000000", + MAINBAR_BACKCOLOR => "#000077", + MAINBAR_TITLECOLOR => "#FFFFFF", + MAINBAR_LINKCOLOR => "#eedd82", + MAINBAR_CURRENTPAGECOLOR => "#d2d2d2", + SUBBAR_BACKCOLOR => "#6060af", + SUBBAR_LINKCOLOR => "#eedd82", + SUBBAR_CURRENTPAGECOLOR => "#000000", + SUBBAR_TITLECOLOR => "#FFFFFF", + }); +$defaultConfig{colorsSubs} = \%colorsSubs; + +sub getIntlSubs{ + my $configHash = shift; + # Strings to translate in the HTML template pages (if I18N is used) + my %intlSubs = ( STRING_THUMBNAILS => _("thumbnails"), + STRING_IMAGELIST => _("Image List"), + STRING_HOME => _("Home"), + STRING_ALBUM => _("Album"), + STRING_UP => _("Up one album"), + STRING_PREV => _("previous"), + STRING_NEXT => _("next"), + STRING_FIRST => _("first"), + STRING_LAST => _("last"), + STRING_SUBALBUMS => _("Sub Albums"), + STRING_INTHISALBUM => _("In This Album"), + STRING_BACK => _("Back"), + STRING_BACKTOTHEIMAGE => _("Back to the image"), + STRING_IMAGE => _("Image"), + STRING_TREE => _("tree"), + STRING_ALBUMTREE => _("Album Tree"), + STRING_ALBUMGENERATEDBY => _("Album generated by"), + STRING_FEEDBACK => _("Send Feedback"), + STRING_YOURIMAGE => _("Your Image"), + STRING_YOURALBUM => _("Your Album"), + STRING_IMAGESEARCH => _("Image Search"), + STRING_SEARCHKEYWORDS => _("Search keywords"), + STRING_SEARCHONLYWHOLEWORDS => _("Search only whole words"), + STRING_SEARCHJSERROR => _("ERROR: your browser must have Javascript enable in order to use the search feature"), + STRING_SEARCH => _("search"), + STRING_SEARCHTITLE => _("Search a picture in all albums"), + STRING_TOOMANYRESULTS => gettext("Too many results, only the first %d are displayed. Try to refine your search."), + STRING_NODOM => gettext("Your browser doesn't support the Level 1 DOM"), + STRING_SEARCHCOMPLETED => gettext("Search completed: %d found"), + BINS_VERSION => "1.1.29", + ENCODING => $defaultConfig{htmlEncoding}, + GENERATED_DATE => _("on "). + local2html(strftime($configHash->{dateString}, + localtime)), + BINS_ID => + '', + ); + return \%intlSubs; +} + +# @knownImageExtentions defines file extensions that BINS can handle as +# input image. BINS _should_ handle all input format of ImageMagick +# (see ImageMagick(1) man page), but there is some formats that cause +# problems. If you have tested successfully a format that is not +# listed here, or if you have a problem with one of the following +# format, please let me know. +my @knownImageExtentions = ("jpg", "jpeg", "gif", "png", "tiff", + "bmp", "tga", "ps", "eps", "fit", "pcx", + "miff", "pix", "pnm", "rgb", "im1", "xcf", + "xwd", "xpm", "avs", "dcm", "dcx", "dib", + "dps", "dpx", "epdf", "epi", "ept", "fpx", + "icb", "mat", "mtv", "pbm", "pcd", "pct", + "pdb", "ppm", "ptif", "pwp", "ras", "thm", + ); + +my @webFormats = ("JPEG", "GIF", "PNG"); # Image formats that can go +# into the web album (other +# formats will be converted +# to JPEG). + +my @filesToLinkExtensions = ( "avi", "mpg", "mpeg", "mov" ); + +############################################################################ +# End of Configuration Section # +############################################################################ + + +#subroutine declarations + +sub usage; + +sub beVerbose; +sub beVerboseN; +sub min; +sub readConfigFile; +sub fileSize; +sub generateAlbumPages; +sub filenameToPreviewName; +sub getSizeLinks; + +sub openTemplate; +sub doSubstitutions; +sub generateTreeLoop; +sub generateImage; +sub getRootDir; +#sub generateScaledImage; +sub getDesc; +sub getExif; +sub readField; +sub trimWhiteSpace; +sub stringToBool; +sub ignoreSet; +sub getFields; +sub getIntlSubs; + +sub generateThumbnailPages; +sub generateThumbEntry; +sub generateThumbPage; +sub generateImagesInAlbum; + +sub write_htaccess; + +print "\nBINS Photo Album 1.1.29 (http://bins.sautret.org/)\n"; +print "Copyright © 2001-2004 Jérôme Sautret (Jerome\@Sautret.org)\n"; +print "Some parts of code:\n"; +print "Copyright © 2000 Brendan McMahan (mcmahahb\@whitman.edu)\n"; +print "Copyright © John Moose (moosejc\@muohio.edu)\n\n"; +print "This is free software with ABSOLUTELY NO WARRANTY.\n"; +print "See COPYING file for details.\n\n"; + +# EVG (Evil Global Variables) +# Some on them should be moved to the config hash so they can be +# managed by config files +my $ignoreOpts=""; # to ignore some albums (see -i) +my $hiddenOpts=""; # to hide some albums +my $genEditableAlbum; # are we creating a editable album (see -e) +my ($imageSource, $oneCopy); # How to handle scaled images +my $appendToDescFile; # write to desc file ? (see -d) +my $templateDir; +my ($picdir, $albumdir); # source and destination directories + +# charset converters +my ($xml2htmlConverter, $html2xmlConverter, $xml2localConverter); +my $optimizeConversion = 0; # this cause problem if set to 1, +# but may help on Sun Solaris. + + +main(); + +# process command line arguments before reading config file +sub preProcessArgs { + my $configHash = shift; + + # process args + my %option; + Getopt::Long::Configure("bundling"); + die "Invalid options\n" + if (!GetOptions(\%option, "h", "p", "r:s", "e", "o:s", "t=s", "d=s", "s=s", + "c=s", "v:i", "i=s", "n=s", "f=s")); + + if (defined($option{v})) { + $verbose = $option{v}; + } + beVerboseN("Verbosity level is set to $verbose.", 2); + + #get the config file from the command line + if ( $option{'f'} ) { + (my $junk ,$defaultConfig{'userConfigDir'},$defaultConfig{'configFileName'}) + = File::Spec->splitpath(File::Spec->rel2abs($option{'f'})); + $defaultConfig{'userConfigDir'} =~ s|/*$||; + } + + return \%option; +} + +# process command line arguments after reading config file +sub postProcessArgs { + my $option = shift; + my $configHash = shift; + + my %option = %{$option}; + + if (defined($option{i})) { + $ignoreOpts="$option{i}"; + beVerboseN("Ignore is set to $ignoreOpts.", 2); + } + + if (defined($option{n})) { + $hiddenOpts="$option{n}"; + beVerboseN("Hidden is set to $hiddenOpts.", 2); + } + + $genEditableAlbum = ($option{e} ? 1 : 0); + + if (defined($option{s})) { + $configHash->{templateStyle} = $option{s}; + } + $imageSource = "scaled"; + $oneCopy = 0; + if (defined($option{o})) { + $oneCopy = 1; + $imageSource = ($option{o} ? $option{o} : "scaled"); + } + + if (defined($option{d})) { + $appendToDescFile = $option{d}; + } else { + $appendToDescFile = "always"; + } + + die 'invalid option for switch -d. Must be one of "always",'. + '"never" or "exist"' + if ( $appendToDescFile ne "always" && $appendToDescFile ne "never" && + $appendToDescFile ne "exist"); + + if ($option{t}) { + $templateDir = $option{t}; + die "template location $templateDir doesn't exist" if (! -d $templateDir); + #if ( substr($templateDir,-1,1) eq "/" ) { + # $templateDir .= "/"; + } + + if ($option{c}) { + $configHash->{colorStyle} = $option{c}; + beVerboseN("Color style $option{c} selected.", 2); + } + + $configHash->{reverseOrder} ||= 0; + if (defined($option{r})) { + if ($option{r} =~ "dirs") { + $configHash->{reverseOrder} = 1; + } + if ($option{r} =~ "pictures") { + $configHash->{reverseOrder} += 2; + } + } + + #$stripOrderNum = ($option{p} ? 1 : 0); + if ($option{p}) { + $configHash->{stripDirPrefix} = 1; + } + my $printHelp = ($option{h} ? 1 : 0); + if ($printHelp) { + usage(); + exit 1; + } + + if ( $oneCopy && !($imageSource =~ /^(scaled|copied|custom)$/) ) { + print "\nInvalid image source for -o. If you are leaving the src\n"; + print "argument off to get the default, put the -o switch at the\n"; + print "end of the line, or use \"-o -\" to get the default.\n\n"; + usage(); + exit 1; + } + + # directories + if ($#ARGV < 1) { + print "source_dir and target_dir are required.\n"; + print "Type bins -h for more help.\n"; + exit 1; + } + + $picdir = $ARGV[0]; + $albumdir = $ARGV[1]; + + { + # check that $albumdir is _not_ a subdirectory of $picdir + # to avoid an infinite recursivity + + # we can't just use $albumdir and $picdir since they may contain + # "../" that will pose problems: + # foo/../bar would be considered a subdirectory of foo/ + + my $pwd = File::Spec->rel2abs("."); + #print "pwd: $pwd\n"; + + chdir($picdir) or warn "Can't chdir $picdir: $!"; + my $picdir2 = File::Spec->rel2abs("."); + chdir($pwd); + + if (! -d "$albumdir") { + mkdir $albumdir, 0755 + or die("\nCannot create $albumdir: $?"); + } + chdir($albumdir) or warn "Can't chdir $albumdir: $!"; + my $albumdir2 = File::Spec->rel2abs("."); + chdir($pwd); + + #print "picdir: $picdir2\n"; + #print "albumdir: $albumdir2\n"; + + die "albumdir_dir ($albumdir) can't be a subdirectory of picdir_dir ($picdir)" + if ($albumdir2 =~ m/^$picdir2\//); + } + + $picdir = File::Spec->rel2abs(File::Spec->canonpath($picdir)); + $albumdir = File::Spec->rel2abs(File::Spec->canonpath($albumdir)); + + $picdir =~ s|/*$|/|; + $albumdir =~ s|/*$|/|; + + die "You must specify a source (picture) directory.\n" if (!$picdir); + die "You must specify a target (web) directory.\n" if (!$albumdir); + + #print "\$oneCopy = $oneCopy\n"; + #print "\$imageSource = $imageSource\n" if $oneCopy; + #print "\$templateDir = $templateDir\n"; + #print "\$picdir = $picdir\n"; + #print "\$albumdir = $albumdir\n"; + +} + +# figure out a relative path from 1 file to another for relative linking +sub relpath { + my $p1 = $_[0]; + my $p2 = $_[1]; + + my $l1 = length($p1); + my $l2 = length($p2); + + my $cl = cls($p1, $p2); + + my $p2r = substr( $p2, $cl, $l2); + + my $p1dc = 0; + my $i = 0; + + #print "relpath($p1, $p2)\n"; + #print " \$p2r = $p2r\n"; + for ( $i=$l1; $i > $cl; $i--) { + if ( substr($p1,$i,1) eq "/" ) { $p1dc++; } + } + + for( $i = 0 ; $i < $p1dc ; $i++ ) { + $p2r = "../" . $p2r; + } + + #print "-> $p2r\n"; + return $p2r; +} + +# taken directly from +# http://www.people.cornell.edu/pages/tco2/papers/common_leading_substring/2000-06-09-0830 +# which is a paper by +# Todd Olson +# Cornell University +# tco2 at cornell.edu +sub cls { + my $l0 = length($_[0]); + my $l1 = length($_[1]); + + my $l = ($l0 < $l1) ? $l0 : $l1; + + my $i = 0; + + for ( $i=0; $i < $l; $i++) { + last if ( substr($_[0],$i,1) ne substr($_[1],$i,1) ); + } + return $i; +} + + +sub initConverters { + if (! $optimizeConversion || + $defaultConfig{xmlEncoding} ne $defaultConfig{htmlEncoding}) { + $xml2htmlConverter = Text::Iconv->new($defaultConfig{xmlEncoding}, + $defaultConfig{htmlEncoding}); + $html2xmlConverter = Text::Iconv->new($defaultConfig{htmlEncoding}, + $defaultConfig{xmlEncoding}); + } + if ($verbose >= 1 && (!$optimizeConversion || + $defaultConfig{xmlEncoding} ne + $defaultConfig{defaultEncoding})) { + $xml2localConverter = Text::Iconv->new($defaultConfig{xmlEncoding}, + $defaultConfig{defaultEncoding}); + } + + beVerboseN("Your system charset encoding is ". + $defaultConfig{defaultEncoding}, 2); +} + +# Convert from XML encoding to HTML encoding +sub xml2html{ + if ($optimizeConversion && + $defaultConfig{xmlEncoding} eq $defaultConfig{htmlEncoding}){ + return shift; + } + return $xml2htmlConverter->convert(shift); +} +# Convert from HTML encoding to XML encoding +sub html2xml{ + if ($optimizeConversion && + $defaultConfig{xmlEncoding} eq $defaultConfig{htmlEncoding}){ + return shift; + } + return $html2xmlConverter->convert(shift); +} +# Convert from XML encoding to local encoding +sub xml2local{ + if ($optimizeConversion && + $defaultConfig{xmlEncoding} eq $defaultConfig{defaultEncoding}){ + return shift; + } + return $xml2localConverter->convert(shift); +} +# Convert from local encoding to XML encoding +sub local2html{ + if ($optimizeConversion && + $defaultConfig{htmlEncoding} eq $defaultConfig{defaultEncoding}){ + return shift; + } + return $local2htmlConverter->convert(shift); +} + +##### main ##### +sub main{ + my @recursiveImageData; + + # create charset converters + initConverters(); + + # pre process command line args before reading config files + my $options = preProcessArgs(\%defaultConfig); + + # read configurations files + my $defaultConfig = readConfigFile(\%defaultConfig); + + # post process command line args after reading config files + postProcessArgs($options, $defaultConfig); + + # Create the Apache .htaccess for charset encoding + write_htaccess($albumdir, $defaultConfig); + + # write files used by the search engine + writeSearchFiles($albumdir, $defaultConfig); + + # generate the root directory, do recursive traversal of all subalbums + my %rootAlbumHash = generateAlbumPages("", \@recursiveImageData, + $defaultConfig); + + # and finally create the tree page. + generateTree($rootAlbumHash{config}, %rootAlbumHash); +} + +# Test if a package is installed on the system at run time. +# We use it to test LOCALE::Gettext is here (or else, we don't do any I18N). +sub have_package { + my $name = shift(@_); + $name =~ s%::%/%g; + foreach my $prefix (@INC) { + if (-f "$prefix/$name.pm") { + return 1; + } + } + return 0; +} + +# return translated string with HTML encoding +sub _ { + if ($I18N) { + return local2html(Locale::gettext::gettext(shift)); + } + return local2html(shift); +} + +# return translated string without any encoding transformation +sub gettext{ + if ($I18N) { + return Locale::gettext::gettext(shift); + } + return shift; +} + + +# return translated string without changing encoding +sub translate { + if ($I18N) { + return Locale::gettext::gettext(shift); + } + return shift; +} + +BEGIN{ + my @done; # list of template styles which have their static dir already copied + sub write_static_dir{ + my $destDir = shift; + my $configHash = shift; + if (grep(/^$configHash->{templateStyle}$/, @done)) { + return; + } + + push @done, $configHash->{templateStyle}; + my $staticDir = templateStaticDir($configHash); + + $destDir =~ s%/$%%; + $destDir .= "/static.".$configHash->{templateStyle}; + + if ($staticDir) { + if (! -d "$destDir") { + mkdir $destDir, 0755 + or die("\nCannot create $destDir: $?"); + } + system("cp", "-R", bsd_glob("$staticDir/*", GLOB_TILDE), "$destDir") == 0 + or die("\nCannot copy $staticDir directory content to $destDir: $?"); + } else { + beVerboseN(" Cannot find any static template directory.", 4); + } + } +} + + +sub write_bg_image { + my $album = shift; + my $configHash = shift; + + my $staticDir = templateStaticDir($configHash); + my $destDir = $albumdir; + + $destDir =~ s%/$%%; + $destDir .= "/static.".$configHash->{templateStyle}; + + if (! -d "$destDir") { + mkdir $destDir, 0755 + or die("\nCannot create $destDir: $?"); + } + system("cp", "-p", "$picdir$album$configHash->{backgroundImage}", "$destDir") == 0 + or die("\nCannot copy file $configHash->{backgroundImage} to $destDir: $?"); +} + +sub write_custom_css { + my $album = shift; + my $configHash = shift; + + my $staticDir = templateStaticDir($configHash); + my $destDir = $albumdir; + + $destDir =~ s%/$%%; + $destDir .= "/static.".$configHash->{templateStyle}; + + if (! -d "$destDir") { + mkdir $destDir, 0755 + or die("\nCannot create $destDir: $?"); + } + system("cp", "-p", "$picdir$album$configHash->{customStyleSheet}", + "$destDir") == 0 + or die("\nCannot copy file $configHash->{customStyleSheet} to $destDir: $?"); +} + +sub createSearchResultTemplate { + my $source = shift; + my $dest = shift; + + open(FILEOUT, ">", $dest) + or die("\nCannot open file $dest for writing: $?"); + + print(FILEOUT "result_html=''+\n"); + + open(FILEIN, $source) or die("\nCannot open file $source: $?"); + while() { + chomp; + print(FILEOUT "'$_\\n'+\n"); + } + close (FILEIN) || die ("can't close $source ($!)"); + print(FILEOUT "''\n"); + close (FILEOUT) || die ("can't close $dest ($!)"); + unlink($source); +} + +# write templates and javascript files used for the search functionality +sub writeSearchFiles { + my $album = shift; + my $configHash = shift; + + if (! $configHash->{searchEngine}) { + return + } + + # transform searchFields parameter in a list + my @l = $configHash->{searchFields} =~ /(\w+)/g; + $configHash->{searchFields}= \@l; + + my $staticDir = templateStaticDir($configHash); + my $destDir = $albumdir; + + if (! -d "$destDir") { + mkdir $destDir, 0755 + or die("\nCannot create $destDir: $?"); + } + + # Copy the javascript + my $source_file; + foreach my $file ("search_data.js",) { + $source_file = templateFileName($file, $configHash); + system("cp", "-p", $source_file, "$destDir") == 0 + or die("\nCannot copy file $file to $destDir: $?"); + } + + # Render the HTML templates + my %subsHash; + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $subsHash{BG_IMAGE} = + $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $subsHash{SEARCH_LIMIT} = $configHash->{searchLimit}; + + renderFile(templateFileName("search.js", $configHash), $album."search.js", + \%subsHash, $configHash); + + $subsHash{HOME_LINK} = $configHash->{homeURL}; + $subsHash{ALBUM_THUMB} = $configHash->{treePreview}; + $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $subsHash{DATE} = strftime($configHash->{dateString}, localtime); + + renderTemplate("search", $album."search.html", + \%subsHash, $configHash); + + my $tmpfile = $album."search_result.html"; + + renderTemplate("search_result", + $tmpfile, \%subsHash, $configHash); + createSearchResultTemplate($tmpfile, $album."search_result.js"); +} + +sub write_htaccess{ + my $dir = shift; + my $configHash = shift; + + if (! $configHash->{createHtaccess}) { + return + } + + my $file = $dir."/.htaccess"; + + mkdir("$dir", 0755) if (! -d "$dir"); + +# 20030422 Hack by Yves Mettier +# don't overwrite .htaccess if it is already OK + if(open(FILE, $file)) { + my $encoding = $configHash->{htmlEncoding}; + while() { + if(/AddDefaultCharset $encoding/) { + close (FILE) || die ("can't close $file ($!)"); + return; + } + if(/AddType text\/html;charset=$encoding html htm/) { + close (FILE) || die ("can't close $file ($!)"); + return; + } + } + close (FILE) || die ("can't close $file ($!)"); + } +# End of hack + + beVerboseN("Writing .htaccess file for album with ". + "charset encoding $configHash->{htmlEncoding}.", 2); + + open(FILE, ">>",$file) + or die("Cannot write to file $file ($!)"); + printf(FILE "AddDefaultCharset %s\n", + ($configHash->{htmlEncoding})); + close (FILE) || die ("can't close $file ($!)"); +} + +sub generateTree{ + # album hash -- other entries as defined in getAlbumInfo + # hash{subalbums} -- returns a ref to a list of subalbums to the album + # each entry in the list is a ref to an albumhash of this format + my ($configHash, %albumHash) = @_; + my $tableHTML = "\n".&generateTreeUL(%albumHash)."\n"; + my %subsHash; + $subsHash{TREE_TABLE} = $tableHTML; + $subsHash{TREE_LOOP} = generateTreeLoop(%albumHash); + + #beVerboseN("Generate tree Table html:\n $tableHTML ", 3); + $subsHash{STATIC_PATH} = "static.".$configHash->{templateStyle}; + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $subsHash{BG_IMAGE} = + $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $subsHash{HOME_LINK} = $configHash->{homeURL}; + $subsHash{ALBUM_THUMB} = $configHash->{treePreview}; + $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $subsHash{DATE} = strftime($configHash->{dateString}, localtime); + + renderTemplate("tree", $albumdir."tree.html", + \%subsHash, $configHash); +} + +sub generateTreeUL{ + my %albumHash = @_; + my $link = "". + $albumHash{title}.""; + + my $UL = "
    \n
  • $link\n"; + my @subAlbumHashRefList = @{$albumHash{subalbums}}; # may be empty + my $subAlbumHashRef; + foreach $subAlbumHashRef (@subAlbumHashRefList) { + $UL .= &generateTreeUL(%{$subAlbumHashRef})."\n"; + } + $UL .= "
\n"; + return $UL; +} + +sub generateTreeLoop { + my(%albumHash) = @_; + my @result; + my @subAlbumHashRefList = @{$albumHash{subalbums}}; # may be empty + my $subAlbumHashRef; + my($prepath, $preimage, $preimageTmp, $preimagePath, $preid); + my $hasChild; + + $prepath = $albumHash{link}; + $preimage = filenameToPreviewName($albumHash{sampleimage}); + $prepath =~ s/index.html//; + $prepath =~ s/^\///; + if ($prepath eq "") { + $preimage =~ s/^[^\/]+//; + } + $preimageTmp = $preimage; + $preimageTmp =~ s/\/([^\/]+)$//; + $preimagePath = $preimageTmp; + while (rindex($prepath, $preimageTmp) == -1 && $preimageTmp ne "") { + if (index($preimageTmp, "/") > -1) { + $preimageTmp =~ s/\/[^\/]+$//; + } else { + $preimageTmp =~ s/^[^\/]+$//; + } + } + $preimage = substr($preimage, length($preimageTmp) + 1); + + $preid = "ID$prepath"; + $preid =~ s/\//_/g; + push(@result, {TREE_NAME => $albumHash{title}, + TREE_LINK => $albumHash{link}, + TREE_IMAGES => "$albumHash{numImages} "._("images"), + TREE_SAMPLE => $prepath . $preimage, + TREE_ALT => $albumHash{title}, + TREE_SAMPLEID => $preid, + TREE_HASCHILD => @subAlbumHashRefList > 0, + }); + if (@subAlbumHashRefList) { + foreach $subAlbumHashRef (@subAlbumHashRefList) { + my $array = generateTreeLoop(%{$subAlbumHashRef}); + push(@result, @$array); + } + push(@result, {TREE_OUT => "1"}); + } + + return \@result; +} + +sub usage { + my $commandname = $0; + $commandname =~ s/^.*\///; + print < fields in + section. If any of the iKeys match those in the + album's "ignore" field, that album will not be + processed. See also the ignore parameter. + + -n iKey,iKey,... Sets "hidden" keywords which will be compared against + the contents of the "ignore" field of the album's + XML file, in the fields in + section. If any of the iKeys match those in the + album's "ignore" field, that album will not be + linked anywhere. See also the hidden parameter. + + -v X X is the verbosity level (between 0 and 3) + + -h print this help message +EoFprint +} + +sub min { + my($a,$b) = @_; + if ($a < $b) { return($a); } + else { return($b); } +} + +sub fileSize { + my($item) = shift(@_); + #print("filesize of $item\n"); + my($filesize) = ((-s "$item") / 1024); #get the file's size in KB. + if ($filesize > 1024) { # is it larger than a MB? + $filesize = ($filesize / 1024); + $filesize =~ s/(\d+[\.,]\d)\d+/$1/; + $filesize = $filesize._("MB"); + } else { + $filesize =~ s/(\d+)[\.,]\d+/$1/; + $filesize = $filesize._("KB"); + } + return $filesize; +} + +# $album is path of album, minus $picdir. +# @parentDirNames is list of parent dirs, not including this one +# for example if album = /album/may_13_2000/party_pics/ +# then dirs might be (Album, May 13th, 2000, Party Pictures) +# @parentDirNames is used for generating the back links in the path +sub generateAlbumPages{ + my ($album, $recursiveImageData, $configHash, @parentDirNames) = @_; + + #print "-------------------\n".Dumper($configHash)."\n"; + + my $oldBackground = $configHash->{backgroundImage}; + my $oldCss = $configHash->{customStyleSheet}; + my $bgchange = 0; + my $albumHashRef; + ($albumHashRef, $configHash) = getAlbumInfo($album, $configHash); + my %albumHash = %{$albumHashRef}; + $albumHash{config} = $configHash; + + # Don't generate anything else (recurse any further) if we want to ignore + # this album + if ( ignoreSet($albumHash{ignore}, $album, $configHash) ) { + return(%albumHash); + } + + # create the directory containing static elements (icons, + # javascript, css, ...) + write_static_dir($albumdir, $configHash); + + # Check if a new backgroundimage has to be copied + if ( $configHash->{backgroundImage} ne "" + && $oldBackground ne $configHash->{backgroundImage}) { + write_bg_image($album, $configHash); + $bgchange = 1; + } + + # Check whether a new stylesheet has to be copied + if ( $configHash->{customStyleSheet} ne "" + && $oldCss ne $configHash->{customStyleSheet}) { + write_custom_css($album, $configHash); + } + + push(@parentDirNames, $albumHash{title}); + $albumHash{parentDirNames} = \@parentDirNames; + + # albumHash info is complete, except for list of subalbums + # which is complete after recursive traversal + my @subalbumHashList; # goes into albumHash + #print "generateAlbumPages($album)\n"; + if ($verbose >=1) { + print xml2local($_)." > " foreach (@parentDirNames); + print "\n"; + } + + # first, make sure web directory exists + # use mkdir -p to make parents as needed + mkdir("$albumdir$album", 0755) if (! -d "$albumdir$album"); + + # returns the names of _all_ files/directories in this album's directory + opendir(DIR, "$picdir$album") || die "can't open dir $picdir$album: $!"; + my @filesInAlbum = grep { !/^\./ } readdir(DIR); + closedir DIR; + + my @tmpDirs; + my @tmpFiles; + foreach my $file (@filesInAlbum) { + if (-d "$picdir$album$file") { + push(@tmpDirs, $file); + } else { + push(@tmpFiles, $file); + } + } + + # Exclude files if needed + @tmpFiles = grep(!/$configHash->{excludeFiles}/, @tmpFiles) + if ($configHash->{excludeFiles}); + + if ( $bgchange + && $configHash->{backgroundImage} + && $configHash->{excludeBackgroundImage}) { + @tmpFiles = grep(!/$configHash->{backgroundImage}/, @tmpFiles); + } + @tmpDirs = grep(!/$configHash->{excludeDirs}/, @tmpDirs) + if ($configHash->{excludeDirs}); + + # Now put them in new sorted order back to @filesInAlbum + if ($configHash->{reverseOrder} & 1) { + @filesInAlbum = sort {$b cmp $a} @tmpDirs; + } else { + @filesInAlbum = sort @tmpDirs; + } + + # + # smr Jan 2004 -- sort according to file album.list (if there) + # + # first show all images in album.list in the order they appear in + # this file any image names preceeded with a . are suppressed for + # the album generation all images in the directory which are not in + # album.list are appended in usual (sorted) order + # + if (-r "$picdir$album/album.list") { + my(%isfile); + foreach(@tmpFiles) { $isfile{$_} = 1; } + open (INLIST, "$picdir$album/album.list") or die "can't open $picdir$album/album.list, $!"; + + while() { + chomp; s/^\s+//; s/\s+$//; + next if /^#/ || /^$/; + if(/^\./) { + s/^\.\s*//; + } else { + push(@filesInAlbum, $_) if $isfile{$_}; + } + $isfile{$_} = 0; + } + close INLIST; + $#tmpFiles = -1; + foreach (keys %isfile) { + push(@tmpFiles, $_) if $isfile{$_} == 1; + } + } + + if ($configHash->{reverseOrder} & 2) { + push(@filesInAlbum, sort {$b cmp $a} @tmpFiles); + } else { + push(@filesInAlbum, sort @tmpFiles); + } + + my($fileInAlbum, @urlimageList, @imageList, @xlinkList, $numAlbums); + $numAlbums = 0; + foreach $fileInAlbum (@filesInAlbum) { + if (-d "$picdir$album$fileInAlbum") { # Is this a subdirectory? + my %localAlbumHash = + generateAlbumPages($album.$fileInAlbum."/", $recursiveImageData, + $configHash, @parentDirNames); + + # If the "ignore" keyword matches one of the ones passed in the + # command line, don't push or count this album. + if (! ignoreAndHiddenSet($localAlbumHash{ignore}, $fileInAlbum, + $configHash) ) { + push(@subalbumHashList, \%localAlbumHash); + $numAlbums++; + } + } else { + my $known = 0; + foreach my $ext (@knownImageExtentions){ + if ($fileInAlbum =~ /\.$ext\Z/i) { + $known = 1; + last; + } + } + if ($known) { + # this is a known image format--remember its name + push @imageList, $fileInAlbum; + } else { + foreach my $ext (@filesToLinkExtensions){ + if ($fileInAlbum =~ /\.$ext\Z/i) { + $known = 1; + last; + } + } + if ($known) { + push @xlinkList, $fileInAlbum; + my $from="$picdir$album$fileInAlbum"; + my $to="$albumdir$album$fileInAlbum"; + if ( ! -f $to ) { + `cp -p "$from" "$to"`; + } + } + } + } + } + + #get virtual images to include + my @virtualInclude = getVirtualInclude($album); + #print "$_, " foreach (@virtualInclude); + push(@imageList, @virtualInclude); + $albumHash{subalbums} = \@subalbumHashList; + $albumHash{numImages} = $#imageList+1; # note that if (! @imageList), + # $#imageList = -1 + $albumHash{numSubAlbums} = $numAlbums; + $albumHash{numXLinks} = $#xlinkList + 1; + + # decide whether index page is first thumbnail page (or subalbum page) + my $firstIsIndex; + if ($numAlbums == 0 ) { + $firstIsIndex = 1; + } else { + $firstIsIndex = 0; + } + $albumHash{thumbIsIndex} = $firstIsIndex; + + + # generate image pages and get image data + my @imageData = generateImagesInAlbum($album, \%albumHash, $firstIsIndex, + $configHash, @imageList); + + $albumHash{sampleimage} = chooseSampleImage(\%albumHash, \@imageData) + if (! $albumHash{sampleimage}); + + # generate thumbnail pages + generateThumbnailPages($album, \%albumHash, + $firstIsIndex, $configHash, \@xlinkList, @imageData); + + # generate image list page + if ($albumHash{numImages} > 0) { + generateImageListPage($album, \%albumHash, \@imageData, \@xlinkList, + $configHash); + } + beVerboseN(" sample image for $album is $albumHash{sampleimage}", 3); + + # generate subalbum page + if ($numAlbums > 0) { + generateSubAlbumPage($album, \%albumHash, $recursiveImageData, + $configHash); + } + + + if ($configHash->{allThumbnailsPage}) { + # Munge the @imageData so that it includes path information, and dump this + # in @recursiveImageData. + for my $thisImageData ( @imageData ) { + $thisImageData->{'thumblink'}=$album.$thisImageData->{'thumblink'}; + for my $width ( 0..$#{$configHash->{scaledWidths}} ) { + $thisImageData->{$width}{'htmlFile'}=$album. + $thisImageData->{$width}{'htmlFile'}; + } + push(@{$recursiveImageData}, $thisImageData); + } + } + + return %albumHash; +} + +sub jssafe_uri_escape +{ + return uri_escape(@_, '^-A-Za-z0-9/_\.'); +} + +# if we don't have a sample image from the initial getAlbumInfo +# then we pick the first one. If we don't have images in this +# album, choose a sampleimage from a subalbum. If that fails, +# then no sampleimage. +# returns name of sampleimage (a file with path info) +sub chooseSampleImage{ + my ( $albumHashRef, $imageDataRef) = @_; + my @imageData = @{ $imageDataRef }; + + if (@imageData) { + my $th = $imageData[0]->{thumblink}; + $th =~ s/_pre\.jpg$/.jpg/; + $th = jssafe_uri_escape($albumHashRef->{dirname}) . "/" . $th; +# print "Returning from ImageData = $th\n"; + return $th; + }else{ + my @subAlbumHashList = @{ $albumHashRef->{subalbums} }; + my $subAlbumHashRef; + foreach $subAlbumHashRef (@subAlbumHashList) { + if ($subAlbumHashRef->{sampleimage}) { + my $th = jssafe_uri_escape($albumHashRef->{dirname}) . "/" . + $subAlbumHashRef->{sampleimage}; +# print "Returning from sub-album \$th = $th\n"; + return($th); + } + } + } + return ""; +} + +# takes in an image name (with some or no path info), and returns +# preview name with equivalent path info +sub filenameToPreviewName { + my $imageName = shift(@_); + my($base, $path, $type); + ($base,$path,$type) = fileparse($imageName, '\.[^.]+\z'); + # what the thumbnail will be named: + my($newPreviewName) = $path.$base . '_pre.jpg'; + beVerboseN("Preview name for $imageName is $newPreviewName", 3); + return $newPreviewName; +} + +sub getVirtualInclude{ + my $album = shift(@_); + my $virtualImageFile = $picdir.$album."include_images.txt"; + return () if (! -e $virtualImageFile); + open (INCLUDE, $virtualImageFile) || + die ("cannot open $virtualImageFile for reading: ($!)"); + my @include; + LINE: while () { + chomp; + next LINE if /^#/; #discard comments + next LINE if /^\s*$/; #ignore total whitespace + push(@include, $_); + } + close (INCLUDE) || die ("can't close $virtualImageFile ($!)"); + return @include; +} + +# return tree and path links of album as a list of hash refs +sub pathLinks{ + my ($album, $title, @dirs) = @_; + + my @result; + + push @result, {PATH_NAME => _("tree"), + PATH_TITLE => _("Tree of all albums and sub-albums"), + PATH_LINK => getRootDir($album)."tree.html", + }; + + my ($i, $pathlinks); + + my $count = $#dirs-1; + $count++ if ($title); + for my $i (0..$count) { + my $url=""; + my $dirname = $dirs[$i]; + #$dirname=~ s/_/ /g; + $url .="../" foreach ($i..($#dirs-1)); + $url .= "index.html"; + + push @result, {PATH_NAME => $dirname, + PATH_TITLE => $dirname, + PATH_LINK => $url, + PATH_ISALBUM => 1, + PATH_FIRST => ($i == 0), + }; + } + my $dirname = $dirs[$#dirs]; + if (!$title) { + push @result, {PATH_NAME => $dirname, + PATH_ISALBUM => 1, + PATH_FIRST => ($#dirs == 0), + }; + }else{ #for image page + push @result, {PATH_NAME => $title, + }; + } + return \@result; +} + +# crntPage is "subalbum", "thumb0", ... etc +# values is thumb0, or subalbum, never index +# return a list or hash refs +sub navBarLinks{ + my ($crntPage, $album, $configHash, %albumHash) = @_; + + my @result; + + my $firstIsIndex = $albumHash{thumbIsIndex}; + my $numImages = $albumHash{numImages}; + + # + my $navrows; + my $numThumbPages = calcNumThumbPages($numImages, $configHash); + + #subalbum link + if ($crntPage eq "subalbum") { # we have a subalbum page (duh) + push @result, {NAV_NAME => _("Sub Albums"), + NAV_ICON => "subalbum.png"}; + } elsif (! $firstIsIndex) { # we have a subalbum page -- it must be + # index.html + push @result, {NAV_NAME => _("Sub Albums"), + NAV_LINK => "index.html", + NAV_ICON => "subalbum.png", + NAV_ID => "sub"}; + } + + #image list link + if ($crntPage eq "imagelist") { + push @result, {NAV_NAME => _("Image List"), + NAV_ICON => "imagelist.png"}; + } elsif ($numImages > 0) { + push @result, {NAV_NAME => _("Image List"), + NAV_LINK => "imagelist.html", + NAV_ICON => "imagelist.png", + NAV_ID => "imgl"}; + } + + #if ($crntPage ne "image") { + # first thumbnail page + if ($numThumbPages > 0) { # we have a first thumbnail page + my($thumbpage); + + if ($numThumbPages == 1) { + $thumbpage = _("Thumbnail Page"); + } else { + $thumbpage = _("Thumbnail Page 1"); + } + if ($crntPage eq "thumb0") { # and we are on it + push @result, {NAV_NAME => $thumbpage, + NAV_ICON => "thumbnails.png"}; + } elsif ($firstIsIndex) { # and it is index.html + push @result, {NAV_NAME => $thumbpage, + NAV_LINK => "index.html", + NAV_ICON => "thumbnails.png", + NAV_ID => "th0"}; + } elsif ($numThumbPages > 0) { # it is thumb0.html + push @result, {NAV_NAME => $thumbpage, + NAV_LINK => "thumb0.html", + NAV_ICON => "thumbnails.png", + NAV_ID => "th0"}; + } + } + + # remaining thumbnail pages + my $i; + for $i (1..($numThumbPages-1)) { + if ($crntPage eq "thumb$i") { + push @result, {NAV_NAME => _("Thumbnail Page") . " ". ($i+1), + NAV_ICON => "thumbnails.png"}; + }else{ + push @result, {NAV_NAME => _("Thumbnail Page") . " ". ($i+1), + NAV_LINK => "thumb".$i.".html", + NAV_ICON => "thumbnails.png", + NAV_ID => "th$i"}; + } + } + #} + + # all thumbnail page + # If we have more than one thumbnail page (this is a thumbnail page), or + # this is the main page (subalbum page), or this is "thumb-2" (a subalbum + # thumb page)... + if ( $configHash->{allThumbnailsPage} && + (($numThumbPages > 1) || ($crntPage eq "subalbum") + || ($crntPage eq "thumb-2") )) { + if ($crntPage eq "thumb-1" || $crntPage eq "thumb-2" ) { # we are on it + push @result, {NAV_NAME => _("All Thumbnails")}; + } else { # we are not on an allthumbnails page + push @result, {NAV_NAME => _("All Thumbnails"), + NAV_LINK => "allthumbs.html"}; + } + } + return \@result; +} + + + +sub getAlbumNumInfo{ + my (%albumHash) = @_; + + my $numInfo=""; + my $numImages = $albumHash{numImages}; + my $numAlbums = $albumHash{numSubAlbums}; + my $numXLinks = $albumHash{numXLinks}; + + if ($numImages == 1) { + $numInfo = "1 "._("image"); + } elsif ($numImages > 1) { + $numInfo = "$numImages "._("images"); + } + $numInfo .= ", " if (length $numInfo > 0 && $numXLinks > 0); + if ($numXLinks == 1) { + $numInfo .= "1 "._("media file"); + } elsif ($numXLinks > 1) { + $numInfo .= "$numXLinks "._("media files"); + } + $numInfo .= ", " if (length $numInfo > 0 && $numAlbums > 0); + if ($numAlbums == 1) { + $numInfo .= "1 "._("subalbum"); + } elsif ($numAlbums > 1) { + $numInfo .= "$numAlbums "._("subalbums"); + } + return $numInfo; +} + +sub generateImageListPage{ + my ($album, $albumHashRef, $imageDataRef, $xlinksRef, $configHash) = @_; + my %albumHash = %{$albumHashRef}; + my @imageData = @{$imageDataRef}; + my $pwd; + + # hash for final substitutions + my %finalsubs; + $finalsubs{NUM_INFO} = getAlbumNumInfo(%albumHash); + $finalsubs{ALBUM_TITLE} = $albumHash{title}; + $finalsubs{NAV_BAR_TABLE} = + navBarLinks('imagelist', $album, $configHash, %albumHash); + $finalsubs{ALBUM_PATH_LINKS} = + pathLinks($album, 0, @{$albumHash{parentDirNames}}); + + $finalsubs{TREE_NAME} = _("tree"); + $finalsubs{TREE_TITLE} = _("Tree of all albums and sub-albums"); + $finalsubs{TREE_LINK} = getRootDir($album)."tree.html"; + + if ($#{$albumHash{parentDirNames}} > 0) { + $finalsubs{UP_NAME} = _("up"); + $finalsubs{UP_TITLE} = _("Up one subalbum"); + $finalsubs{UP_LINK} = "../index.html"; + } + + if ($genEditableAlbum) { + $pwd = `pwd`; + chop($pwd); + $pwd = jssafe_uri_escape($pwd); + $finalsubs{ALBUM_DESC} = "". + $albumHash{longdesc}.""; + } else { + $finalsubs{ALBUM_DESC} = $albumHash{longdesc}; + } + my @tables; + my $thumbid = 0; + foreach my $imageInfoRef (@imageData) { + my %imageInfo = %{$imageInfoRef}; + my %tablesubs; + my @sizes; + + $thumbid += 1; + $tablesubs{THUMB_ID} = $thumbid; + for my $i (0..$imageInfo{'maxSize'}) { + if ($i == $configHash->{defaultSize}) { + push @sizes , {SIZE_LINK => $imageInfo{$i}{'htmlFile'}, + SIZE_NAME => $imageInfoRef->{configuration}{sizeNames}[$i], + SIZE_TITLE => $imageInfoRef->{configuration}{longSizeNames}[$i]. + " ($imageInfo{$i}{'width'}x$imageInfo{$i}{'height'})", + SIZE_FILE => $imageInfoRef->{configuration}{fileSizeNames}[$i], + SIZE_DFLT => 1, + }; + } else { + push @sizes , {SIZE_LINK => $imageInfo{$i}{'htmlFile'}, + SIZE_NAME => $imageInfoRef->{configuration}{sizeNames}[$i], + SIZE_TITLE => $imageInfoRef->{configuration}{longSizeNames}[$i]. + " ($imageInfo{$i}{'width'}x$imageInfo{$i}{'height'})", + SIZE_FILE => $imageInfoRef->{configuration}{fileSizeNames}[$i], + }; + } + } + if ($genEditableAlbum) { + $tablesubs{TITLE} = "". + $imageInfo{title}.""; + } else { + $tablesubs{TITLE} = $imageInfo{title}; + } + $tablesubs{SIZE_LINKS} = \@sizes; + + my $atLeastOneField = 0; + my $tagValue; + + $tablesubs{DESC_TABLE} = getDescTable(\%imageInfo, $configHash); + + if ($configHash->{defaultSize} >= 0){ + $tablesubs{THUMB_DEFAULT_SIZE} = + $imageInfo{$configHash->{defaultSize}}{'htmlFile'}; + } + if ($configHash->{thumbnailInImageList}) { + %tablesubs = ((THUMB_LINK => $imageInfo{'thumblink'}, + THUMB_WIDTH => $imageInfo{'twidth'}, + THUMB_HEIGHT => $imageInfo{'theight'}, + THUMB_ALT => _("Click on one of the size names above to enlarge this image"), + THUMB_LINK_TITLE => _("Click on one of the size names above to enlarge this image"), + ), %tablesubs); + } + push @tables, \%tablesubs; + } + + $finalsubs{XLINK} = getXLinks($xlinksRef); + $finalsubs{STATIC_PATH} = + getRootDir($album)."static.".$configHash->{templateStyle}; + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $finalsubs{BG_IMAGE} = + $finalsubs{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $finalsubs{CUSTOM_CSS} = $configHash->{customStyleSheet}; + $finalsubs{ROOT_PATH} = getRootDir($album); + + $finalsubs{IMAGE_LIST_TABLE} = \@tables; + $finalsubs{HOME_LINK} = $configHash->{homeURL}; + $finalsubs{FEEDBACK_LINK} = $configHash->{feedbackMail}; + $finalsubs{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $finalsubs{DATE} = strftime($configHash->{dateString}, localtime); + + renderTemplate("imagelist", $albumdir.$album."imagelist.html", + \%finalsubs, $configHash); +} +sub generateSubAlbumPage{ + my ($album, $albumHashRef, $recursiveImageData, $configHash) = @_; + my $numAlbums = $albumHashRef->{numSubAlbums}; + if (1 || $numAlbums <= $configHash->{maxAlbumsForLongSubAlbum}) { + generateLongSubAlbumPage($album, $albumHashRef, $configHash); + }else{ + # Short album page is not is not supported for the moment + generateShortSubAlbumPage($album, $albumHashRef, $configHash); + } + + if ($configHash->{allThumbnailsPage}){ + # Create a thumbnail page of all images + generateThumbPage($album, $albumHashRef, "-2", "1", + "allthumbs.html", "", "", "", $configHash, undef, + undef, + $recursiveImageData->[0..$#{$recursiveImageData}]); + } +} + +# XXX Short album page is not supported for the moment +sub generateShortSubAlbumPage{ + my ($album, $albumHashRef, $configHash) = @_; + my %albumHash = %{$albumHashRef}; + + my $pwd = `pwd`; + chop($pwd); + $pwd = jssafe_uri_escape($pwd); + + # hash for final subsitutions + my %finalsubs; + $finalsubs{''} = getAlbumNumInfo(%albumHash); + $finalsubs{''} = $albumHash{title}; + $finalsubs{''} = getNavBarLinks('subalbum', + $album, $configHash, + %albumHash); + $finalsubs{''} = + getPathLinks($album, 0, @{$albumHash{parentDirNames}}); + if ($genEditableAlbum) { + $finalsubs{''} = "'} .= $pwd."/"; + } + $finalsubs{''} .= $albumHash{descFileName}."\">". + $albumHash{longdesc}.""; + } else { + $finalsubs{''} = $albumHash{longdesc}; + } + + my $titleRowText = + ' + +   + '; + + my $descRowText = + ' +     + + '; + + my $subalbumHashRef; + my $tableData = ''; + my @subalbumHashList = @{$albumHash{subalbums}}; + for $subalbumHashRef (@subalbumHashList) { + my %subshash; + my %albuminfo = %{$subalbumHashRef}; + $subshash{''} = &getAlbumNumInfo(%albuminfo); + $subshash{''} = $albuminfo{title}; + $subshash{''} = jssafe_uri_escape($albuminfo{dirname}). + "/index.html"; + if ($genEditableAlbum) { + $subshash{''} = "$albuminfo{shortdesc}"; + } else { + $subshash{''} = $albuminfo{shortdesc}; + } + + + $tableData .= doSubstitutions($titleRowText, $configHash, %subshash); + $tableData .= doSubstitutions($descRowText, $configHash, %subshash) + if (! $subshash{''} =~ + /No short description available/); + #print "table after SUBS\n: $tableData\n"; + } + $tableData .= '
'; + + $finalsubs{''} = $tableData; + my $pageContent = openTemplate("subalbum"); + $pageContent = doSubstitutions($pageContent, $configHash, %finalsubs); + renderTemplate($albumdir.$album."index.html", $pageContent); +} + + +sub generateLongSubAlbumPage{ + my ($album, $albumHashRef, $configHash) = @_; + my %albumHash = %{$albumHashRef}; + + # hash for final subsitutions + my %templateParameters = + ( NUM_INFO => getAlbumNumInfo(%albumHash), + ALBUM_TITLE => $albumHash{title}, + NAV_BAR_TABLE => navBarLinks('subalbum', + $album, $configHash, %albumHash), + ALBUM_PATH_LINKS => pathLinks($album, 0, + @{$albumHash{parentDirNames}}), + TREE_NAME => _("tree"), + TREE_TITLE => _("Tree of all albums and sub-albums"), + TREE_LINK => getRootDir($album)."tree.html", + ); + + if ($configHash->{albumThumbInSubAlbumPage} && $albumHash{numImages} > 0) { + my $sampleImage = "./".filenameToPreviewName($albumHash{sampleimage}); + if ($albumHash{dirname} ne "") { + $sampleImage = "../$sampleImage"; + } + %templateParameters = + (%templateParameters, + ( ALBUM_THUMB => 1, + DESC => $albumHash{shortdesc}, + TITLE => $albumHash{title}, + CURRENT_NUM_INFO => ($albumHash{numImages} > 1) ? + $albumHash{numImages}." "._("images") : "1 "._("image"), + LINK => "thumb0.html", + THUMB_ALT => _("Click to view thumbnails of the current album"), + THUMB_LINK_TITLE => _("Click to view thumbnails of the current album"), + THUMB_LINK => $sampleImage, + ) + ); + } + + my $pwd; + if ($genEditableAlbum) { + $pwd = `pwd`; + chop($pwd); + $pwd = jssafe_uri_escape($pwd); + + $templateParameters{ALBUM_DESC} = + "". + $albumHash{longdesc}.""; + } else { + $templateParameters{ALBUM_DESC} = $albumHash{longdesc}; + } + + my @subAlbunTable; + my $cnt = 0; + for my $subalbumHashRef (@{$albumHash{subalbums}}) { + my %albuminfo = %{$subalbumHashRef}; + + $cnt += 1; + + my %subshash = + ( NUM_INFO => getAlbumNumInfo(%albuminfo), + TITLE => $albuminfo{title}, + LINK => jssafe_uri_escape($albuminfo{dirname})."/index.html", + THUMB_ALT => _("Click to view this album"), + THUMB_LINK_TITLE => _("Click to view this album"), + THUMB_LINK => filenameToPreviewName($albuminfo{sampleimage}), + THUMB_NUM => $cnt + ); + + if ($genEditableAlbum) { + $subshash{'DESC'} = + "". + $albuminfo{shortdesc}.""; + } else { + $subshash{'DESC'} = $albuminfo{shortdesc}; + } + push @subAlbunTable, \%subshash; + } + + $templateParameters{SUBALBUMS_TABLE} = \@subAlbunTable; + + if ($#{$albumHash{parentDirNames}} > 0) { + $templateParameters{UP_NAME} = _("up"); + $templateParameters{UP_TITLE} = _("Up one subalbum"); + $templateParameters{UP_LINK} = "../index.html"; + } + $templateParameters{STATIC_PATH} = + getRootDir($album)."static.".$configHash->{templateStyle}; + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $templateParameters{BG_IMAGE} = + $templateParameters{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $templateParameters{CUSTOM_CSS} = $configHash->{customStyleSheet}; + + $templateParameters{HOME_LINK} = $configHash->{homeURL}; + $templateParameters{FEEDBACK_LINK} = $configHash->{feedbackMail}; + $templateParameters{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $templateParameters{DATE} = strftime($configHash->{dateString}, localtime); + + renderTemplate("subalbum", $albumdir.$album."index.html", + \%templateParameters, $configHash); +} + + +sub getAlbumInfo{ + my $album = shift(@_); + my $configHash = shift(@_); + + my %albuminfo; # what we want to generate + $albuminfo{link} = jssafe_uri_escape($album)."index.html"; + #link is from $albumdir -- otherwise need to add getRootDir to make work + + #$album = $picdir.$album; + # we need this for the root dir to be right -- + # correct behaviour if passed "" + + my $albumdescfile = "$picdir$album"."album.xml"; + if (-e $albumdescfile) { + $albuminfo{descFileName} = jssafe_uri_escape($albumdescfile); + $configHash = getDescAlbum($albumdescfile, \%albuminfo, $configHash); + + # Don't calculate this album's info if it is flagged "ignore". + if ( ignoreSet($albuminfo{ignore}, $album, $configHash) ) + { return(\%albuminfo); } + + if (exists $albuminfo{sampleimage} && $albuminfo{sampleimage}) { + #$albuminfo{sampleimage} = uri_escape($albuminfo{sampleimage}, + # '^-A-Za-z0-9/_\.'); + #add directory name of album to sample image + my $dirname = $album; + $dirname =~ s%^.*/([^/]+)/$%$1/%; + $albuminfo{sampleimage} = $dirname.$albuminfo{sampleimage}; + $albuminfo{sampleimage} = jssafe_uri_escape($albuminfo{sampleimage}); +# $albuminfo{sampleimage} = uri_escape($albuminfo{sampleimage}, +# '^-A-Za-z0-9/_\.'); + } + } else { + $albuminfo{descFileName} = ""; + } + if (! $albuminfo{shortdesc} && ! $albuminfo{longdesc}) { + if ($configHash->{emptyAlbumDesc}) { + $albuminfo{shortdesc} = ""; + $albuminfo{longdesc} = ""; + } else { + $albuminfo{shortdesc} = _("No short description available"); + $albuminfo{longdesc} = _("No long description available"); + } + } elsif (! $albuminfo{shortdesc}) { + $albuminfo{shortdesc} = $albuminfo{longdesc}; + } elsif (! $albuminfo{longdesc}) { + $albuminfo{longdesc} = $albuminfo{shortdesc}; + } + + #get album title + my $albumtitle = $picdir.$album; + # strip ending / + if ($album ne "") { + my $endChar = chop($albumtitle); + $albumtitle = $album if ($endChar ne "/") ; + $albumtitle =~ s/^.*\///g; #remove path + } else { + $albumtitle = "" + } + $albuminfo{dirname} = $albumtitle; #single dir name, without path info + + if (!$albuminfo{title}){ + if ($configHash->{stripDirPrefix}) { + $albumtitle =~ s/^\d+_//g; + } + $albumtitle =~ s/_/ /g; # replace underscores with spaces + $albuminfo{title} = local2html($albumtitle) ; + } + beVerboseN("title for album $album is $albuminfo{title}.", 3); + return (\%albuminfo, $configHash); +} + +sub calcNumThumbPages{ + my $numImages = shift (@_); + my $configHash = shift (@_); + my $numPages=0; + if ($numImages % $configHash->{numThumbsPerPage} == 0) { + $numPages = $numImages/$configHash->{numThumbsPerPage}; + }else{ + $numPages = int($numImages/$configHash->{numThumbsPerPage}) + 1; + } + return $numPages; +} + +sub getXLinks{ + my $xLinksRef = shift; + my @xLinks=(); + foreach my $xLink ( @$xLinksRef ) { + my %row = ( link => $xLink ); + push(@xLinks, \%row); + } + return \@xLinks; +} + +# escape a string so it can used as a javascript string +sub escapeJSString{ + my $string = shift; + $string =~ s/\"/\\\"/g; + $string =~ s/\n//g; + return $string; +} + +# normalize a string by translating accents +sub normalizeString{ + my $charset = shift; + my $string = shift; + my $result; + + $result = lc($string); + $result = unac_string($charset, $result); + if (!defined($result)) { + warn "Invalid encoding $charset for string '$string'"; + return ""; + } + return $result; +} + +# split a string into keywords for search engine +sub splitInKeywords{ + my $string = shift; + + $string =~ s/[^a-z0-9]/ /gi; + return $string; +} + +# write data for the search engine +sub writeSearchString{ + my $imageHashRef = shift; + my $configHash = shift; + my $albumHashRef = shift; + my $album = jssafe_uri_escape(shift); + + beVerboseN(" Writing information for the search engine.", 4); + + my $charset = $configHash->{htmlEncoding}; + my $search_string = " "; + foreach my $field (@{$configHash->{searchFields}}){ + if ($$imageHashRef{$field}) { + $search_string .= escapeJSString(normalizeString($charset, + $$imageHashRef{$field}))." "; + } + } + $search_string = splitInKeywords($search_string); + + my $size = $configHash->{defaultSize}; + if (! $size >= 0){ + $size = 0; + } + my $url = $album.$$imageHashRef{$size}{'htmlFile'}; + + my $album_title = $albumHashRef->{'title'}; + my $album_url = $albumHashRef->{'link'}; + my $thumb_url = $album.$imageHashRef->{'thumblink'}; + my $title = escapeJSString($imageHashRef->{'title'}); + my $width = $imageHashRef->{'twidth'}; + my $height = $imageHashRef->{'theight'}; + + my $file = $albumdir."search_data.js"; + open(FILE, ">>", $file) + or die("Cannot write to file $file ($!)"); + + print(FILE "sd[sd.length] = new io(\"$search_string\", \"$title\", \"$url\", \"$thumb_url\", \"$width\", \"$height\", \"$album_title\", \"$album_url\");\n"); + close (FILE) || die ("can't close $file ($!)"); + +} + +sub generateThumbnailPages{ + my ($album, $albumHashRef, $firstIsIndex, $configHash, + $xlinkListRef, + @imageData) = @_; + my @xlinkList=@$xlinkListRef; + my %albumHash = %{$albumHashRef}; + my $numImages = scalar(@imageData); #element count + my $numPages = calcNumThumbPages($numImages, $configHash); + my $crntPage; + + # generate number link for each page + my @numLink; + if ($firstIsIndex){ + push @numLink, {NUMBER => "1", + NUM_LINK => "index.html" + }; + }else{ + push @numLink, {NUMBER => "1", + NUM_LINK => "thumb0.html" + }; + } + for($crntPage=1; $crntPage < $numPages; $crntPage++) { + push @numLink, {NUMBER => ($crntPage+1), + NUM_LINK => "thumb".$crntPage.".html" + }; + } + + for($crntPage=0; $crntPage < $numPages; $crntPage++) { + #calculate prev, next thumb page + # we set prev and next equal to the empty string + # if we have only 1 page to + # to indicate prev and next buttons should not be displayed. + my ($prev, $next); + if ($numPages == 1) { + $prev = ""; + $next = ""; + }else{ + $prev = ($crntPage -1 + $numPages)%$numPages; + $prev = "thumb".$prev.".html"; + $next = ($crntPage + 1)%$numPages; + $next = "thumb".$next.".html"; + # special cases + if ($configHash->{thumbnailPageCycling} == 1) { + $next = "index.html" + if ($firstIsIndex && ($crntPage == $numPages-1)); + } else { + $prev = "" if ($crntPage == 0); + $next = "" if ($crntPage == $numPages-1); + } + $prev = "index.html" if ($firstIsIndex && ($crntPage == 1)); + } + + #first and last images on page + my $first = $crntPage*$configHash->{numThumbsPerPage}; + my $last = $first + $configHash->{numThumbsPerPage} - 1; + $last = $#imageData if ($#imageData <= $last); + + my @preloadImages; + if ($configHash->{javaScriptPreloadThumbs}) { + # get the names of images on the next page to preload them using + # javascript + my $lastNext = $last + $configHash->{numThumbsPerPage}; + $lastNext = $#imageData if ($#imageData <= $lastNext); + for ( my $i = $last+1 ; $i <= $lastNext ; $i++){ + my %image; + $image{PRELOAD_IMAGE_NB} = $i-$last; + $image{PRELOAD_IMAGE_NAME} = $imageData[$i]->{'thumblink'}; + $image{PRELOAD_IMAGE_WIDTH} = $imageData[$i]->{'twidth'}; + $image{PRELOAD_IMAGE_HEIGHT} = $imageData[$i]->{'theight'}; + push @preloadImages, \%image; + } + } + + #fileName + my $filename = "thumb".$crntPage.".html"; + $filename = "index.html" if ($firstIsIndex && ($crntPage == 0)); + + # generate sequence of number links + my $numLinkSeq = dclone(\@numLink); + $numLinkSeq->[$crntPage]{NUM_LINK} = ""; + + generateThumbPage($album, $albumHashRef, $crntPage, $numPages, + $filename, $prev, $next, $numLinkSeq, $configHash, + \@preloadImages, \@xlinkList, @imageData[$first..$last]); + } + # Generate an "all images" thumbnail page if more than one page was + # generated. + if ( $configHash->{allThumbnailsPage} && $numPages > 1 ) { + generateThumbPage($album, $albumHashRef, "-1", "1", + "allthumbs.html", "", "", "", $configHash, + undef, undef, @imageData[0..$#imageData]); + } +} + +sub generateThumbPage{ + my ($album, $albumHashRef, $pageNumber, $numPages, $filename, + $prevPage, $nextPage, $numberLinks, $configHash, $preloadImages, + $xlinkListRef, @imageData) = @_; + + my @xlinkList; + if (!defined($xlinkListRef)) { + @xlinkList=(); + } else { + @xlinkList=@$xlinkListRef; + } + + my %albumHash = %{$albumHashRef}; + + my $thumbsOnPage = scalar(@imageData); + + # generate the table containing the thumbnails + my $crntImage; + my @thumbTable; + for ($crntImage = 0; $crntImage<$thumbsOnPage; $crntImage++) { + #create the row + my $row = thumbEntry($imageData[$crntImage], + $imageData[$crntImage]{configuration}); + $row->{THUMB_ID} = $crntImage; + # begin new row if needed + if ( (($crntImage+1) % $configHash->{thumbsPerRow} == 0) && + ($crntImage+1 < $thumbsOnPage)) { + $row->{THUMB_NEW_LINE} = 1; + } + push @thumbTable, $row; + } + + # set up all the substitutions, first with subs for header + my %subsHash; + $subsHash{THUMBS_TABLE} = \@thumbTable; + $subsHash{PRELOAD_IMAGES} = $preloadImages; + $subsHash{THUMB_NUMBER_LINKS} = $numberLinks; + $subsHash{NUM_INFO} = getAlbumNumInfo(%albumHash); + $subsHash{ALBUM_TITLE} = $albumHash{title}; + $subsHash{NAV_BAR_TABLE} = + navBarLinks("thumb$pageNumber", $album, $configHash, %albumHash); + $subsHash{ALBUM_PATH_LINKS} = + pathLinks($album, 0, @{$albumHash{parentDirNames}}); + + $subsHash{TREE_NAME} = _("tree"); + $subsHash{TREE_TITLE} = _("Tree of all albums and sub-albums"); + $subsHash{TREE_LINK} = getRootDir($album)."tree.html"; + + if ($nextPage eq "" and $prevPage eq "") { + $subsHash{MULTIPLE_PAGES} = 0; + } else { + $subsHash{MULTIPLE_PAGES} = 1; + } + + if ($#{$albumHash{parentDirNames}} > 0) { + $subsHash{UP_NAME} = _("up"); + $subsHash{UP_TITLE} = _("Up one subalbum"); + $subsHash{UP_LINK} = "../index.html"; + } + + if ($genEditableAlbum) { + $subsHash{ALBUM_DESC} = + "". + $albumHash{longdesc}.""; + } else { + $subsHash{ALBUM_DESC} = $albumHash{longdesc}; + } + + if ($nextPage) { + $subsHash{NEXT_THUMB_PAGE} = $nextPage; + } + + if ($prevPage) { + $subsHash{PREV_THUMB_PAGE} = $prevPage; + } + + # Correct the special-casing of "-1" and "-2" to mean this is an + # "allthumbs" page. + if ( $pageNumber=="-1" || $pageNumber=="-2" ) { + $pageNumber="0"; + } + + my $pagenumber_string = $pageNumber+1; + $pagenumber_string .= "/$numPages"; + $subsHash{THUMB_PAGE_NUMBER} = $pagenumber_string; + + $subsHash{XLINK} = getXLinks(\@xlinkList); + $subsHash{STATIC_PATH} = + getRootDir($album)."static.".$configHash->{templateStyle}; + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $subsHash{BG_IMAGE} = + $subsHash{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $subsHash{CUSTOM_CSS} = $configHash->{customStyleSheet}; + $subsHash{HOME_LINK} = $configHash->{homeURL}; + $subsHash{FEEDBACK_LINK} = $configHash->{feedbackMail}; + $subsHash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $subsHash{DATE} = strftime($configHash->{dateString}, localtime); + if ($albumHashRef->{numSubAlbums} == 0){ + $subsHash{FIRST_PAGE} = "index.html"; + }else{ + $subsHash{FIRST_PAGE} = "thumb0.html"; + } + $subsHash{LAST_PAGE} = "thumb".($numPages-1).".html"; + + renderTemplate("thumbnail", $albumdir.$album.$filename, + \%subsHash, $configHash); +} + + +#creates a single entry in the table of thumbnails for the given image +sub thumbEntry{ + my $imageHashRef = shift; + my $configHash = shift; + + my ($links, $i); + + my %thumb = (THUMB_LINK => $imageHashRef->{'thumblink'}, + THUMB_WIDTH => $imageHashRef->{'twidth'}, + THUMB_HEIGHT => $imageHashRef->{'theight'}, + THUMB_ALT => _("Click on one of the size names below to enlarge this image"), + THUMB_LINK_TITLE => _("Click on one of the size names below to enlarge this image"), + ); + if ($configHash->{defaultSize} >= 0){ + $thumb{THUMB_DEFAULT_SIZE} = + $$imageHashRef{$configHash->{defaultSize}}{'htmlFile'}; + } + if ($configHash->{titleOnThumbnail}) { + $thumb{THUMB_TITLE} = $$imageHashRef{title}; + } + + if (! $configHash->{thumbnailBackground}){ + $thumb{THUMB_BACKGROUND} = + $configHash->{colorsSubs}{$configHash->{colorStyle}}{PAGE_BACKCOLOR}; + }else{ + $thumb{THUMB_BACKGROUND} = + $configHash->{colorsSubs}{$configHash->{colorStyle}}{SUBBAR_BACKCOLOR}; + } + + #print 'for image'.$imageHashRef->{'filename'}.'we have + #$imageHashRef->{\'maxSize\'} = '.$imageHashRef->{'maxSize'}."\n"; + + my @sizes; + for $i (0..($imageHashRef->{'maxSize'})) { + if ($i == $configHash->{defaultSize}) { + push @sizes , {SIZE_LINK => $$imageHashRef{$i}{'htmlFile'}, + SIZE_NAME => $configHash->{sizeNames}[$i], + SIZE_TITLE => $configHash->{longSizeNames}[$i]. + " ($imageHashRef->{$i}{'width'}x". + "$imageHashRef->{$i}{'height'})", + SIZE_FILE => $configHash->{fileSizeNames}[$i], + SIZE_DFLT => 1, + }; + } else { + push @sizes , {SIZE_LINK => $$imageHashRef{$i}{'htmlFile'}, + SIZE_NAME => $configHash->{sizeNames}[$i], + SIZE_TITLE => $configHash->{longSizeNames}[$i]. + " ($imageHashRef->{$i}{'width'}x". + "$imageHashRef->{$i}{'height'})", + SIZE_FILE => $configHash->{fileSizeNames}[$i], + }; + } + } + $thumb{THUMB_SIZES} = \@sizes; + + return \%thumb; +} + +sub generateSecondaryFieldsPage{ + my $imageInfo = shift; + my $album = shift; + my $albumInfo = shift; + my $imageName = shift; + my $configHash = shift; + my $table=""; + my $fileName=""; + + my $tagValue; + my $sectionTitle=""; + my $tableSection=""; + my @sections; + my @fields; + my @tmpFields; + foreach my $tagName (@secondaryFields) { + if ($tagName =~ m/^BINS-SECTION /) { + if ($sectionTitle && @tmpFields){ + push (@fields, {SECTION_TITLE => $sectionTitle}); + @fields = (@fields, @tmpFields); + @tmpFields = (); + push (@sections, {SECTION_TITLE => $sectionTitle}); + } + $sectionTitle = $tagName; + $sectionTitle =~ s/^BINS-SECTION //; + }else{ + $tagValue = getFields($configHash)->{$tagName}; + if ($imageInfo->{$tagName}) { + my %row = (FIELD_NAME => $tagValue->{'Name'}, + FIELD_VALUE => local2html($imageInfo->{$tagName}) + ); + if ($tagValue->{'Tip'}) { + $row{FIELD_TIP} = $tagValue->{'Tip'}; + # Escape double quote to unconfuse emacs code highlighter. + $row{FIELD_TIP} =~ s/\"/"/g; + } + push (@tmpFields, \%row); + } + } + } + + if (! @fields) { + return "" + } + + # on which size of image we go when "back to the image" is clicked and + # JavaScript is deactivated: + my $size = $configHash->{defaultSize}; + if ($size == -1){ + $size = 0; + } + + my %subs_hash; + $subs_hash{TREE_LINK} = getRootDir($album)."tree.html"; + $subs_hash{ALBUM_TITLE} = $album; + $subs_hash{IMAGE_PAGE_LINK} = $imageInfo->{$size}{'htmlFile'}; + $subs_hash{IMAGE_SIZE_NAME} = _("size "). + $configHash->{longSizeNames}[$size]; + $subs_hash{FILE_NAME} = encode_entities($imageName); + $subs_hash{THUMB_PAGE} = $imageInfo->{'thumbpage'}; + $subs_hash{ALBUM_PATH_LINKS} = + pathLinks($album, $imageInfo->{'title'}, + @{$albumInfo->{parentDirNames}}); + $subs_hash{TITLE} = $imageInfo->{'title'}; + $subs_hash{DESC_TABLE} = \@fields; + $subs_hash{LINKS_TABLE} = \@sections; + $subs_hash{STATIC_PATH} = + getRootDir($album)."static.".$configHash->{templateStyle}; + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $subs_hash{BG_IMAGE} = + $subs_hash{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $subs_hash{HOME_LINK} = $configHash->{homeURL}; + $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail}; + $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $subs_hash{DATE} = strftime($configHash->{dateString}, localtime); + $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet}; + + my @array; + push @array, {NAV_NAME => getIntlSubs($configHash)->{STRING_BACKTOTHEIMAGE}, + NAV_LINK => "javascript:history.back();", + NAV_ICON => "back.png", + NAV_ID => "back"}; + # $subs_hash{NAV_BAR_TABLE} = navBarLinks('secondaryFields', $album, $configHash, %albumHash); + $subs_hash{NAV_BAR_TABLE} = \@array; + #### + + $fileName = $imageName.".details.html"; + my $outputPage = $albumdir.$album.$fileName; + renderTemplate("details", $outputPage, \%subs_hash, $configHash); + #$fileName=''. + # _("Additional information on the picture").''; + $fileName = jssafe_uri_escape($fileName); + return $fileName; +} + + +# copy the source image in the dest album, and eventually, perform a +# rotation +sub copyImage { + my ($source, $dest, $imageData, $configHash) = @_; + if ( (! -e "$dest") || (-M "$source" < -M "$dest") ) { + `cp -p "$source" "$dest"`; + system("chmod", "a+r", "$dest") == 0 + or die("\nCannot set write permission on $dest: $?"); + if ($configHash->{rotateImages} eq 'destination') { + # Perform a rotation of the picture if needed + if (rotateImage($dest, $imageData->{'Orientation'}, "", $configHash) + == 1) { + progressifyImage($dest, "", $configHash); + # swap width & height + my $numsizes = $#{$configHash->{scaledWidths}}; + for(my $j=0; $j<=$numsizes; $j++) { + #my $tmp = $imageData->{$j}{'width'}; + #$imageData->{$j}{'width'} = $imageData->{$j}{'height'}; + #$imageData->{$j}{'height'} = $tmp; + ($imageData->{$j}{'width'}, $imageData->{$j}{'height'}) = + ($imageData->{$j}{'height'}, $imageData->{$j}{'width'}); + } + } + } + } +} + +# copy the source file to the given destination +sub copyFile { +} + +sub getHTMLImagePageLink{ + my ($imageData, $size, $num) = @_; + + my $sizeLink = $imageData->{'maxSize'}; + if ($sizeLink >= $size) { + $sizeLink = $size; + } + return jssafe_uri_escape($imageData->{'basename'})."_". + $imageData->{configuration}{sizeNames}[$sizeLink]. + ".jpg.".$num.".html"; +} + +# $album is the album we are generating. +# imagepath is path to actual image location. +sub generateImagesInAlbum{ + my ($album, $albumHashRef, $firstIsIndex, + $configHash, @imagesToDisplay) = @_; + + # an array of references to hashes storing information about each image + my @imageData; + + # generate thumbnails and scaled images for each image + # we will display, and generate HTML + my $numImages = $#imagesToDisplay+1; + my $i; + + #get description information + for($i=0; $i<$numImages; $i++) { + my $crntImage = $album.$imagesToDisplay[$i]; + $imageData[$i] = getDesc($crntImage, $configHash); + #print "-------------------".Dumper($imageData{'configuration')); + } + + #calculate the thumbnail page each image will be on + for($i=0; $i<$numImages; $i++) { + #my $crntImage = $imagesToDisplay[$i]; + $imageData[$i]{'thumbpage'} = getThumbPage($i, $firstIsIndex, + $configHash); + } + + #determine scaled sizes, create scaled copies + my(@largestSize); # array indexed by image number, holds index of largest + # available size for each image. + for ($i=0; $i<$numImages; $i++) { + my $imageConfigHash = $imageData[$i]{'configuration'}; + + $largestSize[$i] = 0; + my $crntImage = $imagesToDisplay[$i]; + beVerbose("\n", 2); + beVerboseN(" Image $crntImage", 1); + + my ($crntImageBase,$imagePath,$crntImageType) = + &fileparse($crntImage, '\.[^.]+\z'); + $imageData[$i]{'filename'} = $crntImage; + $imageData[$i]{'basename'} = $crntImageBase; + $imageData[$i]{'type'} = $crntImageType; + # don't escape the / character + #$imageData[$i]{'imagepath'} = $album; + #print "imagepath : «$imagePath»\n"; + #$imageData[$i]{'imageurl'} = $url; + + # to handle virtual images, make sure the images album exists + # first, make sure web directory exists + if (! -e "$albumdir$album") { + beVerboseN(" Creating dir $albumdir$album", 1); + `mkdir -p "$albumdir$album"` + } + # load the image to check if it is bigger than current max dims + # to see if need new scaled image + + # first try with Image::Size, which is quicker than ImageMagick + my($width, $height) = imgsize("$picdir$album$crntImage"); + if (! defined $width) { + # if Image::Size fails (format not recognized), load with + # ImageMagick and get the size + my($preview) = Image::Magick->new; + # read in the picture + my($x) = $preview->Read("$picdir$album$crntImage"); + warn "$x" if "$x"; + ($width, $height) = $preview->Get('width', 'height'); + } + $imageData[$i]{'width'} = $width; + $imageData[$i]{'height'} = $height; + + #generate thumbnail (with dir if needed) + my $thumbName = "$crntImageBase"."_pre.jpg"; + #$imageData[$i]{'thumblink'} = &getWebBase($album).$thumbName; + $imageData[$i]{'thumblink'} = jssafe_uri_escape($thumbName); + + my ($twidth, $theight); + #if (! -e $albumdir.$album.$thumbName) { + beVerboseN(" Generating thumbnail $album$thumbName from image ". + "$crntImageBase.", 3); + ($twidth, $theight)= + generateThumbnail($album.$crntImage, $album.$thumbName, + $width, $height, $imageData[$i], + $imageData[$i]{'configuration'}); + $imageData[$i]{twidth} = $twidth; + $imageData[$i]{theight} = $theight; + my $j; + # generate scaled sizes, largestSize, write scaled + # version if not oneCopy + my $filling_in_sizes = 0; + my $scaledImage; + my $maxWidth; + my $maxHeight; + my ($scaledWidth, $scaledHeight); + for ($j=0; $j <= $#{$imageConfigHash->{scaledWidths}}; $j++) { + if (! $filling_in_sizes ) { + my $size = $imageConfigHash->{sizeNames}[$j]; + $size =~ s/\.//g ; + $scaledImage = $crntImageBase."_". + $size.".jpg"; + $maxWidth = $imageConfigHash->{scaledWidths}[$j]; + $maxHeight = $imageConfigHash->{scaledHeights}[$j]; + ($scaledWidth, $scaledHeight) = + getScaledSize($width, $height, $maxWidth, $maxHeight, + $imageConfigHash); + beVerboseN(" $imageConfigHash->{sizeNames}[$j] size is ". + "$scaledWidth x $scaledHeight.", 3); + } + # generate scaled version if needed + $imageData[$i]{$j}{'width'} = $scaledWidth; + $imageData[$i]{$j}{'height'} = $scaledHeight; + + if (! $filling_in_sizes ) { + $largestSize[$i] = $j; + if (! $oneCopy) { + ($imageData[$i]{$j}{'width'}, + $imageData[$i]{$j}{'height'}) = + writeRotateScaledVersion($album.$crntImage, + $album.$scaledImage, + $scaledWidth, $scaledHeight, + $imageData[$i], + $imageData[$i]{'configuration'}); + } + #if ( ($width <= $scaledWidth) && ($height <= $scaledHeight) ) { + # $filling_in_sizes = 1; + #} + } + } + # if oneCopy, create a single canonical image copy, + # with the same name as the original + if ($oneCopy) { + if ($imageSource eq "scaled") { + ($imageData[$i]{$j}{'width'}, + $imageData[$i]{$j}{'height'}) = + writeRotateScaledVersion($album.$crntImage, $album.$crntImage, + $imageData[$i]{$largestSize[$i]}{'width'}, + $imageData[$i]{$largestSize[$i]}{'height'}, + $imageData[$i], + $imageData[$i]{'configuration'}); + } elsif ($imageSource eq "copied") { + copyImage($picdir.$album.$crntImage, + $albumdir.$album.$crntImage, + $imageData[$i], $imageConfigHash); + } elsif ($imageSource eq "orig") { #not yet implemented + } elsif ($imageSource eq "custom") { + # gets size in KB + my $fileSize = ((-s "$picdir$album$crntImage") / 1024); + beVerboseN(" Size is $fileSize KB.", 3); + if ($fileSize > 900) { + beVerbose("Image $crntImage is BIG, resizing...", 3); + my( $newW, $newH) = + getScaledSize($imageData[$i]{'width'}, + $imageData[$i]{'height'}, 1600, 1600, + $imageConfigHash); + ($imageData[$i]{$j}{'width'}, + $imageData[$i]{$j}{'height'}) = + writeRotateScaledVersion($album.$crntImage, $album.$crntImage, + $newW, $newH, $imageData[$i], + $imageData[$i]{'configuration'}); + beVerboseN("done.", 3); + } else { + beVerbose("Image $crntImage is SMALL, copying...", 3); + copyImage("$picdir$album$crntImage", + "$albumdir$album$crntImage", + $imageData[$i], $imageConfigHash); + beVerboseN("done.", 3); + } + } + } + beVerboseN(" setting maxSize to $i ($imageData[$i]->{filename})", + 3); + $imageData[$i]->{'maxSize'}= $largestSize[$i]; + } + + # now calculate everything needed to generate html next, prev html + # names, etc + + #print "crntImage is $crntImage\n"; + + for ($i=0; $i < $numImages; $i++) { + my $imageConfigHash = $imageData[$i]{'configuration'}; + + #print "-------------------".Dumper($imageConfigHash); + my $crntImage = $imagesToDisplay[$i]; + my ($crntImageBase,$imagePath,$crntImageType) = + &fileparse($crntImage, '\.[^.]+\z'); + + #now generate scaled versions and HTML + for (my $j=0; $j <= $imageData[$i]->{maxSize}; $j++) { + #print "\$j = $j\n"; + my $maxWidth = $imageConfigHash->{scaledWidths}[$j]; + my $maxHeight = $imageConfigHash->{scaledHeights}[$j]; + # suffix of filename of this size (add .html for html, + # needs prefix) + #my $crntSizeSuffix = ; + my $size = $imageConfigHash->{sizeNames}[$j]; + $size =~ s/\.//g ; + my $scaledImage = $crntImageBase."_".$size.".jpg"; + #determine next, prev image + my $nextImageNum = ($i+1) % ($numImages); + my $prevImageNum = ($i-1+$numImages)% ($numImages) ; + my $npdiff = $nextImageNum - $prevImageNum; + #my ($xbase, $xpath, $xtype) = + #fileparse($imagesToDisplay[$nextImageNum], '\.[^.]+\z'); + + my $sizeLink = $imageData[$nextImageNum]->{'maxSize'}; + if ($sizeLink >= $j ) { + $sizeLink = $j; + } + + $imageData[$i]{$j}{'nextHTML'} = + getHTMLImagePageLink($imageData[$nextImageNum], $j, + $nextImageNum); + + $imageData[$i]{$j}{'preloadIMG'} = + jssafe_uri_escape($imageData[$nextImageNum]{'basename'})."_". + $imageData[$nextImageNum]{configuration}{sizeNames}[$sizeLink]. + ".jpg"; + $imageData[$i]{$j}{'imgNum'} = $i + 1; + $imageData[$i]{$j}{'imgCount'} = $numImages; + $imageData[$i]{$j}{'nextIsFirst'} = ($nextImageNum == 0); + $imageData[$i]{$j}{'prevIsLast'} = (($prevImageNum + 1) == $numImages); + + $imageData[$i]{$j}{'prevHTML'} = + getHTMLImagePageLink($imageData[$prevImageNum], $j, + $prevImageNum); + + $imageData[$i]{$j}{'nextTitle'}= $imageData[$nextImageNum]{'title'}; + $imageData[$i]{$j}{'prevTitle'}= $imageData[$prevImageNum]{'title'}; + + + if ($configHash->{thumbPrevNext}) { + $imageData[$i]{$j}{'nextThumb'} = + jssafe_uri_escape($imageData[$nextImageNum]{'basename'})."_pre.jpg"; + $imageData[$i]{$j}{'prevThumb'} = + jssafe_uri_escape($imageData[$prevImageNum]{'basename'})."_pre.jpg"; + } + + # used in URL + $imageData[$i]{$j}{'htmlFile'}= jssafe_uri_escape($scaledImage). + ".$i.html"; + # used to access the file on disk + $imageData[$i]{$j}{'htmlFileName'}= $scaledImage.".$i.html"; + + # sizedFile is the text to go into the link for this image size + if ($oneCopy) { + #$imageData[$i]{$j}{'sizedFile'} = + # &getWebBase($album).$imageData[$i]{'filename'}; + $imageData[$i]{$j}{'sizedFile'} = $crntImageBase.$crntImageType; + } else { + #$imageData[$i]{$j}{'sizedFile'} = + # &getWebBase($album).$imageData[$i]{'imagepath'}. + # $scaledImage; + $imageData[$i]{$j}{'sizedFile'} = $scaledImage; + } + } + + $imageData[$i]{'detailsLink'} = + generateSecondaryFieldsPage($imageData[$i], $album, $albumHashRef, + $crntImageBase.$crntImageType, + $configHash); + if ( $configHash->{searchEngine}) { + writeSearchString($imageData[$i], $configHash, $albumHashRef, + $album); + } + } + + + + # now generate html + for ($i=0; $i<$numImages; $i++) { + for (my $j=0; $j <= $imageData[$i]->{maxSize}; $j++) { + my $lastImageURL = getHTMLImagePageLink($imageData[$numImages-1], + $j, $numImages-1); + my $firstImageURL = getHTMLImagePageLink($imageData[0], $j, 0); + generateImage($imageData[$i], $j, $album, + $albumHashRef, $imageData[$i]{'configuration'}, + $firstImageURL, $lastImageURL); + } + } + + #printImageData(@imageData); + beVerboseN(" We have ". scalar(@imageData). " images in the album.", 2); + return @imageData; +} + +sub getThumbPage{ + my ($crntNumber, $firstIsIndex, $configHash) = @_; + my $thumbPageNumber = int($crntNumber/$configHash->{numThumbsPerPage}); + return "index.html" if ($thumbPageNumber == 0 && $firstIsIndex); + return "thumb$thumbPageNumber.html"; +} + +sub generateThumbnail{ + my ($crntImage, $thumbName, $width, $height, $imageData, $configHash) = @_; + my ($newWidth, $newHeight) = getScaledSize($width, $height, + $configHash->{previewMaxWidth}, + $configHash->{previewMaxHeight}, + $configHash); + return writeRotateScaledVersion($crntImage, $thumbName, $newWidth, + $newHeight, $imageData, $configHash); +} + +sub getScaledSize{ + my ($width, $height, $maxWidth, $maxHeight, $configHash) = @_; + my $xfactor; + my $yfactor; + + #if width or height are zero, that is a problem + if (not ( $width and $height ) ) { + return ($maxWidth,$maxHeight); + } + if (substr($maxWidth, -1) eq "%") { + chop($maxWidth); + $xfactor = $maxWidth / 100; + $maxWidth = $width * $xfactor; + } else { + $xfactor = $maxWidth/$width; + } + + if (substr($maxHeight, -1) eq "%") { + chop($maxHeight); + $yfactor = $maxHeight / 100; + $maxHeight = $height * $yfactor; + } else { + $yfactor = $maxHeight/$height; + } + + my ($newWidth, $newHeight); + if ($xfactor <= $yfactor) { + $newWidth = $maxWidth; + $newHeight = $xfactor*$height; + }else{ + $newHeight = $maxHeight; + $newWidth = $yfactor*$width; + } + ($newWidth, $newHeight) = (int($newWidth), int($newHeight)); + + if ($width<$newWidth || $height<$newHeight) { + if ($configHash->{whenSrcSmaller} eq 'enlarge') { + beVerboseN("Enlarging small image from ${width}x${height} to ${newWidth}x${newHeight}", 2); + } elsif ($configHash->{whenSrcSmaller} eq 'original') { + beVerboseN("Keeping original size of small image, instead of from ${width}x${height} to ${newWidth}x${newHeight}", 2); + ($newWidth, $newHeight) = ($width, $height); + } elsif ($configHash->{whenSrcSmaller} eq 'skip') { + beVerboseN("Skipping small image instead of scaling from ${width}x${height} to ${newWidth}x${newHeight}", 2); + next; + } else { + warn "Unrecognized configuration setting for \"whenSrcSmaller\":".$configHash->{whenSrcSmaller}; + } + } + + return ($newWidth, $newHeight); +} + +# takes origName and newName with path info (but not pic_dir, of course) +sub writeScaledVersion{ + my ($origName, $newName, $newWidth, $newHeight, + $imageRef, $configHash) = @_; + beVerbose(" Generating scaled version of $picdir$origName\n". + " to be written to $newName... ", 2); + if (-e "$albumdir$newName"){ + if (((lstat("$albumdir$newName"))[9]) >= ((stat("$picdir$origName"))[9])){ + beVerboseN("\n image already exists and is newer, skipping.", 2); + return 0; + } + } + + my($preview) = Image::Magick->new; + my($x) = $preview->Read("$picdir$origName"); # read in the picture + warn "$x" if "$x"; + + my $format = $preview->Get("magick"); + if (!$configHash->{scaleIfSameSize} + and grep (/^$format$/, @webFormats ) ) { + my ($width, $height) = $preview->Get("width", "height"); + if ($width == $newWidth && $height == $newHeight) { + if ($configHash->{linkInsteadOfCopy} && + (! ($configHash->{rotateImages} eq 'destination') || + (! defined $imageRef->{'Orientation'} || + $imageRef->{'Orientation'} eq "top_left"))) { + beVerbose("\n Image has the right size, just linking... ", 2); + my $newpath; + if( $configHash->{linkRelative} ) { + $newpath = relpath("$albumdir$newName", "$picdir$origName"); + } else { + $newpath = "$picdir$origName"; + } + beVerboseN("Linking from $albumdir$newName to $newpath... ", 2); + system("ln", "-sf", $newpath, "$albumdir$newName") == 0 + or die("\nCannot link $albumdir$newName to $newpath: $?"); + # the original file may be r/o but we don't have to modify it + # but it must be readable by the http deamon + if ($configHash->{updateOriginalPerms}) + { + system("chmod", "a+r", "$picdir$origName") == 0 + or die("\nCannot set read permission on $albumdir$newName: $?"); + } + beVerboseN("done.", 2); + return 0; # Image is processed, no need to try to process it + # again. + } else { + beVerbose("\n Image has the right size, just copying... ", 2); + system("cp", "-p", "$picdir$origName", "$albumdir$newName") == 0 + or die("\nCannot copy $picdir$origName to $albumdir$newName: $?"); + # make it writable in case $origName was r/o + system("chmod", "u+w,a+r", "$albumdir$newName") == 0 + or die("\nCannot set write permission on $albumdir$newName: $?"); + beVerboseN("done.", 2); + return 1; + } + } + } + $x = $preview->Set(quality => $configHash->{jpegQuality}); + warn "$x" if "$x"; + if ($configHash->{deExifyImages}) { + $x = $preview->Profile(name => "*", profile => ""); + warn "$x" if "$x"; + } + if ($configHash->{scaleMethod} eq "sample") { + $x = $preview->Sample(width=>$newWidth, height=>$newHeight); + } else { + if ($configHash->{scaleMethod} ne "scale") { + warn "Unknown scaleMethod $configHash->{scaleMethod}, using scale" + } + $x = $preview->Scale(width=>$newWidth, height=>$newHeight); + } + warn "$x" if "$x"; + my $borderOnThumbnails = $configHash->{borderOnThumbnails}; + if ($borderOnThumbnails) { + $x = $preview->Border(color=>'black',width=>$borderOnThumbnails, + height=>$borderOnThumbnails); + warn "$x" if "$x"; + } + beVerbose("\n Writing scaled image $albumdir$newName... ", 3); + $x = $preview->Write("$albumdir$newName"); + warn "$x" if "$x"; + beVerboseN("done.", 2); + return 1; +} + +sub writeRotateScaledVersion{ + my ($origName, $newName, $newWidth, $newHeight, $imageRef, + $configHash) = @_; + + if (writeScaledVersion($origName, $newName, $newWidth, $newHeight, + $imageRef, $configHash)){ + if ($configHash->{rotateImages} eq 'destination'){ + # Perform a rotation of the picture if needed + if (rotateImage("$albumdir$newName", $imageRef->{'Orientation'}, "", + $configHash) == 1){ + progressifyImage("$albumdir$newName", "", $configHash); + return ($newHeight, $newWidth); + } + } + progressifyImage("$albumdir$newName", "", $configHash); + return ($newWidth, $newHeight); + } + ($newWidth, $newHeight) = imgsize($albumdir.$newName); + return ($newWidth, $newHeight); +} + +# $album is album we are generating, _not_ path to image. +# For the actual image path, use $imageHashRef +sub generateImage { + my ($imageHashRef, $size, $album, $albumHashRef, $configHash, + $firstImage, $lastImage) = @_; + my $crntImage = $imageHashRef->{'filename'}; + my $imagetodisplay = $imageHashRef->{$size}{'sizedFile'}; + my $filesize = &fileSize("$albumdir$album$imagetodisplay"); + $imagetodisplay = jssafe_uri_escape($imagetodisplay); + + my $imagetitle = $crntImage; + $imagetitle =~ s/\.(\S+)\Z//; + $imagetitle =~ s/_/ /g; + + my($fileExtension) = $imageHashRef->{'type'}; + if ($fileExtension =~ /jpg|jpeg/i) { + $fileExtension = "JPEG"; + } elsif ($fileExtension =~ /gif/i) { + $fileExtension = "GIF"; + } elsif ($fileExtension =~ /png/i) { + $fileExtension = "PNG"; + } else { + # if the type is unwkown, the image has been converted to JPEG + $fileExtension = "JPEG"; + } + + my $width = $imageHashRef->{$size}{width}; + my $height = $imageHashRef->{$size}{height}; + + my $pictureinfo = + _('$filesize $fileExtension image, $width x $height pixels'); + $pictureinfo = eval "\"$pictureinfo\""; + + #add strings to substitute hash + my %subs_hash; + #$subs_hash{ALBUM_TITLE} = $album; + $subs_hash{WIDTH} = $width+2; + $subs_hash{HEIGHT} = $height+2; + $subs_hash{IMAGE_TO_DISPLAY} = $imagetodisplay; + $subs_hash{PICTURE_INFO} = $pictureinfo; + #$subs_hash{''} = getRootDir($album); + $subs_hash{NEXT_IMAGE} = $imageHashRef->{$size}{nextHTML}; + if ($configHash->{imagePageCycling}) { + $subs_hash{NEXT_IMAGE_PAGE} = 1; + $subs_hash{PREV_IMAGE_PAGE} = 1; + } else { + $subs_hash{NEXT_IMAGE_PAGE} = ! $imageHashRef->{$size}{nextIsFirst}; + $subs_hash{PREV_IMAGE_PAGE} = ! $imageHashRef->{$size}{prevIsLast}; + } + $subs_hash{IMG_NUM} = $imageHashRef->{$size}{imgNum}; + $subs_hash{IMG_COUNT} = $imageHashRef->{$size}{imgCount}; + if ($configHash->{javaScriptPreloadImage}) { + $subs_hash{IMG_PRELOAD} = $imageHashRef->{$size}{preloadIMG}; + } + $subs_hash{PREV_IMAGE} = $imageHashRef->{$size}{prevHTML}; + $subs_hash{NEXT_TITLE} = $imageHashRef->{$size}{nextTitle}; + $subs_hash{PREV_TITLE} = $imageHashRef->{$size}{prevTitle}; + if ($configHash->{thumbPrevNext}) { + $subs_hash{NEXT_THUMB} = $imageHashRef->{$size}{nextThumb}; + $subs_hash{PREV_THUMB} = $imageHashRef->{$size}{prevThumb}; + } + $subs_hash{FILE_NAME} = encode_entities($imageHashRef->{basename}) . + $imageHashRef->{'type'}; + $subs_hash{THUMB_PAGE} = $imageHashRef->{thumbpage}; + $subs_hash{SIZE_LINKS} = getSizeLinks($size, $imageHashRef, $configHash); + my $title = $imageHashRef->{title}; + $subs_hash{ALBUM_PATH_LINKS} = + pathLinks($album, $title, @{$albumHashRef->{parentDirNames}}); + $subs_hash{TITLE} = $title; + $subs_hash{DESC_TABLE} = getDescTable(\%{$imageHashRef}, $configHash); + $subs_hash{DETAILS_LINK} = $imageHashRef->{detailsLink}; + $subs_hash{STRING_DETAILS} = _("Additional information on the picture"); + + $subs_hash{NAV_BAR_TABLE} = + navBarLinks('image', $album, $configHash, %$albumHashRef); + + $subs_hash{TREE_NAME} = _("tree"); + $subs_hash{TREE_TITLE} = _("Tree of all albums and sub-albums"); + $subs_hash{TREE_LINK} = getRootDir($album)."tree.html"; + $subs_hash{STATIC_PATH} = + getRootDir($album)."static.".$configHash->{templateStyle}; + $subs_hash{HOME_LINK} = $configHash->{homeURL}; + $subs_hash{FEEDBACK_LINK} = $configHash->{feedbackMail}; + $subs_hash{DATE} = strftime($configHash->{dateString}, localtime); + if ($configHash->{backgroundImage}) { + # Do not set this if not configured, so that template + # can check for whether defined. + $subs_hash{BG_IMAGE} = + $subs_hash{STATIC_PATH}."/".$configHash->{backgroundImage}; + } + $subs_hash{CUSTOM_CSS} = $configHash->{customStyleSheet}; + $subs_hash{PATH_IMG_NUM} = $configHash->{pathImgNum}; + $subs_hash{PATH_SHOW_ICON} = $configHash->{pathShowIcon}; + $subs_hash{FIRST_IMAGE} = $firstImage; + $subs_hash{LAST_IMAGE} = $lastImage; + $subs_hash{IMAGE_COMMENT} = $imageHashRef->{comment}; + + renderTemplate("image", + $albumdir.$album.$imageHashRef->{$size}{htmlFileName}, + \%subs_hash, $configHash); +} + +sub getSizeLinks{ + my ($size, $imageHashRef, $configHash) = @_; + my @sizes; + + if ( $size eq 'full' ) { + $size = -1; + } + for my $i (0..$imageHashRef->{'maxSize'}) { + if ($i != $size) { # output link + push @sizes, {SIZE_NAME => $configHash->{longSizeNames}[$i], + SIZE_LINK => $imageHashRef->{$i}{'htmlFile'}, + SIZE_TITLE => $imageHashRef->{$i}{'width'}."x". + $imageHashRef->{$i}{'height'}, + SIZE_FILE => $configHash->{fileSizeNames}[$i], + }; + } else { #mark as current page + push @sizes, {SIZE_NAME => $configHash->{longSizeNames}[$i], + SIZE_FILE => $configHash->{fileActiveSizeNames}[$i]}; + } + } + return \@sizes; +} + +sub getDescTable{ + my ($hashref, $configHash) = @_; + + my @descTable; + foreach my $tagName (@mainFields) { + if (${%$hashref}{$tagName}) { + my $value=${%$hashref}{$tagName}; + $value =~ s/'/'/g ; # in case it's used in javascript code + push @descTable, {DESC_FIELD_NAME => getFields($configHash)->{$tagName}->{'Name'}, + DESC_FIELD_VALUE => $value, + }; + } + } + return \@descTable; +} + +# Given a path, not including $picdir, gives the relative path back +# to the root of the hierarchy. Note that the trailing slash is critical for the +# current implementation. For example, if given as input sample_dir/, which +# corresponds to web/sample_dir/ and pic_dir/sample_dir/, the function +# returns "../". +# changed on 11/15 so it only points into base dir of web hierarchy +sub getRootDir{ + my $path = $_[0]; + my $slashcount = 0; + $slashcount++ while($path =~ m'/'g); + my $relPath =""; + my $i; + for($i=$slashcount; $i>0; $i--) { + $relPath = $relPath."../"; + } + return $relPath; +} + +# given an album (path), not including $albumdir, +# provides the relative path back to $albumdir. +# closely related to getRootDir +sub getWebBase{ + my $path = $_[0]; + my $slashcount = 0; + $slashcount++ while($path =~ m'/'g); + my $relPath =""; + my $i; + for($i=$slashcount; $i>0; $i--) { + $relPath = $relPath."../"; + } + return $relPath; +} + + + +# given the name of a directory in $picdir, not including the leading +# $picdir, generates the entry for the table on the home page +sub generateAlbumEntry { + my $album = shift(@_); + my $prettyalbum = $album; + $prettyalbum =~ s/_/ /g; # replaces underscores with spaces + my $result = ''.$prettyalbum.'
'; + return $result; +} + +# return list of directories where templates can be found +sub templateDirs { + my $configHash = shift; + my @dirs; + + if ($templateDir) { + push(@dirs, bsd_glob($templateDir."/templates.". + $configHash->{templateStyle}, GLOB_TILDE)); + } + push(@dirs, bsd_glob($configHash->{userConfigDir}."/templates.". + $configHash->{templateStyle}, GLOB_TILDE)); + push(@dirs, bsd_glob($configHash->{globalDataDir}."/templates.". + $configHash->{templateStyle}, GLOB_TILDE)); + + return @dirs; +} + +# return static directory path (with dir) if it exists. +sub templateStaticDir { + my $configHash = shift; + + my $staticDir; + my @dirs = templateDirs($configHash); + + foreach my $dir (@dirs) { + beVerboseN(" Looking for static template directory in $dir...", 4); + if (-d $dir."/static") { + $staticDir = $dir."/static"; + beVerboseN(" Found static template directory $staticDir.", 4); + last; + } + } + return $staticDir; +} + + +# return template file (with dir) from template name +sub templateFileName { + my $templateName = shift; + my $configHash = shift; + + my $templateFile; + my @dirs = templateDirs($configHash); + + if ($templateDir) { + push(@dirs, bsd_glob($templateDir."/templates.". + $configHash->{templateStyle}, GLOB_TILDE)); + } + push(@dirs, bsd_glob($configHash->{userConfigDir}."/templates.". + $configHash->{templateStyle}, GLOB_TILDE)); + push(@dirs, bsd_glob($configHash->{globalDataDir}."/templates.". + $configHash->{templateStyle}, GLOB_TILDE)); + + foreach my $dir (@dirs) { + beVerboseN(" Looking for HTML template $templateName in $dir...", 4); + if (-f $dir."/".$templateName) { + $templateFile = $dir."/".$templateName; + beVerboseN(" Found HTML template $templateFile...", 4); + last; + } + } + if (!$templateFile) { + print("Error: cannot find HTML template $templateName\n"); + exit 2; + } + return $templateFile; +} + +# render an html file from the templates +sub renderTemplate{ + my $templateName = shift; + my $outputFileName = shift; + my $templateParameters = shift; + my $configHash = shift; + + renderFile(templateFileName($templateName.".html", $configHash), + $outputFileName, + $templateParameters, + $configHash, + ); +} + +# render a file (replace template tags with real values) +sub renderFile{ + my $sourceFileName = shift; + my $outputFileName = shift; + my $templateParameters = shift; + my $configHash = shift; + + %{$templateParameters} = + (%{$templateParameters}, + %{$configHash->{colorsSubs}{$configHash->{colorStyle}}}, + %{getIntlSubs($configHash)}, + ); + + # open the html template + #my $template = + # HTML::Template::JIT->new(jit_debug => 1, + # jit_path => '/tmp', + + my $template = + HTML::Template->new( + filename => $sourceFileName, + blind_cache => 1, + loop_context_vars => 1, + die_on_bad_params => 0, + global_vars => 1, + ); + + # fill in the parameters + $template->param($templateParameters); + + open(OUTFILE, ">$outputFileName") or die "Couldn't open $outputFileName"; + + if ($configHash->{compactHTML}){ + my $page = $template->output(); + my $h = new HTML::Clean(\$page); + $h->strip({whitespace => 1, + shortertags => 1, + blink => 0, + contenttype => 0, + comments => 1, + entities => 0, + dequote => 1, + defcolor => 1, + javascript => 1, + htmldefaults => 0, # when set to 1, htmldefaults cause problems + # with UTF-8 encodings + lowercasetags => 0, + meta => "", + }); + my $page2 = $h->data(); + print OUTFILE $$page2; + } else { + $template->output(print_to => *OUTFILE); + } + close(OUTFILE); +} + +# Given an image file (no album_dir, with path, as usual), returns +# name of the .xml file associated with this image +sub getDescFile{ + my $crntImage = shift(@_); + my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z'); + my $descFile = $picdir.$dir.$base.$type.".xml"; + if( ! -e $descFile){ + $descFile = $picdir.$dir.$base.".xml"; + } + if( ! -e $descFile){ + $descFile = $picdir.$dir.$base.$type.".xml"; + } + return $descFile; +} + +sub getXMLAsGrove{ + my $file=shift(@_); + + #my $sample = XML::Handler::Sample->new(); + + + # Get XML document as a Grove + my $grove_builder = XML::Grove::Builder->new; + my $parser = XML::Parser::PerlSAX->new ( Handler => $grove_builder); + #my $parser = XML::SAX::Expat->new ( Handler => $grove_builder); + return $parser->parse ( Source => { SystemId => $file } ); +} + +sub getDesc{ + my $crntImage = shift(@_); + my $configHash= shift(@_); + + my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z'); + my $descFile = getDescFile($crntImage); + + my %descHash; + my %exifHash; + my $document; + my @priorityList; + + if (-e $descFile) { + beVerboseN(" Reading desc file $descFile.", 3); + $document = getXMLAsGrove($descFile); + %descHash = getDescXML($document, $configHash); + #$descHash{descFileName} = uri_escape($descFile, '^-A-Za-z0-9/_\.'); + $configHash = getConfigXML($document, '/image/bins', $configHash); + %exifHash = getExifXML($document, \@priorityList); + } else { + $descHash{descFileName} = ""; + } + processExif($crntImage, \%descHash, \%exifHash, $document, \@priorityList, + $configHash); + + # If no title is set, use the picture file name. + if (!trimWhiteSpace($descHash{'title'})) { + # If trimming whitespace gets rid of everything (returns empty string) + my $imagetitle = $base; + $imagetitle =~ s/_/ /g; # replace underscores with spaces + $descHash{'title'} = local2html($imagetitle); + } + + $descHash{'configuration'} = $configHash; + return \%descHash; +} + +# Given an XML doc as a Grove, returns +# fields from that images description file. +sub getDescXML{ + my ($document, $configHash) = @_; + my $fieldName; + my $fieldValue; + my %descHash; + + beVerboseN(" Reading user description fields...", 3); + + foreach my $element + (@{$document->at_path('/image/description')->{Contents}}) { + if (UNIVERSAL::isa($element, 'XML::Grove::Element') + && $element->{Name} eq "field") { + $fieldName = $element->{Attributes}{'name'}; + $fieldValue = ""; + if (grep (/^$fieldName$/, keys(%{getFields($configHash)}))) { + beVerbose(" Reading field '$fieldName':", 3); + foreach my $characters (@{$element->{Contents}}) { + #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) { + $fieldValue .= $characters->as_canon_xml(); + } + $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue))); + beVerbose("'".$fieldValue."'\n", 3); + $descHash{$fieldName} = $fieldValue; + } else { + beVerbose(" Ignoring unknown field '$fieldName'.", 3); + } + } + } + return %descHash; +} + +# Given an XML doc as a Grove, returns +# hash from the Exif fields of the description file. +sub getExifXML{ + my $document = shift; + my $priorityList = shift; + #my $fieldNb; + my $fieldName; + my $fieldValue; + my %descHash; + + beVerboseN(" Reading Exif data from description file...", 3); + + @{$priorityList} = @priorityExifTags; + + foreach my $element + (@{$document->at_path('/image/exif')->{Contents}}) { + if (UNIVERSAL::isa($element, 'XML::Grove::Element') + && $element->{Name} eq "tag") { + #$fieldNb = $element->{Attributes}{'no'}; + $fieldName = $element->{Attributes}{'name'}; + $fieldValue = ""; + beVerbose(" Reading Exif field '$fieldName':", 3); + foreach my $characters (@{$element->{Contents}}) { + #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) { + $fieldValue .= $characters->as_canon_xml(); + } + if (defined $fieldValue){ + $fieldValue = trimWhiteSpace(decode_entities(xml2html($fieldValue))); + } else { + $fieldValue = ""; + } + beVerbose("'".$fieldValue."'\n", 3); + + $descHash{$fieldName} = $fieldValue; + + if ($element->{Attributes}{'priority'}){ + push @{$priorityList}, $fieldName; + } + } + } + return %descHash; +} + +sub addExif{ + my $tagName = shift; + my $tagValue = shift; + my $exifHash = shift; + my $priorityList = shift; + + $tagValue = trimWhiteSpace($tagValue); + + if (grep (/^$tagName$/, @{$priorityList})){ + beVerboseN(" Keeping value '$exifHash->{$tagName}' for $tagName", 3); + beVerboseN(" $tagName has priority over exif value '$tagValue'.", 3); + return 0; + } + + if ( ! (exists $exifHash->{$tagName} && + $exifHash->{$tagName} eq $tagValue) ) { + beVerboseN(" Found new value '".$tagValue."' for $tagName", 3); + beVerboseN(" old value was '".$exifHash->{$tagName}."'", 3) + if ($exifHash->{$tagName}); + beVerboseN(" bla '".$exifHash->{$tagName}."'", 3) + if ($exifHash->{$tagName}); + + $exifHash->{$tagName} = $tagValue; + return 1; + } + return 0; +} + +sub addSpecialExif{ + my $tagName = shift; + my $tagValue = shift; + my $exifHash = shift; + my $priorityList = shift; + + if (UNIVERSAL::isa(\$tagValue, 'SCALAR')){ + # Canon special tags + if ($tagName eq "Canon-Tag-0x0007"){ + return addExif("Software", $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "Canon-Tag-0x0009"){ + return addExif("Owner", $tagValue, $exifHash, $priorityList); + } + } elsif (UNIVERSAL::isa($tagValue, 'ARRAY')) { + if ($tagValue->[1] eq 0) { + return 0; + } + if ($tagName eq "SubjectDistance") { + $tagValue = $tagValue->[0]/$tagValue->[1]; + $tagValue .= translate(" meters"); + return addExif($tagName, $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "ExposureTime") { + if (($tagValue->[0] == 0) || ($tagValue->[1] == 0)) { + $tagValue = 0; + } else { + $tagValue = "1/".sprintf("%.0f", 1/($tagValue->[0]/$tagValue->[1])); + } + $tagValue .= translate(" second"); + return addExif($tagName, $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "ShutterSpeedValue") { + # this is the actual APEX Value + my $APEXval = $tagValue->[0]/$tagValue->[1]; + + # ...but we want to see time in seconds instead + if ($APEXval > 0) { + $tagValue = sprintf("1/%d", 2**((1)*$APEXval)); + } else { + $tagValue = sprintf("%d", 2**((-1)*$APEXval)); + } + $tagValue .= translate(" sec"); + return addExif($tagName, $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "FNumber" + || $tagName eq "FocalPlaneXResolution" + || $tagName eq "FocalPlaneYResolution") { + $tagValue = sprintf("%.2f", $tagValue->[0]/$tagValue->[1]); + return addExif($tagName, $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "FocalLength") { + $tagValue = sprintf("%.2f", $tagValue->[0]/$tagValue->[1]). + translate(" millimeters"); + return addExif($tagName, $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "ApertureValue" || $tagName eq "MaxApertureValue") { + $tagValue = "F".sprintf("%.2f", sqrt(2)**($tagValue->[0]/$tagValue->[1])). + " (".sprintf("%.2f", $tagValue->[0]/$tagValue->[1])." APEX)"; + return addExif($tagName, $tagValue, $exifHash, $priorityList); + } + if ($tagName eq "BitsPerSample") { + my $result=""; + $result .= $_." " foreach(@{$tagValue}); + return addExif($tagName, $result, $exifHash, $priorityList); + } + if ($tagName eq "CompressedBitsPerPixel") { + my $result; + SWITCH: { + $tagValue->[0]==1 && do { $result="Basic"; last; }; + $tagValue->[0]==2 && do { $result="Normal"; last; }; + $tagValue->[0]==3 && do { $result="Normal"; last; }; + $tagValue->[0]==4 && do { $result="Fine"; last; }; + $tagValue->[0]==5 && do { $result="Very fine"; last; }; + } + $result .= " ($tagValue->[0]:$tagValue->[1])"; + return addExif($tagName, $result, $exifHash, $priorityList); + } + if ($tagName eq "Canon-Tag-0x0001") { + # CanonMacro + my $value = $tagValue->[1]; + if ($value == 1) { + $value = translate("On"); + } elsif ($value == 2) { + $value = translate("Off"); + } else { + $value=""; + } + my $modified = addExif("CanonMacro", $value, $exifHash, $priorityList) + if($value); + # CanonTimerLength + $value = $tagValue->[2]; + if ($value) { + $value = $value."/10".translate(" second"); + $modified |= addExif("CanonTimerLength", $value, $exifHash, + $priorityList); + } + # CanonQuality + $value = $tagValue->[3]; + if ($value == 2) { + $value = translate("Normal"); + } elsif ($value == 3) { + $value = translate("Fine"); + } elsif ($value == 5) { + $value = translate("Superfine"); + } else { + $value=""; + } + $modified |= addExif("CanonQuality", $value, $exifHash, $priorityList) + if ($value); + # CanonFlashMode + $value = $tagValue->[4]; + if ($value == 1) { + $value = translate("Auto"); + } elsif ($value == 2) { + $value = translate("On"); + } elsif ($value == 3) { + $value = translate("Red-eye reduction"); + } elsif ($value == 4) { + $value = translate("Slow synchro"); + } elsif ($value == 5) { + $value = translate("Auto + red-eye reduction"); + } elsif ($value == 6) { + $value = translate("On + red-eye reduction"); + } elsif ($value == 16) { + $value = translate("External flash"); + } else { + $value=""; + } + $modified |= addExif("CanonFlashMode", $value, $exifHash, $priorityList) + if ($value); + # CanonContinuousDriveMode + $value = $tagValue->[5]; + if ($value) { + $value = translate("On"); + $modified |= addExif("CanonContinuousDriveMode", $value, $exifHash, + $priorityList); + } + # CanonFocusMode + $value = $tagValue->[7]; + if ($value == 1) { + $value = translate("AI Servo"); + } elsif ($value == 2) { + $value = translate("AI Focus"); + } elsif ($value == 3) { + $value = translate("MF"); + } elsif ($value == 4) { + $value = translate("Single"); + } elsif ($value == 5) { + $value = translate("Continuous"); + } elsif ($value == 6) { + $value = translate("MF"); + } elsif ($value == 0) { + $value = translate("One-Shot"); + } else { + $value=""; + } + $modified |= addExif("CanonFocusMode", $value, $exifHash, $priorityList) + if ($value); + # CanonImageSize + $value = $tagValue->[10]; + if ($value == 0) { + $value = translate("Large"); + } elsif ($value == 1) { + $value = translate("Medium"); + } elsif ($value == 2) { + $value = translate("Small"); + } else { + $value=""; + } + $modified |= addExif("CanonImageSize", $value, $exifHash, $priorityList) + if ($value); + # CanonEasyShootingMode + $value = $tagValue->[11]; + if ($value == 1) { + $value = translate("Manual"); + } elsif ($value == 2) { + $value = translate("Landscape"); + } elsif ($value == 3) { + $value = translate("Fast Shutter"); + } elsif ($value == 4) { + $value = translate("Slow Shutter"); + } elsif ($value == 5) { + $value = translate("Night"); + } elsif ($value == 6) { + $value = translate("B&W"); + } elsif ($value == 7) { + $value = translate("Sepia"); + } elsif ($value == 8) { + $value = translate("Portrait"); + } elsif ($value == 9) { + $value = translate("Sports"); + } elsif ($value == 10) { + $value = translate("Macro / Close-Up"); + } elsif ($value == 11) { + $value = translate("Pan Focus"); + } elsif ($value == 0) { + $value = translate("Full Auto"); + } else { + $value=""; + } + $modified |= addExif("CanonEasyShootingMode", $value, $exifHash, + $priorityList) + if ($value); + # CanonDigitalZoom + $value = $tagValue->[12]; + if ($value == 1) { + $value = translate("2x"); + } elsif ($value == 2) { + $value = translate("4x"); + } elsif ($value == 0) { + $value = translate("None"); + } else { + $value=""; + } + $modified |= addExif("CanonDigitalZoom", $value, $exifHash, $priorityList) + if ($value); + # CanonContrast + $value = $tagValue->[13]; + if ($value == 0xFFFF) { + $value = translate("Low"); + } elsif ($value == 0x0000) { + $value = translate("Normal"); + } elsif ($value == 0x0001) { + $value = translate("High"); + } else { + $value=""; + } + $modified |= addExif("CanonContrast", $value, $exifHash, $priorityList) + if ($value); + # CanonSaturation + $value = $tagValue->[14]; + if ($value == 0xFFFF) { + $value = translate("Low"); + } elsif ($value == 0x0000) { + $value = translate("Normal"); + } elsif ($value == 0x0001) { + $value = translate("High"); + } else { + $value=""; + } + $modified |= addExif("CanonSaturation", $value, $exifHash, $priorityList) + if ($value); + # CanonSharpness + $value = $tagValue->[15]; + if ($value == 0xFFFF) { + $value = translate("Low"); + } elsif ($value == 0x0000) { + $value = translate("Normal"); + } elsif ($value == 0x0001) { + $value = translate("High"); + } else { + $value=""; + } + $modified |= addExif("CanonSharpness", $value, $exifHash, $priorityList) + if ($value); + # CanonISO + $value = $tagValue->[16]; + if ($value == 15) { + $value = translate("Auto"); + } elsif ($value == 16) { + $value = "50"; + } elsif ($value == 17) { + $value = "100"; + } elsif ($value == 18) { + $value = "200"; + } elsif ($value == 19) { + $value = "400"; + } else { + $value=""; + } + $modified |= addExif("CanonISO", $value, $exifHash, $priorityList) + if ($value); + # CanonFocusType + if ($tagValue->[18]) { + $value = $tagValue->[18]; + if ($value == 1) { + $value = translate("Auto"); + } elsif ($value == 3) { + $value = translate("Close-up (macro)"); + } elsif ($value == 8) { + $value = translate("locked (pan mode)"); + } elsif ($value == 0) { + $value = translate("Manual"); + } else { + $value=""; + } + $modified |= addExif("CanonFocusType", $value, $exifHash, $priorityList) + if ($value); + } + return $modified; + } + } + return 0; +} + +# Merge picture Exif data with Exif data from desc file, then update +# descHash if tag is in @fields and if description text file field is void. +# Finally, write Exif tags to the desc file if it was updated. +sub processExif{ + my $crntImage = shift(@_); + my $descHash = shift(@_); + my $exifHash = shift(@_); + my $document = shift(@_); + my $priorityList = shift(@_); + my $configHash = shift(@_); + my ($base,$dir,$type) = &fileparse($crntImage, '\.[^.]+\z'); + + beVerboseN(" Reading Exif info from picture file $crntImage...", 2); + my $pictureFile = $picdir.$dir.$base.$type; + my($camerainfo) = image_info($pictureFile); + if (exists $camerainfo->{"error"}) { + beVerboseN(" Can't read info from picture file $crntImage: ". + $camerainfo->{"error"}."\n", 2); + return + } + + # Add new Exif tags from picture file to Exif Hash from desc file + my $tagName; + my $tagValue; + my $modified = 0; + while ( ($tagName, $tagValue) = each(%$camerainfo) ) { + $tagName =~ s/[\x00-\x1F]//g; + if (UNIVERSAL::isa(\$tagValue, 'SCALAR')){ + # strip characters after the first control code (ascii code <32 ) + #$tagValue =~ s/^([\x20-\xFF]*)[^\x20-\xFF].*$/$1/; + $tagValue =~ s/[\x00-\x1F].*$//s; + $modified |= addExif($tagName, $tagValue, $exifHash, + $priorityList); + } + #print "«$tagName» : "; + #print $_, ', ' foreach(@{$tagValue}); + #print "\n"; + $modified |= addSpecialExif($tagName, $tagValue, $exifHash, + $priorityList); + } + + # add value to desc Hash if field is void + foreach my $field (keys(%{getFields($configHash)})) { + my $fieldExif = getFields($configHash)->{$field}->{'EXIF'}; + if ((! $descHash->{$field}) && $fieldExif && $exifHash->{$fieldExif}) { + beVerboseN(" Using '$field' from EXIF data: ". + $exifHash->{$fieldExif}, 3); + + my $func = getFields($configHash)->{$field}->{'Transform'}; + if ($func) { + $_ = $exifHash->{$fieldExif}; + beVerbose(" Evaluating '$func' from '$_'", 4); + eval $func; + if ($@) { + beVerboseN("", 4); + print("Error: evaluation of transformation for $field failed.\n"); + exit 2; + } + $descHash->{$field} = $_; + beVerboseN(" to '$descHash->{$field}'.", 4); + } else { + $descHash->{$field} = $exifHash->{$fieldExif}; + } + } + } + + if ($configHash->{rotateImages} eq 'original'){ + # Perform a rotation of the picture if needed + # we're doing it here because we must change and save the + # orientation tag if the image was rotated + if(rotateImage($pictureFile, $exifHash->{Orientation}, + $exifHash->{file_ext}, $configHash)){ + progressifyImage("$pictureFile", $exifHash->{file_ext}, $configHash); + push @{$priorityList}, "Orientation"; + $exifHash->{Orientation} = "top_left"; + } + } + + # Write Exif tags to desc file + if ($modified && $configHash->{addExifToDescFile}) { + writeExif($crntImage, $exifHash, $document, + $priorityList, $configHash); + } +} + +sub charac_indent{ + my $n = shift(@_); + my $s="\n"; + for (1..$n){ + $s .= " "; + } + return XML::Grove::Characters->new ( Data => $s ); +} + +sub writeExif{ + my $crntImage = shift; + my $descHash = shift; + my $document = shift; + my $priorityList = shift; + my $configHash = shift; + my $description; + my $exif; + my $element; + my $characters; + my $file = getDescFile($crntImage); + + # create Grove document if file didn't exist + if (!$document) { + beVerbose(" Creating new XML description file $file... ", 3); + $description = + XML::Grove::Element->new ( Name => 'description', + Contents => [charac_indent(1)]); + my $bins = + XML::Grove::Element->new ( Name => 'bins', + Contents => [charac_indent(1)]); + $exif = + XML::Grove::Element->new ( Name => 'exif', + Contents => [charac_indent(2)]); + $element = + XML::Grove::Element->new ( Name => 'image', + Contents => + [charac_indent(1), + $description, + charac_indent(1), + $bins, + charac_indent(1), + $exif ]); + $document = XML::Grove::Document->new ( Contents => [ $element ] ); + + if ($configHash->{createEmptyDescFields}){ + # Add standard fields if a new file is generated (so you can + # edit it better) + my @fields = @mainFields; + push @fields, 'title'; + + foreach my $field (@fields) { + $element = XML::Grove::Element->new ( Name => 'field', + Contents => [charac_indent(3), + charac_indent(2)], + Attributes => {"name" => $field}); + push @{$description->{Contents}}, (charac_indent(2), $element); + } + push @{$description->{Contents}}, charac_indent(1); + } + beVerboseN("OK.", 3); + } + + # Add exif tags to the Grove + if (!$exif) { + $exif = $document->at_path('/image/exif'); + @{$exif->{Contents}}=(); + } + my $tagName; + my $tagValue; + while ( ($tagName, $tagValue) = each(%$descHash) ) { + beVerboseN(" Adding Exif tag '$tagName'='$tagValue' in XML desc file.", 3); + #$tagValue = html2xml($tagValue); + $characters = XML::Grove::Characters->new ( Data => $tagValue ); + $element = XML::Grove::Element->new ( Name => 'tag', + Contents => [charac_indent(3), + $characters, + charac_indent(2)], + Attributes => {"name" => $tagName}); + + if (grep (/^$tagName$/, @{$priorityList})){ + $element->{Attributes}{"priority"} = "1"; + beVerboseN(" with 'priority' attribute.\n", 3); + } + + push @{$exif->{Contents}}, (charac_indent(2), $element); + } + push @{$exif->{Contents}}, charac_indent(1); + # Write the Grove to the desc file + beVerbose(" Saving XML description file $file... ", 3); + my $fileHandler = new IO::File; + open($fileHandler, '>', $file) + or die("Cannot open file $file to write Exif tag ($!)"); + binmode($fileHandler, ":utf8") if $^V ge v5.8.0; + +# my $composer = new XML::Handler::Composer (); +# my $my_handler = new XML::Filter::Reindent (Handler => $composer); + + #print Dumper($document); + my $my_handler = + new XML::Handler::YAWriter( + 'Output' => $fileHandler, + 'Encoding' => $configHash->{xmlEncoding}, +# 'Pretty' => { +# 'NoProlog' =>0, +# 'NoDTD' =>0, +# 'NoPI' =>0, +# 'PrettyWhiteIndent'=>1, +# 'PrettyWhiteNewline'=>1, +# 'NoWhiteSpace'=>0, +# 'NoComments'=>0, +# 'AddHiddenNewline'=>0, +# 'AddHiddenAttrTab'=>1, +# } + ); + +# my $my_handler = XML::Handler::XMLWriter->new( Output => $fileHandler, +# Newlines => 0); + $document->parse(DocumentHandler => $my_handler); + close ($fileHandler) || die ("can't close $file ($!)"); + beVerboseN("OK.", 3); +} + + +sub getDescAlbum { + my $descFile = shift(@_); # file name of the album desc file + my $hash = shift(@_); # ref to the album description hash + my $configHash = shift(@_);# ref to the current config hash + my $fieldName; + my $fieldValue; + my @AlbumFieldNames = ( "title", "sampleimage", "shortdesc", "longdesc", + "ignore"); + + beVerboseN("Reading album description file '$descFile'...", 3); + + my $document = getXMLAsGrove($descFile); + # I have to do that, don't ask me why... + + #$XML::UM::ENCDIR="/usr/lib/perl5/XML/Parser/"; + #my $encode = XML::UM::get_encode ( + # Encoding => 'ISO-8859-9', + # EncodeUnmapped => \&XML::UM::encode_unmapped_dec); + + $configHash = getConfigXML($document, "/album/bins", $configHash); + + foreach my $element + (@{$document->at_path('/album/description')->{Contents}}) { + if (UNIVERSAL::isa($element, 'XML::Grove::Element') && $element->{Name} eq "field") { + $fieldName = $element->{Attributes}{'name'}; + $fieldValue = ""; + if (grep (/^$fieldName$/, @AlbumFieldNames)) { + beVerbose(" Reading field '$fieldName':", 3); + foreach my $characters (@{$element->{Contents}}) { + $fieldValue .= $characters->as_canon_xml(); + } + #if ($fieldName ne "shortdesc" && $fieldName ne "longdesc"){ + # $fieldValue = decode_entities($fieldValue); + #} + if ($fieldName eq "sampleimage"){ + $fieldValue = + trimWhiteSpace(decode_entities($fieldValue)); + beVerbose("'".$fieldValue."'\n", 3); + }else{ + $fieldValue = + trimWhiteSpace(decode_entities(xml2html($fieldValue))); + beVerbose("'".$fieldValue."'\n", 3); + } + # $fieldValue = $encode->(trimWhiteSpace($fieldValue)); + $hash->{$fieldName} = $fieldValue; + } else { + beVerboseN(" Ignoring unknown field '$fieldName'.", 3); + } + } + } + return $configHash; +} + + +sub getConfigColors{ + my $colors = shift; + my $style = shift; + my $configHash = shift; + my $modified = 0; + + foreach my $color + (@{$colors->{Contents}}) { + if (UNIVERSAL::isa($color, 'XML::Grove::Element')){ + if ($color->{Name} eq "color") { + if (defined $color->{Attributes}{'name'}){ + my $fieldName = $color->{Attributes}{'name'}; + my $fieldValue; + foreach my $characters (@{$color->{Contents}}) { + $fieldValue .= $characters->as_canon_xml(); + } + if (defined $fieldValue){ + $fieldValue = + trimWhiteSpace(decode_entities(xml2html($fieldValue))); + } + if ($fieldValue){ + $configHash->{colorsSubs}{$style}{$fieldName.'COLOR'} = + $fieldValue; + $modified = 1; + } else { + beVerboseN("Warning, no value for '$fieldName' color, ignoring.", + 1); + } + } else { + beVerboseN("Warning, missing 'name' attribute for tag, ". + "ignoring.", 1); + } + } else { + beVerboseN("Warning, unknown tag <".$color->{Name}. + "> in section, ignoring.", 1); + } + } + } + return $modified; +} + +sub getConfigSizes{ + my $sizes = shift; + my $configHash = shift; + my (@names, @shortNames, @heights, @widths); + + foreach my $size + (@{$sizes->{Contents}}) { + if (UNIVERSAL::isa($size, 'XML::Grove::Element')){ + if ($size->{Name} eq "size") { + if (defined $size->{Attributes}{'name'}){ + push @names, _(trimWhiteSpace($size->{Attributes}{'name'})); + } else { + beVerboseN("Warning, missing parameter 'name' in tag , ". + "ignoring all sizes.", 1); + return 0; + } + if (defined $size->{Attributes}{'shortname'}){ + push @shortNames, + _(trimWhiteSpace($size->{Attributes}{'shortname'})); + } else { + beVerboseN("Warning, missing parameter 'shortname' in tag , ". + "ignoring all sizes.", 1); + return 0; + } + if (defined $size->{Attributes}{'height'}){ + push @heights, _(trimWhiteSpace($size->{Attributes}{'height'})); + } else { + beVerboseN("Warning, missing parameter 'height' in tag , ". + "ignoring all sizes.", 1); + return 0; + } + if (defined $size->{Attributes}{'width'}){ + push @widths, _(trimWhiteSpace($size->{Attributes}{'width'})); + } else { + beVerboseN("Warning, missing parameter 'width' in tag ". + "in section, ignoring all sizes.", 1); + return 0; + } + } else { + beVerboseN("Warning, unknown tag <".$size->{Name}."> ". + "in section, ignoring.", 1); + } + } + } + + if (@names) { + $configHash->{longSizeNames} = \@names; + $configHash->{sizeNames} = \@shortNames; + $configHash->{scaledWidths} = \@widths; + $configHash->{scaledHeights} = \@heights; + return 1; + } else { + print "Warning, section is empty, ignoring.\n" + } + return 0; +} + +# Given an XML doc as a Grove, the path to tag and the current +# configuration hash, returns hash from the config section of files +# ( tag) merged with current hash. +sub getConfigXML{ + my $document = shift(@_); # Grove document + my $path = shift(@_); # path of the tag in the document + my $currentConfigHash = shift(@_); # hash ref to the current configuration + #my $fieldNb; + my $fieldName; + my $fieldValue; + my %configHash = %{ dclone($currentConfigHash) }; + + beVerboseN(" Reading configuration data from file...", 3); + + my $modified = 0; + foreach my $element + (@{$document->at_path($path)->{Contents}}) { + if (UNIVERSAL::isa($element, 'XML::Grove::Element')){ + if ($element->{Name} eq "parameter") { + $fieldName = $element->{Attributes}{'name'}; + $fieldValue = ""; + foreach my $characters (@{$element->{Contents}}) { + #if (UNIVERSAL::isa($characters, 'XML::Grove::Characters')) { + $fieldValue .= $characters->as_canon_xml(); + } + if (defined $fieldValue){ + $fieldValue = + trimWhiteSpace(decode_entities(xml2html($fieldValue))); + } else { + $fieldValue = ""; + } + beVerbose(" Reading configuration parameter '$fieldName':", 3); + $configHash{$fieldName} = $fieldValue; + beVerbose("'".$fieldValue."'\n", 3); + $modified = 1; + } elsif ($element->{Name} eq "sizes") { + beVerbose(" Reading sizes parameters...", 3); + $modified |= getConfigSizes($element, \%configHash); + } elsif ($element->{Name} eq "colors") { + my $style = $element->{Attributes}{'style'}; + if ($style){ + beVerboseN(" Reading color style parameter '$style'...", 3); + $modified |= getConfigColors($element, $style, \%configHash); + } else { + beVerboseN(" Warning, no 'style' attribute to tag ". + "in section, ignoring.", 1); + } + } else { + beVerboseN(" Warning, unknown tag <$element->{Name}> ". + "in section, ignoring.", 1); + } + } + } + if (!$modified){ + return $currentConfigHash; # return the original to not keep the + # new copy in memory + } + return \%configHash; +} + + +sub commandAvailable{ + my $command = shift; + my $exists = 0; + my (@PATH) = split (/:/, $ENV{"PATH"}); + foreach (@PATH) { + beVerboseN(" looking for $_/$command...", 4); + $exists++ if (-f "$_/$command" && -x "$_/$command"); + last if $exists; + } + return $exists; +} + + +BEGIN { + my $rotateGeneric="none"; + sub rotateGenericCommand{ + my $file = shift; + my $degrees = shift; + my $verbose = shift; + + if ($rotateGeneric eq "none") { + beVerbose(" Looking for a generic rotation utility (mogrify)... ", 3); + if (commandAvailable("mogrify")) { + $rotateGeneric = 'mogrify -rotate %s "%s"'; + beVerboseN(" found mogrify.", 3); + } else { + beVerboseN(" not found, cannot rotate.", 3); + $rotateGeneric = ""; + } + } + + if ($rotateGeneric) { + $file =~ s|\\|\\\\|g; + $file =~ s|\$|\\\$|g; + $file =~ s|"|\\"|g; + $file =~ s|`|\\`|g; + return sprintf($rotateGeneric, $degrees, $file); + } + return ""; + } +} + +BEGIN { + my $rotateJPEG="none"; + sub rotateJPEGCommand{ + my $file = shift; + my $degrees = shift; + my $verbose = shift; + + if ($rotateJPEG eq "none") { + beVerbose("\n Looking for a JPEG rotation utility (jpegtran)... ", 3); + if (commandAvailable("jpegtran")) { + $rotateJPEG = 'jpegtran -copy all -rotate %s -outfile "%s.tmp" "%s" && mv "%s.tmp" "%s"'; + beVerboseN(" found jpegtran.", 3); + } elsif (commandAvailable("jpegtran-mmx")) { + $rotateJPEG = 'jpegtran-mmx -copy all -rotate %s -outfile "%s.tmp" "%s" && mv "%s.tmp" "%s"'; + beVerboseN(" found jpegtran-mmx.", 3); + } else { + $rotateJPEG = ""; + beVerboseN(" not found, trying a generic one.", 3); + } + } + + if ($rotateJPEG) { + $file =~ s|\\|\\\\|g; + $file =~ s|\$|\\\$|g; + $file =~ s|"|\\"|g; + $file =~ s|`|\\`|g; + return sprintf($rotateJPEG, $degrees, $file, $file, $file, $file); + } + return rotateGenericCommand($file, $degrees); + } +} + +# return the best command available for rotating a given file type +sub rotateCommand{ + my $type = shift; + my $file = shift; + my $degrees = shift; + my $configHash = shift; + + if ($type eq "jpg" && $configHash->{rotateWithJpegtran}){ + return rotateJPEGCommand($file, $degrees, $verbose); + }else{ + return rotateGenericCommand($file, $degrees, $verbose); + } +} + +# Rotate an image. Return 0 if no rotation was performed, 1 if a +# rotation was performed and it changed width and heigth, and 2 if it +# was a 180 degree rotation +sub rotateImage{ + my $imageName = shift; + my $orientation = shift; + my $ext = shift; + my $configHash = shift; + + #if (!$imageInfo->{'BinsRotated'} && $imageInfo->{'Orientation'}){ + if (!$orientation){ + return 0; + } + + beVerboseN(" Orientation for picture is '$orientation'", 3); + + if ($orientation eq "top_left"){ + return 0; + } + if ($imageName =~ m/$configHash->{noRotation}/) { + return 0; + } + my $degrees; + if ($orientation eq "right_top"){ + $degrees = 90; + }elsif ($orientation eq "left_bot"){ + $degrees = 270; + }elsif ($orientation eq "bot_right"){ + $degrees = 180; + } else { + print "Warning, Orientation field has an unknown value '$orientation'.\n"; + return 0; + } + + beVerbose(" Performing $degrees degrees rotation clockwise on $imageName... ", + 2); + my $type = ""; + if ($ext) { + $type = $ext; + }else{ + $type="jpg"; + } + my $command = rotateCommand($type, $imageName, $degrees, $configHash); + if ($command){ + beVerbose("\n Running '$command'... ", 3); + if(!system($command)){ + #$imageInfo->{'BinsRotated'}="yes"; + beVerboseN("OK", 2); + if ($degrees == 180){ + return 2; + } + return 1; + } + }else{ + beVerboseN("impossible.\n Cannot perform rotation, no utility installed.", + 2); + } + return 0; +} + + +BEGIN { + my $progressifyJPEG="none"; + sub progressifyJPEGCommand{ + my $filein = shift; + my $fileout = shift; + my $verbose = shift; + + if ($progressifyJPEG eq "none") { + beVerbose("\n Looking for a progressive JPEG utility (jpegtran)... ", 3); + if (commandAvailable("jpegtran")) { + $progressifyJPEG = 'jpegtran -copy all -progressive -outfile "%s" "%s"'; + beVerboseN(" found jpegtran.", 3); + } else { + $progressifyJPEG = ""; + beVerboseN(" not found, cannot make JPEGs progressive", 3); + } + } + + if ($progressifyJPEG) { + $filein =~ s|\\|\\\\|g; + $filein =~ s|\$|\\\$|g; + $filein =~ s|"|\\"|g; + $filein =~ s|`|\\`|g; + $fileout =~ s|\\|\\\\|g; + $fileout =~ s|\$|\\\$|g; + $fileout =~ s|"|\\"|g; + $fileout =~ s|`|\\`|g; + return sprintf($progressifyJPEG, $fileout, $filein); + } + return ""; + } +} + +# fileSizeCmp(file1, file2): return file1.file-size - file2.file-size +sub fileSizeCmp { + my $file1 = shift; + my $file2 = shift; + return ((stat($file1))[7]) - ((stat($file2))[7]); +} + +# progressifyJPEGImage(imageName, configHash) +# make a JPEG image progressive. Return 0 if no conversion was performed, and 1 if a +# conversion was performed +sub progressifyJPEGImage{ + my $imageName = shift; + my $configHash = shift; + my $tempFile = "$imageName.tmp"; + + beVerbose(" Making $imageName progressive JPEG... ", 2); + + my $command = progressifyJPEGCommand($imageName, $tempFile, $verbose); + + if ($command) { + beVerbose("\n Running '$command'... ", 3); + if(!system($command)){ + if (($configHash->{jpegProgressify} eq "always")) { + rename($tempFile, $imageName) + or die("\nfailed to rename $tempFile to $imageName: $?"); + beVerboseN("OK", 2); + return 1; + } + my $diff = fileSizeCmp($tempFile, $imageName); + if ($diff < 0) { + rename($tempFile, $imageName) + or die("\nfailed to rename $tempFile to $imageName: $?"); + $diff = -$diff; + beVerbose("$diff bytes smaller; ", 3); + beVerboseN("OK", 2); + return 1; + } else { + beVerbose("$diff bytes larger; ", 3); + beVerboseN("NO", 2); + unlink($tempFile) or warn("\nfailed to unlink $tempFile: $?"); + } + } + }else{ + beVerboseN("impossible.\n Cannot make progressive JPEG, no utility installed.", + 2); + } + return 0; +} + +# progressifyImage(imageName, ext, configHash) +# make a JPEG image progressive. Return 0 if no conversion was performed, and 1 if a +# conversion was performed. Skips non JPEG images or when jpegProgressify +# is false (returns 0). +sub progressifyImage{ + my $imageName = shift; + my $ext = shift; + my $configHash = shift; + + my $type = $ext ? $ext : "jpg"; + if (($type eq "jpg") && ($configHash->{jpegProgressify} ne "never")) { + return progressifyJPEGImage($imageName, $configHash); + } + return 0; +} + + + +sub readConfigFile{ + my $configHash = shift(@_); + my $document; + + my $globalConfigFile = bsd_glob($configHash->{'globalConfigDir'}."/". + $configHash->{'configFileName'}, GLOB_TILDE); + if ($globalConfigFile && -e $globalConfigFile) { + beVerboseN("Reading global configuration file $globalConfigFile.", 3); + $document = getXMLAsGrove($globalConfigFile); + $configHash = getConfigXML($document, "/bins", $configHash); + }else{ + beVerboseN("No global configuration file ". + $configHash->{'globalConfigDir'}."/". + $configHash->{'configFileName'}." found.", 3); + } + my $userConfigFile = bsd_glob($configHash->{'userConfigDir'}."/". + $configHash->{'configFileName'}, GLOB_TILDE); + if ($userConfigFile && -e $userConfigFile) { + beVerboseN("Reading user configuration file $userConfigFile.", 3); + $document = getXMLAsGrove($userConfigFile); + $configHash = getConfigXML($document, "/bins", $configHash); + }else{ + beVerboseN("No user configuration file ".$configHash->{'userConfigDir'}."/". + $configHash->{'configFileName'}." found.", 3); + } + return $configHash; +} + +sub stringToBool{ + my $string = shift(@_); + if (! $string) {return 0;} + if (trimWhiteSpace($string) eq "true") { return 1;} + return 0; +} + +sub trimWhiteSpace{ + my $string = shift(@_); + return "" if (! defined $string); + for ($string) { + s/^\s+//; + s/\s+$//; + } + return $string; +} + + + +sub BEGIN { + my %ignoredSets; + my %hiddenSets; + # this function is only used by ignoreSet, hiddenSet and + # ignoreAndHiddenSet functions below + sub ignoreOrHiddenSet{ + my $ignoreLine = shift; + my $album = shift; + my $type = shift; + my $configHash = shift; + + my ($set, $userSet); + if ($type eq "ignore") { + $set = \%ignoredSets; + $userSet = $ignoreOpts; + if ($ignoreOpts && $configHash->{ignore}){ + $userSet .= "," + } + $userSet .= $configHash->{ignore}; + } elsif ($type eq "hidden") { + $set = \%hiddenSets; + $userSet = $hiddenOpts; + if ($hiddenOpts && $configHash->{hidden}){ + $userSet .= "," + } + $userSet .= $configHash->{hidden}; + } else { + return 0; + } + + # Remove the trailing slash if there is one (or more!) in $album, so + # that the hash works for $fileInAlbum invocations as well. + if ( defined($album) ) { + $album =~ s/\/$//sog; + } + + # Use the hash to optimize the checking of ignoreSets(), since this is + # called three times for each album, and it could be expensive if there + # are many ignore directives. If we have already checked this album, + # just report what we concluded last time. + if ( defined($set->{$album}) && $album ne "" ) { + return( $set->{$album} ); + } + + if ( defined($ignoreLine) ) { + for my $thisIgnoreOpts ( split(',', $userSet ) ) { + for my $thisIgnoreLine ( split('\s*,\s*',$ignoreLine) ) { + if ( $thisIgnoreOpts eq $thisIgnoreLine ) { + $set->{$album}=1; + beVerboseN("Skipping \"$album\" because of \"$thisIgnoreOpts\"". + " IGNORE directive.", 2); + return(1); + } + } + } + } + $set->{$album}=0; + return(0); + } +} + +# return 1 if one of $ignoreLine is a ignored album +sub ignoreSet{ + my $ignoreLine = shift; + my $album = shift; + my $configHash = shift; + + return ignoreOrHiddenSet($ignoreLine, $album, "ignore", $configHash); +} + +# return 1 if one of $ignoreLine is a hidden album +sub hiddenSet{ + my $ignoreLine = shift; + my $album = shift; + my $configHash = shift; + + return ignoreOrHiddenSet($ignoreLine, $album, "hidden", $configHash); +} + +# return 1 if one of $ignoreLine is a ignored or hidden album +sub ignoreAndHiddenSet{ + my $ignoreLine = shift; + my $album = shift; + my $configHash = shift; + + if (ignoreOrHiddenSet($ignoreLine, $album, "ignore", $configHash)) { + return 1 + } + return ignoreOrHiddenSet($ignoreLine, $album, "hidden", $configHash); +} + +sub beVerbose { + my $output = shift; + my $level = shift; + print "$output" if ($verbose >= $level); +} + +sub beVerboseN { + my $output = shift; + my $level = shift; + if ($verbose >= $level){ + beVerbose($output, $level); + print "\n"; + } +} + +# for .po generation, until xgettext handles Perl correctly +sub dummy_I18N{ + _("Thumbnail Page"); + _("Thumbnail Page 1"); + _("tree"); + _("subalbum"); + _("subalbums"); + _("Hg"); + _("Huge"); + _("image"); + _("images"); + _("Click on one of the size names above to enlarge this image"); + _("Click to view thumbnails of the current album"); + _("Click to view this album"); +} diff -r 000000000000 -r a84c32f131df bins-edit-gui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bins-edit-gui Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,887 @@ +#!/usr/bin/perl +# bins-edit-gui -- graphical editor for BINS-format XML tag files +# +# Copyright 2002 Mark W. Eichin +# The Herd of Kittens +# +# -- GPL notice -- +$gpl_text = <" ."\n". +_("This is free software distributed under the GPL. There is NO WARRANTY.") ."\n". + + +GetOptions('debug+' => \$debug, + 'h|help' => \$o_help, + 'v|version' => \$o_version, + ) or usage('error'); +usage('user') if $o_help; +if ($o_version) { + print $fullversion; + exit 0; +} + +# take this out when we have a file browser +usage('error') if scalar(@ARGV) < 1; + +# find a way to do this in Perl; nl_langinfo seems to be coming in 5.8 +my $localEncoding = ""; +my $codeset; +eval { + require I18N::Langinfo; + I18N::Langinfo->import(qw(langinfo CODESET)); + $codeset = langinfo(CODESET()); +}; +# ANSI is unspeakably primitive, keep LATIN1 instead +# Solaris refers to ISO 646 as "646" but that is not a valid codeset +if (!$@ && $codeset && $codeset ne "ANSI_X3.4-1968" && $codeset ne "646") { + $localEncoding = $codeset; + print "Forcing encoding to $codeset"; +} + +chop($localEncoding); +if (! $localEncoding) { + $localEncoding = "LATIN1"; +} +$Latin2UTF = Text::Iconv->new($localEncoding, "UTF-8"); +$UTF2Latin = Text::Iconv->new("UTF-8", $localEncoding); + +my_init_rotations(); + +Gtk->set_locale; + +Gnome->init("bins-edit-gui", $version); +Gtk->init; + +$glade = "/usr/local/share/bins/bins-edit-gui.glade"; +if (! -r $glade) { + $glade = "bins-edit-gui.glade" ; # developer hack + print "DEVELOPER HACK\n"; + if (! -r $glade) { + die "No bins-edit-gui.glade available"; + } +} +#$g = new Gtk::GladeXML($glade, 'image_edit_top'); +$g = new Gtk::GladeXML($glade); +die "Gtk::GladeXML($glade) initialization failed; check required packages" unless $g; + +$g->signal_autoconnect_from_package('main'); # main:: so we can grab stuff directly + +# get the "global" widgets + +$w = $g->get_widget('image_edit_pixmap'); # GTK-Interface/widget/name +$w->signal_connect(expose_event => \&on_test_expose_event); +$name_entry = $g->get_widget("field_name_entry"); +$value_entry = $g->get_widget("field_value_entry"); +$ilist = $g->get_widget("image_prop_list"); +$ilist->column_titles_passive; # shouldn't this be in the .glade? +# work around libglade 0.17 bug (debbugs #147051) +$ilist->set_column_title(0, _("Property")); +$ilist->set_column_title(1, _("Value")); +# end workaround. +$statusbar = $g->get_widget('statusbar1'); +$aboutbox = $g->get_widget('about_box'); +$aboutbox->close_hides; +$licensebox = $g->get_widget('license_box'); +$licensetext = $g->get_widget('license_text'); # GtkText +#print ref($licensetext),": ",join("\n\t|",%Gtk::Text::),"\n"; +$gpl_text =~ s/^\# ?//gm; +$licensetext->insert(undef, undef, undef, $gpl_text); + +## album-panel globals +$albumedit = $g->get_widget('album_edit_top'); +$albumfile = $g->get_widget('album_edit_filename'); +$albumprop = $g->get_widget('album_prop_list'); +# work around libglade 0.17 bug (debbugs #147051) +$albumprop->set_column_title(0, _("Property")); +$albumprop->set_column_title(1, _("Value")); +# end workaround. +$albumname = $g->get_widget('album_name_entry'); +$albumname->set_popdown_strings(@album_tags); +$albumvalue = $g->get_widget('album_edit_text'); + +sub on_dismiss_about_clicked { + $licensebox->hide; + status(_("Thank you for sharing.")); +} + +sub status { + my $msg = shift; + $statusbar->pop(1); + $statusbar->push(1, $msg); +} + +use Image::Info; + +# original orientation for viewing +sub get_image_orientation($) { + my $filename = shift; + # bail directly if we have a tag-loaded value + return $newimage_tagged_or if (defined $newimage_tagged_or); + # try and find a way to get this from imlib? + my $info = Image::Info::image_info($filename); + if (exists $info->{'error'}) { + my $msg = sprintf(_("Couldn't read EXIF info from %s: %s, ignoring."), + $filename, $info->{'error'}); + + status($msg); + print $msg if $debug; + return "top_left"; + } + $info->{'Orientation'}; +} + +# see http://www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html +# for the meaning of the orientations. +# see /usr/share/doc/imlib-base/html/index.html +# for the not-quite-apology for the function supplied. +# see the qiv sources for an example of how to use it anyway... + +sub my_real_gdk_rotate($$) { + my ($newimage,$orientation) = @_; + my $degrees; + + if ($orientation eq "right_top"){ + $degrees = 90; + $newimage->rotate_image('a 45 degree mirror is *not* a rotate'); + $newimage->flip_image_horizontal (); + } elsif ($orientation eq "left_bot"){ + $degrees = 270; + $newimage->rotate_image('a 45 degree mirror is *not* a rotate'); + $newimage->flip_image_vertical (); + } elsif ($orientation eq "right_bot"){ + $degrees = 180; + # test this, maybe simplify... + $newimage->rotate_image('a 45 degree mirror is *not* a rotate'); + $newimage->flip_image_horizontal (); + $newimage->rotate_image('a 45 degree mirror is *not* a rotate'); + $newimage->flip_image_horizontal (); + } elsif ($orientation eq "top_left"){ + # do nothing + return; + } else { + print STDERR "Warning, Orientation field has an unknown value '$orientation'.\n" if $debug; + # still do nothing + return 0; + } + +} + + +sub load_image_tags($); + +sub load_image { # no proto, we're cheating and doing 1arg/2arg (or should that be @?) + my $test_filename = shift; + my $extra = shift; + my $ilabel = $g->get_widget('image_filename_label'); + $newimage->destroy_image() if defined $newimage; + $newimage = load_image Gtk::Gdk::ImlibImage($test_filename); + if (defined $newimage) { + if ($extra ne "keeptags") { + load_image_tags $test_filename; + $newimage_loaded_or = $newimage_or = get_image_orientation($test_filename); + print "$test_filename: got orientation <$newimage_or>\n" if $debug; + } + my_real_gdk_rotate($newimage,$newimage_or); + $ilabel->set($test_filename); + status(sprintf(_("Loaded %s."), $test_filename)); + } else { + # no image - just skip. we've already filtered xml files, though. + $ilabel->set(_("Load failed: ") . $test_filename); + status(sprintf(_("IMLib failed loading %s."), $test_filename)); + } + # regardless, rerender + if (defined $imagesized) { + undef $imagesized; + on_test_expose_event(); # cheat, it doesn't use it + } + +} +# from bins_edit + +# my $grove_builder = XML::Grove::Builder->new; +# my $parser = XML::Parser::PerlSAX->new ( Handler => $grove_builder ); +# $document = $parser->parse ( Source => { SystemId => "${test_filename}.xml" } ); + +# now only internal for load... +sub load_image_tags($) { + %old_field = %field; # save for later recall + + my $test_filename = shift; + $parser = new XML::DOM::Parser; + if (-r "${test_filename}.xml" ) { + $doc = $parser->parsefile ("${test_filename}.xml"); + } else { + # literal minimal tree + $doc = $parser->parse(''); + status(sprintf(_("%s not found, constructing"), "${test_filename}.xml")); + } + + + undef %field; + + for my $i (@image_tags) { + $field{$i} = ""; + } + + for my $field_node ($doc->xql("image/description/field")) { + # + # Sjenka + my $fieldname = ($field_node->xql("\@name"))[0]->xql_toString; + my $fieldval = $field_node->xql_toString; + print "N: $fieldname V: $fieldval\n" if $debug; + my $newval; + do { + $newval = $UTF2Latin->convert($fieldval); + charmap_failed("load_image_tags", $fieldval) if (not defined $newval); + } until defined $newval; + $field{$fieldname} = $newval; + } + + undef $newimage_tagged_or; + # right_top + for my $tag_node ($doc->xql("image/exif/tag[\@name = 'Orientation']")) { + my $tagname = ($tag_node->xql("\@name"))[0]->xql_toString; + my $tagprio_node = ($tag_node->xql("\@priority"))[0]; + my $tagprio; + $tagprio = $tagprio_node->xql_toString if defined $tagprio_node; + my $tagval = $tag_node->xql_toString; + # but actually, we only care about orientation + print "N: $tagname V: $tagval P: $tagprio\n" if $debug; + if ($tagprio == 1) { + $newimage_tagged_or = $tagval; + } + } + + # a clist is output only. someday, replace it with a list of + # editboxes, even if we have to write one all in perl. + + # in the mean time, we vector out to a pair of combo boxes and let + # the user edit there, while copying the changes back live. + + # save last index if any... + my $oldtag; + my $oldrow = $ilist->focus_row(); + $oldtag = $ilist->get_text($oldrow, 0) if $oldrow > 0; + print "old $oldrow: $oldtag\n" if $debug; + $ilist->clear; + for my $i (sort keys %field) { + $ilist->append("\u$i", $field{$i}); + } + if ($oldrow > 0) { + my $newrow = my_gtk_find_row($ilist, 0, $oldtag); + print "new $newrow\n" if $debug; + if ($newrow > 0) { + $ilist->set_focus_row($newrow); + $ilist->grab_focus(); + } + } + + # help the user enter stuff + $name_entry->set_popdown_strings(@known_tags); + # tag as unchanged + $dirty = 0; +} + +sub charmap_failed($$) { + my $ipop = $g->get_widget('iconv_failed_dialog'); + + my $lbutton = $g->get_widget('iconv_latin1_charmap_button'); + $lbutton->set_active(Gtk->true); # really we mean it + my $ubutton = $g->get_widget('iconv_user_charmap_button'); + my $uentry = $g->get_widget('iconv_user_charmap_entry'); + # if there's a value there, it is from the previous attempt, and is wrong. + $uentry->set_text(""); + + $ipop->run_and_close; + + if ($lbutton->get_active) { + set_encoding("LATIN1"); + } elsif ($ubutton->get_active) { + set_encoding($uentry->get_text); + } + +} + +sub save_image_tags { + my $test_filename = shift; + if ((not $dirty) && ($newimage_or eq $newimage_loaded_or)) { + status(sprintf(_("%s not dirty, not saved"), ${test_filename})); + return; + } + + my $parent = ($doc->xql("image/description"))[0]; # first one + my $exif = ($doc->xql("image/exif"))[0]; + my %f = %field; + + # write out the tree... + for my $xmlfield ($doc->xql("image/description/field")) { + my $namestr = $xmlfield->getAttribute("name"); + if (defined $f{$namestr}) { + # delete this node so we can append it later + $xmlfield->getParentNode->removeChild($xmlfield); + } + } + # now append the remaining ones... + for my $k (keys %f) { + next if ($f{$k} eq ""); + my $newfield = new XML::DOM::Element($doc, "field"); + print "creating <$k> with <$f{$k}>\n" if $debug; + $newfield->setAttribute("name", $k); # needs quoting! + print "k: ", $k, " f: ", $f{$k}, " L2U: ", $Latin2UTF->convert($f{$k}), "\n" if $debug; + my $newval; + do { + $newval = $Latin2UTF->convert($f{$k}); + charmap_failed("save_image_tags", $f{$k}) if (not defined $newval); + } until defined $newval; + + $newfield->addText($newval); + $parent->appendChild($newfield); + print "created $k with $f{$k}\n" if $debug; + } + + # and orientation, if set + if ($newimage_or ne $newimage_loaded_or) { + for my $tag_node ($doc->xql("image/exif/tag[\@name = 'Orientation']")) { + # delete the node, then construct a new one + $tag_node->getParentNode->removeChild($tag_node); + } + my $newtag = new XML::DOM::Element($doc, "tag"); + $newtag->setAttribute("name", "Orientation"); + $newtag->setAttribute("priority", "1"); + $newtag->addText($Latin2UTF->convert($newimage_or)); # unneeded conversion + $exif->appendChild($newtag); + print "Set orientation <$newimage_or> (loaded $newimage_loaded_or) in exif tag\n" if $debug; + } + + $doc->printToFile("${test_filename}.xml"); + status(sprintf(_("Saved %s."), $test_filename)); + # undirty it + $dirty = 0; + $newimage_loaded_or = $newimage_or; +} + +# if they enter or select a known one, snarf the value +$name_entry->entry->signal_connect('changed', sub { + my $entry = shift; + my $val = $field{$entry->get_text}; + if (defined $val) { + $value_entry->entry->set_text($val); + $value_entry->entry->set_editable(Gtk->true); + } + $dirty = 1; +}); + +sub my_album_replace_text($$) { + my $aw = shift; + my $text = shift; + $aw->set_point(0); + $aw->forward_delete($albumvalue->get_length()); + $aw->insert(undef, undef, undef, $text); + $aw->set_editable(Gtk->true); +} + +# album version +$albumname->entry->signal_connect('changed', sub { + my $entry = shift; + my $val = $album{$entry->get_text}; + if (defined $val) { + my_album_replace_text($albumvalue, $val); + } + $album_dirty = 1; +}); + +sub my_gtk_find_row { # returns row + my ($clist, $col, $value) = @_; + for my $i (0..$clist->rows) { + my $cell = $clist->get_text($i, $col); + return $i if ($cell ne "" && lc($cell) eq lc($value)); + } + return -1; +} + +# if the value changes, update the text +$value_entry->entry->signal_connect('changed', sub { + my $entry = shift; + my $newval = $entry->get_text; + my $tag = lc($name_entry->entry->get_text); + $field{$tag} = $newval; + $dirty = 1; + my $row = my_gtk_find_row($ilist, 0, $tag); + print "row: $row tag: $tag newval: $newval\n" if $debug; + # oh, no tag yet, add one + if ($row != -1) { + $ilist->set_text($row, 1, $newval); + } else { + # triggers select-row? + $ilist->append("\u$tag", $newval); + # update the dropdown too + @known_tags = sort (@known_tags, $tag); + $name_entry->set_popdown_strings(@known_tags); + # force it all back + $ilist->select_row($ilist->rows()-1, 0); + } +}); +# album version +$albumvalue->signal_connect('changed', sub { + my $entry = shift; + my $newval = $entry->get_chars(0,-1); + my $tag = lc($albumname->entry->get_text); + $album{$tag} = $newval; + $dirty = 1; + my $row = my_gtk_find_row($albumprop, 0, $tag); + print "row: $row tag: $tag newval: $newval\n" if $debug; + # oh, no tag yet, add one + if ($row != -1) { + $albumprop->set_text($row, 1, $newval); + } else { + # triggers select-row? + $albumprob->append("\u$tag", $newval); + # update the dropdown too + @known_tags = sort (@known_tags, $tag); + $albumname->set_popdown_strings(@known_tags); + # force it all back + $albumprob->select_row($ilist->rows()-1, 0); + } +}); + +$ilist->signal_connect('select-row', sub { + my ($clist, $row, $col, $event, $udata) = @_; + print "list: $clist row: $row col: $col event: $event udata: $udata\n" + if $debug; + $name_entry->entry->set_text($clist->get_text($row,0)); + $value_entry->entry->set_text($clist->get_text($row,1)); + print "focus on $value_entry\n" if $debug; + $value_entry->entry->grab_focus(); +}); + +# album version +$albumprop->signal_connect('select-row', sub { + my ($clist, $row, $col, $event, $udata) = @_; + print "list: $clist row: $row col: $col event: $event udata: $udata\n" + if $debug; + $albumname->entry->set_text($clist->get_text($row,0)); + my_album_replace_text($albumvalue, $clist->get_text($row,1)); + print "focus on $value_entry\n" if $debug; + $albumvalue->grab_focus(); +}); + + +# filter out .xml files, as we always derive them from the images +# (also lets us eventually use the images as database keys instead) +@filenames = grep (!/\.xml$/, @ARGV); + +$current_n = 0; +$current_filename = $filenames[$current_n]; + +load_image $current_filename; + +Gtk->main; + +## callbacks.. +# new callbacks +# sgrep -o '%r\n' 'stag("HANDLER") __ etag("HANDLER") ' bins-edit-gui.glade | while read sub; do grep "$sub" bins-edit-gui.pl >/dev/null || echo "$sub"; done + +sub on_save1_activate { + save_image_tags $current_filename; +} + +sub on_about2_activate { + $aboutbox->show; +} + +sub on_license1_activate { + $licensebox->show; + my $button = $g->get_widget('dismiss_about'); + $button->grab_focus(); +} + +sub on_open2_activate { + status(_("File browser not yet implemented.")); +} + +sub on_revert1_activate { + load_image $current_filename; + status(sprintf(_("Reverted from %s."), $current_filename)); +} + +sub set_filename_index($) { + $current_n = shift; + $current_filename = $filenames[$current_n]; + load_image $current_filename; +} + +sub move_filename_index($) { + save_image_tags $current_filename; + my $delta = shift; + my $new_n = $current_n + $delta; + + # clamp it + $new_n = 0 if $new_n < 0; + $new_n = $#filenames if $new_n > $#filenames; + if ($new_n == $current_n) { + # we didn't move + status(_("Out of filenames.")); + } else { + set_filename_index($new_n); + } +} + +sub on_next_file1_activate { + move_filename_index(+1); +} + +sub on_prev_file1_activate { + move_filename_index(-1); +} + +sub on_forward_10_activate { + move_filename_index(+10); +} +sub on_back_10_activate { + move_filename_index(-10); +} +sub on_start_of_list1_activate { + set_filename_index(0); +} +sub on_end_of_list1_activate { + set_filename_index($#filenames); +} + + +# auto fill from old_field +sub on_auto_fill1_activate { + for my $k (keys %old_field) { + if ($field{$k} eq "" && $old_field{$k} ne "") { + $field{$k} = $old_field{$k}; + $dirty = 1; + # and change it on-screen + my $row = my_gtk_find_row($ilist, 0, $k); + if ($row != -1) { + print "updating row $row with $k ($field{$k})\n" if $debug; + $ilist->set_text($row, 1, $field{$k}); + } # maybe warn, or add field, if not found? + } + } + status("Auto-filled fields."); +} + +sub on_exit1_activate { + save_image_tags $current_filename; + + Gtk->main_quit; +} + +sub on_test_expose_event { + my($widget) = @_; + + # print "otee, ",defined $imagesized,"\n"; + # if (1 || not defined $imagesized) { + my $w = $g->get_widget('image_edit_pixmap'); # GTK-Interface/widget/name + # x,y,width,height + my ($alloc_x, $alloc_y, $alloc_w, $alloc_h) = @{$w->allocation}; + my $widget_w = $alloc_w; + my $widget_h = $alloc_h; + + if ($debug) { + print join("| ", @{$w->allocation}); + print "| x: $widget_w y: $widget_h "; + print + "nw: ", $newimage->rgb_width, + " nh: ", $newimage->rgb_height, "\n" if defined $newimage; + print + " x/y: ",$widget_w/$widget_h, + " nw/x: ", $newimage->rgb_width/$widget_w, + " nh/y: ", $newimage->rgb_height/$widget_h, + "\n" if defined $newimage; + } + # print "ox: $old_x oy: $old_y ww: $widget_w wh: $widget_h I:$imagesized\n"; + # return if same size + if ($old_x == $widget_w && $old_y == $widget_h) { + # but not if we never dealt before + return if (defined $imagesized); + } + if (defined $newimage) { + my ($use_w, $use_h) = ($newimage->rgb_width, $newimage->rgb_height); + my $rat_w = $use_w/$widget_w; + my $rat_h = $use_h/$widget_h; + $rat = ($rat_w > $rat_h) ? $rat_w : $rat_h; + + $use_w = $use_w / $rat; + $use_h = $use_h / $rat; + $newimage->render($use_w, $use_h); + my $my_image = $newimage->copy_image(); # returns Gtk::Gdk::Pixmap + my $my_mask = $newimage->copy_mask(); # returns Gtk::Gdk::Bitmap + $w->set($my_image, $my_mask); + $my_image->imlib_free(); + } else { + # come up with more clever "test pattern" later? + $w->set(undef, undef); + } + $old_x = $widget_w; + $old_y = $widget_h; + #undef $imagesized; + $imagesized = 1; + # } +} + +# rotations that override, or rather compound, the EXIF values +# build the rotation-ring first +sub my_init_rotations { + my @rotation_order = ("right_top", "right_bot", "left_bot", "top_left"); + my $left = $rotation_order[$#rotation_order]; + for my $curr (@rotation_order) { + $rotate_right{$left} = $curr; + $rotate_left{$curr} = $left; + $left = $curr; + } + # special case none->top_left + $rotate_right{""} = $rotate_right{"top_left"}; + $rotate_left{""} = $rotate_left{"top_left"}; +} + +sub my_image_rotate { + my $delta_or = shift; + print "$test_filename: new user-requested rotation $delta_or to $newimage_or\n" if $debug; + + my_real_gdk_rotate($newimage,$delta_or); + # consider noticing that $newimage_or == $newimage_loaded_or and reverting. + # regardless, rerender + if (defined $imagesized) { + undef $imagesized; + on_test_expose_event(); + } + status(_("Rotated.")); +} + +sub on_rotate_right_cw1_activate { + $newimage_or = $rotate_right{$newimage_or}; + my_image_rotate("right_top"); +} + +sub on_rotate_left1_activate { + $newimage_or = $rotate_left{$newimage_or}; + my_image_rotate("left_bot"); +} + +# don't actually undo the rotations, just reload - but don't lose tags +sub on_cancel_rotation1_activate { + load_image $current_filename, "keeptags"; + status(_("Image restored.")); +} + +### album stuff ### +sub load_album_tags($); + +sub on_album1_activate { + $current_album = $current_filename; + # basename + $current_album =~ s{[^/]*$}{}; # } perl-mode-sucks + # make a complete name out of it + $current_album .= "album.xml"; #if (-d "${current_album}"); + $albumfile->set($current_album); + load_album_tags($current_album); + $albumedit->show; +} + +sub on_open2_activate { + status(_("File browser not yet implemented.")); +} + +sub on_close1_activate { + # save if dirty + save_album_tags($current_album) if $album_dirty; + $albumedit->hide; +} + +sub on_revert2_activate { + load_album_tags $current_album; + status(sprintf(_("Reverted from %s."), $current_album)); +} + +sub save_album_tags; + +sub on_save2_activate { + save_album_tags $current_album; +} + +# sub on_exit2_activate { +# &on_exit1_activate(@_); +# } + +sub load_album_tags($) { + %old_album = %album; # save for later recall + + my $test_filename = shift; + $parser = new XML::DOM::Parser; + if (-r "${test_filename}" ) { + $album_doc = $parser->parsefile ("${test_filename}"); + } else { + # literal minimal tree + $album_doc = $parser->parse(''); + status(sprintf(_("%s not found, constructing"), "${test_filename}")); + } + + + undef %album; + + for my $i (@album_tags) { + $album{$i} = ""; + } + + for my $field_node ($album_doc->xql("album/description/field")) { + my $fieldname = ($field_node->xql("\@name"))[0]->xql_toString; + my $fieldval = $field_node->xql_toString; + print "N: $fieldname V: $fieldval\n" if $debug; + $album{$fieldname} = $UTF2Latin->convert($fieldval); + } + + # a clist is output only. someday, replace it with a list of + # editboxes, even if we have to write one all in perl. + + # in the mean time, we vector out to a pair of combo boxes and let + # the user edit there, while copying the changes back live. + + # save last index if any... + my $oldtag; + my $oldrow = $albumprop->focus_row(); + $oldtag = $albumprop->get_text($oldrow, 0) if $oldrow > 0; + print "old $oldrow: $oldtag\n" if $debug; + $albumprop->clear; + for my $i (sort keys %album) { + $albumprop->append("\u$i", $album{$i}); + } + if ($oldrow > 0) { + my $newrow = my_gtk_find_row($albumprop, 0, $oldtag); + print "new $newrow\n" if $debug; + if ($newrow > 0) { + $albumprop->set_focus_row($newrow); + $albumprop->grab_focus(); + } + } + + # help the user enter stuff + $albumname->set_popdown_strings(@known_tags); + # tag as unchanged + $dirty = 0; +} + +sub save_album_tags { + my $test_filename = shift; + if (not $album_dirty) { + status(sprintf(_("%s not dirty, not saved"), ${test_filename})); + return; + } + + my $parent = ($album_doc->xql("album/description"))[0]; # first one + my %f = %album; + + # write out the tree... + for my $xmlfield ($album_doc->xql("album/description/field")) { + my $namestr = $xmlfield->getAttribute("name"); + if (defined $f{$namestr}) { + # delete this node so we can append it later + $xmlfield->getParentNode->removeChild($xmlfield); + } + } + # now append the remaining ones... + for my $k (keys %f) { + next if ($f{$k} eq ""); + my $newfield = new XML::DOM::Element($album_doc, "field"); + print "creating <$k> with <$f{$k}>\n" if $debug; + $newfield->setAttribute("name", $k); # needs quoting! + $newfield->addText($Latin2UTF->convert($f{$k})); + $parent->appendChild($newfield); + print "created $k with $f{$k}\n" if $debug; + } + + $album_doc->printToFile("${test_filename}"); + status(sprintf(_("Saved %s."), $test_filename)); + # undirty it + $album_dirty = 0; +} diff -r 000000000000 -r a84c32f131df bins-edit-gui.glade --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bins-edit-gui.glade Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,1137 @@ + + + + + Bins-edit + bins-edit + + src + pixmaps + Perl + True + True + True + + + + GtkWindow + image_edit_top + BINS Image Property Editor + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + False + True + False + + + GtkVBox + vbox3 + False + 0 + + + GtkMenuBar + menubar2 + GTK_SHADOW_OUT + + 0 + False + False + + + + GtkMenuItem + file2 + GNOMEUIINFO_MENU_FILE_TREE + + + GtkMenu + file2_menu + + + GtkPixmapMenuItem + save1 + + activate + on_save1_activate + Wed, 01 May 2002 14:29:53 GMT + + GNOMEUIINFO_MENU_SAVE_ITEM + + + + GtkPixmapMenuItem + open2 + + activate + on_open2_activate + Wed, 01 May 2002 14:29:53 GMT + + GNOMEUIINFO_MENU_OPEN_ITEM + + + + GtkPixmapMenuItem + revert1 + + activate + on_revert1_activate + Wed, 01 May 2002 14:29:53 GMT + + GNOMEUIINFO_MENU_REVERT_ITEM + + + + GtkPixmapMenuItem + album1 + Edit the album this picture is in. + + 0 + GDK_F2 + activate + + + activate + on_album1_activate + Mon, 03 Jun 2002 06:17:05 GMT + + + False + GNOME_STOCK_MENU_BOOK_OPEN + + + + GtkPixmapMenuItem + exit1 + + activate + on_exit1_activate + Thu, 02 May 2002 16:04:05 GMT + + GNOMEUIINFO_MENU_EXIT_ITEM + + + + + + GtkMenuItem + go1 + + False + + + GtkMenu + go1_menu + + + GtkPixmapMenuItem + next_file1 + Save this file and load the next one. + + GDK_CONTROL_MASK + GDK_n + activate + + + activate + on_next_file1_activate + Mon, 06 May 2002 21:18:06 GMT + + + False + GNOME_STOCK_MENU_FORWARD + + + + GtkPixmapMenuItem + forward_10 + + GDK_MOD1_MASK + GDK_Page_Down + activate + + + activate + on_forward_10_activate + Sun, 12 May 2002 21:26:57 GMT + + + False + GNOME_STOCK_MENU_DOWN + + + + GtkPixmapMenuItem + prev_file1 + Save this file and load the previous one. + + GDK_CONTROL_MASK + GDK_p + activate + + + activate + on_prev_file1_activate + Mon, 06 May 2002 21:18:06 GMT + + + False + GNOME_STOCK_MENU_BACK + + + + GtkPixmapMenuItem + back_10 + + GDK_MOD1_MASK + GDK_Page_Up + activate + + + activate + on_back_10_activate + Sun, 12 May 2002 21:26:57 GMT + + + False + GNOME_STOCK_MENU_UP + + + + GtkPixmapMenuItem + start_of_list1 + + GDK_MOD1_MASK + GDK_Home + activate + + + activate + on_start_of_list1_activate + Sun, 12 May 2002 21:26:57 GMT + + + False + GNOME_STOCK_MENU_FIRST + + + + GtkPixmapMenuItem + end_of_list1 + + GDK_MOD1_MASK + GDK_End + activate + + + activate + on_end_of_list1_activate + Sun, 12 May 2002 21:26:57 GMT + + + False + GNOME_STOCK_MENU_LAST + + + + + + GtkMenuItem + edit1 + GNOMEUIINFO_MENU_EDIT_TREE + + + GtkMenu + edit1_menu + + + GtkPixmapMenuItem + auto_fill1 + Fill in blank fields from most recently seen image. + + GDK_CONTROL_MASK + GDK_a + activate + + + activate + on_auto_fill1_activate + Wed, 08 May 2002 18:43:48 GMT + + + False + GNOME_STOCK_MENU_CONVERT + + + + GtkPixmapMenuItem + rotate_right_cw1 + Rotate image to the Right (Clockwise) + + GDK_SHIFT_MASK + GDK_greater + activate + + + activate + on_rotate_right_cw1_activate + Sun, 19 May 2002 00:11:57 GMT + + + False + GNOME_STOCK_MENU_REDO + + + + GtkPixmapMenuItem + rotate_left1 + Rotate image to the Left (Counter Clockwise) + + GDK_SHIFT_MASK + GDK_less + activate + + + activate + on_rotate_left1_activate + Sun, 19 May 2002 00:11:57 GMT + + + False + GNOME_STOCK_MENU_UNDO + + + + GtkPixmapMenuItem + cancel_rotation1 + Restore original orientation (based on the EXIF tags, if any) + + activate + on_cancel_rotation1_activate + Sun, 19 May 2002 00:11:57 GMT + + + False + GNOME_STOCK_MENU_STOP + + + + + + GtkMenuItem + help2 + GNOMEUIINFO_MENU_HELP_TREE + + + GtkMenu + help2_menu + + + GtkPixmapMenuItem + about2 + + activate + on_about2_activate + Wed, 01 May 2002 14:29:53 GMT + + GNOMEUIINFO_MENU_ABOUT_ITEM + + + + GtkPixmapMenuItem + license1 + GPL + + activate + on_license1_activate + Fri, 10 May 2002 04:50:31 GMT + + + False + GNOME_STOCK_MENU_BOOK_OPEN + + + + + + + GtkHPaned + hpaned2 + 10 + 6 + + 0 + True + True + + + + GtkScrolledWindow + scrolledwindow2 + GTK_POLICY_ALWAYS + GTK_POLICY_ALWAYS + GTK_UPDATE_CONTINUOUS + GTK_UPDATE_CONTINUOUS + + True + True + + + + GtkCList + image_prop_list + True + 2 + 80,80 + GTK_SELECTION_SINGLE + True + GTK_SHADOW_IN + + + GtkLabel + CList:title + label4 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + + + GtkLabel + CList:title + label5 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + + + + + GtkVBox + vbox6 + False + 0 + + True + True + + + + GtkLabel + image_filename_label + + GTK_JUSTIFY_LEFT + False + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GtkPixmap + image_edit_pixmap + 0 + 0 + 0 + 0 + True + + 0 + True + True + + + + + + + GtkHBox + fixed1 + False + 0 + + 0 + False + False + + + + GtkVBox + vbox4 + False + 0 + + 0 + False + False + + + + GtkLabel + label6 + + GTK_JUSTIFY_LEFT + False + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GnomeEntry + field_name_entry + 175 + 15 + + 0 + True + True + + + + GtkEntry + GnomeEntry:entry + combo-entry1 + True + True + True + 0 + + + + + + + GtkVBox + vbox5 + False + 0 + + 0 + True + True + + + + GtkLabel + label7 + + GTK_JUSTIFY_LEFT + False + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GnomeEntry + field_value_entry + 10 + + 0 + True + True + + + + GtkEntry + GnomeEntry:entry + combo-entry2 + True + True + True + 0 + + + + + + + + GtkStatusbar + statusbar1 + + 0 + False + False + + + + + + + GtkDialog + license_box + False + BINS License + GTK_WINDOW_DIALOG + GTK_WIN_POS_NONE + True + True + True + False + + + GtkVBox + Dialog:vbox + dialog-vbox1 + False + 0 + + + GtkHBox + Dialog:action_area + dialog-action_area1 + 10 + True + 5 + + 0 + False + True + GTK_PACK_END + + + + GtkHButtonBox + hbuttonbox1 + GTK_BUTTONBOX_DEFAULT_STYLE + 30 + 85 + 27 + 7 + 0 + + 0 + True + True + + + + GtkButton + dismiss_about + True + True + + clicked + on_dismiss_about_clicked + Fri, 10 May 2002 05:07:03 GMT + + GNOME_STOCK_BUTTON_OK + GTK_RELIEF_NORMAL + + + + + + GtkScrolledWindow + scrolledwindow3 + GTK_POLICY_NEVER + GTK_POLICY_ALWAYS + GTK_UPDATE_CONTINUOUS + GTK_UPDATE_CONTINUOUS + + 0 + True + True + + + + GtkText + license_text + True + False + + + + + + + + GnomeAbout + about_box + False + True + Copyright 2002 Mark W. Eichin <eichin@thok.org> + Mark W. Eichin <eichin@thok.org> + The Herd Of Kittens + + See bins-edit-gui(1) and bins_edit(1) for details. + + + + GtkWindow + album_edit_top + False + window1 + GTK_WIN_POS_NONE + False + False + True + False + + + GtkVBox + vbox7 + False + 0 + + + GtkMenuBar + menubar3 + GTK_SHADOW_OUT + + 0 + False + False + + + + GtkMenuItem + file3 + GNOMEUIINFO_MENU_FILE_TREE + + + GtkMenu + file3_menu + + + GtkPixmapMenuItem + save2 + + activate + on_save2_activate + Mon, 03 Jun 2002 05:56:22 GMT + + GNOMEUIINFO_MENU_SAVE_ITEM + + + + GtkPixmapMenuItem + revert2 + + activate + on_revert2_activate + Mon, 03 Jun 2002 05:56:22 GMT + + GNOMEUIINFO_MENU_REVERT_ITEM + + + + GtkPixmapMenuItem + close1 + + activate + on_close1_activate + Mon, 03 Jun 2002 05:56:22 GMT + + GNOMEUIINFO_MENU_CLOSE_ITEM + + + + GtkPixmapMenuItem + exit2 + + activate + on_exit1_activate + Mon, 03 Jun 2002 05:56:22 GMT + + GNOMEUIINFO_MENU_EXIT_ITEM + + + + + + GtkMenuItem + help3 + GNOMEUIINFO_MENU_HELP_TREE + + + GtkMenu + help3_menu + + + GtkPixmapMenuItem + about3 + + activate + on_about2_activate + Mon, 03 Jun 2002 05:56:22 GMT + + GNOMEUIINFO_MENU_ABOUT_ITEM + + + + + + + GtkLabel + album_edit_filename + + GTK_JUSTIFY_LEFT + False + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GtkHPaned + hpaned3 + 10 + 6 + + 0 + True + True + + + + GtkScrolledWindow + scrolledwindow4 + GTK_POLICY_ALWAYS + GTK_POLICY_ALWAYS + GTK_UPDATE_CONTINUOUS + GTK_UPDATE_CONTINUOUS + + True + True + + + + GtkCList + album_prop_list + True + 2 + 80,80 + GTK_SELECTION_SINGLE + True + GTK_SHADOW_IN + + + GtkLabel + CList:title + label9 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + + + GtkLabel + CList:title + label10 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + + + + + GtkVBox + vbox8 + False + 0 + + True + True + + + + GtkHBox + hbox1 + False + 0 + + 0 + False + False + + + + GtkLabel + label11 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GnomeEntry + album_name_entry + 10 + + 0 + True + True + + + + GtkEntry + GnomeEntry:entry + combo-entry3 + True + True + True + 0 + + + + + + + GtkVBox + vbox9 + False + 0 + + 0 + True + True + + + + GtkLabel + label12 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + 0 + False + False + + + + + GtkScrolledWindow + scrolledwindow5 + GTK_POLICY_NEVER + GTK_POLICY_ALWAYS + GTK_UPDATE_CONTINUOUS + GTK_UPDATE_CONTINUOUS + + 0 + True + True + + + + GtkText + album_edit_text + True + True + + + + + + + + + + + GnomeDialog + iconv_failed_dialog + False + GTK_WINDOW_POPUP + GTK_WIN_POS_MOUSE + True + False + False + False + True + True + + + GtkVBox + GnomeDialog:vbox + dialog-vbox4 + False + 8 + + 4 + True + True + + + + GtkHButtonBox + GnomeDialog:action_area + dialog-action_area4 + GTK_BUTTONBOX_END + 8 + 85 + 27 + 7 + 0 + + 0 + False + True + GTK_PACK_END + + + + GtkButton + button2 + True + True + GNOME_STOCK_BUTTON_OK + + + + + + GtkVBox + vbox10 + False + 0 + + 0 + True + True + + + + GtkLabel + label13 + + GTK_JUSTIFY_CENTER + False + 0.5 + 0.5 + 0 + 0 + + 0 + True + True + + + + + GtkRadioButton + iconv_latin1_charmap_button + True + + True + True + iconv_failed_group + + 0 + True + True + + + + + GtkHBox + hbox3 + False + 0 + + 0 + True + True + + + + GtkRadioButton + iconv_user_charmap_button + True + + False + True + iconv_failed_group + + 0 + True + True + + + + + GnomeEntry + entry1 + 10 + + 0 + True + True + + + + GtkEntry + GnomeEntry:entry + iconv_user_charmap_entry + True + True + True + 0 + + + + + + + + + + diff -r 000000000000 -r a84c32f131df bins_edit --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bins_edit Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,436 @@ +#!/usr/bin/perl -w + +# bins_edit for BINS Photo Album version 1.1.29 +# Copyright (C) 2001-2004 Jérôme Sautret (Jerome@Sautret.org) +# +# $Id: bins_edit,v 1.21 2004/10/24 13:19:16 jerome Exp $ +# +# 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; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +# Type "bins_edit -h" on command line for usage information. + +use strict; + +use Getopt::Long; +use IO::File; +use UNIVERSAL qw(isa); + +# XML parsing & writing +use XML::Grove; +use XML::Grove::Builder; +use XML::Grove::Path; +use XML::Grove::PerlSAX; +use XML::Parser::PerlSAX; +#use XML::Handler::XMLWriter; +use XML::Handler::YAWriter; +use Text::Iconv; +use HTML::Entities; + +my $verbose = 1; +my $html=0; + +my $localEncoding; +$localEncoding = `locale charmap`; +if ($? != 0 ) { + $localEncoding = "LATIN1"; +} +else { +if (! $localEncoding or ($localEncoding eq "ANSI_X3.4-1968")) { + chop($localEncoding); + # ANSI is unspeakably primitive, promote it. + $localEncoding = "LATIN1"; + print "Forcing encoding to $localEncoding\n" if ($verbose >=2); +} +} +my $converter = Text::Iconv->new($localEncoding, "UTF-8"); + +# decode HTML entites which doesn't exist in XML +sub decodeEntites{ + my $s = shift; + + my %entities = ( + AElig => 'Æ', # capital AE diphthong (ligature) + Aacute => 'Á', # capital A, acute accent + Acirc => 'Â', # capital A, circumflex accent + Agrave => 'À', # capital A, grave accent + Aring => 'Å', # capital A, ring + Atilde => 'Ã', # capital A, tilde + Auml => 'Ä', # capital A, dieresis or umlaut mark + Ccedil => 'Ç', # capital C, cedilla + ETH => 'Ð', # capital Eth, Icelandic + Eacute => 'É', # capital E, acute accent + Ecirc => 'Ê', # capital E, circumflex accent + Egrave => 'È', # capital E, grave accent + Euml => 'Ë', # capital E, dieresis or umlaut mark + Iacute => 'Í', # capital I, acute accent + Icirc => 'Î', # capital I, circumflex accent + Igrave => 'Ì', # capital I, grave accent + Iuml => 'Ï', # capital I, dieresis or umlaut mark + Ntilde => 'Ñ', # capital N, tilde + Oacute => 'Ó', # capital O, acute accent + Ocirc => 'Ô', # capital O, circumflex accent + Ograve => 'Ò', # capital O, grave accent + Oslash => 'Ø', # capital O, slash + Otilde => 'Õ', # capital O, tilde + Ouml => 'Ö', # capital O, dieresis or umlaut mark + THORN => 'Þ', # capital THORN, Icelandic + Uacute => 'Ú', # capital U, acute accent + Ucirc => 'Û', # capital U, circumflex accent + Ugrave => 'Ù', # capital U, grave accent + Uuml => 'Ü', # capital U, dieresis or umlaut mark + Yacute => 'Ý', # capital Y, acute accent + aacute => 'á', # small a, acute accent + acirc => 'â', # small a, circumflex accent + aelig => 'æ', # small ae diphthong (ligature) + agrave => 'à', # small a, grave accent + aring => 'å', # small a, ring + atilde => 'ã', # small a, tilde + auml => 'ä', # small a, dieresis or umlaut mark + ccedil => 'ç', # small c, cedilla + eacute => 'é', # small e, acute accent + ecirc => 'ê', # small e, circumflex accent + egrave => 'è', # small e, grave accent + eth => 'ð', # small eth, Icelandic + euml => 'ë', # small e, dieresis or umlaut mark + iacute => 'í', # small i, acute accent + icirc => 'î', # small i, circumflex accent + igrave => 'ì', # small i, grave accent + iuml => 'ï', # small i, dieresis or umlaut mark + ntilde => 'ñ', # small n, tilde + oacute => 'ó', # small o, acute accent + ocirc => 'ô', # small o, circumflex accent + ograve => 'ò', # small o, grave accent + oslash => 'ø', # small o, slash + otilde => 'õ', # small o, tilde + ouml => 'ö', # small o, dieresis or umlaut mark + szlig => 'ß', # small sharp s, German (sz ligature) + thorn => 'þ', # small thorn, Icelandic + uacute => 'ú', # small u, acute accent + ucirc => 'û', # small u, circumflex accent + ugrave => 'ù', # small u, grave accent + uuml => 'ü', # small u, dieresis or umlaut mark + yacute => 'ý', # small y, acute accent + yuml => 'ÿ', # small y, dieresis or umlaut mark + + # Some extra Latin 1 chars that are listed in the HTML3.2 draft (21-May-96) + copy => '©', # copyright sign + reg => '®', # registered sign + nbsp => "\240", # non breaking space + + # Additional ISO-8859/1 entities listed in rfc1866 (section 14) + iexcl => '¡', + cent => '¢', + pound => '£', + curren => '¤', + yen => '¥', + brvbar => '¦', + sect => '§', + uml => '¨', + ordf => 'ª', + laquo => '«', + 'not' => '¬', # not is a keyword in perl + shy => '­', + macr => '¯', + deg => '°', + plusmn => '±', + sup1 => '¹', + sup2 => '²', + sup3 => '³', + acute => '´', + micro => 'µ', + para => '¶', + middot => '·', + cedil => '¸', + ordm => 'º', + raquo => '»', + frac14 => '¼', + frac12 => '½', + frac34 => '¾', + iquest => '¿', + 'times' => '×', # times is a keyword in perl + divide => '÷', + ); + + while (my($entity, $char) = each(%entities)) { + $s =~ s/\&$entity\;/$char/g; + } + return $s; +} + +sub charac_indent{ + my $n = shift(@_); + my $s="\n"; + for (1..$n){ + $s .= " "; + } + return XML::Grove::Characters->new ( Data => $s ); +} + +sub setField{ + my $field = shift(@_); # field to add or modify + my $value = shift(@_); # value to set to field + my $fileType = shift(@_); # type of file (iamge or album) + my $document = shift(@_); # XML document as a Grove + + if (! $html) { + $value = encode_entities($value, '\00-\31<&"'); + } + + my $characters = + XML::Grove::Characters->new( Data => + decodeEntites($value)); + #my $characters = XML::Grove::Characters->new ( Data => $value ); + + my $fieldName; + my $fieldValue; + foreach my $element + (@{$document->at_path('/'.$fileType.'/description')->{Contents}}) { + if (isa($element, 'XML::Grove::Element') && $element->{Name} eq "field") { + $fieldName = $element->{Attributes}{'name'}; + $fieldValue = ""; + if ($fieldName eq $field) { + print " Modifying field '$fieldName' to '$value'... " + if ($verbose >= 3); + @{$element->{Contents}} = ( charac_indent(3), + $characters, + charac_indent(2)); + print "OK.\n" if ($verbose >= 3); + return; + } + } + } + + print " Adding field '$field' with value '$value'... " if ($verbose >= 2); + my $element = XML::Grove::Element->new ( Name => 'field', + Contents => [charac_indent(3), + $characters, + charac_indent(2)], + Attributes => {"name" => $field}); + push @{$document->at_path('/'.$fileType.'/description')->{Contents}}, + (charac_indent(2), $element, charac_indent(1)); + + print "OK.\n" if ($verbose >= 2); +} + +sub setFields{ + my $file = shift(@_); + my $fields = shift(@_); + my $album = shift(@_); # type of file (0 if image or 1 if album) + my $document; + + my $fileType; + if ($album) { + $fileType = "album"; + } else{ + $fileType = "image"; + } + + if (-e $file) { + # Get XML document as a Grove + print " Reading file '$file'... " if ($verbose >= 2); + my $grove_builder = XML::Grove::Builder->new; + my $parser = XML::Parser::PerlSAX->new ( Handler => $grove_builder ); + $document = $parser->parse ( Source => { SystemId => $file } ); + print "OK.\n" if ($verbose >= 2); + } else { + print " Creating file '$file'... " if ($verbose >= 2); + my @elements; + push @elements, (charac_indent(1), + XML::Grove::Element->new ( Name => 'description', + Contents => [charac_indent(1)]), + charac_indent(1), + XML::Grove::Element->new ( Name => 'bins', + Contents => [charac_indent(1)]), + ); + if (!$album) { + push @elements, ( charac_indent(1), + XML::Grove::Element->new ( Name => 'exif', + Contents => + [charac_indent(1)]), + ); + } + push @elements, charac_indent(0); + my $element = + XML::Grove::Element->new ( Name => $fileType, + Contents => \@elements); + $document = XML::Grove::Document->new ( Contents => [ $element ] ); + print "OK.\n" if ($verbose >= 3); + } + + my $fieldName; + my $fieldValue; + while ( ($fieldName, $fieldValue) = each(%$fields) ) { + if (defined $fieldValue) { + setField($fieldName, $fieldValue, $fileType, $document); + } + } + + print " Writing file '$file'... " if ($verbose >= 2); + # Write the Grove to the desc file + my $fileHandler = new IO::File; + open($fileHandler, '>', $file) + or die("Cannot open file $file to write Exif tag ($!)"); + binmode($fileHandler, ":utf8") if $^V ge v5.8.0; + + my $my_handler = new XML::Handler::YAWriter( 'Output' => $fileHandler, + # 'Escape' => { + # '--' => '—', + #'&' => '&', + # }, + 'Encoding' => "UTF-8", + ); +# my $my_handler = XML::Handler::XMLWriter->new( Output => $fileHandler, +# Newlines => 0); + $document->parse(DocumentHandler => $my_handler); + close ($fileHandler) || bail ("can't close $file ($!)"); + print "OK.\n" if ($verbose >= 2); +} + +sub copyleft{ +print "\nbins_edit for BINS Photo Album 1.1.29 (http://bins.sautret.org/)\n"; +print "Copyright © 2001-2004 Jérôme Sautret (Jerome\@Sautret.org)\n"; +print "This is free software with ABSOLUTELY NO WARRANTY.\n"; +print "See COPYING file for details.\n\n"; +} + +sub usage{ + my $exit=shift; # should we exit after usage information ? + copyleft(); + + print <BINS is cool' file.jpg + +Set the title short description and sample image of the album +in the current directory (note the dot as final parameter): +bins_edit -a -t "My Album" --sample image.jpg --shortdesc "This is my album" . + +EoF + + exit 1; +} + + +sub main{ + my %values; + my $album = 0; # 1 if it a album description file + + + # process args + Getopt::Long::Configure("bundling"); + GetOptions('t|title:s' => \$values{title}, + 'e|event:s' => \$values{event}, + 'l|location:s' => \$values{location}, + 'p|people:s' => \$values{people}, + 'y|date:s' => \$values{date}, + 'd|description:s' => \$values{description}, + 'longdesc:s' => \$values{longdesc}, + 'shortdesc:s' => \$values{shortdesc}, + 'sample:s' => \$values{sampleimage}, + 'g|generic=s%' => \%values, + 'm|html' => \$html, + 'a|album' => \$album, + 'v|verbose+' => \$verbose, + 'q|quiet' => sub { $verbose = 0 }, + 'h|help' => sub { help() }, + 'copyright' => sub { copyleft() }, + ) + or usage(1); + + my @files; + if ($#ARGV < 0) { + if ($album) { + @files = ("."); + } else { + print "No files specified.\n"; + usage(1) + } + } else { + @files = @ARGV; + } + + copyleft() if ($verbose >=2); + + foreach my $file (@files) { + if ($album) { + $file .= "/album.xml"; + } + if ($file !~ m/.xml$/) { + $file .= ".xml"; + } + print "Processing file '$file'... " if ($verbose >= 1); + print "\n" if ($verbose >= 2); + setFields($file, \%values, $album); + print "OK.\n" if ($verbose == 1); + } +} + +main(); diff -r 000000000000 -r a84c32f131df binsrc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/binsrc Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,534 @@ + + + + + + + + + + + swigs + + + + + / + + + + + + + + + blue + + + + + 1 + + + + + 0 + + + + + 1 + + + + + 0 + + + + + 1 + + + + + 1 + + + + + + + + + + + + + 1 + + + + + 1 + + + + + 0 + + + + + 1 + + + + + %c + + + + + hidden + + + + + ignore + + + + + 75 + + + + + smaller + + + + + 1 + + + + + 0 + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 0 + + + + + 0 + + + + + 0 + + + + + 16 + + + + + 4 + + + + + 150 + + + + + 150 + + + + + 1 + + + + + destination + + + + + 0 + + + + + scale + + + + + enlarge + + + + + 0 + + + + + 0 + + + + + 1 + + + + + 40 + + + + + 1 + + + + + _Orig$ + + + + + ^(CVS|RCS)$ + + + + + + + + + 1 + + + + + 1 + + + + + 1 + + + + + + 1 + + + + + 50 + + + + + title description people location event comment + + + + + 1 + + + + + 1 + + + + + + + + + + + + + + + + + #FFFFFF + + + #000000 + + + #000077 + + + #FFFFFF + + + #EEDD82 + + + #D2D2D2 + + + #6060AF + + + #EEDD82 + + + #FFFFFF + + + #FFFFFF + + + + + #FFFFFF + + + #000000 + + + #90A080 + + + #FFFFFF + + + #000082 + + + #000000 + + + #A8B898 + + + #0000A0 + + + #000000 + + + #FFFFFF + + + + + #8B7E66 + + + #FFFFFF + + + #FFFFF0 + + + #8B4513 + + + #CD853F + + + #000000 + + + #FFEBCD + + + #8B8682 + + + #000000 + + + #8B795E + + + + + #FFC0CB + + + #B03060 + + + #FFFFFF + + + #FFBBFF + + + #000000 + + + #FF69B4 + + + #FFBBFF + + + #000000 + + + #FFFFFF + + + diff -r 000000000000 -r a84c32f131df doc/album.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/album.xml Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,60 @@ + + + + + + + + + This is my holidays pictures taken during various travels. + + + + Holidays + + + + Holidays pictures. + + + + Travel1/IMG_0005.JPG + + + + + + + green + + + 150 + + + 150 + + + 20 + + + 5 + + + 0 + + + 1 + + + + + + + + diff -r 000000000000 -r a84c32f131df doc/autolayout.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/autolayout.xml Wed Oct 15 23:28:56 2008 +0200 @@ -0,0 +1,77 @@ + + + +2001-2005Jérôme SAUTRET