#!/bin/env python # -*- coding: utf-8 -*- import sys,os from PIL import Image import ftplib import getopt from elementtree import ElementTree import unicodedata import ConfigParser class Options: def __init__(self, argv, usage = "", app_name = None ): self.options = {} self.argv = argv if app_name: self.app_name = app_name else: # si aucun nom n'est précisé, on l'extrait de l'argv, mais sans l'extension self.app_name = os.path.splitext( os.path.basename( argv[0] ) )[0] # Message à afficher en cas de mauvaise utilisation des options usage += "\nUsage: %s [OPTIONS] arguments" % self.app_name self.usage = usage def __configure_file( self, filename ): # prend les valeurs indiquées dans le fichier de configuration config = ConfigParser.ConfigParser() try: config.readfp( open( filename ) ) except: return # Pour chaque option prévue for o in self.options: try: # essaye de la lire dans le fichier de conf a = config.get('default',o) # si c'est un flag if self.options[o]['flag'] == True: # il faut convertir en booléen self.options[o]['value'] = bool( eval( a ) ) else: # sinon c'est un string self.options[o]['value'] = a # on ajoute que ce fichier de conf à changer la valeur self.options[o]['origin'] = filename except: pass def __configure_command( self ): # construction de la chaine argument pour getopt gos_short = '' gos_long = '' for o in self.options: opt = self.options[o] # getopt demande deux chaines pour les typographies longues et courtes gos_short += opt['short'] gos_long += opt['long'] # si l'option prend un paramètre if not opt['flag']: gos_short += ':' gos_long += '=' # parse la ligne de commande try: opts, args = getopt.getopt( self.argv[1:], gos_short, gos_long ) except getopt.GetoptError: print 'Unkown option' self.print_usage() sys.exit(2) s2l = {} # associations court:long for o in self.options: short = self.options[o]['short'] # lève une erreur si l'option courte a déjà été déclarée if short in s2l: raise "Short option '%s' already declared for the options '%s', please use another letter for the option '%s'." % (short, s2l[short], o ) s2l[ self.options[o]['short'] ] = self.options[o]['long'] # prend les valeurs indiquées sur la ligne de commande for o,a in opts: # court => on enlève un tiret os = o[1:] # long => on enlève deux tirets ol = o[2:] # si c'est une option courte if os in s2l: # si c'est un flag if self.options[ s2l[os] ]['flag']: # demandé => vrai self.options[ s2l[os] ]['value'] = True else: # prend la valeur indiquée self.options[ s2l[os] ]['value'] = a self.options[ s2l[os] ]['origin'] = 'command line' # si c'est une option longue elif ol in self.options: if self.options[ol]['flag']: self.options[ ol ]['value'] = True else: self.options[ol]['value'] = a self.options[ol]['origin'] = 'command line' # retourne tout ce qui n'a pas été parsé return args def parse(self): # on essaye d'abord le fichier de conf général self.__configure_file( '%s.conf' % self.app_name ) # puis on essaye le fichier de conf utilisateur self.__configure_file( os.path.join( os.path.expanduser('~'), '.%s.conf' % self.app_name ) ) # enfin, la ligne de commande args = self.__configure_command() return args def add( self, short, long, description, default='' ): flag = False if default==True or default==False: flag = True # si l'option est déjà présente if long in self.options: raise "Long option '%s' already declared, please use another one." % long else: self.options [ long ] = { 'short':short, # identifiant court (une lettre) 'long':long, # identifiant long 'description':description, # texte de description 'origin':'hard coded', # source de la valeur 'flag':flag, # indicateur de flag 'value':default } # valeur de l'option def print_usage(self): print self.usage for o in self.options: fs = "\t-%s, --%s\t\t%s" # si pas un flag, indique qu'il faut un paramètre if not self.options[o]['flag']: fs = "\t-%s, --%s\t=VAL\t%s" print fs % ( self.options[o]['short'], self.options[o]['long'], self.options[o]['description'] ) def print_state(self): print "Options settings:" for o in self.options: print "\t%s='%s' (%s)" % ( self.options[o]['long'], self.options[o]['value'], self.options[o]['origin'] ) def get( self, long ): return self.options[long]['value'] # # XPath-friendlier ElementTree namespace helper # http://infix.se/2007/02/21/xpath-friendlier-elementtree-namespace-helper # class NS: def __init__(self, uri): self.uri = '{'+uri+'}' def __getattr__(self, tag): return self.uri + tag def __call__(self, path): return "/".join((tag not in ("", ".", "*")) and getattr(self, tag) or tag for tag in path.split("/")) class Stripit: def __init__( self, verbose=False ): self.verbose = verbose # # wrapper around PIL 1.1.6 Image.save to preserve PNG metadata # # public domain, Nick Galbreath # http://blog.modp.com/2007/08/python-pil-and-png-metadata-take-2.html # def pngsave(self, im, file): # these can be automatically added to Image.info dict # they are not user-added metadata reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect') # undocumented class from PIL import PngImagePlugin meta = PngImagePlugin.PngInfo() # copy metadata into new object for k,v in im.info.iteritems(): if k in reserved: continue meta.add_text(k, v, 0) # and save im.save(file, "PNG", pnginfo=meta) def export( self, file_we, options='', max_size=None ): """file name only, without extension""" if self.verbose: print '\tCreating the PNG from the SVG...', sys.stdout.flush() size_arg='' if max_size: w = os.popen( 'inkscape --query-width %s.svg' % (file_we) ) h = os.popen( 'inkscape --query-height %s.svg' % (file_we) ) width = float(w.read()) height = float(h.read()) ratio = height/width max_size = float( max_size ) w.close() h.close() if height > width: size_arg += ' --export-height=%f' % (max_size) size_arg += ' --export-width=%f' % (max_size / ratio) else: size_arg += ' --export-height=%f' % (max_size * ratio) size_arg += ' --export-width=%f' % (max_size) cmd = 'inkscape -z %s --export-png %s.png %s.svg ' % (size_arg, file_we,file_we) cmd += options #if self.verbose: # print ' " %s " ' % (cmd) # sys.stdout.flush() os.popen( cmd ) if self.verbose: print '\tok' def xfind( self, tree, ns ): # on ne veut passer qu'un liste d'élements q = '//' + '/'.join( ns ) # requête sur le xml res = unicode( tree.findtext( q ) ) # la norme PNG demande de l'ASCII, on essaye de convertir au mieux return unicodedata.normalize('NFKD', res ).encode('ASCII', 'ignore') def xfind_attribute( self, tree, ns ): # pour une raison qui m'échappe, ElementTree ne considère pas les attributs comme des sous-éléments de chaque noeud # cette fonction est donc un hack pour pallier le problème # les premiers éléments sont considérés comme des noeuds de la requête xpath q = '//' + '/'.join( ns[0:-1] ) # le dernier est l'attribut attribute = ns[-1] # on récupère l'objet élément el = tree.find( q) # les attributs sont récupérables dans une liste de tuples (!) # on convertit donc en dictionnaire, plus logique # TODO vérifier (quand même) s'il ne peut pas y avoir plusieurs attributs identiques attr = dict( el.items() ) # ce qui permet de récupérer le contenu directement avec l'identifiant res = unicode( attr[attribute] ) return unicodedata.normalize('NFKD', res ).encode('ASCII', 'ignore') def get_svg_metadata( self, file_we ): if self.verbose: print '\tGet SVG metadata...', sys.stdout.flush() # raccourcis pour les namespaces DC = NS('http://purl.org/dc/elements/1.1/') CC = NS('http://web.resource.org/cc/') RDF = NS('http://www.w3.org/1999/02/22-rdf-syntax-ns#') SVG = NS('http://www.w3.org/2000/svg') tree = ElementTree.parse( file_we + '.svg') # PNG metadata textual informations : # (from http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.Anc-text ) # # Title Short (one line) title or caption for image # Author Name of image's creator # Description Description of image (possibly long) # Copyright Copyright notice # Creation Time Time of original image creation # Software Software used to create the image # Disclaimer Legal disclaimer # Warning Warning of nature of content # Source Device used to create the image # Comment Miscellaneous comment; conversion from # GIF comment metadata = {} metadata['Author'] = self.xfind( tree, [ CC('Agent'), DC('title') ] ) metadata['Description'] = self.xfind( tree, [ CC('Work'), DC('description') ]) metadata['Copyright'] = self.xfind_attribute( tree, [ CC('Work'),CC('license'),RDF('resource') ] ) metadata['Creation Time'] = self.xfind( tree, [ CC('Work'), DC('date') ] ) metadata['Software'] = 'www.inkscape.org / stripit.sourceforge.net' # FIXME pas très élégant metadata['Disclaimer'] ='' metadata['Warning'] = '' metadata['Source'] = self.xfind( tree, [ CC('Work'), DC('source') ]) lang = self.xfind( tree, [ CC('Work'), DC('language') ]) metadata['Comment'] = 'Language: %s' % lang if self.verbose: print '\t\t\tok' return metadata def save_png_with_metadata( self, file_we, metadata ): if self.verbose: print '\tWrite metadata in the PNG...\t', sys.stdout.flush() Image.init() im = Image.open( file_we+'.png' ) im.info = metadata #print im.info self.pngsave( im, file_we+'.png' ) if self.verbose: print '\tok' def upload( self, file_we, host, user, dir, password ): if self.verbose: print '\tUpload of %s on %s.\t' % (file_we, host), sys.stdout.flush() ftp = ftplib.FTP( host, user, password ) ftp.cwd( dir ) if self.verbose: print '.', sys.stdout.flush() f_png = open( '%s.png' % file_we ) ftp.storbinary( 'STOR %s.png' % file_we, f_png ) f_png.close() if self.verbose: print '.', sys.stdout.flush() f_svg = open( '%s.svg' % file_we ) ftp.storbinary( 'STOR %s.svg' % file_we, f_svg ) f_svg.close() ftp.quit() if self.verbose: print '\ŧok' if __name__=="__main__": usage = """Aide à l'export et au téléchargement pour StripIt. \tCe script va exporter un ou plusieurs fichiers SVG au format PNG, en préservant les métadonnées ; \tpuis télécharger le tout sur un serveur FTP.""" oo = Options( sys.argv, usage ) oo.add( 'H', 'host', 'Serveur FTP', '' ) oo.add( 'u', 'user', 'Login FTP', 'anonymous' ) oo.add( 'd', 'dir', 'Dossier FTP', 'strips' ) oo.add( 'p', 'port', 'Port FTP', '21' ) oo.add( 'P', 'pass', 'Mot de passe FTP', '' ) oo.add( 'x', 'no-export', "Pas d'export PNG", False ) oo.add( 'n', 'no-upload', 'Pas de téléchargement FTP', False ) oo.add( 'v', 'verbose', "Afficher plus d'informations", False ) oo.add( 's', 'supp', 'Options supplémentaires pour inkscape', '' ) oo.add( 'h', 'help', "Ce message d'aide", False ) oo.add( 'z', 'size', "Limiter la taille du PNG", False ) oo.add( 'm', 'max-size', 'Taille maximale en hauteur ou en largeur, en pixels', '800') args = oo.parse() if oo.get('verbose'): oo.print_state() if oo.get('help'): oo.print_usage() sys.exit() while args: f = args.pop() # supprime l'extension f = os.path.splitext( f ) [0] if oo.get('verbose'): print "Processing %s:" % os.path.basename( f ) si = Stripit( oo.get('verbose') ) if not oo.get('no-export'): if oo.get('size'): si.export( f, oo.get('supp'), oo.get('max-size') ) else: si.export( f, options=oo.get('supp') ) md = si.get_svg_metadata( f ) si.save_png_with_metadata( f, md ) if not oo.get('no-upload'): si.upload( f, oo.get('host'), oo.get('user'), oo.get('dir'), oo.get('pass') )