#!/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')
			 )

