#!/usr/bin/ruby # drezza - Get a picture collection ready for publishing # # Copyright (C) 2005 Enrico Zini # # 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 # Usage: drezza [directory] # If the directory is not given, display the current directory. # # Thumbnails are taken from the EXIF data in the JPEG files. If the EXIF # thumbnail is missing, it will be generated using exiftran. # # All image rotations are performed with exiftran, do not cause loss of # information in the image and are respectful of existing EXIF data. # Depends: libexif-ruby, libgnome2-ruby, exiftran require 'gnome2' require 'tempfile' require 'exif' # Define custom icons # The image data has been generated using gdk-pixbuf-csource -rle, taking the # icons from eog BROWSE_STOCK_ROTATE_90 = :"drezza-rotate90-icon" ICON_ROTATE_90 = "GdkP"+ "\0\0\2!"+ "\2\1\0\2"+ "\0\0\0@"+ "\0\0\0\20"+ "\0\0\0\20"+ "\242\0\0\0\0\205\0\0\0\377\2\0\0\0\247\0\0\0\32\211\0\0\0\0\10\0\0\0"+ "\377\310\327\345\377\307\326\345\375\311\327\345\366\312\330\345\363"+ "\275\303\310\341(((\311\0\0\0d\210\0\0\0\0\2\0\0\0\377\304\324\344\377"+ "\202\235\270\322\377\5\243\274\325\377\242\273\323\373\221\245\270\333"+ "111\335\0\0\0^\207\0\0\0\0\2\0\0\0\377\304\324\344\377\202\235\270\322"+ "\377\6\244\275\325\377\237\271\323\377\237\271\322\373\215\234\252\310"+ "\0\0\0\240\0\0\0\2\206\0\0\0\0\204\0\0\0\377\7\224\241\255\364\261\307"+ "\333\377\242\274\324\377\237\267\316\362\0\0\0\377\0\0\0\10\0\0\0\1\205"+ "\0\0\0\0\13\0\0\0\1\0\0\0\13\0\0\0\34\0\0\0+\0\0\0\377\303\323\343\377"+ "\235\270\322\377\247\276\324\372\0\0\0\377\0\0\0\23\0\0\0\2\206\0\0\0"+ "\0\1\0\0\0\1\203\0\0\0\377\3\304\324\344\377\235\270\322\377\234\267"+ "\321\375\203\0\0\0\377\1\0\0\0\1\206\0\0\0\0\4***\377\304\324\344\377"+ "\324\340\353\377\302\323\343\377\202\235\270\322\377\5\261\307\333\377"+ "\301\322\342\377\0\0\0\377\0\0\0\11\0\0\0\1\205\0\0\0\0\2\0\0\0""2\0"+ "\0\0\377\205\235\270\322\377\4\0\0\0\377\0\0\0L\0\0\0\22\0\0\0\2\205"+ "\0\0\0\0\3\0\0\0\1\0\0\0#\0\0\0\377\203\235\270\322\377\5\0\0\0\377\0"+ "\0\0\205\0\0\0+\0\0\0\14\0\0\0\1\206\0\0\0\0\11\0\0\0\2\0\0\0'\0\0\0"+ "\377\235\270\322\377\0\0\0\377\0\0\0\203\0\0\0""0\0\0\0\17\0\0\0\3\210"+ "\0\0\0\0\7\0\0\0\2\0\0\0'\0\0\0\377\0\0\0o\0\0\0-\0\0\0\17\0\0\0\3\212"+ "\0\0\0\0\5\0\0\0\2\0\0\0:\0\0\0\23\0\0\0\14\0\0\0\2\214\0\0\0\0\3\0\0"+ "\0\1\0\0\0\2\0\0\0\1\205\0\0\0\0"; BROWSE_STOCK_ROTATE_180 = :"drezza-rotate180-icon" ICON_ROTATE_180 = "GdkP"+ "\0\0\3C"+ "\2\1\0\2"+ "\0\0\0@"+ "\0\0\0\20"+ "\0\0\0\20"+ "\203\0\0\0\0\1\0\0\0\1\205\0\0\0\377\4\0\0\0\254\0\0\0#\0\0\0\5\0\0\0"+ "\2\206\0\0\0\0\13\0\0\0\2\0\0\0\377\310\327\345\377\307\326\345\375\310"+ "\327\345\366\300\316\332\364\250\255\262\346'''\316\0\0\0o\0\0\0\11\0"+ "\0\0\3\205\0\0\0\0\3\0\0\0\2\0\0\0\377\304\324\344\377\202\235\270\322"+ "\377\7\243\274\325\377\242\273\323\373\217\244\266\335000\341\0\0\0m"+ "\0\0\0\13\0\0\0\3\204\0\0\0\0\3\0\0\0\2\0\0\0\377\304\324\344\377\202"+ "\235\270\322\377\10\244\275\325\377\237\271\323\377\240\272\323\372\203"+ "\221\237\315\0\0\0\253\0\0\0\27\0\0\0\7\0\0\0\1\203\0\0\0\0\1\0\0\0\2"+ "\204\0\0\0\377\10\226\243\260\357\261\307\333\377\242\274\324\377\227"+ "\256\306\365\0\0\0\377\0\0\0#\0\0\0\14\0\0\0\2\203\0\0\0\0\15\0\0\0\2"+ "\0\0\0E***\377\0\0\0\377\0\0\0>\0\0\0\377\303\323\343\377\235\270\322"+ "\377\241\273\322\373\0\0\0\377\0\0\0,\0\0\0\21\0\0\0\2\203\0\0\0\0\15"+ "\0\0\0\33\0\0\0\377\304\324\344\377\0\0\0\377\0\0\0""8\0\0\0\377\305"+ "\324\344\377\235\270\322\377\237\271\322\374\0\0\0\377\0\0\0""0\0\0\0"+ "\22\0\0\0\2\202\0\0\0\0\4\0\0\0\33\0\0\0\377\302\322\343\377\235\270"+ "\322\377\202\0\0\0\377\14\253\265\277\361\314\332\347\377\240\273\323"+ "\377\225\255\305\367\0\0\0\377\0\0\0""0\0\0\0\22\0\0\0\2\0\0\0\0\0\0"+ "\0\33\0\0\0\377\302\322\343\377\202\235\270\322\377\15\301\322\342\377"+ "\312\331\345\377\314\331\346\377\252\301\330\377\237\271\322\373z\207"+ "\224\322\0\0\0\262\0\0\0,\0\0\0\21\0\0\0\2\0\0\0""1\0\0\0\377\314\331"+ "\347\377\205\235\270\322\377\7\243\274\325\377\242\273\323\373\216\242"+ "\264\337000\344\0\0\0~\0\0\0#\0\0\0\14\202\0\0\0\2\2\0\0\0H\0\0\0\377"+ "\204\235\270\322\377\10\237\271\321\376\232\262\310\375\210\221\233\362"+ "\0\0\0""6\0\0\0.\0\0\0#\0"+ "\0\0\25\0\0\0\12\0\0\0\3\0\0\0\1\203\0\0\0\0\3\0\0\0\3\0\0\0\13\0\0\0"+ "2\202\0\0\0\377\6\0\0\0""4\0\0\0\37\0\0\0\22\0\0\0\14\0\0\0\5\0\0\0\2"+ "\206\0\0\0\0\7\0\0\0\3\0\0\0\12\0\0\0\31\0\0\0'\0\0\0#\0\0\0\17\0\0\0"+ "\4\202\0\0\0\1\210\0\0\0\0\6\0\0\0\2\0\0\0\7\0\0\0\16\0\0\0\15\0\0\0"+ "\5\0\0\0\1\206\0\0\0\0"; BROWSE_STOCK_ROTATE_270 = :"drezza-rotate270-icon" ICON_ROTATE_270 = "GdkP"+ "\0\0\2""1"+ "\2\1\0\2"+ "\0\0\0@"+ "\0\0\0\20"+ "\0\0\0\20"+ "\247\0\0\0\0\4\0\0\0\31\0\0\0\206\0\0\0\377\1\1\1\377\203\0\0\0\377\1"+ "\0\0\0\1\207\0\0\0\0\12\0\0\0RBBB\317\301\303\310\377\315\332\344\373"+ "\264\310\334\363\235\270\322\373\235\270\322\377\0\0\0\377\0\0\0\13\0"+ "\0\0\1\205\0\0\0\0\5\0\0\0JMMM\314\327\340\351\373\263\310\332\377\244"+ "\274\324\377\203\235\270\322\377\3\0\0\0\377\0\0\0\34\0\0\0\3\205\0\0"+ "\0\0\5\0\0\0u\310\314\322\377\266\312\334\377\245\275\325\377\257\305"+ "\332\377\203\235\270\322\377\3\0\0\0\377\0\0\0$\0\0\0\4\205\0\0\0\0\5"+ "\0\0\0\377\310\327\344\375\251\301\327\377\235\270\322\377\210\226\243"+ "\362\204\0\0\0\377\2\0\0\0\34\0\0\0\3\205\0\0\0\0\2\0\0\0\377\272\315"+ "\337\371\202\235\270\322\377\7\0\0\0\377\0\0\0`\0\0\0@\0\0\0+\0\0\0\34"+ "\0\0\0\13\0\0\0\1\203\0\0\0\0\203\0\0\0\377\1\307\326\345\375\202\235"+ "\270\322\377\203\0\0\0\377\3\0\0\0\12\0\0\0\4\0\0\0\1\204\0\0\0\0\4\0"+ "\0\0\377\267\312\336\377\325\341\354\377\331\343\355\377\204\235\270"+ "\322\377\3\0\0\0\377\0\0\0\12\0\0\0\1\205\0\0\0\0\2\0\0\0\33\0\0\0\377"+ "\205\235\270\322\377\4\0\0\0\377\0\0\0^\0\0\0\22\0\0\0\2\205\0\0\0\0"+ "\3\0\0\0\1\0\0\0M\0\0\0\377\203\235\270\322\377\5\0\0\0\377\0\0\0i\0"+ "\0\0+\0\0\0\13\0\0\0\1\206\0\0\0\0\11\0\0\0\3\0\0\0I\0\0\0\377\235\270"+ "\322\377\0\0\0\377\0\0\0k\0\0\0""0\0\0\0\17\0\0\0\2\210\0\0\0\0\7\0\0"+ "\0\3\0\0\0I\0\0\0\377\0\0\0T\0\0\0-\0\0\0\17\0\0\0\2\212\0\0\0\0\5\0"+ "\0\0\2\0\0\0:\0\0\0\23\0\0\0\14\0\0\0\2\214\0\0\0\0\3\0\0\0\1\0\0\0\2"+ "\0\0\0\1\206\0\0\0\0"; # Get a Tempfile object opened to a file containing the thumbnail for the given # image def getThumbnail (file) exif = Exif.new(file) begin # Read the thumbnail tempFile = Tempfile.new("exifthumb") exif.thumbnail(tempFile) rescue Exif::Error::ThumbnailNotFound # Regenerate the thumbnail if missing puts `exiftran -g -i -p #{file}` exif.thumbnail(tempFile) end tempFile.flush return tempFile end # Image viewer # # Even if we scale the image to always fit the widget, we need a scrollbar # because Gtk::Image does not allow its size to shrink class ImageView < Gtk::ScrolledWindow def initialize super set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) @image = Gtk::Image.new add_with_viewport(@image) # Scale the image when we are resized signal_connect('size-allocate') do |w, rec| if (@ww != rec.width || @wh != rec.height) #puts "SA: #{rec.width} #{rec.height}" @ww = rec.width @wh = rec.height self.reload end end end def load (fname) @filename = fname self.reload end # Scale dimensions w and h to fix the maxw and maxh box, maintaining the aspect # ratio def scaleDimensions (w, h) if @ww*h/w < @wh return [ @ww, @ww*h/w ] else return [ @wh*w/h, @wh ] end end def reload if @ww && @filename @unscaled = Gdk::Pixbuf.new(@filename) w, h = self.scaleDimensions(@unscaled.width, @unscaled.height) @image.pixbuf = @unscaled.scale(w, h) GC.start end end def rotateClockwise print `exiftran -i -9 -p #{@filename}` self.reload end def rotateUpsideDown print `exiftran -i -1 -p #{@filename}` self.reload end def rotateCounterclockwise print `exiftran -i -2 -p #{@filename}` self.reload end end class ImageList < Gtk::ScrolledWindow type_register # Signal emitted when an image gets selected in the list signal_new("selected", # name GLib::Signal::RUN_FIRST, # flags nil, # accumulator (XXX: not supported yet) nil, # return type (void == nil) String # parameter types ) def initialize super set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) # Liststore has 3 columns: full path, file name only and thumbnail @files = Gtk::ListStore.new(String, String, Gdk::Pixbuf) @fileList = Gtk::TreeView.new(@files) renderer = Gtk::CellRendererPixbuf.new col = Gtk::TreeViewColumn.new("Thumb", renderer, :pixbuf => 2) @fileList.append_column(col) renderer = Gtk::CellRendererText.new col = Gtk::TreeViewColumn.new("File name", renderer, :text => 1) @fileList.append_column(col) self.add @fileList sel = @fileList.selection # Emit the 'selected' signal when the selection changes sel.signal_connect("changed") do |sel| if iter = sel.selected self.signal_emit("selected", iter[0]) end end w, h = self.get_size_request self.set_size_request 300, h end def signal_do_selected(file) #puts "Selected " + file #p caller end def updateCurrentThumbnail sel = @fileList.selection if iter = sel.selected puts "Regenerating thumbnail for #{iter[0]}" thumb = getThumbnail(iter[0]) iter[2] = Gdk::Pixbuf.new thumb.path thumb.close(true) end end # Add one image to the list def addImage(pathname, name) thumb = getThumbnail(pathname) iter = @files.append iter[0] = pathname iter[1] = name iter[2] = Gdk::Pixbuf.new thumb.path thumb.close(true) end end class BrowseApp < Gnome::App NAME = 'drezza' TITLE = 'Drézza - Get a picture collection ready for publishing' VERSION = '0.1' @quit_requested = false; def initialize super(NAME, TITLE) self.set_default_size(600, 400) self.signal_connect("delete_event") do self.quitProgram end # Allow the user to manually allocate space between the image # and the list hb = Gtk::HPaned.new self.set_contents(hb) @imageView = ImageView.new # The imageView expands when the window is resized # The imageView also shrinks when the window is resized hb.pack1(@imageView, true, true) # When an image is selected, load it in the imageView @imageList = ImageList.new @imageList.signal_connect("selected") do |imagelist, filename| @imageView.load(filename) end hb.pack2(@imageList, false, false) do_rotate_90 = proc do @imageView.rotateClockwise @imageList.updateCurrentThumbnail end do_rotate_180 = proc do @imageView.rotateUpsideDown @imageList.updateCurrentThumbnail end do_rotate_270 = proc do @imageView.rotateCounterclockwise @imageList.updateCurrentThumbnail end file_menu = [ Gnome::UIInfo::menu_quit_item(proc { self.quitProgram }, 'Quit'), ] main_menu = [ Gnome::UIInfo::menu_file_tree(file_menu), ] main_toolbar = [ [Gnome::App::UI_ITEM, "Rotate counterclockwise", "Rotate counterclockwise", do_rotate_270, 'toolbar/rotate270', Gnome::App::PIXMAP_STOCK, BROWSE_STOCK_ROTATE_270, 0, 0, nil], [Gnome::App::UI_ITEM, "Rotate upside-down", "Rotate upside-down", do_rotate_180, 'toolbar/rotate180', Gnome::App::PIXMAP_STOCK, BROWSE_STOCK_ROTATE_180, 0, 0, nil], [Gnome::App::UI_ITEM, "Rotate clockwise", "Rotate clockwise", do_rotate_90, 'toolbar/rotate90', Gnome::App::PIXMAP_STOCK, BROWSE_STOCK_ROTATE_90, 0, 0, nil], ] @appBar = Gnome::AppBar.new(true, true, Gnome::PREFERENCES_USER) self.statusbar = @appBar menu_info = self.create_menus(main_menu) @appBar.install_menu_hints(menu_info) self.create_toolbar(main_toolbar) end # Fill in the list with all the jpeg files in a directory def displayDirectory (root) #files = Dir["#{root}/*.{jpg,jpeg}"] files = Dir.entries(root) progress = @appBar.progress progress.fraction = 0.0 progress.text = "Reading directory #{root}" done = 0.0; #Dir.foreach root do |dir| files.each do |dir| return if @quit_requested file = root+'/'+dir if !FileTest.directory?(file) && dir =~ /\.jpe?g$/ && FileTest.readable?(file) @imageList.addImage(file, dir) # Process a round of GTK events, so that # loading thumbnails won't stall the interface while GLib::MainContext.default.pending? GLib::MainContext.default.iteration(false) end end done += 1 progress.fraction = done/files.size.to_f end progress.text = nil progress.fraction = 0.0 end def quitProgram @quit_requested = true; Gtk::main_quit() if Gtk.main_level > 0; end def mainLoop Gtk::main if not @quit_requested end end # Add custom icons to icon factory factory = Gtk::IconFactory.new icon = Gdk::Pixbuf.new ICON_ROTATE_90.unpack("C*"), true set = Gtk::IconSet.new icon factory.add BROWSE_STOCK_ROTATE_90.to_s, set icon = Gdk::Pixbuf.new ICON_ROTATE_180.unpack("C*"), true set = Gtk::IconSet.new icon factory.add BROWSE_STOCK_ROTATE_180.to_s, set icon = Gdk::Pixbuf.new ICON_ROTATE_270.unpack("C*"), true set = Gtk::IconSet.new icon factory.add BROWSE_STOCK_ROTATE_270.to_s, set factory.add_default Gnome::Program.new(BrowseApp::NAME, BrowseApp::VERSION) app = BrowseApp.new() app.show_all() app.displayDirectory(ARGV[0] || '.') app.mainLoop