442 lines
13 KiB
Python
Executable file
442 lines
13 KiB
Python
Executable file
#!/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')
|
|
)
|
|
|