1#!/usr/bin/python
2
3# Audio Tools, a module and set of tools for manipulating audio data
4# Copyright (C) 2008-2014  Brian Langenberger
5
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20from __future__ import print_function
21import sys
22import os
23import os.path
24from io import BytesIO
25import audiotools
26import audiotools.text as _
27
28
29# returns the dimensions of image_surface scaled to match the
30# display surface size, but without changing its aspect ratio
31def image_size(display_surface, image_surface):
32    display_ratio = float(display_surface[0]) / float(display_surface[1])
33
34    image_ratio = float(image_surface[0]) / float(image_surface[1])
35
36    if image_ratio > display_ratio:  # image wider than display, when scaled
37        new_width = display_surface[0]
38        new_height = image_surface[1] / (float(image_surface[0]) /
39                                         float(display_surface[0]))
40    else:                              # image taller than display, when scaled
41        new_width = image_surface[0] / (float(image_surface[1]) /
42                                        float(display_surface[1]))
43        new_height = display_surface[1]
44
45    (new_width, new_height) = map(int, (new_width, new_height))
46
47    return (new_width, new_height)
48
49try:
50    import pygtk
51    pygtk.require("2.0")
52    import gtk
53    import gtk.gdk
54
55    class ImageWidget_Gtk(gtk.DrawingArea):
56        def __init__(self):
57            gtk.DrawingArea.__init__(self)
58            self.full_pixbuf = None
59            self.scaled_thumbnail = None
60            self.connect("expose_event", self.expose_event)
61            self.connect("configure_event", self.configure_event)
62            self.widget_width = 0
63            self.widget_height = 0
64
65        def set_pixbuf(self, pixbuf):
66            self.full_pixbuf = pixbuf
67            self.scaled_thumbnail = None
68            self.queue_draw()
69
70        def clear(self):
71            self.full_pixbuf = None
72            self.queue_draw()
73
74        def expose_event(self, widget, area):
75            self.draw_image()
76
77        def configure_event(self, widget, area):
78            self.widget_width = area.width
79            self.widget_height = area.height
80            self.draw_image()
81
82        def draw_image(self):
83            if self.full_pixbuf is None:
84                return
85
86            (image_width,
87             image_height) = image_size((self.widget_width,
88                                         self.widget_height),
89                                        (self.full_pixbuf.get_width(),
90                                         self.full_pixbuf.get_height()))
91
92            if (((self.scaled_thumbnail is None) or
93                 (self.scaled_thumbnail.get_width() != image_width) or
94                 (self.scaled_thumbnail.get_height() != image_height))):
95                self.scaled_thumbnail = self.full_pixbuf.scale_simple(
96                    image_width,
97                    image_height,
98                    gtk.gdk.INTERP_HYPER)
99
100            self.window.draw_pixbuf(
101                gc=self.get_style().fg_gc[gtk.STATE_NORMAL],
102                pixbuf=self.scaled_thumbnail,
103                src_x=0,
104                src_y=0,
105                dest_x=(self.widget_width - image_width) // 2,
106                dest_y=(self.widget_height - image_height) // 2,
107                width=image_width,
108                height=image_height)
109
110    class Coverview_Gtk:
111        def __init__(self):
112            self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
113            self.window.connect("delete_event", self.delete_event)
114            self.window.connect("destroy", self.destroy)
115
116            table = gtk.Table(rows=2, columns=2, homogeneous=False)
117
118            self.list = gtk.ListStore(str, gtk.gdk.Pixbuf,
119                                      str, str, str, str)
120
121            self.list_widget = gtk.TreeView(self.list)
122            self.list_widget.append_column(
123                gtk.TreeViewColumn(
124                    None, gtk.CellRendererText(), text=0))
125            self.list_widget.set_headers_visible(False)
126            list_scroll = gtk.ScrolledWindow()
127            list_scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
128            list_scroll.add(self.list_widget)
129            self.list_widget.connect("cursor-changed", self.image_selected)
130
131            self.image = ImageWidget_Gtk()
132
133            accelgroup = gtk.AccelGroup()
134            self.window.add_accel_group(accelgroup)
135            menubar = gtk.MenuBar()
136            file_menu = gtk.Menu()
137            file_item = gtk.MenuItem("File")
138            file_item.set_submenu(file_menu)
139
140            help_menu = gtk.Menu()
141            help_item = gtk.MenuItem("Help")
142            help_item.set_right_justified(True)
143            help_item.set_submenu(help_menu)
144
145            open_item = gtk.ImageMenuItem(gtk.STOCK_OPEN, accelgroup)
146            open_item.connect("activate", self.open)
147            file_menu.append(open_item)
148
149            quit_item = gtk.ImageMenuItem(gtk.STOCK_QUIT, accelgroup)
150            quit_item.connect("activate", self.destroy)
151            file_menu.append(quit_item)
152
153            about_item = gtk.ImageMenuItem(gtk.STOCK_ABOUT, accelgroup)
154            about_item.connect("activate", self.about)
155            help_menu.append(about_item)
156
157            menubar.append(file_item)
158            menubar.append(help_item)
159
160            image_info_bar = gtk.HBox()
161            self.size_bits = gtk.Label()
162            self.size_pixels = gtk.Label()
163            self.bits = gtk.Label()
164            self.type = gtk.Label()
165
166            self.size_bits.set_alignment(1.0, 0.0)
167            self.size_pixels.set_alignment(1.0, 0.0)
168            self.bits.set_alignment(1.0, 0.0)
169            self.type.set_alignment(1.0, 0.0)
170
171            image_info_bar.pack_start(self.size_bits)
172            image_info_bar.pack_start(gtk.VSeparator(),
173                                      expand=False, fill=False, padding=3)
174            image_info_bar.pack_start(self.size_pixels)
175            image_info_bar.pack_start(gtk.VSeparator(),
176                                      expand=False, fill=False, padding=3)
177            image_info_bar.pack_start(self.bits)
178            image_info_bar.pack_start(gtk.VSeparator(),
179                                      expand=False, fill=False, padding=3)
180            image_info_bar.pack_start(self.type)
181
182            self.statusbar = gtk.Statusbar()
183            self.statusbar.push(0, "")
184
185            table.attach(menubar, 0, 2, 0, 1, yoptions=0)
186            table.attach(list_scroll, 0, 1, 1, 2, xoptions=0, xpadding=5)
187            table.attach(self.image, 1, 2, 1, 2)
188            table.attach(gtk.HSeparator(), 0, 2, 2, 3, yoptions=0)
189            table.attach(image_info_bar, 0, 2, 3, 4, yoptions=0)
190            table.attach(self.statusbar, 0, 2, 5, 6, yoptions=0)
191
192            self.file_dialog = gtk.FileChooserDialog(
193                title=_.LAB_CHOOSE_FILE,
194                action=gtk.FILE_CHOOSER_ACTION_OPEN,
195                buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
196                         gtk.STOCK_OPEN, gtk.RESPONSE_OK))
197
198            self.about_dialog = gtk.AboutDialog()
199
200            self.about_dialog.set_program_name(u"coverview")
201            self.about_dialog.set_version(audiotools.VERSION)
202            self.about_dialog.set_copyright(u"(c) Brian Langenberger")
203            self.about_dialog.set_comments(_.LAB_COVERVIEW_ABOUT)
204            self.about_dialog.set_website(_.LAB_AUDIOTOOLS_URL)
205
206            self.window.add(table)
207            self.window.set_default_size(800, 600)
208            self.window.show_all()
209
210        def delete_event(self, widget, event, data=None):
211            return False
212
213        def open(self, widget, data=None):
214            response = self.file_dialog.run()
215            if response == gtk.RESPONSE_OK:
216                filename = self.file_dialog.get_filename()
217                try:
218                    self.set_metadata(
219                        audiotools.open(filename).get_metadata())
220                    self.set_filename(filename)
221                except audiotools.UnsupportedFile:
222                    error = gtk.MessageDialog(
223                        type=gtk.MESSAGE_ERROR,
224                        message_format=_.ERR_UNSUPPORTED_FILE % (filename),
225                        buttons=gtk.BUTTONS_OK)
226                    error.run()
227                    error.destroy()
228                except audiotools.InvalidFile:
229                    error = gtk.MessageDialog(
230                        type=gtk.MESSAGE_ERROR,
231                        message_format=_.ERR_INVALID_FILE % (filename),
232                        buttons=gtk.BUTTONS_OK)
233                    error.run()
234                    error.destroy()
235                except IOError:
236                    error = gtk.MessageDialog(
237                        type=gtk.MESSAGE_ERROR,
238                        message_format=_.ERR_OPEN_IOERROR % (filename),
239                        buttons=gtk.BUTTONS_OK)
240                    error.run()
241                    error.destroy()
242            elif response == gtk.RESPONSE_CANCEL:
243                pass
244            self.file_dialog.hide()
245
246        def about(self, widget, data=None):
247            self.about_dialog.run()
248            self.about_dialog.hide()
249
250        def destroy(self, widget, data=None):
251            gtk.main_quit()
252
253        def image_selected(self, treeview=None):
254            (liststore,
255             selection) = self.list_widget.get_selection().get_selected_rows()
256
257            row = selection[0][0]
258            treeiter = liststore.get_iter(row)
259
260            self.image.set_pixbuf(liststore.get_value(treeiter, 1))
261            self.size_bits.set_text(liststore.get_value(treeiter, 2))
262            self.size_pixels.set_text(liststore.get_value(treeiter, 3))
263            self.bits.set_text(liststore.get_value(treeiter, 4))
264            self.type.set_text(liststore.get_value(treeiter, 5))
265
266        def image_unselected(self):
267            self.image.clear()
268            self.size_bits.set_text(u"")
269            self.size_pixels.set_text(u"")
270            self.bits.set_text(u"")
271            self.type.set_text(u"")
272
273        def set_filename(self, path):
274            self.statusbar.pop(0)
275            self.statusbar.push(0, path)
276
277        def set_metadata(self, metadata):
278            def get_pixbuf(imagedata):
279                l = gtk.gdk.PixbufLoader()
280                l.write(imagedata)
281                l.close()
282                return l.get_pixbuf()
283
284            self.list.clear()
285
286            images = metadata.images()
287            first_image = None
288            for image in images:
289                tree_iter = self.list.append()
290                self.list.set(tree_iter, 0,
291                              image.type_string())
292                self.list.set(tree_iter, 1,
293                              get_pixbuf(image.data))
294                self.list.set(tree_iter, 2,
295                              _.LAB_BYTE_SIZE % (len(image.data)))
296                self.list.set(tree_iter, 3,
297                              _.LAB_DIMENSIONS % (image.width, image.height))
298                self.list.set(tree_iter, 4,
299                              _.LAB_BITS_PER_PIXEL % (image.color_depth))
300                self.list.set(tree_iter, 5, image.mime_type.decode('ascii'))
301
302                if first_image is None:
303                    first_image = tree_iter
304
305            if len(images) > 0:
306                self.list_widget.get_selection().select_iter(first_image)
307                self.image_selected()
308            else:
309                self.image_unselected()
310
311    def main_gtk(initial_audiofile=None):
312        coverview = Coverview_Gtk()
313        if initial_audiofile is not None:
314            coverview.set_filename(initial_audiofile.filename)
315            coverview.set_metadata(initial_audiofile.get_metadata())
316        gtk.main()
317
318    GTK_AVAILABLE = True
319except ImportError:
320    GTK_AVAILABLE = False
321
322try:
323    from Tkinter import *
324    import Image
325    import ImageTk
326    import tkFileDialog
327    import tkMessageBox
328
329    try:
330        import _imaging as test
331    except ImportError as err:
332        if os.uname()[0].lower() == 'darwin':
333            print("""*** Unable to load Python Imaging Library.
334You may need to run Python in 32-bit mode in order for it to load correctly.
335If running a Bourne-like shell, try:
336
337% export VERSIONER_PYTHON_PREFER_32_BIT=yes
338
339or, if running a C-like shell, try:
340
341% setenv VERSIONER_PYTHON_PREFER_32_BIT yes
342
343Consult "man python" for further details.
344""")
345        raise err
346
347    class ImagePair_Tk:
348        def __init__(self, path, image_obj):
349            """image_obj is an audiotools.Image object"""
350
351            self.path = path
352            self.audiotools = image_obj
353            self.pil = Image.open(BytesIO(image_obj.data))
354
355    class Statusbar_Tkinter(Frame):
356        def __init__(self, master):
357            Frame.__init__(self, master)
358
359            self.image_info = Frame(self)
360            self.path_info = Frame(self)
361
362            self.path = Label(self.path_info, bd=1, relief=SUNKEN, anchor=W)
363            self.path.pack(fill=X)
364
365            self.size_bits = Label(self.image_info, bd=1, relief=SUNKEN,
366                                   anchor=E)
367            self.size_pixels = Label(self.image_info, bd=1, relief=SUNKEN,
368                                     anchor=E)
369            self.bits = Label(self.image_info, bd=1, relief=SUNKEN, anchor=E)
370            self.type = Label(self.image_info, bd=1, relief=SUNKEN, anchor=E)
371            self.size_bits.grid(row=0, column=0, sticky=E + W)
372            self.size_pixels.grid(row=0, column=1, sticky=E + W)
373            self.bits.grid(row=0, column=2, sticky=E + W)
374            self.type.grid(row=0, column=3, sticky=E + W)
375            self.image_info.columnconfigure(0, weight=1)
376            self.image_info.columnconfigure(1, weight=1)
377            self.image_info.columnconfigure(2, weight=1)
378            self.image_info.columnconfigure(3, weight=1)
379
380            self.image_info.pack(fill=X)
381            self.path_info.pack(fill=X)
382
383        def set_path(self, path):
384            self.path.config(text=path)
385
386        def set_image_data(self, image):
387            if image is None:
388                return
389            else:
390                at_image = image.audiotools
391            self.path.config(text=image.path)
392            self.size_bits.config(text=_.LAB_BYTE_SIZE % (len(at_image.data)))
393            self.size_pixels.config(text=_.LAB_DIMENSIONS % (at_image.width,
394                                                             at_image.height))
395            self.bits.config(
396                text=_.LAB_BITS_PER_PIXEL % (at_image.color_depth))
397            self.type.config(text=at_image.mime_type)
398
399        def clear_image_data(self):
400            self.path.config(text="")
401            self.size_bits.config(text="")
402            self.size_pixels.config(text="")
403            self.bits.config(text="")
404            self.type.config(text="")
405
406    class About_Tkinter(Frame):
407        def __init__(self, master):
408            Frame.__init__(self, master, padx=10, pady=10)
409
410            Label(self, text="coverview",
411                  font=("Helvetica", 48)).pack(side=TOP)
412            Label(self,
413                  text="Python Audio Tools %s" %
414                  (audiotools.VERSION)).pack(side=TOP)
415            Label(self,
416                  text="(c) by Brian Langenberger",
417                  font=("Helvetica", 10)).pack(side=TOP)
418            Label(self,
419                  text=_.LAB_COVERVIEW_ABOUT).pack(side=TOP)
420            Label(self,
421                  text=_.LAB_AUDIOTOOLS_URL).pack(side=TOP)
422
423            close = Button(self, text=_.LAB_CLOSE)
424            close.bind(sequence="<ButtonRelease-1>", func=self.close)
425            close.pack(side=BOTTOM)
426
427            self.master = master
428
429        def close(self, event):
430            self.master.withdraw()
431
432    class Coverview_Tkinter:
433        def __init__(self, master):
434            self.master = master
435
436            frame = Frame(master, width=800, height=600)
437
438            self.statusbar = Statusbar_Tkinter(frame)
439            self.statusbar.pack(side=BOTTOM, fill=X)
440
441            self.images = Listbox(frame, selectmode=BROWSE)
442            self.images.pack(fill=Y, expand=0, side=LEFT)
443            self.images.bind(sequence="<ButtonRelease-1>",
444                             func=self.event_selected)
445            self.images.bind(sequence="<KeyRelease>",
446                             func=self.event_selected)
447
448            self.canvas = Canvas(frame)
449            self.canvas.pack(fill=BOTH, expand=1, side=LEFT)
450            self.canvas.bind(sequence="<Configure>", func=self.event_resized)
451
452            self.photo_image_id = None
453            self.current_image = None
454            self.image_objects = []
455
456            frame.pack(fill=BOTH, expand=1, side=LEFT)
457            frame.master.title("coverview")
458            frame.master.geometry("%dx%d" % (800, 600))
459
460            self.image_width = int(self.canvas.cget("width"))
461            self.image_height = int(self.canvas.cget("height"))
462
463            self.menubar = Menu(master)
464            file_menu = Menu(self.menubar, tearoff=0)
465            file_menu.add_command(label="Open", command=self.open)
466            file_menu.add_command(label="Quit", command=self.quit)
467            self.menubar.add_cascade(label="File", menu=file_menu)
468
469            help_menu = Menu(self.menubar, tearoff=0)
470            help_menu.add_command(label="About", command=self.about)
471            self.menubar.add_cascade(label="Help", menu=help_menu)
472
473            # master.bind_all(sequence="<Control-o>", func=self.open)
474            # master.bind_all(sequence="<Control-q>", func=self.quit)
475            master.config(menu=self.menubar)
476
477            self.about_window = Toplevel()
478            self.about_window.withdraw()
479            self.about_window_frame = About_Tkinter(self.about_window)
480            self.about_window_frame.pack(fill=BOTH, expand=1)
481
482        def open(self, *args):
483            new_file = tkFileDialog.askopenfilename(
484                parent=self.master,
485                title=_.LAB_CHOOSE_FILE)
486
487            if len(new_file) > 0:
488                try:
489                    self.set_metadata(new_file,
490                                      audiotools.open(new_file).get_metadata())
491                except audiotools.UnsupportedFile:
492                    self.show_error(_.ERR_UNSUPPORTED_FILE % (new_file))
493                except audiotools.InvalidFile:
494                    self.show_error(_.ERR_INVALID_FILE % (new_file))
495                except IOError as err:
496                    self.show_error(_.ERR_OPEN_IOERROR % (new_file))
497
498        def quit(self, *args):
499            self.master.quit()
500
501        def about(self, *args):
502            self.about_window.state("normal")
503
504        def show_error(self, error):
505            if self.photo_image_id is not None:
506                self.canvas.delete(self.photo_image_id)
507            self.statusbar.clear_image_data()
508            self.images.selection_clear(0, END)
509            self.images.delete(0, END)
510            self.image_objects = []
511            self.statusbar.set_path(error)
512
513        def event_selected(self, event):
514            try:
515                self.current_image = self.image_objects[
516                    int(self.images.curselection()[0])]
517                self.set_image(self.current_image)
518            except IndexError:
519                self.current_image = None
520                self.set_image(None)
521
522        def event_resized(self, event):
523            self.image_width = event.width
524            self.image_height = event.height
525            self.set_image(self.current_image)
526
527        def set_image(self, image):
528            if image is None:
529                if self.photo_image_id is not None:
530                    self.canvas.delete(self.photo_image_id)
531                self.statusbar.clear_image_data()
532                return
533
534            (image_width, image_height) = image_size(
535                (self.image_width,
536                 self.image_height),
537                (self.current_image.pil.size[0],
538                 self.current_image.pil.size[1]))
539
540            resized_image = self.current_image.pil.resize((image_width,
541                                                           image_height),
542                                                          Image.ANTIALIAS)
543
544            self.photo_image = ImageTk.PhotoImage(resized_image)
545
546            if self.photo_image_id is not None:
547                self.canvas.delete(self.photo_image_id)
548
549            self.photo_image_id = self.canvas.create_image(
550                (self.image_width - image_width) // 2,
551                (self.image_height - image_height) // 2,
552                image=self.photo_image,
553                anchor=NW)
554
555            self.statusbar.set_image_data(image)
556
557        def set_metadata(self, path, metadata):
558            self.images.selection_clear(0, END)
559            self.images.delete(0, END)
560            self.image_objects = []
561            if metadata is not None:
562                image_list = metadata.images()
563                for image in image_list:
564                    self.images.insert(END, image.type_string())
565                    self.image_objects.append(ImagePair_Tk(path, image))
566
567                if len(image_list) > 0:
568                    self.images.index(0)
569                    self.images.selection_set(0)
570                    self.current_image = self.image_objects[0]
571                    self.set_image(self.current_image)
572                else:
573                    self.set_image(None)
574                    self.statusbar.set_path(path)
575            else:
576                self.set_image(None)
577                self.statusbar.set_path(path)
578
579    def main_tkinter(initial_audiofile=None):
580        root = Tk()
581
582        app = Coverview_Tkinter(root)
583        if initial_audiofile is not None:
584            app.set_metadata(initial_audiofile.filename,
585                             initial_audiofile.get_metadata())
586
587        root.mainloop()
588
589    TKINTER_AVAILABLE = True
590except ImportError:
591    TKINTER_AVAILABLE = False
592
593if (__name__ == '__main__'):
594    import argparse
595
596    parser = argparse.ArgumentParser(description=_.DESCRIPTION_COVERVIEW)
597
598    parser.add_argument("--version",
599                        action="version",
600                        version="Python Audio Tools %s" % (audiotools.VERSION))
601
602    if GTK_AVAILABLE:
603        parser.add_argument('--no-gtk',
604                            dest="no_gtk",
605                            action='store_true',
606                            default=False,
607                            help=_.OPT_NO_GTK)
608
609    if TKINTER_AVAILABLE:
610        parser.add_argument('--no-tkinter',
611                            dest="no_tkinter",
612                            action='store_true',
613                            default=False,
614                            help=_.OPT_NO_TKINTER)
615
616    parser.add_argument("filenames",
617                        metavar="FILENAME",
618                        nargs="*",
619                        help=_.OPT_INPUT_FILENAME)
620
621    options = parser.parse_args()
622
623    args = options.filenames
624
625    messenger = audiotools.Messenger()
626
627    use_gtk = (hasattr(options, "no_gtk") and not options.no_gtk)
628    use_tkinter = (hasattr(options, "no_tkinter") and not options.no_tkinter)
629
630    if use_gtk:
631        if len(args) > 0:
632            try:
633                main_gtk(audiotools.open(args[0]))
634            except audiotools.UnsupportedFile:
635                messenger.error(_.ERR_UNSUPPORTED_FILE %
636                                (audiotools.Filename(args[0]),))
637                sys.exit(1)
638            except audiotools.InvalidFile:
639                messenger.error(_.ERR_INVALID_FILE %
640                                (audiotools.Filename(args[0]),))
641                sys.exit(1)
642            except IOError:
643                messenger.error(_.ERR_OPEN_IOERROR %
644                                (audiotools.Filename(args[0]),))
645                sys.exit(1)
646        else:
647            main_gtk(None)
648    elif use_tkinter:
649        if len(args) > 0:
650            try:
651                main_tkinter(audiotools.open(args[0]))
652            except audiotools.UnsupportedFile:
653                messenger.error(_.ERR_UNSUPPORTED_FILE %
654                                (audiotools.Filename(args[0]),))
655                sys.exit(1)
656            except audiotools.InvalidFile:
657                messenger.error(_.ERR_INVALID_FILE %
658                                (audiotools.Filename(args[0]),))
659                sys.exit(1)
660            except IOError:
661                messenger.error(_.ERR_OPEN_IOERROR %
662                                (audiotools.Filename(args[0]),))
663                sys.exit(1)
664        else:
665            main_tkinter(None)
666    else:
667        messenger.error(_.ERR_NO_GUI)
668        sys.exit(1)
669