diff --git a/README b/README new file mode 100644 index 0000000..d58cfae --- /dev/null +++ b/README @@ -0,0 +1,77 @@ +pyGTKtalog 0.8 +============== + +pyGTKtalog Linux/FreeBSD program for indexing CD/DVD or directories on +filesystem. It is similar to gtktalog or +gwhere . There is no coincidence in name of +application, because it's ment to be replacement (in some way) for gtktalog, +which seems to be dead project for years. + +FEATURES +======== + +- scanning for files in selected media +- generating thumbnails +- tagging files + +REQUIREMENTS +============ + +pyGTKtalog is written in python with following dependencies: + +- pygtk +- pysqlite2 (unnecessary, if python 2.5 is used) +# - mx.DateTime + +Optional modules: + +- PIL for image manipulation + +- pyExcelerator for export to + excel capability + +Additional pyGTKtalog uses EXIF module by Gene Cash which is included in +sources. + +pyGTKtalog extensivly uses external programs in unix spirit, however there is +small possibility of using it Windows (probably with liitations) and quite big +possiblity to run it on other sofisticated unix-like systems (i.e. +BeOS/ZETA/Haiku, QNX or MacOSX). + +INSTALATION +=========== + +All you have to do is: + +- put pyGTKtalog directory into your destination of choice (/usr/local/share, + /opt or ~/ is typical choice) +- modify pyGTKtalog/pyGTKtalog line 4 to match right directory +- copy/link pyGTKtalog/pyGTKtalog shell script to /usr/bin, /usr/local/bin or in + other place, where PATH variable is pointing or you feel like. + +Then, just run pyGTKtalog script. + +TODO +==== + +- searching database +- taggin files +- files properties +- adding images +- generating/saving thumbnails +- command line query support (text output) +- internationalization support +- statistics +- moving hardcoded files extensions into config + +NOTES +===== + +Catalog file is tared and optionaly compressed sqlite database and directory +with thumbnails. If there are more images, the size of catalog file will grow. +So be carefull with adding big images in your catalog file! + +BUGS +==== + +All bugs please report to Roman 'gryf' Dobosz diff --git a/pyGTKtalog b/pyGTKtalog index 32182b4..47c1d94 100755 --- a/pyGTKtalog +++ b/pyGTKtalog @@ -1,7 +1,11 @@ #!/bin/sh # Simple wraper to launch python application from desired place. +# adjust to your needs: +data_dir="/home/gryf/Devel/Python/pyGTKtalog" +python_intrpreter="/usr/bin/python" + +# don't change anything below. current_dir="`pwd`" -cd ~/Devel/Python/pyGTKtalog -#export PYTHONPATH="$PYTHONPATH:/home/gryf/Devel/Python/pyGTKtalog/mvc" -#exec python -OO pygtktalog.py $@ -exec python pygtktalog.py "$current_dir" "$@" +cd $data_dir +#exec $python_intrpreter -OO pygtktalog.py "$current_dir" "$@" +exec $python_intrpreter pygtktalog.py "$current_dir" "$@" diff --git a/pygtktalog.py b/pygtktalog.py index 983e575..0f18dcf 100644 --- a/pygtktalog.py +++ b/pygtktalog.py @@ -68,11 +68,11 @@ def check_requirements(): except: print "pyGTKtalog uses SQLite DB.\nYou'll need to get it and the python bindings as well.\nhttp://www.sqlite.org\nhttp://initd.org/tracker/pysqlite" sys.exit(1) - try: - import mx.DateTime - except: - print "pyGTKtalog uses Egenix mx.DateTime.\nYou can instal it from your distribution repositry,\nor get it at: http://www.egenix.com" - sys.exit(1) + #try: + # import mx.DateTime + #except: + # print "pyGTKtalog uses Egenix mx.DateTime.\nYou can instal it from your distribution repositry,\nor get it at: http://www.egenix.com" + # sys.exit(1) if conf.confd['exportxls']: try: diff --git a/resources/glade/config.glade b/resources/glade/config.glade index ac55982..938fa34 100644 --- a/resources/glade/config.glade +++ b/resources/glade/config.glade @@ -1,892 +1,575 @@ - - - + + + - - - 550 - 400 - Preferences - pyGTKtalog - GTK_WINDOW_TOPLEVEL - GTK_WIN_POS_NONE - True - True - False - True - False - False - GDK_WINDOW_TYPE_HINT_NORMAL - GDK_GRAVITY_NORTH_WEST - True - False - True - - - - True - False - 0 - - - - True - GTK_BUTTONBOX_END - - - - True - True - True - gtk-cancel - True - GTK_RELIEF_NORMAL - True - -6 - - - - - - - True - True - True - gtk-save - True - GTK_RELIEF_NORMAL - True - -5 - - - - - - 0 - False - True - GTK_PACK_END - - - - - - 168 - True - True - 140 - - - - True - True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - False - True - False - True - False - False - False - - - - - - True - False - - - - - - True - False - 0 - - - - True - - False - True - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 3 - 3 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - False - False - - - - - - True - True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_NONE - GTK_CORNER_TOP_LEFT - - - - True - GTK_SHADOW_IN - - - - True - False - 0 - - - - 1 - False - 0 - - - - 5 - True - 0 - 0.5 - GTK_SHADOW_ETCHED_IN - - - - 5 - True - True - 0.5 - 0.5 - 1 - 1 - 0 - 0 - 12 - 0 - - - - 5 - True - 2 - 3 - False - 3 - 3 - - - - True - Mount point: - False - False - GTK_JUSTIFY_LEFT - False - False - 0 - 0.5 - 0 - 0 - mnt_entry - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - 1 - 0 - 1 - - - - - - - - 100 - True - True - True - True - 0 - - True - * - False - - - 1 - 2 - 0 - 1 - - - - - - - True - True - Browse... - True - GTK_RELIEF_NORMAL - True - - - - - 2 - 3 - 0 - 1 - - - - - - - - 100 - True - True - True - True - 0 - - True - * - False - - - 1 - 2 - 1 - 2 - - - - - - - True - True - Browse... - True - GTK_RELIEF_NORMAL - True - - - - - 2 - 3 - 1 - 2 - - - - - - - - True - Eject program: - False - False - GTK_JUSTIFY_LEFT - False - False - 0 - 0.5 - 0 - 0 - ejt_entry - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - 1 - 1 - 2 - - - - - - - - - - - - True - <b>CD/DVD drive options</b> - False - True - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - label_item - - - - - 0 - True - True - - - - - 0 - True - True - - - - - - 1 - False - 0 - - - - 5 - True - 0 - 0.5 - GTK_SHADOW_ETCHED_IN - - - - 5 - True - True - 0.5 - 0.5 - 1 - 1 - 0 - 0 - 12 - 0 - - - - True - False - 0 - - - - True - True - Save main window size - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Save paned window sizes - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Eject CD/DVD after scan - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - - - - - True - <b>General options</b> - False - True - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - label_item - - - - - 0 - True - True - - - - - - 5 - True - 0 - 0.5 - GTK_SHADOW_ETCHED_IN - - - - 5 - True - True - 0.5 - 0.5 - 1 - 1 - 0 - 0 - 12 - 0 - - - - True - False - 0 - - - - True - True - Possible export to XLS - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - - - - - True - <b>Misc</b> - False - True - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - label_item - - - - - 0 - True - True - - - - - - 5 - True - 0 - 0.5 - GTK_SHADOW_ETCHED_IN - - - - 5 - True - True - 0.5 - 0.5 - 1 - 1 - 0 - 0 - 12 - 0 - - - - True - False - 0 - - - - True - True - Confirm quit if there are unsaved data - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Confirm "new" if there are unsaved data - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Warn about mount/umount errors - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Warn on delete - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - - - - - True - <b>Confirmations</b> - False - True - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - label_item - - - - - 0 - True - True - - - - - 0 - True - True - - - - - - 1 - False - 0 - - - - 5 - True - 0 - 0.5 - GTK_SHADOW_ETCHED_IN - - - - 5 - True - True - 0.5 - 0.5 - 1 - 1 - 0 - 0 - 12 - 0 - - - - True - False - 0 - - - - True - True - Create thumbnails for images - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Scan EXIF data - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - True - True - Include gThumb image description - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - - - - - - True - <b>Scan options</b> - False - True - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - label_item - - - - - 0 - True - True - - - - - 0 - True - True - - - - - - - - - 0 - True - True - - - - - True - True - - - - - 0 - True - True - - - - - - + + 550 + 400 + Preferences - pyGTKtalog + True + GDK_WINDOW_TYPE_HINT_NORMAL + + + True + + + 168 + True + True + 140 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + + + True + True + False + True + + + + + + False + True + + + + + True + + + True + 3 + 3 + True + + + False + False + + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + + + True + + + 1 + + + True + 5 + 0 + + + True + True + 5 + 12 + + + True + 5 + 2 + 3 + 3 + 3 + + + True + 0 + Eject program: + ejt_entry + + + 1 + 2 + + + + + + + True + True + Browse... + True + 0 + + + + + 2 + 3 + 1 + 2 + + + + + + + 100 + True + True + + + 1 + 2 + 1 + 2 + + + + + + True + True + Browse... + True + 0 + + + + + 2 + 3 + + + + + + + 100 + True + True + + + 1 + 2 + + + + + + True + 0 + Mount point: + mnt_entry + + + + + + + + + + + + + True + <b>CD/DVD drive options</b> + True + + + label_item + + + + + + + + + 1 + + + True + 5 + 0 + + + True + True + 5 + 12 + + + True + + + True + True + Save main window size + True + 0 + True + + + False + False + + + + + True + True + Save paned window sizes + True + 0 + True + + + False + False + 1 + + + + + True + True + Eject CD/DVD after scan + True + 0 + True + + + False + False + 2 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Compress collection + 0 + True + + + 3 + + + + + + + + + True + <b>General options</b> + True + + + label_item + + + + + + + True + 5 + 0 + + + True + True + 5 + 12 + + + True + + + True + True + Export to XLS + True + 0 + True + + + False + False + + + + + + + + + True + <b>Misc</b> + True + + + label_item + + + + + 1 + + + + + True + 5 + 0 + + + True + True + 5 + 12 + + + True + + + True + True + Confirm quit if there are unsaved data + True + 0 + True + + + False + False + + + + + True + True + Confirm "new" if there are unsaved data + True + 0 + True + + + False + False + 1 + + + + + True + True + Warn about mount/umount errors + True + 0 + True + + + False + False + 2 + + + + + True + True + Warn on delete + True + 0 + True + + + False + False + 3 + + + + + + + + + True + <b>Confirmations</b> + True + + + label_item + + + + + 2 + + + + + 1 + + + + + 1 + + + True + 5 + 0 + + + True + True + 5 + 12 + + + True + + + True + True + Create thumbnails for images + True + 0 + True + + + False + False + + + + + True + True + Scan EXIF data + True + 0 + True + + + False + False + 1 + + + + + True + True + Include gThumb image description + True + 0 + True + + + False + False + 2 + + + + + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Retrive extra information + 0 + True + + + + label_item + + + + + + + 2 + + + + + + + + + 1 + + + + + True + True + + + + + 2 + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-cancel + True + -6 + + + + + + True + True + True + gtk-save + True + -5 + + + + 1 + + + + + False + GTK_PACK_END + + + + + diff --git a/resources/glade/main.glade b/resources/glade/main.glade index ccf9292..9cab256 100644 --- a/resources/glade/main.glade +++ b/resources/glade/main.glade @@ -1,911 +1,688 @@ - - - + + + - - - mainW - GTK_WINDOW_TOPLEVEL - GTK_WIN_POS_NONE - False - True - False - True - False - False - GDK_WINDOW_TYPE_HINT_NORMAL - GDK_GRAVITY_NORTH_WEST - True - False - - - - - - True - False - 0 - - - - True - GTK_PACK_DIRECTION_LTR - GTK_PACK_DIRECTION_LTR - - - - True - _File - True - - - - - - - True - gtk-new - True - - - - - - - True - gtk-open - True - - - - - - - True - gtk-save - True - - - - - - - True - gtk-save-as - True - - - - - - - True - - - - - - True - Recent files - True - - - - - - - True - - - - - - True - gtk-quit - True - - - - - - - - - - - True - _Edit - True - - - - - - - True - gtk-cut - True - - - - - - - True - gtk-copy - True - - - - - - - True - gtk-paste - True - - - - - - - True - gtk-delete - True - - - - - - - True - - - - - - True - gtk-find - True - - - - - - - True - - - - - - True - gtk-preferences - True - - - - - - - - - - - True - _Catalog - True - - - - - - - True - Add CD/DVD - True - - - - - - - - True - Add Directory - True - - - - - - - True - - - - - - True - Cancel - True - - - - - True - gtk-cancel - 1 - 0.5 - 0.5 - 0 - 0 - - - - - - - - - - - - True - _View - True - - - - - - - True - Toolbar - True - False - - - - - - - True - Status bar - True - False - - - - - - - - - - - True - _Help - True - - - - - - - True - gtk-about - True - - - - - - - - - - 0 - False - False - - - - - - True - GTK_ORIENTATION_HORIZONTAL - GTK_TOOLBAR_BOTH - True - True - - - - True - Create new catalog - gtk-new - True - True - False - - - - False - True - - - - - - True - Open catalog file - gtk-open - True - True - False - - - - False - True - - - - - - True - Save catalog - gtk-save - True - True - False - - - - False - True - - - - - - True - True - True - False - - - - True - - - - - False - False - - - - - - True - Add CD/DVD to catalog - Add CD - True - gtk-cdrom - True - True - False - - - - False - True - - - - - - True - Find file - gtk-find - True - True - False - - - - False - True - - - - - - True - True - True - False - - - - True - - - - - False - False - - - - - - True - False - Cancel - True - gtk-cancel - True - True - False - - - - False - True - - - - - - True - Quit pyGTKtalog - gtk-quit - True - True - False - - - - False - True - - - - - - True - Debug - True - gtk-dialog-info - True - True - False - - - - False - True - - - - - 0 - False - False - - - - - - True - True - - - - True - True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - False - True - False - True - False - False - False - - - - - - - - True - False - - - - - - True - True - - - - True - True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - True - True - False - True - False - False - False - - - - - - - True - False - - - - - - True - True - True - True - GTK_POS_TOP - False - False - - - - True - True - GTK_POLICY_ALWAYS - GTK_POLICY_ALWAYS - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - False - False - True - GTK_JUSTIFY_LEFT - GTK_WRAP_NONE - True - 0 - 0 - 0 - 0 - 0 - 0 - - - - - - False - True - - - - - - True - Details - False - False - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - tab - - - - - - True - True - GTK_POLICY_ALWAYS - GTK_POLICY_ALWAYS - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - True - False - False - True - False - False - False - - - - - False - True - - - - - - True - Exif - False - False - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - tab - - - - - - True - False - 0 - - - - True - 0.5 - 0.5 - 0 - 0 - - - 0 - True - True - - - - - - True - True - GTK_POLICY_ALWAYS - GTK_POLICY_ALWAYS - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - True - False - False - True - False - False - False - - - - - 0 - True - True - - - - - False - True - - - - - - True - Anime - False - False - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - tab - - - - - True - True - - - - - False - True - - - - - 0 - True - True - - - - - - True - - - 1 - False - True - - - - - - True - False - 0 - - - - True - False - - - 0 - True - True - - - - - - True - GTK_PROGRESS_LEFT_TO_RIGHT - 0 - 0.10000000149 - PANGO_ELLIPSIZE_NONE - - - 0 - False - False - - - - - 0 - False - True - - - - - - - - - - - True - Expand all nodes - _Expand all - True - - - - - - - True - Collapse all nodes - _Collapse all - True - - - - - - - True - - - - - - True - _Update - True - - - - - - - True - _Edit - True - - - - - - - True - _Delete - True - - - - - - - True - _Statistics - True - - - - - + + pyGTKtalog + + + + + True + + + True + + + True + _File + True + + + + + True + gtk-new + True + True + + + + + + True + gtk-open + True + True + + + + + + True + gtk-save + True + True + + + + + + True + gtk-save-as + True + True + + + + + + True + + + + + True + Recent files + True + + + + + + True + + + + + True + gtk-quit + True + True + + + + + + + + + + True + _Edit + True + + + + + True + gtk-cut + True + True + + + + + + True + gtk-copy + True + True + + + + + + True + gtk-paste + True + True + + + + + + True + gtk-delete + True + True + + + + + + True + + + + + True + gtk-find + True + True + + + + + + True + + + + + True + gtk-preferences + True + True + + + + + + + + + + True + _Catalog + True + + + + + True + Add CD/DVD + True + + + + + + + True + Add Directory + True + + + + + + True + + + + + True + Cancel + True + + + + True + gtk-cancel + 1 + + + + + + + + + + + True + _View + True + + + + + True + Toolbar + True + + + + + + True + Status bar + True + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + menuitem1 + True + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Show as a list + List + True + True + True + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Show as thumbnails + Thumbnails + True + True + + + + + + + + + True + _Help + True + + + + + True + gtk-about + True + True + + + + + + + + + + False + False + + + + + True + GTK_TOOLBAR_BOTH + + + True + Create new catalog + gtk-new + + + + False + + + + + True + Open catalog file + gtk-open + + + + False + + + + + True + Save catalog + gtk-save + + + + False + + + + + True + + + True + + + + + False + False + + + + + True + Add CD/DVD to catalog + Add CD + True + gtk-cdrom + + + + False + + + + + True + Find file + gtk-find + + + + False + + + + + True + + + True + + + + + False + False + + + + + True + False + Cancel + True + gtk-cancel + + + + False + + + + + True + Quit pyGTKtalog + gtk-quit + + + + False + + + + + True + Debug + True + gtk-dialog-info + + + + False + + + + + False + False + 1 + + + + + True + True + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + True + False + True + + + + + + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + True + + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Keywords + + + label_item + + + + + False + False + 1 + + + + + False + True + + + + + True + True + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + + + True + True + True + + + + + + + False + True + + + + + + + + True + False + + + + + 2 + + + + + True + + + False + 1 + 3 + + + + + True + + + True + False + + + + + True + 0.10000000149 + + + False + False + 1 + + + + + False + 4 + + + + + + + + + True + Expand all nodes + _Expand all + True + + + + + + True + Collapse all nodes + _Collapse all + True + + + + + + True + + + + + True + _Update + True + + + + + + True + _Edit + True + + + + + + True + _Delete + True + + + + + + True + _Statistics + True + + + + + + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-missing-image + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + True + + + 1 + + + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + True + + + 1 + + + + + + + diff --git a/src/ctrls/c_config.py b/src/ctrls/c_config.py index 388a67e..e5b705d 100644 --- a/src/ctrls/c_config.py +++ b/src/ctrls/c_config.py @@ -50,6 +50,7 @@ class ConfigController(Controller): self.view['ch_thumb'].set_active(self.model.confd['pil']) self.view['ch_exif'].set_active(self.model.confd['exif']) self.view['ch_gthumb'].set_active(self.model.confd['gthumb']) + self.view['ch_compress'].set_active(self.model.confd['compress']) # initialize tree view self.__setup_category_tree() @@ -92,17 +93,21 @@ class ConfigController(Controller): self.model.confd['pil'] = self.view['ch_thumb'].get_active() self.model.confd['exif'] = self.view['ch_exif'].get_active() self.model.confd['gthumb'] = self.view['ch_gthumb'].get_active() + self.model.confd['compress'] = self.view['ch_compress'].get_active() self.model.save() self.view['config'].destroy() return - def on_button_ejt_clicked(self,button): + def on_button_ejt_clicked(self, button): self.__show_filechooser() return - def on_button_mnt_clicked(self,button): + def on_button_mnt_clicked(self, button): self.__show_dirchooser() return + + def on_ch_retrive_toggled(self, widget): + return ############################ # private controller methods @@ -166,157 +171,4 @@ class ConfigController(Controller): dialog.destroy() pass # end of class -''' -import sys -import os -import pygtk -import gtk -import gtk.glade -import gobject - -from config import Config -import dialogs - -class Preferences: - def __init__(self): - self.category_dict = {'Disk options':'disk_group','General':'general_group','Scan options':'scan_group'} - self.category_order = ['General','Disk options','Scan options'] - self.conf = Config() - self.conf.load() - - self.gladefile = "glade/prefs.glade" - - self.glade = gtk.glade.XML(self.gladefile,"prefs") - dic = { - "on_button_ejt_clicked" :self.show_filechooser, - "on_button_mnt_clicked" :self.show_dirchooser, - "on_category_tree_cursor_changed" :self.activate_pan, - } - self.glade.signal_autoconnect(dic) - - self.pref = self.glade.get_widget("prefs") - self.pref.set_title("Preferences - pyGTKtalog") - self.desc = self.glade.get_widget("desc") - - self.cd = self.glade.get_widget("mnt_entry") - self.cd.set_text(self.conf.confd['cd']) - - self.eject = self.glade.get_widget("ejt_entry") - self.eject.set_text(self.conf.confd['ejectapp']) - - self.ch_win = self.glade.get_widget("ch_win") - self.ch_win.set_active(self.conf.confd['savewin']) - self.ch_pan = self.glade.get_widget("ch_pan") - self.ch_pan.set_active(self.conf.confd['savepan']) - self.ch_eject = self.glade.get_widget("ch_eject") - self.ch_eject.set_active(self.conf.confd['eject']) - self.ch_xls = self.glade.get_widget("ch_xls") - self.ch_xls.set_active(self.conf.confd['exportxls']) - self.ch_quit = self.glade.get_widget("ch_quit") - self.ch_quit.set_active(self.conf.confd['confirmquit']) - self.ch_wrnmount = self.glade.get_widget("ch_wrnmount") - self.ch_wrnmount.set_active(self.conf.confd['mntwarn']) - self.ch_warnnew = self.glade.get_widget("ch_warnnew") - self.ch_warnnew.set_active(self.conf.confd['confirmabandon']) - - self.ch_thumb = self.glade.get_widget("ch_thumb") - self.ch_thumb.set_active(self.conf.confd['pil']) - self.ch_exif = self.glade.get_widget("ch_exif") - self.ch_exif.set_active(self.conf.confd['exif']) - self.ch_gthumb = self.glade.get_widget("ch_gthumb") - self.ch_gthumb.set_active(self.conf.confd['gthumb']) - - self.tree = self.glade.get_widget("category_tree") - self.model = gtk.ListStore(gobject.TYPE_STRING) - self.model.clear() - self.tree.set_model(self.model) - self.tree.set_headers_visible(False) - self.tree.show() - - for i in self.category_order: - myiter = self.model.insert_before(None,None) - self.model.set_value(myiter,0,i) - - renderer=gtk.CellRendererText() - column=gtk.TreeViewColumn("Name",renderer, text=0) - column.set_resizable(True) - self.tree.append_column(column) - if self.pref.run() == gtk.RESPONSE_OK: - self.conf.confd['cd'] = self.cd.get_text() - self.conf.confd['ejectapp'] = self.eject.get_text() - self.conf.confd['savewin'] = self.ch_win.get_active() - self.conf.confd['savepan'] = self.ch_pan.get_active() - self.conf.confd['eject'] = self.ch_eject.get_active() - self.conf.confd['pil'] = self.ch_thumb.get_active() - self.conf.confd['exif'] = self.ch_exif.get_active() - self.conf.confd['gthumb'] = self.ch_gthumb.get_active() - self.conf.confd['exportxls'] = self.ch_xls.get_active() - self.conf.confd['confirmquit'] = self.ch_quit.get_active() - self.conf.confd['mntwarn'] = self.ch_wrnmount.get_active() - self.conf.confd['confirmabandon'] = self.ch_warnnew.get_active() - self.conf.save() - self.pref.destroy() - - def show_filechooser(self,widget): - """dialog for choose eject""" - dialog = gtk.FileChooserDialog( - title="Choose eject program", - action=gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=( - gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, - gtk.RESPONSE_OK - ) - ) - - dialog.set_default_response(gtk.RESPONSE_OK) - - response = dialog.run() - if response == gtk.RESPONSE_OK: - self.eject.set_text(dialog.get_filename()) - - dialog.destroy() - - def show_dirchooser(self,widget): - """dialog for point the mountpoint""" - dialog = gtk.FileChooserDialog( - title="Choose mount point", - action=gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=( - gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, - gtk.RESPONSE_OK - ) - ) - - dialog.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) - dialog.set_filename(self.conf.confd['cd']) - dialog.set_default_response(gtk.RESPONSE_OK) - - response = dialog.run() - if response == gtk.RESPONSE_OK: - self.cd.set_text(dialog.get_filename()) - dialog.destroy() - - def activate_pan(self,treeview): - model = treeview.get_model() - selected = model.get_value(model.get_iter(treeview.get_cursor()[0]),0) - iterator = treeview.get_model().get_iter_first(); - while iterator != None: - if model.get_value(iterator,0) == selected: - self.glade.get_widget(self.category_dict[model.get_value(iterator,0)]).show() - self.desc.set_markup("%s" % selected) - else: - self.glade.get_widget(self.category_dict[model.get_value(iterator,0)]).hide() - iterator = treeview.get_model().iter_next(iterator); - -if __name__ == "__main__": - try: - app=Preferences() - gtk.main() - except KeyboardInterrupt: - gtk.main_quit -''' diff --git a/src/ctrls/c_details.py b/src/ctrls/c_details.py new file mode 100644 index 0000000..4eb0213 --- /dev/null +++ b/src/ctrls/c_details.py @@ -0,0 +1,37 @@ +# This Python file uses the following encoding: utf-8 +# +# Author: Roman 'gryf' Dobosz gryf@elysium.pl +# +# Copyright (C) 2007 by Roman 'gryf' Dobosz +# +# This file is part of pyGTKtalog. +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# ------------------------------------------------------------------------- + +import os.path +from utils import deviceHelper +from gtkmvc import Controller + +class DetailsController(Controller): + """Controller for details view""" + def __init__(self, model): + """Initialize controller""" + Controller.__init__(self, model) + return + + def register_view(self, view): + Controller.register_view(self, view) diff --git a/src/ctrls/c_main.py b/src/ctrls/c_main.py index b03a945..45284c5 100644 --- a/src/ctrls/c_main.py +++ b/src/ctrls/c_main.py @@ -22,7 +22,7 @@ # ------------------------------------------------------------------------- -__version__ = "0.7" +__version__ = "0.8" licence = \ """ GPL v2 @@ -34,6 +34,7 @@ from utils import deviceHelper from gtkmvc import Controller from c_config import ConfigController +from c_details import DetailsController from views.v_config import ConfigView from models.m_config import ConfigModel @@ -47,12 +48,12 @@ class MainController(Controller): """Controller for main application window""" scan_cd = False widgets = ( - "discs","files","details", + "discs","files", 'save1','save_as1','cut1','copy1','paste1','delete1','add_cd','add_directory1', 'tb_save','tb_addcd','tb_find', ) widgets_all = ( - "discs","files","details", + "discs","files", 'file1','edit1','add_cd','add_directory1','help1', 'tb_save','tb_addcd','tb_find','tb_new','tb_open','tb_quit', ) @@ -61,6 +62,7 @@ class MainController(Controller): def __init__(self, model): """Initialize controller""" Controller.__init__(self, model) + self.details = DetailsController(model.details) return def register_view(self, view): @@ -74,21 +76,19 @@ class MainController(Controller): for widget in self.widgets_cancel: self.view[widget].set_sensitive(False) - # ukryj przycisk "debug", jeśli nie debugujemy. + # hide "debug" button, if production (i.e. python OT running with -OO option) if __debug__: self.view['debugbtn'].show() else: self.view['debugbtn'].hide() # ustaw domyślne właściwości dla poszczególnych widżetów - self.view['main'].set_title('pyGTKtalog'); # self.view['main'].set_icon_list(gtk.gdk.pixbuf_new_from_file("pixmaps/mainicon.png")) #self.view['detailplace'].set_sensitive(False) - self.view['details'].hide() - self.view['exifTab'].hide() - self.view['animeTab'].hide() + #self.view['exifTab'].hide() + #self.view['animeTab'].hide() - # załaduj konfigurację/domyślne ustawienia i przypisz je właściwościom + # load configuration/defaults and set it to properties self.view['toolbar1'].set_active(self.model.config.confd['showtoolbar']) if self.model.config.confd['showtoolbar']: self.view['maintoolbar'].show() @@ -115,10 +115,13 @@ class MainController(Controller): if self.model.filename != None: self.__activateUI(self.model.filename) + # register detail subview + #self.view.create_sub_view(self.details) + + # generate recent menu self.__generate_recent_menu() # Show main window self.view['main'].show(); - return ######################################################################### @@ -194,12 +197,12 @@ class MainController(Controller): def on_discs_cursor_changed(self, widget): """Show files on right treeview, after clicking the left disc treeview.""" model = self.view['discs'].get_model() - selected_item = self.model.discsTree.get_value(self.model.discsTree.get_iter(self.view['discs'].get_cursor()[0]),0) + selected_item = self.model.discs_tree.get_value(self.model.discs_tree.get_iter(self.view['discs'].get_cursor()[0]),0) if __debug__: print "c_main.py, on_discs_cursor_changed()",selected_item self.model.get_root_entries(selected_item) - self.__getItemInfo(selected_item) + self.__get_item_info(selected_item) return def on_discs_row_activated(self, treeview, path, treecolumn): @@ -229,7 +232,7 @@ class MainController(Controller): treeview.get_selection().unselect_all() treeview.get_selection().select_path(path) - if self.model.discsTree.get_value(self.model.discsTree.get_iter(path),3) == 1: + if self.model.discs_tree.get_value(self.model.discs_tree.get_iter(path),3) == 1: # if ancestor is 'root', then activate "update" menu item self.view['update1'].set_sensitive(True) else: @@ -239,7 +242,7 @@ class MainController(Controller): # elif event.button == 1: # Left click # """Show files on right treeview, after clicking the left disc treeview.""" # model = self.view['discs'].get_model() - # selected_item = self.model.discsTree.get_value(self.model.discsTree.get_iter(path),0) + # selected_item = self.model.discs_tree.get_value(self.model.discs_tree.get_iter(path),0) # if __debug__: # print "c_main.py, on_discs_cursor_changed()",selected_item # self.model.get_root_entries(selected_item) @@ -266,16 +269,16 @@ class MainController(Controller): itera = model.get_iter(paths[0]) if model.get_value(itera,4) == 1: #directory, do nothin', just turn off view - self.view['details'].hide() + '''self.view['details'].hide() buf = self.view['details'].get_buffer() buf.set_text('') - self.view['details'].set_buffer(buf) + self.view['details'].set_buffer(buf)''' if __debug__: print "c_main.py: on_files_cursor_changed() directory selected" else: #file, show what you got. - selected_item = self.model.filesList.get_value(model.get_iter(treeview.get_cursor()[0]),0) - self.__getItemInfo(selected_item) + selected_item = self.model.files_list.get_value(model.get_iter(treeview.get_cursor()[0]),0) + self.__get_item_info(selected_item) if __debug__: print "c_main.py: on_files_cursor_changed() some other thing selected" except: @@ -285,11 +288,11 @@ class MainController(Controller): def on_files_row_activated(self, files_obj, row, column): """On directory doubleclick in files listview dive into desired branch.""" - # TODO: można by też podczepić klawisz backspace do przechodzenia poziom wyżej. - f_iter = self.model.filesList.get_iter(row) - current_id = self.model.filesList.get_value(f_iter,0) + # TODO: map backspace key for moving to upper level of directiories + f_iter = self.model.files_list.get_iter(row) + current_id = self.model.files_list.get_value(f_iter,0) - if self.model.filesList.get_value(f_iter,4) == 1: + if self.model.files_list.get_value(f_iter,4) == 1: # ONLY directories. files are omitted. self.model.get_root_entries(current_id) @@ -298,12 +301,12 @@ class MainController(Controller): if not self.view['discs'].row_expanded(d_path): self.view['discs'].expand_row(d_path,False) - new_iter = self.model.discsTree.iter_children(self.model.discsTree.get_iter(d_path)) + new_iter = self.model.discs_tree.iter_children(self.model.discs_tree.get_iter(d_path)) if new_iter: while new_iter: - if self.model.discsTree.get_value(new_iter,0) == current_id: - self.view['discs'].set_cursor(self.model.discsTree.get_path(new_iter)) - new_iter = self.model.discsTree.iter_next(new_iter) + if self.model.discs_tree.get_value(new_iter,0) == current_id: + self.view['discs'].set_cursor(self.model.discs_tree.get_path(new_iter)) + new_iter = self.model.discs_tree.iter_next(new_iter) return def on_cancel1_activate(self, widget): @@ -313,7 +316,7 @@ class MainController(Controller): self.__abort() def on_tb_find_clicked(self, widget): - # TODO: zaimplementować wyszukiwarkę + # TODO: implement searcher return def recent_item_response(self, path): @@ -329,50 +332,47 @@ class MainController(Controller): if self.model.get_source(path) == self.model.CD: if self.__addCD(label): - self.model.delete(self.model.discsTree.get_iter(path[0],0)) + self.model.delete(self.model.discs_tree.get_iter(path[0],0)) pass elif self.model.get_source(path) == self.model.DR: if self.__addDirectory(filepath, label): - self.model.delete(self.model.discsTree.get_iter(path[0])) + self.model.delete(self.model.discs_tree.get_iter(path[0])) pass return def on_delete2_activate(self, menu_item): model = self.view['discs'].get_model() try: - selected_iter = self.model.discsTree.get_iter(self.view['discs'].get_cursor()[0]) + selected_iter = self.model.discs_tree.get_iter(self.view['discs'].get_cursor()[0]) except: return if self.model.config.confd['delwarn']: - name = self.model.discsTree.get_value(selected_iter,1) + name = self.model.discs_tree.get_value(selected_iter,1) obj = Dialogs.Qst('Delete %s' % name, 'Delete %s?' % name, 'Object will be permanently removed.') if not obj.run(): return self.model.delete(selected_iter) self.model.unsaved_project = True - if self.model.filename != None: - self.view['main'].set_title("%s - pyGTKtalog *" % self.model.filename) - else: - self.view['main'].set_title("untitled - pyGTKtalog *") + self.__setTitle(filepath=self.model.filename, modified=True) return def on_debugbtn_clicked(self,widget): - """Debug. To remove in stable version including button in GUI""" + """Debug. To remove in stable version, including button in GUI""" if __debug__: print "\nc_main.py: on_debugbtn_clicked()" print "------" print "unsaved_project = %s" % self.model.unsaved_project print "filename = %s" % self.model.filename - print "internal_filename = %s" % self.model.internal_filename + print "internal_filename = %s" % self.model.internal_dirname print "db_connection = %s" % self.model.db_connection print "abort = %s" % self.model.abort print "self.model.config.recent = %s" % self.model.config.recent - it = self.model.discsTree.get_iter_first() - myit = self.model.discsTree.insert_before(None,None) - self.model.discsTree.set_value(myit,0,0) - self.model.discsTree.set_value(myit,1,"nazwa") - self.model.discsTree.set_value(myit,3,3) - self.model.discsTree.set_value(myit,2,gtk.STOCK_INFO) + it = self.model.tags_list.get_iter_first() + myit = self.model.tags_list.insert_before(None,None) + self.model.tags_list.set_value(myit,0,0) + self.model.tags_list.set_value(myit,1,"nazwa") + self.model.tags_list.set_value(myit,2,231233) + print "source: %s" % self.model.source ##################### # observed properetis @@ -423,7 +423,7 @@ class MainController(Controller): if path: if not self.model.open(path): - Dialogs.Err("Error opening file - pyGTKtalog","Cannot open file %s." % self.opened_catalog) + Dialogs.Err("Error opening file - pyGTKtalog","Cannot open file %s." % path) else: self.__generate_recent_menu() self.__activateUI(path) @@ -431,10 +431,9 @@ class MainController(Controller): def __save(self): """Save catalog to file""" - #{{{ if self.model.filename: self.model.save() - self.view['main'].set_title("%s - pyGTKtalog *" % self.model.filename) + self.__setTitle(filepath=self.model.filename) else: self.__save_as() pass @@ -443,7 +442,7 @@ class MainController(Controller): """Save database to file under different filename.""" path = Dialogs.ChooseDBFilename().show_dialog() if path: - self.view['main'].set_title("%s - pyGTKtalog" % path) + self.__setTitle(filepath=path) self.model.save(path) self.model.config.add_recent(path) pass @@ -461,11 +460,8 @@ class MainController(Controller): self.view[widget].set_sensitive(False) self.model.source = self.model.CD self.model.scan(self.model.config.confd['cd'],label) - self.unsaved_project = True - if self.model.filename != None: - self.view['main'].set_title("%s - pyGTKtalog *" % self.model.filename) - else: - self.view['main'].set_title("untitled - pyGTKtalog *") + self.model.unsaved_project = True + self.__setTitle(filepath=self.model.filename, modified=True) return True else: Dialogs.Wrn("Error mounting device - pyGTKtalog", @@ -485,11 +481,8 @@ class MainController(Controller): self.scan_cd = False self.model.source = self.model.DR self.model.scan(path, label) - self.unsaved_project = True - if self.model.filename != None: - self.view['main'].set_title("%s - pyGTKtalog *" % self.model.filename) - else: - self.view['main'].set_title("untitled - pyGTKtalog *") + self.model.unsaved_project = True + self.__setTitle(filepath=self.model.filename, modified=True) return True def __doQuit(self): @@ -516,10 +509,10 @@ class MainController(Controller): self.model.new() # clear "details" buffer - txt = "" + '''txt = "" buf = self.view['details'].get_buffer() buf.set_text(txt) - self.view['details'].set_buffer(buf) + self.view['details'].set_buffer(buf)''' self.__activateUI() @@ -527,7 +520,27 @@ class MainController(Controller): def __setup_disc_treeview(self): """Setup TreeView discs widget as tree.""" - self.view['discs'].set_model(self.model.discsTree) + self.view['discs'].set_model(self.model.discs_tree) + + c = gtk.TreeViewColumn('Filename') + + # one row contains image and text + cellpb = gtk.CellRendererPixbuf() + cell = gtk.CellRendererText() + c.pack_start(cellpb, False) + c.pack_start(cell, True) + c.set_attributes(cellpb, stock_id=2) + c.set_attributes(cell, text=1) + + self.view['discs'].append_column(c) + + # registration of treeview signals: + + return + + def __setup_tags_treeview(self): + """Setup TreeView discs widget as tree.""" + self.view['tags'].set_model(self.model.tagsTree) c = gtk.TreeViewColumn('Filename') @@ -547,7 +560,7 @@ class MainController(Controller): def __setup_files_treeview(self): """Setup TreeView files widget, as columned list.""" - self.view['files'].set_model(self.model.filesList) + self.view['files'].set_model(self.model.files_list) self.view['files'].get_selection().set_mode(gtk.SELECTION_MULTIPLE) @@ -588,10 +601,10 @@ class MainController(Controller): self.model.abort = True return - def __activateUI(self, name='untitled'): + def __activateUI(self, name=False): """Make UI active, and set title""" self.model.unsaved_project = False - self.view['main'].set_title("%s - pyGTKtalog" % name) + self.__setTitle(filepath=name) for widget in self.widgets: try: self.view[widget].set_sensitive(True) @@ -601,7 +614,20 @@ class MainController(Controller): while gtk.events_pending(): gtk.main_iteration() return - + + def __setTitle(self, filepath=None, modified=False): + """Set main window title""" + if modified: + mod = " *" + else: + mod = "" + + if filepath: + self.view['main'].set_title("%s - pyGTKtalog%s" % (os.path.basename(filepath), mod)) + else: + self.view['main'].set_title("untitled - pyGTKtalog%s" % mod) + return + def __storeSettings(self): """Store window size and pane position in config file (using config object from model)""" if self.model.config.confd['savewin']: @@ -620,8 +646,6 @@ class MainController(Controller): self.recent_menu = gtk.Menu() for i in self.model.config.recent: name = os.path.basename(i) - if name.endswith(".pgt"): - name = name[:-4] item = gtk.MenuItem("%s" % name) item.connect_object("activate", self.recent_item_response, i) self.recent_menu.append(item) @@ -629,11 +653,11 @@ class MainController(Controller): self.view['recent_files1'].set_submenu(self.recent_menu) return - def __getItemInfo(self, item): - self.view['details'].show() + def __get_item_info(self, item): + '''self.view['details'].show() txt = self.model.get_file_info(item) buf = self.view['details'].get_buffer() buf.set_text(txt) - self.view['details'].set_buffer(buf) + self.view['details'].set_buffer(buf)''' return pass # end of class diff --git a/src/models/m_config.py b/src/models/m_config.py index 77b0690..fa0721a 100644 --- a/src/models/m_config.py +++ b/src/models/m_config.py @@ -76,6 +76,7 @@ class ConfigModel(Model): 'showtoolbar':True, 'showstatusbar':True, 'delwarn':True, + 'compress':True, } dictconf = { @@ -96,6 +97,7 @@ class ConfigModel(Model): 'confirm abandon current catalog':'confirmabandon', 'show toolbar':'showtoolbar', 'show statusbar and progress bar':'showstatusbar', + 'compress collection':'compress', } dbool = ( @@ -112,6 +114,8 @@ class ConfigModel(Model): 'confirmabandon', 'showtoolbar', 'showstatusbar', + 'delwarn', + 'compress', ) recent = [] diff --git a/src/models/m_details.py b/src/models/m_details.py new file mode 100644 index 0000000..baad5c3 --- /dev/null +++ b/src/models/m_details.py @@ -0,0 +1,34 @@ +# This Python file uses the following encoding: utf-8 +# +# Author: Roman 'gryf' Dobosz gryf@elysium.pl +# +# Copyright (C) 2007 by Roman 'gryf' Dobosz +# +# This file is part of pyGTKtalog. +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# ------------------------------------------------------------------------- + +from gtkmvc import Model + +class DetailsModel(Model): + def __init__(self): + Model.__init__(self) + return + + def __str__(self): + """show prefs in string way""" + return "fubar" diff --git a/src/models/m_main.py b/src/models/m_main.py index 70a9e50..f2492d5 100644 --- a/src/models/m_main.py +++ b/src/models/m_main.py @@ -22,76 +22,107 @@ # ------------------------------------------------------------------------- +import os +import sys +import base64 +import shutil +import tarfile + +import gtk +import gobject + from gtkmvc.model_mt import ModelMT +from pysqlite2 import dbapi2 as sqlite +from datetime import datetime +#import mx.DateTime try: import threading as _threading except ImportError: if __debug__: print "m_main.py: import exception: _threading" import dummy_threading as _threading +try: + import Image, ImageEnhance +except: + if __debug__: + print "m_main.py: import exception: Image|ImageEnhance" + pass -from pysqlite2 import dbapi2 as sqlite -import datetime -import mx.DateTime -import os -import sys -import mimetypes -import bz2 - -import gtk -import gobject +from utils import EXIF from m_config import ConfigModel +from m_details import DetailsModel + +class Thumbnail(object): + def __init__(self, *args): + self.x = None + self.y = None + +class Picture(object): + def __init__(self, *args): + self.x = None + self.y = None class MainModel(ModelMT): - """Create, load, save, manipulate db file which is container for our data""" + """Create, load, save, manipulate db file which is container for data""" __properties__ = {'busy': False, 'statusmsg': '', 'progress': 0} - config = ConfigModel() - unsaved_project = False - filename = None - internal_filename = None - db_connection = None - db_cursor = None - abort = False + # constants instead of dictionary tables + # type of files + LAB = 0 # root of the tree - label/collection name + DIR = 1 # directory + FIL = 2 # file + LIN = 3 # symbolic link + # filetype kind of + F_UNK = 0 # unknown - default + F_IMG = 1 # images - jpg, gif, tiff itd + F_MOV = 2 # movies and clips - mpg, ogm, mkv, avi, asf, wmv itd + F_MUS = 4 # music - flac, mp3, mpc, ogg itd + F_APP = 5 # applications + F_DOC = 6 # all kind of documents txt/pdf/doc/odf itd - # Drzewo katalogów: id, nazwa, ikonka, typ - discsTree = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING, str, gobject.TYPE_INT) - # Lista plików wskazanego katalogu: child_id (?), filename, size, date, typ, ikonka - filesList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_UINT64, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING, str) - - LAB = 0 # label/nazwa kolekcji --- najwyższy poziom drzewa przypiętego do korzenia - DIR = 1 # katalog - podlega innemu katalogu lub lejbelu - FIL = 2 # plik - podleka katalogu lub lejbelu - - CD = 1 # podczas skanowania, wstawiane jest źródło na płytę CD/DVD - DR = 2 # podczas skanowania, wstawiane jest źródło na katalog w fs - - # default source is CD/DVD - source = CD + CD = 1 # sorce: cd/dvd + DR = 2 # source: filesystem def __init__(self): ModelMT.__init__(self) + self.config = ConfigModel() + self.unsaved_project = False + self.filename = None # collection saved/opened filename + self.internal_dirname = None + self.db_connection = None + self.db_cursor = None + self.abort = False + self.source = self.CD self.config.load() + self.details = DetailsModel() + + # Directory tree: id, nazwa, ikonka, typ + self.discs_tree = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING, + str, gobject.TYPE_INT) + # File list of selected directory: child_id(?), filename, size, + # date, type, icon + self.files_list = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, + gobject.TYPE_UINT64, + gobject.TYPE_STRING, gobject.TYPE_INT, + gobject.TYPE_STRING, str) + # Tag list: id, name, count + self.tags_list = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, + gobject.TYPE_UINT64, str) + return def cleanup(self): self.__close_db_connection() - if self.internal_filename != None: + if self.internal_dirname != None: try: - os.unlink(self.internal_filename) + shutil.rmtree(self.internal_dirname) except: if __debug__: - print "m_main.py: cleanup()", self.internal_filename - pass - try: - os.unlink(self.internal_filename + '-journal') - except: - if __debug__: - print "m_main.py: cleanup()", self.internal_filename+'-journal' + print "m_main.py: cleanup()", self.internal_dirname pass return @@ -100,19 +131,11 @@ class MainModel(ModelMT): def new(self): self.unsaved_project = False - self.__create_internal_filename() + self.__create_internal_dirname() self.__connect_to_db() self.__create_database() - try: - self.discsTree.clear() - except: - pass - - try: - self.filesList.clear() - except: - pass + self.__clear_trees() return @@ -125,42 +148,23 @@ class MainModel(ModelMT): def open(self, filename=None): """try to open db file""" self.unsaved_project = False - self.__create_internal_filename() + self.__create_internal_dirname() self.filename = filename try: - source = bz2.BZ2File(filename, 'rb') + tar = tarfile.open(filename, "r:gz") except: - print "%s: file cannot be read!" % self.filename - self.filename = None - self.internal_filename = None - return - - destination = open(self.internal_filename, 'wb') - while True: try: - data = source.read(1024000) + tar = tarfile.open(filename, "r") except: - # smth went wrong - print "%s: Wrong database format!" % self.filename - if __debug__: - print "m_main.py: open() something goes bad" + print "%s: file cannot be read!" % filename self.filename = None - self.internal_filename = None - try: - self.discsTree.clear() - except: - pass - - try: - self.filesList.clear() - except: - pass - return False - if not data: break - destination.write(data) - destination.close() - source.close() + self.internal_dirname = None + return + + os.chdir(self.internal_dirname) + tar.extractall() + tar.close() self.__connect_to_db() self.__fetch_db_into_treestore() @@ -168,7 +172,7 @@ class MainModel(ModelMT): return True - def scan(self,path,label): + def scan(self, path, label): """scan files in separated thread""" # flush buffer to release db lock. @@ -183,9 +187,10 @@ class MainModel(ModelMT): self.thread.start() return - def get_root_entries(self,id=None): + def get_root_entries(self, id=None): + """Get all children down from sepcified root""" try: - self.filesList.clear() + self.files_list.clear() except: pass # parent for virtual '..' dir @@ -194,50 +199,61 @@ class MainModel(ModelMT): #self.filemodel.set_value(myiter,0,self.cur.fetchone()[0]) #self.filemodel.set_value(myiter,1,'..') #if __debug__: - # print datetime.datetime.fromtimestamp(ch[3]) + # print datetime.fromtimestamp(ch[3]) # directories first - self.db_cursor.execute("SELECT id, filename, size, date FROM files WHERE parent_id=? AND type=1 ORDER BY filename",(id,)) + self.db_cursor.execute("SELECT id, filename, size, date FROM files \ + WHERE parent_id=? AND type=1 \ + ORDER BY filename", (id,)) data = self.db_cursor.fetchall() for ch in data: - myiter = self.filesList.insert_before(None,None) - self.filesList.set_value(myiter,0,ch[0]) - self.filesList.set_value(myiter,1,ch[1]) - self.filesList.set_value(myiter,2,ch[2]) - self.filesList.set_value(myiter,3,datetime.datetime.fromtimestamp(ch[3])) - self.filesList.set_value(myiter,4,1) - self.filesList.set_value(myiter,5,'direktorja') - self.filesList.set_value(myiter,6,gtk.STOCK_DIRECTORY) + myiter = self.files_list.insert_before(None, None) + self.files_list.set_value(myiter, 0, ch[0]) + self.files_list.set_value(myiter, 1, ch[1]) + self.files_list.set_value(myiter, 2, ch[2]) + self.files_list.set_value(myiter, 3, + datetime.fromtimestamp(ch[3])) + self.files_list.set_value(myiter, 4, 1) + self.files_list.set_value(myiter, 5, 'direktorja') + self.files_list.set_value(myiter, 6, gtk.STOCK_DIRECTORY) # all the rest - self.db_cursor.execute("SELECT id, filename, size, date, type FROM files WHERE parent_id=? AND type!=1 ORDER BY filename",(id,)) + self.db_cursor.execute("SELECT id, filename, size, date, type \ + FROM files WHERE parent_id=? AND type!=1 \ + ORDER BY filename", (id,)) data = self.db_cursor.fetchall() for ch in data: - myiter = self.filesList.insert_before(None,None) - self.filesList.set_value(myiter,0,ch[0]) - self.filesList.set_value(myiter,1,ch[1]) - self.filesList.set_value(myiter,2,ch[2]) - self.filesList.set_value(myiter,3,datetime.datetime.fromtimestamp(ch[3])) - self.filesList.set_value(myiter,4,ch[4]) - self.filesList.set_value(myiter,5,'kategoria srategoria') - self.filesList.set_value(myiter,6,gtk.STOCK_FILE) - #print datetime.datetime.fromtimestamp(ch[3]) + myiter = self.files_list.insert_before(None, None) + self.files_list.set_value(myiter, 0, ch[0]) + self.files_list.set_value(myiter, 1, ch[1]) + self.files_list.set_value(myiter, 2, ch[2]) + self.files_list.set_value(myiter, 3, datetime.fromtimestamp(ch[3])) + self.files_list.set_value(myiter, 4, ch[4]) + self.files_list.set_value(myiter, 5, 'kategoria srategoria') + if ch[4] == self.FIL: + self.files_list.set_value(myiter, 6, gtk.STOCK_FILE) + elif ch[4] == self.LIN: + self.files_list.set_value(myiter, 6, gtk.STOCK_INDEX) return def get_file_info(self, id): """get file info from database""" - self.db_cursor.execute("SELECT filename, date, size, type FROM files WHERE id = ?",(id,)) + self.db_cursor.execute("SELECT filename, date, size, type \ + FROM files WHERE id = ?", (id,)) set = self.db_cursor.fetchone() if set == None: return '' - string = "Filename: %s\nDate: %s\nSize: %s\ntype: %s" % (set[0],datetime.datetime.fromtimestamp(set[1]),set[2],set[3]) + string = "Filename: %s\nDate: %s\nSize: %s\ntype: %s" % \ + (set[0], datetime.fromtimestamp(set[1]), set[2], set[3]) return string def get_source(self, path): """get source of top level directory""" - bid = self.discsTree.get_value(self.discsTree.get_iter(path[0]),0) - self.db_cursor.execute("select source from files where id = ?", (bid,)) + bid = self.discs_tree.get_value(self.discs_tree.get_iter(path[0]), + 0) + self.db_cursor.execute("select source from files where id = ?", + (bid,)) res = self.db_cursor.fetchone() if res == None: return False @@ -245,8 +261,10 @@ class MainModel(ModelMT): def get_label_and_filepath(self, path): """get source of top level directory""" - bid = self.discsTree.get_value(self.discsTree.get_iter(path[0]),0) - self.db_cursor.execute("select filepath, filename from files where id = ? and parent_id = 1", (bid,)) + bid = self.discs_tree.get_value(self.discs_tree.get_iter(path[0]), + 0) + self.db_cursor.execute("select filepath, filename from files \ + where id = ? and parent_id = 1", (bid,)) res = self.db_cursor.fetchone() if res == None: return None, None @@ -255,13 +273,38 @@ class MainModel(ModelMT): def delete(self, branch_iter): if not branch_iter: return - self.__remove_branch_form_db(self.discsTree.get_value(branch_iter,0)) - self.discsTree.remove(branch_iter) + self.__remove_branch_form_db(self.discs_tree.get_value(branch_iter,0)) + self.discs_tree.remove(branch_iter) return # private class functions + def __clear_trees(self): + self.__clear_tags_tree() + self.__clear_files_tree() + self.__clear_discs_tree() + + def __clear_tags_tree(self): + try: + self.tags_list.clear() + except: + pass + + def __clear_discs_tree(self): + try: + self.discs_tree.clear() + except: + pass + + def __clear_files_tree(self): + try: + self.files_list.clear() + except: + pass + def __connect_to_db(self): - self.db_connection = sqlite.connect("%s" % self.internal_filename, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) + self.db_connection = sqlite.connect("%s" % \ + (self.internal_dirname + '/db.sqlite'), + detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) self.db_cursor = self.db_connection.cursor() return @@ -274,22 +317,28 @@ class MainModel(ModelMT): self.db_connection = None return - def __create_internal_filename(self): + def __create_internal_dirname(self): self.cleanup() - self.internal_filename = "/tmp/pygtktalog%d.db" % datetime.datetime.now().microsecond + self.internal_dirname = "/tmp/pygtktalog%d" % datetime.now().microsecond + try: + os.mkdir(self.internal_dirname) + except: + if __debug__: + print "m_main.py: __create_internal_dirname(): cannot create \ + temporary directory, or directory exists" + pass return def __compress_and_save(self): - source = open(self.internal_filename, 'rb') - destination = bz2.BZ2File(self.filename, 'w') + if self.config.confd['compress']: + tar = tarfile.open(self.filename, "w:gz") + else: + tar = tarfile.open(self.filename, "w") + + os.chdir(self.internal_dirname) + tar.add('.') + tar.close() - while True: - data = source.read(1024000) - if not data: break - destination.write(data) - - destination.close() - source.close() self.unsaved_project = False return @@ -303,24 +352,44 @@ class MainModel(ModelMT): date datetime, size integer, type integer, - source integer);""") - self.db_cursor.execute("insert into files values(1, 1, 'root', null, 0, 0, 0, 0);") + source integer, + size_x integer, + size_y integer, + filetype integer, + note TEXT);""") + self.db_cursor.execute("""create table + tags(id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id INTEGER, + tag TEXT);""") + self.db_cursor.execute("""create table + descriptions(id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id INTEGER, + desc TEXT, + image TEXT, + image_x INTEGER, + image_y INTEGER, + thumb TEXT, + thumb_x INTEGER, + thumb_y INTEGER, + thumb_mode TEXT);""") + self.db_cursor.execute("insert into files values(1, 1, 'root', null, \ + 0, 0, 0, 0, null, null, null, null);") def __scan(self): """scan content of the given path""" self.busy = True - # jako, że to jest w osobnym wątku, a sqlite się przypieprza, że musi mieć - # konekszyn dla tego samego wątku, więc robimy osobne połączenie dla tego zadania. - db_connection = sqlite.connect("%s" % self.internal_filename, - detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES, - isolation_level="EXCLUSIVE") + # new conection for this task, because it's running in separate thread + db_connection = sqlite.connect("%s" % \ + (self.internal_dirname + '/db.sqlite'), + detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES, + isolation_level="EXCLUSIVE") db_cursor = db_connection.cursor() - timestamp = datetime.datetime.now() + timestamp = datetime.now() - mime = mimetypes.MimeTypes() - mov_ext = ('mkv','avi','ogg','mpg','wmv','mp4','mpeg') + # TODO: file types has to be moved to configuration model + mov_ext = ('mkv', 'avi', 'ogg', 'mpg', 'wmv', 'mp4', 'mpeg') img_ext = ('jpg','jpeg','png','gif','bmp','tga','tif','tiff','ilbm','iff','pcx') # count files in directory tree @@ -328,8 +397,13 @@ class MainModel(ModelMT): self.statusmsg = "Calculating number of files in directory tree..." count = 0 - for root, dirs, files in os.walk(self.path): - count += len(files) + try: + for root, dirs, files in os.walk(self.path): + count += len(files) + except: + if __debug__: + print 'm_main.py: os.walk in %s' % self.path + pass if count > 0: step = 1.0/count @@ -342,45 +416,81 @@ class MainModel(ModelMT): self.fresh_disk_iter = None - def __recurse(parent_id, name, path, date, size, filetype, discs_tree_iter=None): - """recursive scans the path""" + def __recurse(parent_id, name, path, date, size, filetype, + discs_tree_iter=None): + """recursive scans given path""" if self.abort: return -1 _size = size - myit = self.discsTree.append(discs_tree_iter,None) + myit = self.discs_tree.append(discs_tree_iter,None) if parent_id == 1: self.fresh_disk_iter = myit - self.discsTree.set_value(myit,2,gtk.STOCK_CDROM) - db_cursor.execute("insert into files(parent_id, filename, filepath, date, size, type, source) values(?,?,?,?,?,?,?)", - (parent_id, name, path, date, size, filetype, self.source)) + self.discs_tree.set_value(myit,2,gtk.STOCK_CDROM) + sql = """insert into + files(parent_id, filename, filepath, date, size, type, source) + values(?,?,?,?,?,?,?)""" + db_cursor.execute(sql, (parent_id, name, path, date, size, + filetype, self.source)) else: - self.discsTree.set_value(myit,2,gtk.STOCK_DIRECTORY) - db_cursor.execute("insert into files(parent_id, filename, filepath, date, size, type) values(?,?,?,?,?,?)", + self.discs_tree.set_value(myit,2,gtk.STOCK_DIRECTORY) + sql = """ + insert into + files(parent_id, filename, filepath, date, size, type) + values(?,?,?,?,?,?) + """ + db_cursor.execute(sql, (parent_id, name, path, date, size, filetype)) - db_cursor.execute("select seq FROM sqlite_sequence WHERE name='files'") + + sql = """select seq FROM sqlite_sequence WHERE name='files'""" + db_cursor.execute(sql) currentid=db_cursor.fetchone()[0] - self.discsTree.set_value(myit,0,currentid) - self.discsTree.set_value(myit,1,name) - self.discsTree.set_value(myit,3,parent_id) - - root,dirs,files = os.walk(path).next() + self.discs_tree.set_value(myit,0,currentid) + self.discs_tree.set_value(myit,1,name) + self.discs_tree.set_value(myit,3,parent_id) + try: + root,dirs,files = os.walk(path).next() + except: + if __debug__: + print "m_main.py: cannot access ", path + #return -1 + return 0 + for i in dirs: if self.fsenc: j = i.decode(self.fsenc) else: j = i + try: st = os.stat(os.path.join(root,i)) st_mtime = st.st_mtime except OSError: st_mtime = 0 - dirsize = __recurse(currentid, j, os.path.join(path,i), st_mtime, 0, self.DIR, myit) + # do NOT follow symbolic links + if os.path.islink(os.path.join(root,i)): + l = os.readlink(os.path.join(root,i)) + if self.fsenc: + l = l.decode(self.fsenc) + else: + l = l + + sql = """ + insert into files(parent_id, filename, filepath, date, size, type) + values(?,?,?,?,?,?) + """ + db_cursor.execute(sql, (currentid, j + " -> " + l, + os.path.join(path,i), st_mtime, 0, + self.LIN)) + dirsize = 0 + else: + dirsize = __recurse(currentid, j, os.path.join(path,i), + st_mtime, 0, self.DIR, myit) if dirsize == -1: break @@ -399,15 +509,20 @@ class MainModel(ModelMT): st_mtime = 0 st_size = 0 - ### scan files - # if i[-3:].lower() in mov_ext or \ - # mime.guess_type(i)!= (None,None) and \ - # mime.guess_type(i)[0].split("/")[0] == 'video': + ### TODO: scan files + #if i.split('.').[-1].lower() in mov_ext: # # video only # info = filetypeHelper.guess_video(os.path.join(root,i)) - # elif i[-3:].lower() in img_ext or \ - # mime.guess_type(i)!= (None,None) and \ - # mime.guess_type(i)[0].split("/")[0] == 'image': + + + if i.split('.')[-1].lower() in img_ext: + exif_info = EXIF.process_file(open(os.path.join(path,i), + 'rb')) + if exif_info.has_key('JPEGThumbnail'): + print "%s got thumbnail" % i + else: + print "%s has not got thumbnail" % i + # pass ### end of scan @@ -424,20 +539,26 @@ class MainModel(ModelMT): j = i if self.fsenc: j = i.decode(self.fsenc) - db_cursor.execute("insert into files(parent_id, filename, filepath, date, size, type) values(?,?,?,?,?,?)", - (currentid, j, os.path.join(path,i), st_mtime, st_size, self.FIL)) + + sql = """ + insert into files(parent_id, filename, filepath, date, size, type) + values(?,?,?,?,?,?) + """ + db_cursor.execute(sql, (currentid, j, os.path.join(path,i), + st_mtime, st_size, self.FIL)) - db_cursor.execute("update files set size=? where id=?",(_size, currentid)) + sql = """update files set size=? where id=?""" + db_cursor.execute(sql,(_size, currentid)) if self.abort: return -1 else: return _size - if __recurse(1, self.label, self.path, 0, 0, self.DIR) == -1: if __debug__: - print "m_main.py: __scan() __recurse() interrupted self.abort = True" - self.discsTree.remove(self.fresh_disk_iter) + print "m_main.py: __scan() __recurse() \ + interrupted self.abort = True" + self.discs_tree.remove(self.fresh_disk_iter) db_cursor.close() db_connection.rollback() else: @@ -447,7 +568,7 @@ class MainModel(ModelMT): db_connection.commit() db_connection.close() if __debug__: - print "m_main.py: __scan() time: ", (datetime.datetime.now() - timestamp) + print "m_main.py: __scan() time: ", (datetime.now() - timestamp) self.busy = False @@ -458,22 +579,27 @@ class MainModel(ModelMT): def __fetch_db_into_treestore(self): """load data from DB to tree model""" # cleanup treeStore - self.discsTree.clear() + self.__clear_discs_tree() #connect - db_connection = sqlite.connect("%s" % self.internal_filename, + db_connection = sqlite.connect("%s" % \ + (self.internal_dirname + '/db.sqlite'), detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) db_cursor = db_connection.cursor() # fetch all the directories try: - db_cursor.execute("SELECT id, parent_id, filename FROM files WHERE type=1 ORDER BY parent_id, filename") + sql = """ + SELECT id, parent_id, filename FROM files + WHERE type=1 ORDER BY parent_id, filename + """ + db_cursor.execute(sql) data = db_cursor.fetchall() except: # cleanup self.cleanup() self.filename = None - self.internal_filename = None + self.internal_dirname = None print "%s: Wrong database format!" % self.filename return @@ -481,36 +607,40 @@ class MainModel(ModelMT): """fetch all children and place them in model""" for row in data: if row[1] == parent_id: - myiter = self.discsTree.insert_before(iterator,None) - self.discsTree.set_value(myiter,0,row[0]) # id - self.discsTree.set_value(myiter,1,row[2]) # name - self.discsTree.set_value(myiter,3,row[1]) # parent_id + myiter = self.discs_tree.insert_before(iterator, None) + self.discs_tree.set_value(myiter, 0, row[0]) # id + self.discs_tree.set_value(myiter, 1, row[2]) # name + self.discs_tree.set_value(myiter, 3, row[1]) # parent_id get_children(row[0], myiter) # isroot? if iterator == None: - self.discsTree.set_value(myiter,2,gtk.STOCK_CDROM) + self.discs_tree.set_value(myiter, 2, gtk.STOCK_CDROM) else: - self.discsTree.set_value(myiter,2,gtk.STOCK_DIRECTORY) + self.discs_tree.set_value(myiter, 2, + gtk.STOCK_DIRECTORY) return if __debug__: - start_date = datetime.datetime.now() + start_date = datetime.now() # launch scanning. get_children() if __debug__: - print "m_main.py: __fetch_db_into_treestore() tree generation time: ", (datetime.datetime.now() - start_date) + print "m_main.py: __fetch_db_into_treestore() tree generation time: ", + (datetime.now() - start_date) db_connection.close() return def __remove_branch_form_db(self, root_id): parent_ids = [root_id,] - self.db_cursor.execute("select id from files where parent_id = ? and type = 1", (root_id,)) + sql = """select id from files where parent_id = ? and type = 1""" + self.db_cursor.execute(sql, (root_id,)) ids = self.db_cursor.fetchall() def get_children(fid): parent_ids.append(fid) - self.db_cursor.execute("select id from files where parent_id = ? and type = 1", (fid,)) + sql = """select id from files where parent_id = ? and type = 1""" + self.db_cursor.execute(sql, (fid,)) res = self.db_cursor.fetchall() for i in res: get_children(i[0]) @@ -522,8 +652,10 @@ class MainModel(ModelMT): for c in parent_ids: yield (c,) - self.db_cursor.executemany("delete from files where type = 1 and parent_id = ?", generator()) - self.db_cursor.executemany("delete from files where id = ?",generator()) + sql = """delete from files where type = 1 and parent_id = ?""" + self.db_cursor.executemany(sql, generator()) + sql = """delete from files where id = ?""" + self.db_cursor.executemany(sql, generator()) self.db_connection.commit() return @@ -531,36 +663,40 @@ class MainModel(ModelMT): def __append_added_volume(self): """append branch from DB to existing tree model""" #connect - db_connection = sqlite.connect("%s" % self.internal_filename, + db_connection = sqlite.connect("%s" % \ + (self.internal_dirname + '/db.sqlite'), detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) db_cursor = db_connection.cursor() - db_cursor.execute("SELECT id, parent_id, filename FROM files WHERE type=1 ORDER BY parent_id, filename") + sql = """SELECT id, parent_id, filename FROM files WHERE type=1 ORDER BY parent_id, filename""" + db_cursor.execute(sql) data = db_cursor.fetchall() def get_children(parent_id = 1, iterator = None): """fetch all children and place them in model""" for row in data: if row[1] == parent_id: - myiter = self.discsTree.insert_before(iterator,None) - self.discsTree.set_value(myiter,0,row[0]) - self.discsTree.set_value(myiter,1,row[2]) - self.discsTree.set_value(myiter,3,row[1]) + myiter = self.discs_tree.insert_before(iterator, None) + self.discs_tree.set_value(myiter, 0, row[0]) + self.discs_tree.set_value(myiter, 1, row[2]) + self.discs_tree.set_value(myiter, 3, row[1]) get_children(row[0], myiter) # isroot? if iterator == None: - self.discsTree.set_value(myiter,2,gtk.STOCK_CDROM) + self.discs_tree.set_value(myiter, 2, gtk.STOCK_CDROM) else: - self.discsTree.set_value(myiter,2,gtk.STOCK_DIRECTORY) + self.discs_tree.set_value(myiter, 2, + gtk.STOCK_DIRECTORY) return if __debug__: - start_date = datetime.datetime.now() + start_date = datetime.now() # launch scanning. get_children() if __debug__: - print "m_main.py: __fetch_db_into_treestore() tree generation time: ", (datetime.datetime.now() - start_date) + print "m_main.py: __fetch_db_into_treestore() tree generation time: ", + (datetime.now() - start_date) db_connection.close() return diff --git a/src/utils/EXIF.py b/src/utils/EXIF.py new file mode 100644 index 0000000..8efbb19 --- /dev/null +++ b/src/utils/EXIF.py @@ -0,0 +1,1193 @@ +# Library to extract EXIF information in digital camera image files +# +# To use this library call with: +# f=open(path_name, 'rb') +# tags=EXIF.process_file(f) +# tags will now be a dictionary mapping names of EXIF tags to their +# values in the file named by path_name. You can process the tags +# as you wish. In particular, you can iterate through all the tags with: +# for tag in tags.keys(): +# if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', +# 'EXIF MakerNote'): +# print "Key: %s, value %s" % (tag, tags[tag]) +# (This code uses the if statement to avoid printing out a few of the +# tags that tend to be long or boring.) +# +# The tags dictionary will include keys for all of the usual EXIF +# tags, and will also include keys for Makernotes used by some +# cameras, for which we have a good specification. +# +# Contains code from "exifdump.py" originally written by Thierry Bousch +# and released into the public domain. +# +# Updated and turned into general-purpose library by Gene Cash +# +# This copyright license is intended to be similar to the FreeBSD license. +# +# Copyright 2002 Gene Cash All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# This means you may do anything you want with this code, except claim you +# wrote it. Also, if it breaks you get to keep both pieces. +# +# Patch Contributors: +# * Simon J. Gerraty +# s2n fix & orientation decode +# * John T. Riedl +# Added support for newer Nikon type 3 Makernote format for D70 and some +# other Nikon cameras. +# * Joerg Schaefer +# Fixed subtle bug when faking an EXIF header, which affected maker notes +# using relative offsets, and a fix for Nikon D100. +# +# 21-AUG-99 TB Last update by Thierry Bousch to his code. +# 17-JAN-02 CEC Discovered code on web. +# Commented everything. +# Made small code improvements. +# Reformatted for readability. +# 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs. +# Added ability to extract JPEG formatted thumbnail. +# Added ability to read GPS IFD (not tested). +# Converted IFD data structure to dictionaries indexed by +# tag name. +# Factored into library returning dictionary of IFDs plus +# thumbnail, if any. +# 20-JAN-02 CEC Added MakerNote processing logic. +# Added Olympus MakerNote. +# Converted data structure to single-level dictionary, avoiding +# tag name collisions by prefixing with IFD name. This makes +# it much easier to use. +# 23-JAN-02 CEC Trimmed nulls from end of string values. +# 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote. +# 26-JAN-02 CEC Added ability to extract TIFF thumbnails. +# Added Nikon, Fujifilm, Casio MakerNotes. +# 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an +# IFD_Tag() object. +# 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L. +# + +# field type descriptions as (length, abbreviation, full name) tuples +FIELD_TYPES=( + (0, 'X', 'Proprietary'), # no such type + (1, 'B', 'Byte'), + (1, 'A', 'ASCII'), + (2, 'S', 'Short'), + (4, 'L', 'Long'), + (8, 'R', 'Ratio'), + (1, 'SB', 'Signed Byte'), + (1, 'U', 'Undefined'), + (2, 'SS', 'Signed Short'), + (4, 'SL', 'Signed Long'), + (8, 'SR', 'Signed Ratio') + ) + +# dictionary of main EXIF tag names +# first element of tuple is tag name, optional second element is +# another dictionary giving names to values +EXIF_TAGS={ + 0x0100: ('ImageWidth', ), + 0x0101: ('ImageLength', ), + 0x0102: ('BitsPerSample', ), + 0x0103: ('Compression', + {1: 'Uncompressed TIFF', + 6: 'JPEG Compressed'}), + 0x0106: ('PhotometricInterpretation', ), + 0x010A: ('FillOrder', ), + 0x010D: ('DocumentName', ), + 0x010E: ('ImageDescription', ), + 0x010F: ('Make', ), + 0x0110: ('Model', ), + 0x0111: ('StripOffsets', ), + 0x0112: ('Orientation', + {1: 'Horizontal (normal)', + 2: 'Mirrored horizontal', + 3: 'Rotated 180', + 4: 'Mirrored vertical', + 5: 'Mirrored horizontal then rotated 90 CCW', + 6: 'Rotated 90 CW', + 7: 'Mirrored horizontal then rotated 90 CW', + 8: 'Rotated 90 CCW'}), + 0x0115: ('SamplesPerPixel', ), + 0x0116: ('RowsPerStrip', ), + 0x0117: ('StripByteCounts', ), + 0x011A: ('XResolution', ), + 0x011B: ('YResolution', ), + 0x011C: ('PlanarConfiguration', ), + 0x0128: ('ResolutionUnit', + {1: 'Not Absolute', + 2: 'Pixels/Inch', + 3: 'Pixels/Centimeter'}), + 0x012D: ('TransferFunction', ), + 0x0131: ('Software', ), + 0x0132: ('DateTime', ), + 0x013B: ('Artist', ), + 0x013E: ('WhitePoint', ), + 0x013F: ('PrimaryChromaticities', ), + 0x0156: ('TransferRange', ), + 0x0200: ('JPEGProc', ), + 0x0201: ('JPEGInterchangeFormat', ), + 0x0202: ('JPEGInterchangeFormatLength', ), + 0x0211: ('YCbCrCoefficients', ), + 0x0212: ('YCbCrSubSampling', ), + 0x0213: ('YCbCrPositioning', ), + 0x0214: ('ReferenceBlackWhite', ), + 0x828D: ('CFARepeatPatternDim', ), + 0x828E: ('CFAPattern', ), + 0x828F: ('BatteryLevel', ), + 0x8298: ('Copyright', ), + 0x829A: ('ExposureTime', ), + 0x829D: ('FNumber', ), + 0x83BB: ('IPTC/NAA', ), + 0x8769: ('ExifOffset', ), + 0x8773: ('InterColorProfile', ), + 0x8822: ('ExposureProgram', + {0: 'Unidentified', + 1: 'Manual', + 2: 'Program Normal', + 3: 'Aperture Priority', + 4: 'Shutter Priority', + 5: 'Program Creative', + 6: 'Program Action', + 7: 'Portrait Mode', + 8: 'Landscape Mode'}), + 0x8824: ('SpectralSensitivity', ), + 0x8825: ('GPSInfo', ), + 0x8827: ('ISOSpeedRatings', ), + 0x8828: ('OECF', ), + # print as string + 0x9000: ('ExifVersion', lambda x: ''.join(map(chr, x))), + 0x9003: ('DateTimeOriginal', ), + 0x9004: ('DateTimeDigitized', ), + 0x9101: ('ComponentsConfiguration', + {0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'Red', + 5: 'Green', + 6: 'Blue'}), + 0x9102: ('CompressedBitsPerPixel', ), + 0x9201: ('ShutterSpeedValue', ), + 0x9202: ('ApertureValue', ), + 0x9203: ('BrightnessValue', ), + 0x9204: ('ExposureBiasValue', ), + 0x9205: ('MaxApertureValue', ), + 0x9206: ('SubjectDistance', ), + 0x9207: ('MeteringMode', + {0: 'Unidentified', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot'}), + 0x9208: ('LightSource', + {0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten', + 10: 'Flash', + 17: 'Standard Light A', + 18: 'Standard Light B', + 19: 'Standard Light C', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 255: 'Other'}), + 0x9209: ('Flash', {0: 'No', + 1: 'Fired', + 5: 'Fired (?)', # no return sensed + 7: 'Fired (!)', # return sensed + 9: 'Fill Fired', + 13: 'Fill Fired (?)', + 15: 'Fill Fired (!)', + 16: 'Off', + 24: 'Auto Off', + 25: 'Auto Fired', + 29: 'Auto Fired (?)', + 31: 'Auto Fired (!)', + 32: 'Not Available'}), + 0x920A: ('FocalLength', ), + 0x927C: ('MakerNote', ), + # print as string + 0x9286: ('UserComment', lambda x: ''.join(map(chr, x))), + 0x9290: ('SubSecTime', ), + 0x9291: ('SubSecTimeOriginal', ), + 0x9292: ('SubSecTimeDigitized', ), + # print as string + 0xA000: ('FlashPixVersion', lambda x: ''.join(map(chr, x))), + 0xA001: ('ColorSpace', ), + 0xA002: ('ExifImageWidth', ), + 0xA003: ('ExifImageLength', ), + 0xA005: ('InteroperabilityOffset', ), + 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP + 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - - + 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - - + 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - - + 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 - - + 0xA214: ('SubjectLocation', ), # 0x9214 - - + 0xA215: ('ExposureIndex', ), # 0x9215 - - + 0xA217: ('SensingMethod', ), # 0x9217 - - + 0xA300: ('FileSource', + {3: 'Digital Camera'}), + 0xA301: ('SceneType', + {1: 'Directly Photographed'}), + 0xA302: ('CVAPattern',), + } + +# interoperability tags +INTR_TAGS={ + 0x0001: ('InteroperabilityIndex', ), + 0x0002: ('InteroperabilityVersion', ), + 0x1000: ('RelatedImageFileFormat', ), + 0x1001: ('RelatedImageWidth', ), + 0x1002: ('RelatedImageLength', ), + } + +# GPS tags (not used yet, haven't seen camera with GPS) +GPS_TAGS={ + 0x0000: ('GPSVersionID', ), + 0x0001: ('GPSLatitudeRef', ), + 0x0002: ('GPSLatitude', ), + 0x0003: ('GPSLongitudeRef', ), + 0x0004: ('GPSLongitude', ), + 0x0005: ('GPSAltitudeRef', ), + 0x0006: ('GPSAltitude', ), + 0x0007: ('GPSTimeStamp', ), + 0x0008: ('GPSSatellites', ), + 0x0009: ('GPSStatus', ), + 0x000A: ('GPSMeasureMode', ), + 0x000B: ('GPSDOP', ), + 0x000C: ('GPSSpeedRef', ), + 0x000D: ('GPSSpeed', ), + 0x000E: ('GPSTrackRef', ), + 0x000F: ('GPSTrack', ), + 0x0010: ('GPSImgDirectionRef', ), + 0x0011: ('GPSImgDirection', ), + 0x0012: ('GPSMapDatum', ), + 0x0013: ('GPSDestLatitudeRef', ), + 0x0014: ('GPSDestLatitude', ), + 0x0015: ('GPSDestLongitudeRef', ), + 0x0016: ('GPSDestLongitude', ), + 0x0017: ('GPSDestBearingRef', ), + 0x0018: ('GPSDestBearing', ), + 0x0019: ('GPSDestDistanceRef', ), + 0x001A: ('GPSDestDistance', ) + } + +# Nikon E99x MakerNote Tags +# http://members.tripod.com/~tawba/990exif.htm +MAKERNOTE_NIKON_NEWER_TAGS={ + 0x0002: ('ISOSetting', ), + 0x0003: ('ColorMode', ), + 0x0004: ('Quality', ), + 0x0005: ('Whitebalance', ), + 0x0006: ('ImageSharpening', ), + 0x0007: ('FocusMode', ), + 0x0008: ('FlashSetting', ), + 0x0009: ('AutoFlashMode', ), + 0x000B: ('WhiteBalanceBias', ), + 0x000C: ('WhiteBalanceRBCoeff', ), + 0x000F: ('ISOSelection', ), + 0x0012: ('FlashCompensation', ), + 0x0013: ('ISOSpeedRequested', ), + 0x0016: ('PhotoCornerCoordinates', ), + 0x0018: ('FlashBracketCompensationApplied', ), + 0x0019: ('AEBracketCompensationApplied', ), + 0x0080: ('ImageAdjustment', ), + 0x0081: ('ToneCompensation', ), + 0x0082: ('AuxiliaryLens', ), + 0x0083: ('LensType', ), + 0x0084: ('LensMinMaxFocalMaxAperture', ), + 0x0085: ('ManualFocusDistance', ), + 0x0086: ('DigitalZoomFactor', ), + 0x0088: ('AFFocusPosition', + {0x0000: 'Center', + 0x0100: 'Top', + 0x0200: 'Bottom', + 0x0300: 'Left', + 0x0400: 'Right'}), + 0x0089: ('BracketingMode', + {0x00: 'Single frame, no bracketing', + 0x01: 'Continuous, no bracketing', + 0x02: 'Timer, no bracketing', + 0x10: 'Single frame, exposure bracketing', + 0x11: 'Continuous, exposure bracketing', + 0x12: 'Timer, exposure bracketing', + 0x40: 'Single frame, white balance bracketing', + 0x41: 'Continuous, white balance bracketing', + 0x42: 'Timer, white balance bracketing'}), + 0x008D: ('ColorMode', ), + 0x008F: ('SceneMode?', ), + 0x0090: ('LightingType', ), + 0x0092: ('HueAdjustment', ), + 0x0094: ('Saturation', + {-3: 'B&W', + -2: '-2', + -1: '-1', + 0: '0', + 1: '1', + 2: '2'}), + 0x0095: ('NoiseReduction', ), + 0x00A7: ('TotalShutterReleases', ), + 0x00A9: ('ImageOptimization', ), + 0x00AA: ('Saturation', ), + 0x00AB: ('DigitalVariProgram', ), + 0x0010: ('DataDump', ) + } + +MAKERNOTE_NIKON_OLDER_TAGS={ + 0x0003: ('Quality', + {1: 'VGA Basic', + 2: 'VGA Normal', + 3: 'VGA Fine', + 4: 'SXGA Basic', + 5: 'SXGA Normal', + 6: 'SXGA Fine'}), + 0x0004: ('ColorMode', + {1: 'Color', + 2: 'Monochrome'}), + 0x0005: ('ImageAdjustment', + {0: 'Normal', + 1: 'Bright+', + 2: 'Bright-', + 3: 'Contrast+', + 4: 'Contrast-'}), + 0x0006: ('CCDSpeed', + {0: 'ISO 80', + 2: 'ISO 160', + 4: 'ISO 320', + 5: 'ISO 100'}), + 0x0007: ('WhiteBalance', + {0: 'Auto', + 1: 'Preset', + 2: 'Daylight', + 3: 'Incandescent', + 4: 'Fluorescent', + 5: 'Cloudy', + 6: 'Speed Light'}) + } + +# decode Olympus SpecialMode tag in MakerNote +def olympus_special_mode(v): + a={ + 0: 'Normal', + 1: 'Unknown', + 2: 'Fast', + 3: 'Panorama'} + b={ + 0: 'Non-panoramic', + 1: 'Left to right', + 2: 'Right to left', + 3: 'Bottom to top', + 4: 'Top to bottom'} + return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]]) + +MAKERNOTE_OLYMPUS_TAGS={ + # ah HAH! those sneeeeeaky bastids! this is how they get past the fact + # that a JPEG thumbnail is not allowed in an uncompressed TIFF file + 0x0100: ('JPEGThumbnail', ), + 0x0200: ('SpecialMode', olympus_special_mode), + 0x0201: ('JPEGQual', + {1: 'SQ', + 2: 'HQ', + 3: 'SHQ'}), + 0x0202: ('Macro', + {0: 'Normal', + 1: 'Macro'}), + 0x0204: ('DigitalZoom', ), + 0x0207: ('SoftwareRelease', ), + 0x0208: ('PictureInfo', ), + # print as string + 0x0209: ('CameraID', lambda x: ''.join(map(chr, x))), + 0x0F00: ('DataDump', ) + } + +MAKERNOTE_CASIO_TAGS={ + 0x0001: ('RecordingMode', + {1: 'Single Shutter', + 2: 'Panorama', + 3: 'Night Scene', + 4: 'Portrait', + 5: 'Landscape'}), + 0x0002: ('Quality', + {1: 'Economy', + 2: 'Normal', + 3: 'Fine'}), + 0x0003: ('FocusingMode', + {2: 'Macro', + 3: 'Auto Focus', + 4: 'Manual Focus', + 5: 'Infinity'}), + 0x0004: ('FlashMode', + {1: 'Auto', + 2: 'On', + 3: 'Off', + 4: 'Red Eye Reduction'}), + 0x0005: ('FlashIntensity', + {11: 'Weak', + 13: 'Normal', + 15: 'Strong'}), + 0x0006: ('Object Distance', ), + 0x0007: ('WhiteBalance', + {1: 'Auto', + 2: 'Tungsten', + 3: 'Daylight', + 4: 'Fluorescent', + 5: 'Shade', + 129: 'Manual'}), + 0x000B: ('Sharpness', + {0: 'Normal', + 1: 'Soft', + 2: 'Hard'}), + 0x000C: ('Contrast', + {0: 'Normal', + 1: 'Low', + 2: 'High'}), + 0x000D: ('Saturation', + {0: 'Normal', + 1: 'Low', + 2: 'High'}), + 0x0014: ('CCDSpeed', + {64: 'Normal', + 80: 'Normal', + 100: 'High', + 125: '+1.0', + 244: '+3.0', + 250: '+2.0',}) + } + +MAKERNOTE_FUJIFILM_TAGS={ + 0x0000: ('NoteVersion', lambda x: ''.join(map(chr, x))), + 0x1000: ('Quality', ), + 0x1001: ('Sharpness', + {1: 'Soft', + 2: 'Soft', + 3: 'Normal', + 4: 'Hard', + 5: 'Hard'}), + 0x1002: ('WhiteBalance', + {0: 'Auto', + 256: 'Daylight', + 512: 'Cloudy', + 768: 'DaylightColor-Fluorescent', + 769: 'DaywhiteColor-Fluorescent', + 770: 'White-Fluorescent', + 1024: 'Incandescent', + 3840: 'Custom'}), + 0x1003: ('Color', + {0: 'Normal', + 256: 'High', + 512: 'Low'}), + 0x1004: ('Tone', + {0: 'Normal', + 256: 'High', + 512: 'Low'}), + 0x1010: ('FlashMode', + {0: 'Auto', + 1: 'On', + 2: 'Off', + 3: 'Red Eye Reduction'}), + 0x1011: ('FlashStrength', ), + 0x1020: ('Macro', + {0: 'Off', + 1: 'On'}), + 0x1021: ('FocusMode', + {0: 'Auto', + 1: 'Manual'}), + 0x1030: ('SlowSync', + {0: 'Off', + 1: 'On'}), + 0x1031: ('PictureMode', + {0: 'Auto', + 1: 'Portrait', + 2: 'Landscape', + 4: 'Sports', + 5: 'Night', + 6: 'Program AE', + 256: 'Aperture Priority AE', + 512: 'Shutter Priority AE', + 768: 'Manual Exposure'}), + 0x1100: ('MotorOrBracket', + {0: 'Off', + 1: 'On'}), + 0x1300: ('BlurWarning', + {0: 'Off', + 1: 'On'}), + 0x1301: ('FocusWarning', + {0: 'Off', + 1: 'On'}), + 0x1302: ('AEWarning', + {0: 'Off', + 1: 'On'}) + } + +MAKERNOTE_CANON_TAGS={ + 0x0006: ('ImageType', ), + 0x0007: ('FirmwareVersion', ), + 0x0008: ('ImageNumber', ), + 0x0009: ('OwnerName', ) + } + +# see http://www.burren.cx/david/canon.html by David Burren +# this is in element offset, name, optional value dictionary format +MAKERNOTE_CANON_TAG_0x001={ + 1: ('Macromode', + {1: 'Macro', + 2: 'Normal'}), + 2: ('SelfTimer', ), + 3: ('Quality', + {2: 'Normal', + 3: 'Fine', + 5: 'Superfine'}), + 4: ('FlashMode', + {0: 'Flash Not Fired', + 1: 'Auto', + 2: 'On', + 3: 'Red-Eye Reduction', + 4: 'Slow Synchro', + 5: 'Auto + Red-Eye Reduction', + 6: 'On + Red-Eye Reduction', + 16: 'external flash'}), + 5: ('ContinuousDriveMode', + {0: 'Single Or Timer', + 1: 'Continuous'}), + 7: ('FocusMode', + {0: 'One-Shot', + 1: 'AI Servo', + 2: 'AI Focus', + 3: 'MF', + 4: 'Single', + 5: 'Continuous', + 6: 'MF'}), + 10: ('ImageSize', + {0: 'Large', + 1: 'Medium', + 2: 'Small'}), + 11: ('EasyShootingMode', + {0: 'Full Auto', + 1: 'Manual', + 2: 'Landscape', + 3: 'Fast Shutter', + 4: 'Slow Shutter', + 5: 'Night', + 6: 'B&W', + 7: 'Sepia', + 8: 'Portrait', + 9: 'Sports', + 10: 'Macro/Close-Up', + 11: 'Pan Focus'}), + 12: ('DigitalZoom', + {0: 'None', + 1: '2x', + 2: '4x'}), + 13: ('Contrast', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 14: ('Saturation', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 15: ('Sharpness', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 16: ('ISO', + {0: 'See ISOSpeedRatings Tag', + 15: 'Auto', + 16: '50', + 17: '100', + 18: '200', + 19: '400'}), + 17: ('MeteringMode', + {3: 'Evaluative', + 4: 'Partial', + 5: 'Center-weighted'}), + 18: ('FocusType', + {0: 'Manual', + 1: 'Auto', + 3: 'Close-Up (Macro)', + 8: 'Locked (Pan Mode)'}), + 19: ('AFPointSelected', + {0x3000: 'None (MF)', + 0x3001: 'Auto-Selected', + 0x3002: 'Right', + 0x3003: 'Center', + 0x3004: 'Left'}), + 20: ('ExposureMode', + {0: 'Easy Shooting', + 1: 'Program', + 2: 'Tv-priority', + 3: 'Av-priority', + 4: 'Manual', + 5: 'A-DEP'}), + 23: ('LongFocalLengthOfLensInFocalUnits', ), + 24: ('ShortFocalLengthOfLensInFocalUnits', ), + 25: ('FocalUnitsPerMM', ), + 28: ('FlashActivity', + {0: 'Did Not Fire', + 1: 'Fired'}), + 29: ('FlashDetails', + {14: 'External E-TTL', + 13: 'Internal Flash', + 11: 'FP Sync Used', + 7: '2nd("Rear")-Curtain Sync Used', + 4: 'FP Sync Enabled'}), + 32: ('FocusMode', + {0: 'Single', + 1: 'Continuous'}) + } + +MAKERNOTE_CANON_TAG_0x004={ + 7: ('WhiteBalance', + {0: 'Auto', + 1: 'Sunny', + 2: 'Cloudy', + 3: 'Tungsten', + 4: 'Fluorescent', + 5: 'Flash', + 6: 'Custom'}), + 9: ('SequenceNumber', ), + 14: ('AFPointUsed', ), + 15: ('FlashBias', + {0XFFC0: '-2 EV', + 0XFFCC: '-1.67 EV', + 0XFFD0: '-1.50 EV', + 0XFFD4: '-1.33 EV', + 0XFFE0: '-1 EV', + 0XFFEC: '-0.67 EV', + 0XFFF0: '-0.50 EV', + 0XFFF4: '-0.33 EV', + 0X0000: '0 EV', + 0X000C: '0.33 EV', + 0X0010: '0.50 EV', + 0X0014: '0.67 EV', + 0X0020: '1 EV', + 0X002C: '1.33 EV', + 0X0030: '1.50 EV', + 0X0034: '1.67 EV', + 0X0040: '2 EV'}), + 19: ('SubjectDistance', ) + } + +# extract multibyte integer in Motorola format (little endian) +def s2n_motorola(str): + x=0 + for c in str: + x=(x << 8) | ord(c) + return x + +# extract multibyte integer in Intel format (big endian) +def s2n_intel(str): + x=0 + y=0L + for c in str: + x=x | (ord(c) << y) + y=y+8 + return x + +# ratio object that eventually will be able to reduce itself to lowest +# common denominator for printing +def gcd(a, b): + if b == 0: + return a + else: + return gcd(b, a % b) + +class Ratio: + def __init__(self, num, den): + self.num=num + self.den=den + + def __repr__(self): + self.reduce() + if self.den == 1: + return str(self.num) + return '%d/%d' % (self.num, self.den) + + def reduce(self): + div=gcd(self.num, self.den) + if div > 1: + self.num=self.num/div + self.den=self.den/div + +# for ease of dealing with tags +class IFD_Tag: + def __init__(self, printable, tag, field_type, values, field_offset, + field_length): + # printable version of data + self.printable=printable + # tag ID number + self.tag=tag + # field type as index into FIELD_TYPES + self.field_type=field_type + # offset of start of field in bytes from beginning of IFD + self.field_offset=field_offset + # length of data field in bytes + self.field_length=field_length + # either a string or array of data items + self.values=values + + def __str__(self): + return self.printable + + def __repr__(self): + return '(0x%04X) %s=%s @ %d' % (self.tag, + FIELD_TYPES[self.field_type][2], + self.printable, + self.field_offset) + +# class that handles an EXIF header +class EXIF_header: + def __init__(self, file, endian, offset, fake_exif, debug=0): + self.file=file + self.endian=endian + self.offset=offset + self.fake_exif=fake_exif + self.debug=debug + self.tags={} + + # convert slice to integer, based on sign and endian flags + # usually this offset is assumed to be relative to the beginning of the + # start of the EXIF information. For some cameras that use relative tags, + # this offset may be relative to some other starting point. + def s2n(self, offset, length, signed=0): + self.file.seek(self.offset+offset) + slice=self.file.read(length) + if self.endian == 'I': + val=s2n_intel(slice) + else: + val=s2n_motorola(slice) + # Sign extension ? + if signed: + msb=1L << (8*length-1) + if val & msb: + val=val-(msb << 1) + return val + + # convert offset to string + def n2s(self, offset, length): + s='' + for i in range(length): + if self.endian == 'I': + s=s+chr(offset & 0xFF) + else: + s=chr(offset & 0xFF)+s + offset=offset >> 8 + return s + + # return first IFD + def first_IFD(self): + return self.s2n(4, 4) + + # return pointer to next IFD + def next_IFD(self, ifd): + entries=self.s2n(ifd, 2) + return self.s2n(ifd+2+12*entries, 4) + + # return list of IFDs in header + def list_IFDs(self): + i=self.first_IFD() + a=[] + while i: + a.append(i) + i=self.next_IFD(i) + return a + + # return list of entries in this IFD + def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0): + entries=self.s2n(ifd, 2) + for i in range(entries): + # entry is index of start of this IFD in the file + entry=ifd+2+12*i + tag=self.s2n(entry, 2) + # get tag name. We do it early to make debugging easier + tag_entry=dict.get(tag) + if tag_entry: + tag_name=tag_entry[0] + else: + tag_name='Tag 0x%04X' % tag + field_type=self.s2n(entry+2, 2) + if not 0 < field_type < len(FIELD_TYPES): + # unknown field type + raise ValueError, \ + 'unknown type %d in tag 0x%04X' % (field_type, tag) + typelen=FIELD_TYPES[field_type][0] + count=self.s2n(entry+4, 4) + offset=entry+8 + if count*typelen > 4: + # offset is not the value; it's a pointer to the value + # if relative we set things up so s2n will seek to the right + # place when it adds self.offset. Note that this 'relative' + # is for the Nikon type 3 makernote. Other cameras may use + # other relative offsets, which would have to be computed here + # slightly differently. + if relative: + tmp_offset=self.s2n(offset, 4) + offset=tmp_offset+ifd-self.offset+4 + if self.fake_exif: + offset=offset+18 + else: + offset=self.s2n(offset, 4) + field_offset=offset + if field_type == 2: + # special case: null-terminated ASCII string + if count != 0: + self.file.seek(self.offset+offset) + values=self.file.read(count) + values=values.strip().replace('\x00','') + else: + values='' + else: + values=[] + signed=(field_type in [6, 8, 9, 10]) + for j in range(count): + if field_type in (5, 10): + # a ratio + value_j=Ratio(self.s2n(offset, 4, signed), + self.s2n(offset+4, 4, signed)) + else: + value_j=self.s2n(offset, typelen, signed) + values.append(value_j) + offset=offset+typelen + # now "values" is either a string or an array + if count == 1 and field_type != 2: + printable=str(values[0]) + else: + printable=str(values) + # compute printable version of values + if tag_entry: + if len(tag_entry) != 1: + # optional 2nd tag element is present + if callable(tag_entry[1]): + # call mapping function + printable=tag_entry[1](values) + else: + printable='' + for i in values: + # use lookup table for this tag + printable+=tag_entry[1].get(i, repr(i)) + self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag, + field_type, + values, field_offset, + count*typelen) + if self.debug: + print ' debug: %s: %s' % (tag_name, + repr(self.tags[ifd_name+' '+tag_name])) + + # extract uncompressed TIFF thumbnail (like pulling teeth) + # we take advantage of the pre-existing layout in the thumbnail IFD as + # much as possible + def extract_TIFF_thumbnail(self, thumb_ifd): + entries=self.s2n(thumb_ifd, 2) + # this is header plus offset to IFD ... + if self.endian == 'M': + tiff='MM\x00*\x00\x00\x00\x08' + else: + tiff='II*\x00\x08\x00\x00\x00' + # ... plus thumbnail IFD data plus a null "next IFD" pointer + self.file.seek(self.offset+thumb_ifd) + tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00' + + # fix up large value offset pointers into data area + for i in range(entries): + entry=thumb_ifd+2+12*i + tag=self.s2n(entry, 2) + field_type=self.s2n(entry+2, 2) + typelen=FIELD_TYPES[field_type][0] + count=self.s2n(entry+4, 4) + oldoff=self.s2n(entry+8, 4) + # start of the 4-byte pointer area in entry + ptr=i*12+18 + # remember strip offsets location + if tag == 0x0111: + strip_off=ptr + strip_len=count*typelen + # is it in the data area? + if count*typelen > 4: + # update offset pointer (nasty "strings are immutable" crap) + # should be able to say "tiff[ptr:ptr+4]=newoff" + newoff=len(tiff) + tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:] + # remember strip offsets location + if tag == 0x0111: + strip_off=newoff + strip_len=4 + # get original data and store it + self.file.seek(self.offset+oldoff) + tiff+=self.file.read(count*typelen) + + # add pixel strips and update strip offset info + old_offsets=self.tags['Thumbnail StripOffsets'].values + old_counts=self.tags['Thumbnail StripByteCounts'].values + for i in range(len(old_offsets)): + # update offset pointer (more nasty "strings are immutable" crap) + offset=self.n2s(len(tiff), strip_len) + tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:] + strip_off+=strip_len + # add pixel strip to end + self.file.seek(self.offset+old_offsets[i]) + tiff+=self.file.read(old_counts[i]) + + self.tags['TIFFThumbnail']=tiff + + # decode all the camera-specific MakerNote formats + + # Note is the data that comprises this MakerNote. The MakerNote will + # likely have pointers in it that point to other parts of the file. We'll + # use self.offset as the starting point for most of those pointers, since + # they are relative to the beginning of the file. + # + # If the MakerNote is in a newer format, it may use relative addressing + # within the MakerNote. In that case we'll use relative addresses for the + # pointers. + # + # As an aside: it's not just to be annoying that the manufacturers use + # relative offsets. It's so that if the makernote has to be moved by the + # picture software all of the offsets don't have to be adjusted. Overall, + # this is probably the right strategy for makernotes, though the spec is + # ambiguous. (The spec does not appear to imagine that makernotes would + # follow EXIF format internally. Once they did, it's ambiguous whether + # the offsets should be from the header at the start of all the EXIF info, + # or from the header at the start of the makernote.) + def decode_maker_note(self): + note=self.tags['EXIF MakerNote'] + make=self.tags['Image Make'].printable + model=self.tags['Image Model'].printable + + # Nikon + # The maker note usually starts with the word Nikon, followed by the + # type of the makernote (1 or 2, as a short). If the word Nikon is + # not at the start of the makernote, it's probably type 2, since some + # cameras work that way. + if make in ('NIKON', 'NIKON CORPORATION'): + if note.values[0:7] == [78, 105, 107, 111, 110, 00, 01]: + if self.debug: + print "Looks like a type 1 Nikon MakerNote." + self.dump_IFD(note.field_offset+8, 'MakerNote', + dict=MAKERNOTE_NIKON_OLDER_TAGS) + elif note.values[0:7] == [78, 105, 107, 111, 110, 00, 02]: + if self.debug: + print "Looks like a labeled type 2 Nikon MakerNote" + if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]: + raise ValueError, "Missing marker tag '42' in MakerNote." + # skip the Makernote label and the TIFF header + self.dump_IFD(note.field_offset+10+8, 'MakerNote', + dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1) + else: + # E99x or D1 + if self.debug: + print "Looks like an unlabeled type 2 Nikon MakerNote" + self.dump_IFD(note.field_offset, 'MakerNote', + dict=MAKERNOTE_NIKON_NEWER_TAGS) + return + + # Olympus + if make[:7] == 'OLYMPUS': + self.dump_IFD(note.field_offset+8, 'MakerNote', + dict=MAKERNOTE_OLYMPUS_TAGS) + return + + # Casio + if make == 'Casio': + self.dump_IFD(note.field_offset, 'MakerNote', + dict=MAKERNOTE_CASIO_TAGS) + return + + # Fujifilm + if make == 'FUJIFILM': + # bug: everything else is "Motorola" endian, but the MakerNote + # is "Intel" endian + endian=self.endian + self.endian='I' + # bug: IFD offsets are from beginning of MakerNote, not + # beginning of file header + offset=self.offset + self.offset+=note.field_offset + # process note with bogus values (note is actually at offset 12) + self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS) + # reset to correct values + self.endian=endian + self.offset=offset + return + + # Canon + if make == 'Canon': + self.dump_IFD(note.field_offset, 'MakerNote', + dict=MAKERNOTE_CANON_TAGS) + for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001), + ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)): + self.canon_decode_tag(self.tags[i[0]].values, i[1]) + return + + # decode Canon MakerNote tag based on offset within tag + # see http://www.burren.cx/david/canon.html by David Burren + def canon_decode_tag(self, value, dict): + for i in range(1, len(value)): + x=dict.get(i, ('Unknown', )) + if self.debug: + print i, x + name=x[0] + if len(x) > 1: + val=x[1].get(value[i], 'Unknown') + else: + val=value[i] + # it's not a real IFD Tag but we fake one to make everybody + # happy. this will have a "proprietary" type + self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None, + None, None) + +# process an image file (expects an open file object) +# this is the function that has to deal with all the arbitrary nasty bits +# of the EXIF standard +def process_file(file, debug=0): + # determine whether it's a JPEG or TIFF + data=file.read(12) + if data[0:4] in ['II*\x00', 'MM\x00*']: + # it's a TIFF file + file.seek(0) + endian=file.read(1) + file.read(1) + offset=0 + elif data[0:2] == '\xFF\xD8': + # it's a JPEG file + # skip JFIF style header(s) + fake_exif=0 + while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'): + length=ord(data[4])*256+ord(data[5]) + file.read(length-8) + # fake an EXIF beginning of file + data='\xFF\x00'+file.read(10) + fake_exif=1 + if data[2] == '\xFF' and data[6:10] == 'Exif': + # detected EXIF header + offset=file.tell() + endian=file.read(1) + else: + # no EXIF information + return {} + else: + # file format not recognized + return {} + + # deal with the EXIF info we found + if debug: + print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format' + hdr=EXIF_header(file, endian, offset, fake_exif, debug) + ifd_list=hdr.list_IFDs() + ctr=0 + for i in ifd_list: + if ctr == 0: + IFD_name='Image' + elif ctr == 1: + IFD_name='Thumbnail' + thumb_ifd=i + else: + IFD_name='IFD %d' % ctr + if debug: + print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i) + hdr.dump_IFD(i, IFD_name) + # EXIF IFD + exif_off=hdr.tags.get(IFD_name+' ExifOffset') + if exif_off: + if debug: + print ' EXIF SubIFD at offset %d:' % exif_off.values[0] + hdr.dump_IFD(exif_off.values[0], 'EXIF') + # Interoperability IFD contained in EXIF IFD + intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset') + if intr_off: + if debug: + print ' EXIF Interoperability SubSubIFD at offset %d:' \ + % intr_off.values[0] + hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability', + dict=INTR_TAGS) + # GPS IFD + gps_off=hdr.tags.get(IFD_name+' GPSInfo') + if gps_off: + if debug: + print ' GPS SubIFD at offset %d:' % gps_off.values[0] + hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS) + ctr+=1 + + # extract uncompressed TIFF thumbnail + thumb=hdr.tags.get('Thumbnail Compression') + if thumb and thumb.printable == 'Uncompressed TIFF': + hdr.extract_TIFF_thumbnail(thumb_ifd) + + # JPEG thumbnail (thankfully the JPEG data is stored as a unit) + thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat') + if thumb_off: + file.seek(offset+thumb_off.values[0]) + size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0] + hdr.tags['JPEGThumbnail']=file.read(size) + + # deal with MakerNote contained in EXIF IFD + if hdr.tags.has_key('EXIF MakerNote'): + hdr.decode_maker_note() + + # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote + # since it's not allowed in a uncompressed TIFF IFD + if not hdr.tags.has_key('JPEGThumbnail'): + thumb_off=hdr.tags.get('MakerNote JPEGThumbnail') + if thumb_off: + file.seek(offset+thumb_off.values[0]) + hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length) + + return hdr.tags + +# library test/debug function (dump given files) +if __name__ == '__main__': + import sys + + if len(sys.argv) < 2: + print 'Usage: %s files...\n' % sys.argv[0] + sys.exit(0) + + for filename in sys.argv[1:]: + try: + file=open(filename, 'rb') + except: + print filename, 'unreadable' + print + continue + print filename+':' + # data=process_file(file, 1) # with debug info + data=process_file(file) + if not data: + print 'No EXIF information found' + continue + + x=data.keys() + x.sort() + for i in x: + if i in ('JPEGThumbnail', 'TIFFThumbnail'): + continue + try: + print ' %s (%s): %s' % \ + (i, FIELD_TYPES[data[i].field_type][2], data[i].printable) + except: + print 'error', i, '"', data[i], '"' + if data.has_key('JPEGThumbnail'): + print 'File has JPEG thumbnail' + print diff --git a/src/views/v_details.py b/src/views/v_details.py new file mode 100644 index 0000000..4bb3104 --- /dev/null +++ b/src/views/v_details.py @@ -0,0 +1,43 @@ +# This Python file uses the following encoding: utf-8 +# +# Author: Roman 'gryf' Dobosz gryf@elysium.pl +# +# Copyright (C) 2007 by Roman 'gryf' Dobosz +# +# This file is part of pyGTKtalog. +# +# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# ------------------------------------------------------------------------- + +from gtkmvc import View +import os.path +import utils.globals + +class DetailsView(View): + """ """ + GLADE = os.path.join(utils.globals.GLADE_DIR, "main.glade") + def __init__(self, ctrl, stand_alone=True): + top_widget = "hbox_details" + if stand_alone: + top_widget = "window_details" + + View.__init__(self, ctrl, self.GLADE, top_widget) + return + + def is_stand_alone(self): + return self["window_details"] is not None + + pass # end of class diff --git a/src/views/v_main.py b/src/views/v_main.py index 3a8ab50..5af43c3 100644 --- a/src/views/v_main.py +++ b/src/views/v_main.py @@ -22,9 +22,10 @@ # ------------------------------------------------------------------------- -from gtkmvc import View import os.path import utils.globals +from gtkmvc import View +from v_details import DetailsView class MainView(View): """This handles only the graphical representation of the @@ -33,6 +34,13 @@ class MainView(View): GLADE = os.path.join(utils.globals.GLADE_DIR, "main.glade") def __init__(self, ctrl): View.__init__(self, ctrl, self.GLADE) + self.details = None return + def create_sub_view(self, details_ctrl): + """attach sub view""" + self.details = DetailsView(details_ctrl, False) + vpan = self['vpaned1'] + vpan.add2(self.details.get_top_widget()) + return pass # end of class