Modification du fichier stripit.py afin de n'avoir que des tabulations (et pas un mix entre tabulation et espace) Suppression du todo concernant le namespace des creative commons dans la classe Strip
455 lines
13 KiB
Python
Executable file
455 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'
|
|
|
|
# generation de la miniature pour la galerie
|
|
if self.verbose:
|
|
print '\tCreating the thumbnail PNG from the SVG...',
|
|
sys.stdout.flush()
|
|
|
|
cmd = 'inkscape -z --export-width=345 --export-png %s.thumb.png %s.svg ' % (file_we,file_we)
|
|
cmd += options
|
|
|
|
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')
|
|
)
|
|
|