2 # -*- coding: utf-8 -*-
4 # (c) Copyright 2003-2009 Hewlett-Packard Development Company, L.P.
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.
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 __title__ = 'Photo Card Access Utility'
26 __doc__ = "Access inserted photo cards on supported HPLIP printers. This provides an alternative for older devices that do not support USB mass storage or for access to photo cards over a network."
47 from base import device, utils, tui, module
49 from pcard import photocard
52 # Console class (from ASPN Python Cookbook)
53 # Author: James Thiele
56 # Location: http://www.eskimo.com/~jet/python/examples/cmd/
57 # Copyright (c) 2004, James Thiele
59 class Console(cmd.Cmd):
61 def __init__(self, pc):
62 cmd.Cmd.__init__(self)
63 self.intro = "Type 'help' for a list of commands. Type 'exit' to quit."
65 disk_info = self.pc.info()
66 pc.write_protect = disk_info[8]
68 log.warning("Photo card is write protected.")
69 self.prompt = log.bold("pcard: %s > " % self.pc.pwd())
72 def do_hist(self, args):
73 """Print a list of commands that have been entered"""
76 def do_exit(self, args):
77 """Exits from the console"""
80 def do_quit(self, args):
81 """Exits from the console"""
84 # Command definitions to support Cmd object functionality
85 def do_EOF(self, args):
86 """Exit on system end of file character"""
87 return self.do_exit(args)
89 def do_help(self, args):
90 """Get help on commands
91 'help' or '?' with no arguments prints a list of commands for which help is available
92 'help <command>' or '? <command>' gives help on <command>
94 # The only reason to define this method is for the help text in the doc string
95 cmd.Cmd.do_help(self, args)
97 # Override methods in Cmd object
99 """Initialization before prompting user for commands.
100 Despite the claims in the Cmd documentaion, Cmd.preloop() is not a stub.
102 cmd.Cmd.preloop(self) # sets up command completion
103 self._hist = [] # No history yet
104 self._locals = {} # Initialize execution namespace for user
108 """Take care of any unfinished business.
109 Despite the claims in the Cmd documentaion, Cmd.postloop() is not a stub.
111 cmd.Cmd.postloop(self) # Clean up command completion
114 def precmd(self, line):
115 """ This method is called after the line has been input but before
116 it has been interpreted. If you want to modifdy the input line
117 before execution (for example, variable substitution) do it here.
119 self._hist += [line.strip()]
122 def postcmd(self, stop, line):
123 """If you want to stop the console, return something that evaluates to true.
124 If you want to do some post command processing, do it here.
129 """Do nothing on empty input line"""
132 def default(self, line):
133 print log.bold("ERROR: Unrecognized command. Use 'help' to list commands.")
135 def do_ldir(self, args):
136 """ List local directory contents."""
139 def do_lls(self, args):
140 """ List local directory contents."""
143 def do_dir(self, args):
144 """Synonym for the ls command."""
145 return self.do_ls(args)
147 def do_ls(self, args):
148 """List photo card directory contents."""
149 args = args.strip().lower()
150 files = self.pc.ls(True, args)
153 formatter = utils.TextFormatter(
155 {'width': 14, 'margin' : 2},
156 {'width': 12, 'margin' : 2, 'alignment' : utils.TextFormatter.RIGHT},
157 {'width': 30, 'margin' : 2},
162 print log.bold(formatter.compose(("Name", "Size", "Type")))
165 for d in self.pc.current_directories():
166 if d[0] in ('.', '..'):
167 print formatter.compose((d[0], "", "directory"))
169 print formatter.compose((d[0] + "/", "", "directory"))
171 for f in self.pc.current_files():
172 print formatter.compose((f[0], utils.format_bytes(f[2]), self.pc.classify_file(f[0])))
176 print log.bold("% d files, %s" % (num_files, utils.format_bytes(total_size, True)))
179 def do_df(self, args):
180 """Display free space on photo card.
182 -h\tDisplay in human readable format
184 freespace = self.pc.df()
186 if args.strip().lower() == '-h':
187 fs = utils.format_bytes(freespace)
189 fs = utils.commafy(freespace)
191 print "Freespace = %s Bytes" % fs
194 def do_cp(self, args, remove_after_copy=False):
195 """Copy files from photo card to current local directory.
197 \tcp FILENAME(S)|GLOB PATTERN(S)
199 \tCopy all JPEG and GIF files and a file named thumbs.db from photo card to local directory:
200 \tcp *.jpg *.gif thumbs.db
202 args = args.strip().lower()
204 matched_files = self.pc.match_files(args)
206 if len(matched_files) == 0:
207 print "ERROR: File(s) not found."
209 total, delta = self.pc.cp_multiple(matched_files, remove_after_copy, self.cp_status_callback, self.rm_status_callback)
211 print log.bold("\n%s transfered in %d sec (%d KB/sec)" % (utils.format_bytes(total), delta, (total/1024)/(delta)))
213 def do_unload(self, args):
214 """Unload all image files from photocard to current local directory.
216 \tSubdirectories on photo card are not preserved
218 -x\tDon't remove files after copy
219 -p\tPrint unload list but do not copy or remove files"""
220 args = args.lower().strip().split()
223 if self.pc.write_protect:
224 log.error("Photo card is write protected. -x not allowed.")
230 unload_list = self.pc.get_unload_list()
233 if len(unload_list) > 0:
237 for u in unload_list:
238 max_len = max(max_len, len(u[0]))
240 formatter = utils.TextFormatter(
242 {'width': max_len+2, 'margin' : 2},
243 {'width': 12, 'margin' : 2, 'alignment' : utils.TextFormatter.RIGHT},
244 {'width': 12, 'margin' : 2},
249 print log.bold(formatter.compose(("Name", "Size", "Type")))
252 for u in unload_list:
253 print formatter.compose(('%s' % u[0], utils.format_bytes(u[1]), '%s/%s' % (u[2], u[3])))
257 print log.bold("Found %d files to unload, %s" % (len(unload_list), utils.format_bytes(total, True)))
259 print log.bold("Unloading %d files..." % len(unload_list))
260 total, delta, was_cancelled = self.pc.unload(unload_list, self.cp_status_callback, self.rm_status_callback, dont_remove)
261 print log.bold("\n%s unloaded in %d sec (%d KB/sec)" % (utils.format_bytes(total), delta, (total/1024)/delta))
264 print "No image, audio, or video files found."
267 def cp_status_callback(self, src, trg, size):
270 print log.bold("Copying %s..." % src)
272 print "\nCopied %s to %s (%s)..." % (src, trg, utils.format_bytes(size))
274 def rm_status_callback(self, src):
275 print "Removing %s..." % src
279 def do_rm(self, args):
280 """Remove files from photo card."""
281 if self.pc.write_protect:
282 log.error("Photo card is write protected. rm not allowed.")
285 args = args.strip().lower()
287 matched_files = self.pc.match_files(args)
289 if len(matched_files) == 0:
290 print "ERROR: File(s) not found."
292 for f in matched_files:
297 def do_mv(self, args):
298 """Move files off photocard"""
299 if self.pc.write_protect:
300 log.error("Photo card is write protected. mv not allowed.")
302 self.do_cp(args, True)
304 def do_lpwd(self, args):
305 """Print name of local current/working directory."""
308 def do_lcd(self, args):
309 """Change current local working directory."""
311 os.chdir(args.strip())
313 print log.bold("ERROR: Directory not found.")
316 def do_pwd(self, args):
317 """Print name of photo card current/working directory
322 def do_cd(self, args):
323 """Change current working directory on photo card.
325 \tYou may only specify one directory level at a time.
329 args = args.lower().strip()
332 if self.pc.pwd() != '/':
342 matched_dirs = self.pc.match_dirs(args)
344 if len(matched_dirs) == 0:
345 print "Directory not found"
347 elif len(matched_dirs) > 1:
348 print "Pattern matches more than one directory"
351 self.pc.cd(matched_dirs[0])
353 self.prompt = log.bold("pcard: %s > " % self.pc.pwd())
355 def do_cdup(self, args):
356 """Change to parent directory."""
359 #def complete_cd( self, text, line, begidx, endidx ):
360 # print text, line, begidx, endidx
363 def do_cache(self, args):
364 """Display current cache entries, or turn cache on/off.
368 \tTurn off: cache off
370 args = args.strip().lower()
373 self.pc.cache_control(True)
376 self.pc.cache_control(False)
379 if self.pc.cache_state():
380 cache_info = self.pc.cache_info()
382 t = cache_info.keys()
386 print "sector %d (%d hits)" % (s, cache_info[s])
388 print log.bold("Total cache usage: %s (%s maximum)" % (utils.format_bytes(len(t)*512), utils.format_bytes(photocard.MAX_CACHE * 512)))
389 print log.bold("Total cache sectors: %s of %s" % (utils.commafy(len(t)), utils.commafy(photocard.MAX_CACHE)))
391 print "Cache is off."
393 def do_sector(self, args):
394 """Display sector data.
396 \tsector <sector num>
398 args = args.strip().lower()
403 print "Sector must be specified as a number"
406 if self.pc.cache_check(sector) > 0:
407 print "Cached sector"
409 print repr(self.pc.sector(sector))
412 def do_tree(self, args):
413 """Display photo card directory tree."""
414 tree = self.pc.tree()
416 self.print_tree(tree)
418 def print_tree(self, tree, level=0):
420 if type(tree[d]) == type({}):
421 print ''.join([' '*level*4, d, '/'])
422 self.print_tree(tree[d], level+1)
425 def do_reset(self, args):
426 """Reset the cache."""
427 self.pc.cache_reset()
430 def do_card(self, args):
431 """Print info about photocard."""
433 print "Device URI = %s" % self.pc.device.device_uri
434 print "Model = %s" % self.pc.device.model_ui
435 print "Working dir = %s" % self.pc.pwd()
436 disk_info = self.pc.info()
437 print "OEM ID = %s" % disk_info[0]
438 print "Bytes/sector = %d" % disk_info[1]
439 print "Sectors/cluster = %d" % disk_info[2]
440 print "Reserved sectors = %d" % disk_info[3]
441 print "Root entries = %d" % disk_info[4]
442 print "Sectors/FAT = %d" % disk_info[5]
443 print "Volume label = %s" % disk_info[6]
444 print "System ID = %s" % disk_info[7]
445 print "Write protected = %d" % disk_info[8]
446 print "Cached sectors = %s" % utils.commafy(len(self.pc.cache_info()))
449 def do_display(self, args):
450 """Display an image with ImageMagick.
452 \tdisplay <filename>"""
453 args = args.strip().lower()
454 matched_files = self.pc.match_files(args)
456 if len(matched_files) == 1:
458 typ = self.pc.classify_file(args).split('/')[0]
461 fd, temp_name = utils.make_temp_file()
462 self.pc.cp(args, temp_name)
463 os.system('display %s' % temp_name)
467 print "File is not an image."
469 elif len(matched_files) == 0:
470 print "File not found."
473 print "Only one file at a time may be specified for display."
475 def do_show(self, args):
476 """Synonym for the display command."""
477 self.do_display(args)
479 def do_thumbnail(self, args):
480 """Display an embedded thumbnail image with ImageMagick.
482 \tOnly works with JPEG/JFIF images with embedded JPEG/TIFF thumbnails
484 \tthumbnail <filename>"""
485 args = args.strip().lower()
486 matched_files = self.pc.match_files(args)
488 if len(matched_files) == 1:
489 typ, subtyp = self.pc.classify_file(args).split('/')
491 if typ == 'image' and subtyp in ('jpeg', 'tiff'):
492 exif_info = self.pc.get_exif(args)
494 dir_name, file_name=os.path.split(args)
495 photo_name, photo_ext=os.path.splitext(args)
497 if 'JPEGThumbnail' in exif_info:
498 temp_file_fd, temp_file_name = utils.make_temp_file()
499 open(temp_file_name, 'wb').write(exif_info['JPEGThumbnail'])
500 os.system('display %s' % temp_file_name)
501 os.remove(temp_file_name)
503 elif 'TIFFThumbnail' in exif_info:
504 temp_file_fd, temp_file_name = utils.make_temp_file()
505 open(temp_file_name, 'wb').write(exif_info['TIFFThumbnail'])
506 os.system('display %s' % temp_file_name)
507 os.remove(temp_file_name)
510 print "No thumbnail found."
513 print "Incorrect file type for thumbnail."
515 elif len(matched_files) == 0:
516 print "File not found."
518 print "Only one file at a time may be specified for thumbnail display."
520 def do_thumb(self, args):
521 """Synonym for the thumbnail command."""
522 self.do_thumbnail(args)
524 def do_exif(self, args):
525 """Display EXIF info for file.
528 args = args.strip().lower()
529 matched_files = self.pc.match_files(args)
531 if len(matched_files) == 1:
532 typ, subtyp = self.pc.classify_file(args).split('/')
533 #print "'%s' '%s'" % (typ, subtyp)
535 if typ == 'image' and subtyp in ('jpeg', 'tiff'):
536 exif_info = self.pc.get_exif(args)
538 formatter = utils.TextFormatter(
540 {'width': 40, 'margin' : 2},
541 {'width': 40, 'margin' : 2},
546 print log.bold(formatter.compose(("Tag", "Value")))
548 ee = exif_info.keys()
551 if e not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename'):
552 #if e != 'EXIF MakerNote':
553 print formatter.compose((e, '%s' % exif_info[e]))
555 # print formatter.compose( ( e, ''.join( [ chr(x) for x in exif_info[e].values if chr(x) in string.printable ] ) ) )
557 print "Incorrect file type for thumbnail."
559 elif len(matched_files) == 0:
560 print "File not found."
562 print "Only one file at a time may be specified for thumbnail display."
564 def do_info(self, args):
565 """Synonym for the exif command."""
568 def do_about(self, args):
569 utils.log_title(__title__, __version__)
572 def status_callback(src, trg, size):
575 print log.bold("Copying %s..." % src)
577 print "\nCopied %s to %s (%s)..." % (src, trg, utils.format_bytes(size))
581 mod = module.Module(__mod__, __title__, __version__, __doc__, None,
582 (GUI_MODE, INTERACTIVE_MODE, NON_INTERACTIVE_MODE),
585 mod.setUsage(module.USAGE_FLAG_DEVICE_ARGS,
586 extra_options=[("Output directory:", "-o<dir> or --output=<dir> (Defaults to current directory)(Only used for non-GUI modes)", "option", False)],
587 see_also_list=['hp-toolbox'])
589 opts, device_uri, printer_name, mode, ui_toolkit, loc = \
590 mod.parseStdOpts('o', ['output='])
592 output_dir = os.getcwd()
595 if o in ('-o', '--output'):
599 if not utils.canEnterGUIMode():
600 mode = INTERACTIVE_MODE
603 if ui_toolkit == 'qt4':
604 log.error("%s does not support Qt4. Please use Qt3 or run in -i or -n modes.")
607 if mode in (INTERACTIVE_MODE, NON_INTERACTIVE_MODE):
609 device_uri = mod.getDeviceUri(device_uri, printer_name,
610 filter={'pcard-type' : (operator.eq, 1)})
613 pc = photocard.PhotoCard( None, device_uri, printer_name )
615 log.error("Unable to start photocard session: %s" % e.msg)
618 pc.set_callback(update_spinner)
623 log.error("Unable to mount photo card on device. Check that device is powered on and photo card is correctly inserted.")
626 #pc.device.sendEvent(EVENT_PCARD_UNABLE_TO_MOUNT, typ='error')
629 log.info(log.bold("\nPhotocard on device %s mounted" % pc.device.device_uri))
630 log.info(log.bold("DO NOT REMOVE PHOTO CARD UNTIL YOU EXIT THIS PROGRAM"))
632 output_dir = os.path.realpath(os.path.normpath(os.path.expanduser(output_dir)))
637 print log.bold("ERROR: Output directory %s not found." % output_dir)
641 if mode == INTERACTIVE_MODE: # INTERACTIVE_MODE
642 console = Console(pc)
646 except KeyboardInterrupt:
647 log.error("Aborted.")
649 log.error("An error occured: %s" % e)
654 #pc.device.sendEvent(EVENT_END_PCARD_JOB)
657 else: # NON_INTERACTIVE_MODE
658 print "Output directory is %s" % os.getcwd()
660 unload_list = pc.get_unload_list()
663 if len(unload_list) > 0:
666 for u in unload_list:
667 max_len = max(max_len, len(u[0]))
669 formatter = utils.TextFormatter(
671 {'width': max_len+2, 'margin' : 2},
672 {'width': 12, 'margin' : 2, 'alignment' : utils.TextFormatter.RIGHT},
673 {'width': 12, 'margin' : 2},
678 print log.bold(formatter.compose(("Name", "Size", "Type")))
681 for u in unload_list:
682 print formatter.compose(('%s' % u[0], utils.format_bytes(u[1]), '%s/%s' % (u[2], u[3])))
686 print log.bold("Found %d files to unload, %s\n" % (len(unload_list), utils.format_bytes(total, True)))
687 print log.bold("Unloading files...\n")
688 total, delta, was_cancelled = pc.unload(unload_list, status_callback, None, True)
689 print log.bold("\n%s unloaded in %d sec (%d KB/sec)" % (utils.format_bytes(total), delta, (total/1024)/delta))
695 except KeyboardInterrupt:
696 log.error("User exit")
699 else: # GUI_MODE (qt3 only)
702 from ui import unloadform
704 log.error("Unable to load Qt3 support. Is it installed?")
707 app = QApplication(sys.argv)
708 QObject.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()"))
711 loc = user_conf.get('ui', 'loc', 'system')
712 if loc.lower() == 'system':
713 loc = str(QTextCodec.locale())
714 log.debug("Using system locale: %s" % loc)
716 if loc.lower() != 'c':
719 l, x = loc.split('.')
720 loc = '.'.join([l, e])
723 loc = '.'.join([loc, e])
725 log.debug("Trying to load .qm file for %s locale." % loc)
726 trans = QTranslator(None)
728 qm_file = 'hplip_%s.qm' % l
729 log.debug("Name of .qm file: %s" % qm_file)
730 loaded = trans.load(qm_file, prop.localization_dir)
733 app.installTranslator(trans)
738 log.debug("Using default 'C' locale")
740 log.debug("Using locale: %s" % loc)
741 QLocale.setDefault(QLocale(loc))
744 locale.setlocale(locale.LC_ALL, locale.normalize(loc))
749 w = unloadform.UnloadForm(['cups'], device_uri, printer_name)
751 log.error("Unable to connect to HPLIP I/O. Please (re)start HPLIP and try again.")