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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-
+
+ pyGTKtalog
+
+
+
+
+ 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
+
+
+
+
+
+
+
+ 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