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