<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>®om&#039;s blog &#187; scripts</title>
	<atom:link href="http://blog.rom1v.com/tag/scripts/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.rom1v.com</link>
	<description>Un blog libre</description>
	<lastBuildDate>Thu, 02 Feb 2012 20:03:59 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Résoudre le cube-serpent en Python</title>
		<link>http://blog.rom1v.com/2011/09/resoudre-le-cube-serpent-en-python/</link>
		<comments>http://blog.rom1v.com/2011/09/resoudre-le-cube-serpent-en-python/#comments</comments>
		<pubDate>Tue, 27 Sep 2011 21:10:34 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Astuces]]></category>
		<category><![CDATA[Outils]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[développement]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[scripts]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/?p=2954</guid>
		<description><![CDATA[Je me suis amusé à écrire un petit programme en Python qui résout le cube-serpent (ainsi nous pouvons dire qu&#8217;un serpent en résout un autre). Mon but était surtout d&#8217;apprendre le langage Python, avec un problème intéressant, pas trop compliqué (c&#8217;est de la force brute). Il m&#8217;a permis de découvrir différents aspects de Python. Je [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://blog.rom1v.com/wp-content/uploads/2011/09/cube-serpent.jpg"><img src="http://blog.rom1v.com/wp-content/uploads/2011/09/cube-serpent.jpg" alt="" title="cube-serpent" width="218" height="191" class="alignright size-full wp-image-2964" /></a></p>
<p>Je me suis amusé à écrire un petit programme en <a href="http://fr.wikipedia.org/wiki/Python_%28langage%29">Python</a> qui résout le <a href="http://fr.wikipedia.org/wiki/Cube_serpent">cube-serpent</a> (ainsi nous pouvons dire qu&#8217;un serpent en résout un autre).<br />
Mon but était surtout d&#8217;apprendre le langage <em>Python</em>, avec un problème intéressant, pas trop compliqué (c&#8217;est de la <a href="http://fr.wikipedia.org/wiki/Recherche_par_force_brute">force brute</a>). Il m&#8217;a permis de découvrir différents aspects de <em>Python</em>.<br />
<ins datetime="2011-10-19T00:00:00+01:00">Je l&#8217;ai également <a href="http://blog.rom1v.com/2011/10/resoudre-le-cube-serpent-300-fois-plus-rapidement-en-c/">implémenté en C</a>.</ins></p>
<p>L&#8217;algorithme proposé résout un cube-serpent généralisé. En effet, il sait trouver des solutions pour obtenir un cube de 3×3×3, mais également d&#8217;autres tailles, comme 2×2×2 ou 4×4×4. Il sait également résoudre des volumes non cubiques, comme 2×3×4. Et pour être totalement générique, il fonctionne pour un nombre quelconque de dimensions (2×2, 3×5×4×2, 2×3×2×2×4). Comme ça, vous saurez replier un serpent en <a href="http://fr.wikipedia.org/wiki/Hypercube">hypercube</a>…</p>
<p>Je vais d&#8217;abord présenter le problème et décrire l&#8217;algorithme de résolution et survoler l&#8217;implémentation, puis je vais m&#8217;attarder sur certaines fonctionnalités de <em>Python</em> qui m&#8217;ont semblé très intéressantes.</p>
<h3>Résolution</h3>
<h4>Problème</h4>
<p>Le but est de replier la structure du serpent pour qu&#8217;elle forme un volume, par exemple un cube.</p>
<p>La structure peut être vue comme une liste de vecteurs orthogonaux consécutifs, ayant chacun une norme (une longueur). Elle peut donc être caractérisée par la liste des normes de ces vecteurs. Ainsi, la structure du serpent présenté sur <a href="http://fr.wikipedia.org/wiki/Fichier:Snakecube_1.jpg">Wikipedia</a> est la suivante&nbsp;:</p>
<pre>[2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2]</pre>
<p>Le volume cible peut être représenté par un graphe, un ensemble de sommets reliés par des arêtes, auquel on ajoute la notion d&#8217;orientation dans l&#8217;espace (il est important de distinguer les arêtes orthogonales entre elles). En clair, chaque sommet représente une position dans le cube&nbsp;: il y a donc 27 sommets pour un cube 3×3×3.</p>
<p>L&#8217;objectif est de trouver un <a href="http://fr.wikipedia.org/wiki/Graphe_hamiltonien#Chemin_hamiltonien">chemin hamiltonien</a> (un chemin qui passe par tous les sommets du graphe une fois et une seule) qui respecte la contrainte de la structure du serpent.</p>
<h4>Principe</h4>
<p>Pour trouver les solutions, il suffit de partir d&#8217;un sommet et tenter de placer les vecteurs de la structure consécutivement un à un, en respectant trois contraintes&nbsp;:</p>
<ul>
<li>rester dans le volume (évidemment)&nbsp;;</li>
<li>le n<sup>ième</sup> vecteur doit être orthogonal au (n-1)<sup>ième</sup> vecteur (cette règle ne s&#8217;applique pas pour le tout premier vecteur)&nbsp;;</li>
<li>le vecteur &laquo;&nbsp;placé&nbsp;&raquo; dans le cube ne doit pas passer par une position déjà occupée (physiquement, il n&#8217;est pas possible de faire passer une partie du serpent à travers une autre).</li>
</ul>
<p>Il faut donc placer récursivement tous les vecteurs possibles, c&#8217;est-à-dire tous les vecteurs orthogonaux au précédent, qui ne sortent pas du cube et qui ne passent pas par une position déjà occupée. Jusqu&#8217;à arriver soit à une impossibilité (plus aucun vecteur ne respecte ces 3 contraintes), soit à une solution (tous les vecteurs sont placés).</p>
<p>Pour ne manquer aucune solution, il faut répéter cet algorithme en démarrant avec chacun des points de départ (donc les 27 sommets pour un cube 3×3×3).</p>
<h4>Limites</h4>
<p>Cet algorithme ne détecte pas les symétries ni les rotations, il donne alors plusieurs solutions &laquo;&nbsp;identiques&nbsp;&raquo;. Une amélioration serait de les détecter &laquo;&nbsp;au plus tôt&nbsp;&raquo; et de ne pas les construire.</p>
<p><ins datetime="2011-10-01T00:00:00+01:00">La version 0.2 gère les symétries et les rotations, pour éviter de calculer plusieurs solutions identiques. Plus d&#8217;explications dans <a href="#comment-72460">ce commentaire</a> et <a href="#comment-72497">le suivant</a>.</ins></p>
<h3>Implémentation</h3>
<p>Voici une explication succincte des différentes parties du programme (pour plus d&#8217;informations, lire les commentaires dans le code).</p>
<h4>Vector</h4>
<p>Nous avons besoins de vecteurs, mais pas n&#8217;importe lesquels&nbsp;: seulement ceux qui ont une et une seule composante non-nulle, c&#8217;est-à-dire des multiples des vecteurs de la base. En effet, par exemple en 3 dimensions, la direction de chacun des vecteurs sera soit droite-gauche, soit dans haut-bas, soit avant-arrière, mais jamais en diagonale avant-droite vers arrière-gauche.</p>
<p>Ainsi, au lieu de stocker toutes les composantes, le <code>Vector</code> ne contient que la valeur de la composante non-nulle ainsi que sa position (plus facile à manipuler).</p>
<pre>Vector(position, value)</pre>
<h4>VolumeHelper</h4>
<p>Cette classe définit l&#8217;outil que va utiliser le solveur pour noter tout ce qu&#8217;il fait&nbsp;: le chemin emprunté et les sommets déjà visités. À chaque fois qu&#8217;il place un vecteur dans le volume, il &laquo;&nbsp;allume les petites lumières&nbsp;&raquo; associées aux sommets visités, et quand il revient en arrière (pour chercher d&#8217;autres solutions), il éteint ces petites lumières (par <em>lumières</em>, comprenez <em>booléens</em>).</p>
<h4><ins datetime="2011-10-01T00:00:00+01:00">SymmetryHelper</ins></h4>
<p><ins datetime="2011-10-01T00:00:00+01:00">Cette classe a été ajoutée dans la version 0.2.<br />
Elle définit l&#8217;outil que va utiliser le solveur pour n&#8217;explorer que les solutions nécessaires, en ignorant les symétries et les rotations.</ins></p>
<h4>Solver</h4>
<p>Le solveur place récursivement les vecteurs de la structure dans toutes les positions possibles (en s&#8217;aidant du <code>VolumeHelper</code>) afin de trouver toutes les solutions.</p>
<h4>Solutions</h4>
<p>Lors de l&#8217;exécution du script, les solutions s&#8217;affichent au fur et à mesure&nbsp;:</p>
<pre>$ ./snakesolver.py
([0, 0, 0], [2x, y, -x, 2z, y, -2z, x, z, -2y, -2x, y, -z, y, 2z, -2y, 2x, 2y])
([0, 0, 0], [2x, z, -x, 2y, z, -2y, x, y, -2z, -2x, z, -y, z, 2y, -2z, 2x, 2z])
([0, 0, 0], [2y, x, -y, 2z, x, -2z, y, z, -2x, -2y, x, -z, x, 2z, -2x, 2y, 2x])
...</pre>
<p>Considérons la première solution&nbsp;:</p>
<pre>([0, 0, 0], [2x, y, -x, 2z, y, -2z, x, z, -2y, -2x, y, -z, y, 2z, -2y, 2x, 2y])</pre>
<p>Le point de départ est <code>[0, 0, 0]</code>. On se déplace d&#8217;abord de <code>2</code> sur l&#8217;axe <code>x</code>, puis de <code>1</code> sur l&#8217;axe <code>y</code>, de <code>-1</code> sur l&#8217;axe <code>x</code>, etc.<br />
Voici la représentation graphique de cette solution&nbsp;:<br />
<a href="http://blog.rom1v.com/wp-content/uploads/2011/09/solution-cube-serpent.png"><img src="http://blog.rom1v.com/wp-content/uploads/2011/09/solution-cube-serpent-268x300.png" alt="" title="solution-cube-serpent" width="268" height="300" class="aligncenter size-medium wp-image-2972" /></a></p>
<h3>Fonctionnalités de Python</h3>
<p>Maintenant, voici quelques éléments essentiels du langage <em>Python</em> dont je me suis servi pour ce programme.</p>
<h4>Compréhension de liste</h4>
<p>La <a href="http://docs.python.org/tutorial/datastructures.html#list-comprehensions">compréhension de liste</a> (ou <a href="http://fr.wikipedia.org/wiki/G%C3%A9n%C3%A9rateur_%28informatique%29">liste en compréhension</a>) est très pratique. Je l&#8217;ai utilisée plusieurs fois dans l&#8217;algorithme. Je vais détailler deux exemples.</p>
<p>D&#8217;abord, dans la classe <code>VolumeHelper</code>&nbsp;:</p>
<pre>def all_points(self, index=0):
    if index == len(self.dimensions):
        return [[]]
    return <strong>([h] + t for h in xrange(self.dimensions[index])</strong>
            <strong>for t in self.all_points(index + 1))</strong></pre>
<p>La partie mise en gras signifie&nbsp;:</p>
<blockquote><p>tous les éléments de la forme <code>[h] + t</code> (l&#8217;élément <code>h</code> en tête de liste suivie de la queue de la liste) pour <code>h</code> compris entre <code>0</code> et <code>self.dimensions[index]</code> (un entier) et pour tout <code>t</code> compris dans les résultats de l&#8217;appel récursif</p></blockquote>
<p>Ça ne vous éclaire pas&nbsp;? Dit plus simplement&nbsp;:</p>
<blockquote><p>le résultat de la concaténation de chacun des nombres de <code>0</code> à <code>n</code> (avec <code>n = self.dimensions[index]</code>) à chacune des listes fournies par l&#8217;appel récursif</p></blockquote>
<p>En fait, cette fonction fournit tous les points possibles pour les dimensions données.<br />
Par exemple, si <code>dimensions = [2, 2]</code>, alors le résultat sera&nbsp;:</p>
<pre>[[0, 0], [0, 1], [1, 0], [1, 1]]</pre>
<p>Pour <code>dimensions = [2, 2, 3]</code>, le résultat sera&nbsp;:</p>
<pre>[[0, 0, 0], [0, 0, 1], [0, 0, 2], [0, 1, 0], [0, 1, 1], [0, 1, 2], [1,
0, 0], [1, 0, 1], [1, 0, 2], [1, 1, 0], [1, 1, 1], [1, 1, 2]]</pre>
<p>Sans compréhension de liste, il serait difficile d&#8217;écrire le corps de cette fonction en 3 lignes&nbsp;!</p>
<p><em>Remarque&nbsp;: la compréhension de liste retourne une liste si elle est définie entre <code>[]</code>, alors qu&#8217;elle retourne un <a href="http://fr.wikipedia.org/wiki/G%C3%A9n%C3%A9rateur_%28informatique%29">générateur</a> (un </em>itérateur<em>) si elle est définie entre <code>()</code>.</em></p>
<p>Second exemple, dans <code>Solver.__solve_rec(…)</code>&nbsp;:</p>
<pre>for possible_vector in <strong>( Vector(i, v)</strong>
                         <strong>for v in [ norm, -norm ]</strong>
                         <strong>for i in xrange(len(self.dimensions))</strong>
                         <strong>if i != previous_position )</strong>:</pre>
<p>Cette partie fournit un ensemble de <code>Vector(i, v)</code>, pour toutes les combinaisons de <code>i</code> et de <code>v</code> dans leurs ensembles respectifs, qui vérifient la condition (qui ici ne porte que sur <code>i</code>).<br />
En clair, ici nous récupérons tous les vecteurs possibles, c&#8217;est-à-dire orthogonaux au précédent et avec la norme (la longueur) imposée par la structure.</p>
<h4>Itérateur</h4>
<p>La notion d&#8217;<a href="http://fr.wikipedia.org/wiki/It%C3%A9rateur">itérateur</a> est présente dans beaucoup d&#8217;autres langages. Un <a href="http://docs.python.org/library/stdtypes.html#iterator-types">itérateur</a> retourne un nouvel élément à chaque appel à la méthode <code>next()</code>. En pratique, il est souvent utilisé de manière transparente dans une boucle <code>for <em>variable</em> in <em>iterator</em></code>&nbsp;:</p>
<pre>for i in xrange(10):
    print i</pre>
<p><a href="http://docs.python.org/library/functions.html#xrange"><code>xrange(…)</code></a> retourne un <em>itérateur</em> et fournit les valeurs au fur et à mesure, alors que <a href="http://docs.python.org/library/functions.html#range"><code>range(…)</code></a> crée la liste de toutes les valeurs, qui est ensuite parcourue.</p>
<h4>Yield</h4>
<p>Les <a href="http://docs.python.org/reference/expressions.html#yieldexpr">expressions yield</a> permettent de créer un <em>itérateur</em> très simplement.</p>
<p>Pour résoudre le cube-serpent, il est préférable d&#8217;une part de <strong>fournir les solutions au fur et à mesure qu&#8217;elles sont trouvées</strong>, et d&#8217;autre part <strong>de pouvoir ne calculer que les <em>k</em> premières solutions</strong>.</p>
<p>La première contrainte est souvent résolue grâce à des <a href="http://en.wikipedia.org/wiki/Callback_%28computer_science%29">callbacks</a>&nbsp;: la fonction de calcul prend en paramètre une fonction, qui sera appelée à chaque résultat trouvé, le passant alors en paramètre.</p>
<p>La seconde est plus délicate&nbsp;: elle implique que l&#8217;algorithme s&#8217;arrête dès qu&#8217;il trouve une solution, et que lors d&#8217;un prochain appel il reprenne le calcul là où il s&#8217;était arrêté, afin calculer les solutions suivantes. Cela nécessite de conserver un état. Pour un <em>itérateur</em> simple comme celui d&#8217;une liste, il suffit de stocker l&#8217;index courant de parcours, et de l&#8217;incrémenter à chaque appel à <code>next()</code>. Gérer manuellement l&#8217;itération sur les solutions du cube-serpent semble beaucoup plus complexe, d&#8217;autant plus que les solutions sont trouvées dans des appels récursifs.</p>
<p>C&#8217;est là qu&#8217;interviennent les expressions <em>yield</em>, qui répondent aux deux besoins en même temps. Utiliser une expression <em>yield</em> dans le corps d&#8217;une fonction suffit à transformer cette fonction en un <em>générateur</em>. Il n&#8217;est donc plus possible de retourner de valeur grâce à <code>return</code>.</p>
<p>Dès que l&#8217;expression <em>yield</em> est rencontrée, la valeur est transmise et l&#8217;exécution de la fonction s&#8217;arrête. Elle reprendra lors du prochain appel.</p>
<p>Afin d&#8217;utiliser ce principe pour la génération des solutions, les fonctions <code>SnakeCubeSolver.solve()</code> et <code>SnakeCubeSolver.__solve_rec(…)</code> ne sont donc pas des fonctions ordinaires, mais des <em>générateurs</em>&nbsp;:</p>
<pre>if step == len(self.structure):
    yield init_cursor, self.volume_helper.path[:]</pre>
<p>Grâce à cette implémentation, il est possible de parcourir toutes les solutions&nbsp;:</p>
<pre>for solution in solver.solve():
    print solution</pre>
<p>ou alors de ne générer que les <code>k</code> premières&nbsp;:</p>
<pre>max_solutions = 5
solutions = solver.solve()
for i in xrange(max_solutions):
    try:
        print solutions.next()
    except StopIteration:
        break</pre>
<h4>Lambdas</h4>
<p><em>Python</em> supporte aussi les expressions <a href="http://docs.python.org/reference/expressions.html#lambda">lambda</a>, issues du <a href="http://fr.wikipedia.org/wiki/Lambda-calcul">lambda-calcul</a>, qui permettent d&#8217;écrire des fonctions anonymes simplement.</p>
<p>J&#8217;utilise cette fonctionnalité une fois dans le programme&nbsp;:</p>
<pre>needed_length = reduce(<strong>lambda x, y: x * y</strong>, self.dimensions) - 1</pre>
<p>Il s&#8217;agit de la déclaration d&#8217;une fonction avec deux arguments, qui retourne leur produit.</p>
<p>La fonction <a href="http://docs.python.org/library/functions.html#reduce"><code>reduce(function, iterable, …)</code></a> permet d&#8217;appliquer cumulativement la fonction aux éléments de l&#8217;<em>iterable</em>, de gauche à droite, de manière à réduire l&#8217;<em>iterable</em> en une seule valeur.<br />
<em>Même si «&nbsp;<a href="http://fr.wikipedia.org/wiki/Nicolas_Boileau">ce qui se conçoit bien s&#8217;énonce clairement</a>&nbsp;», la fonction </em>reduce<em> est bien plus facile à comprendre qu&#8217;à expliquer en quelques mots…</em></p>
<p>Ici, donc, <code>needed_length</code> contient le produit de tous les éléments de la liste <code>self.dimensions</code>.</p>
<h3>Conclusion</h3>
<p>La résolution du cube-serpent est intéressante, tout comme sa généralisation à n&#8217;importe quel volume de dimensions quelconque. Je me suis arrêté là, mais la détection des symétries et des rotations &laquo;&nbsp;au plus tôt&nbsp;&raquo; serait une amélioration non négligeable (et pas si évidente).</p>
<p>Débutant tout juste en <em>Python</em>, ce micro-projet m&#8217;a permis de beaucoup apprendre, et de découvrir quelques bonnes surprises comme les expressions <em>yield</em> que je ne connaissais pas.</p>
<p>J&#8217;espère que ça vous a amusé aussi.</p>
<h3>Script</h3>
<p><ins datetime="2011-10-01T00:00:00+01:00">J&#8217;ai remplacé le script par sa version 0.2, qui prend en compte les symétries et les rotations.<br />
L&#8217;historique des scripts est disponible <a href="http://dl.rom1v.com/snakesolver/">ici</a>.</ins><br />
Voici le code source. Il est également disponible ici&nbsp;: <a href="http://dl.rom1v.com/snakesolver/snakesolver-0.2.1.py">snakesolver-0.2.1.py</a>.</p>
<pre>#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# snakesolver v0.2.1, 5th october 2011
#
# changelog:
#   0.2.1
#     - get_useful_points() did not work for non-(hyper-)cubic volumes
#   0.2
#     - give all the solutions ignoring those which are equivalent by symmetry
#       or rotation
#   0.1
#     - initial version
#
# Solver for generalized snake-cube:
# http://en.wikipedia.org/wiki/Snake_cube
# http://fr.wikipedia.org/wiki/Cube_serpent
#
# By Romain Vimont (®om)
#   rom@rom1v.com

# snake structure (list of consecutives vector norms)
# Wikipedia example
SNAKE_STRUCTURE = [2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2]

# size of each dimension of the target volume
VOLUME_DIMENSIONS = [3, 3, 3]

# first variable names, the next will be k1, k2, k3...
VARIABLES = ['x', 'y', 'z', 't']

class Vector:

    """Base vector, with only one non-zero value."""

    def __init__(self, position, value):
        """Create a new base vector.

        Examples:
          - 2 dimensions:
              Vector(0, 2) means (2, 0)
              Vector(1, 3) means (0, 3)
          - 3 dimensions:
              Vector(0, 2) means (2, 0, 0)
              Vector(1, 3) means (0, 3, 0)
          - n dimensions:
              Vector(k, i) means (0, 0, ..., 0, i at position k, 0, ..., 0, 0)
        """
        self.position = position
        self.value = value

    @staticmethod
    def __get_variable(position):
        """Returns the variable name associated to position.

        Variable names are : ['x', 'y', 'z', 't', 'k1', 'k2', 'k3', ...].
        """
        if position &lt; len(VARIABLES):
            return VARIABLES[position]
        return 'k' + str(position - len(VARIABLES) + 1)

    @staticmethod
    def __get_canonical(number):
        """Removes the 1 if number is 1 or -1.

        Used for formatting …, -3x, -2x, -x, x, 2x, 3x, …
        """
        if number == 1:
            return ''
        if number == -1:
            return '-'
        return str(number)

    def __repr__(self):
        return Vector.__get_canonical(self.value) +\
               Vector.__get_variable(self.position)

class VolumeHelper:

    """Volume helper for the solver.

    A volume must be considered as an "hyper-volume", it can have any number of
    dimensions. For example, a volume 3x3 is a square, while a volume 3x3x3x3
    is an hypercube.

    Keep a flags volume (1 boolean per "point"), indicating if the case is
    already filled.
    """

    def __init__(self, dimensions):
        """Create a volume helper.

        Keyword arguments:
        dimensions -- the dimensions of the target volume (e.g. [3, 3, 3])
        init_cursor -- the starting position in the volume (e.g. [0, 0, 0])
        """
        self.dimensions = dimensions
        self.path = []
        self.flags = VolumeHelper.__create_volume_flags(dimensions)
        self.init_cursor = None

    def set_cursor(self, cursor):
        """Change the initial cursor position."""
        assert len(self.path) == 0, 'Changing cursor cannot happen in the ' +\
                                    'middle of a path calculation'
        if self.init_cursor is not None:
            # set to False the flag for the old cursor
            self.__set_flag(self.init_cursor, False)
        self.init_cursor = cursor[:]
        self.cursor = self.init_cursor
        self.__set_flag(self.cursor, True)

    def __get_flag(self, cursor):
        """Return the flag for the specified cursor."""
        tmp = self.flags
        # dereference n times
        for i in xrange(len(cursor)):
            tmp = tmp[cursor[i]]
        # after the last iteration, tmp contains the value
        return tmp

    def __set_flag(self, cursor, value):
        """Change the flag for the specified cursor."""
        tmp = self.flags
        for i in xrange(len(cursor) - 1):
            tmp = tmp[cursor[i]]
        tmp[cursor[-1]] = value

    def can_move(self, vector):
        """Indicates if it is possible to move the cursor by the move defined
        by the vector."""

        # the new vector will change the cursor at position vector.position,
        # adding vector.value
        # for example, if the cursor = [1, 2, 0], and vector = Vector(2, 2),
        # then cursor will take the value [1, 2, 2]
        cursor_position_value = self.cursor[vector.position]
        new_value = cursor_position_value + vector.value
        if new_value &lt; 0 or new_value &gt;= self.dimensions[vector.position]:
            # outside volume
            return False

        # copy the cursor for not modifying the real one
        future_cursor = self.cursor[:]
        if vector.value &lt; 0:
            sign = -1
        else:
            sign = 1
        for i in xrange(sign * vector.value):
            future_cursor[vector.position] += sign
            # if the flag at future_cursor is already True, then we cannot move
            # to this case, it is already filled
            if self.__get_flag(future_cursor):
                return False
        return True

    def move(self, vector):
        """Move the cursor by the vector, and updates flags and path."""
        self.path.append(vector)
        if vector.value &lt; 0:
            sign = -1
        else:
            sign = 1
        for i in xrange(sign * vector.value):
            self.cursor[vector.position] += sign
            self.__set_flag(self.cursor, True)

    def back(self):
        """Cancel the last move.

        Used for the recursivity, for avoiding to create a new flags volume for
        each recursivity step.
        """
        vector = self.path.pop()
        if vector.value &lt; 0:
            sign = -1
        else:
            sign = 1
        for i in xrange(sign * vector.value):
            self.__set_flag(self.cursor, False)
            self.cursor[vector.position] += -sign

    @staticmethod
    def __create_volume_flags(dimensions, index=0):
        """Create a multi-dimensional array filled with False values."""
        if index == len(dimensions) - 1:
            return [False] * dimensions[-1]
        return [VolumeHelper.__create_volume_flags(dimensions, index + 1)
                 for i in xrange(dimensions[index])]

    def __repr__(self):
        return repr(cube_flags)

class SymmetryHelper:

    """Symmetry helper for the solver.

    It manages equivalence classes for equivalent points and equivalent pathes
    by symmetry and/or rotation.

    As symmetries and rotations only concern permutation and inversion of
    vector components (positions), then equivalences classes concern
    dimensions.
    If x and z axis are equivalent (at a specific step), we could represent the
    equivalence classes like this: [[0, 2], [1]]. In that case, the solver
    would only try vectors on x axis, but will ignore z axis (because it is
    equivalent).
    Then, after a move, they are not equivalent anymore, then the equivalence
    classes could be represented by [[0], [1], [2]].

    But there is a far better representation for handling quickly the
    equivalence classes: a simple array, with the same length as dimensions.
    For each dimension i, the eq_classes array contains the lower index of an
    equivalent axis:
      - i if there is no axis with lower index which is equivalent;
      - j if there is an axis j with a lower index (the lowest) which is
        equivalent.
    For example, if x and z are equivalent, then eq_classes is [0, 1, 0].
    If y and z are equivalent, then eq_classes is [0, 1, 1].
    If x and y are equivalent, then eq_classes is [0, 0, 2].
    If x, y and z are equivalent, then eq_classes is [0, 0, 0].
    If none are equivalent, then eq_classes is [0, 1, 2].

    With this representation, we can easily pick only 1 vector per equivalence
    class (and ignore the others): the ones with eq_classes[i] == i.

    eq_classes_path stores the list of eq_classes used, to restore previous
    ones when calling back(). Equivalence classes at index i are always
    computed from the equivalence classes at index i-1 (except if i == 0),
    and are always "as or more splitted".
    """

    def __init__(self, dimensions):
        self.dimensions = dimensions
        # eq_classes is always eq_classes_path[-1]
        self.eq_classes = self.__create_eq_classes_from_dimensions()
        self.eq_classes_path = [self.eq_classes]

    def __create_eq_classes_from_dimensions(self):
        """Compute the first equivalences classes from the dimensions.

        The dimensions which have the same length are equivalent.
        """
        eq_classes = range(len(self.dimensions))
        for i in xrange(1, len(self.dimensions)):
            value = self.dimensions[i]
            for j in xrange(0, i):
                if self.dimensions[j] == value:
                    eq_classes[i] = j
                    break
        return eq_classes

    def set_cursor(self, cursor):
        """Change the initial cursor position."""
        # the first item in eq_classes_path is computed from the dimensions
        # the second item is computed from the init_point
        # the next items are computed from the next moves (vectors)
        assert len(self.eq_classes_path) == 1 or\
               len(self.eq_classes_path) == 2, 'Changing cursor cannot ' +\
               'happen in the middle of a path calculation'
        if len(self.eq_classes_path) == 2:
            # this is not the first initialisation, we have to remove the
            # eq_classes associated with the previous cursor
            self.eq_classes_path.pop()
        self.cursor = cursor

        # make a copy to not modify the previous one (must be immutable)
        cursor_eq_classes = self.eq_classes_path[0][:]
        for i in xrange(len(cursor_eq_classes)):
            # if the equivalence class must be splitted
            # i.e. the value for the axis has changed
            if cursor_eq_classes[i] != i and not self.__eq_cmp(
                    cursor[cursor_eq_classes[i]],\
                    cursor[i], self.dimensions[i]):
                old_class = cursor_eq_classes[i]
                cursor_eq_classes[i] = i
                # If eq_classes = [0, 0, 0], and we detected that the first
                # dimension is not equivalent anymore with the two others,
                # then the new eq_classes will be [0, 1, 1]:
                # we have to check the next dimensions to change their value
                for j in xrange(i + 1, len(cursor_eq_classes)):
                    if cursor_eq_classes[j] == old_class:
                        cursor_eq_classes[j] = i

        self.eq_classes = cursor_eq_classes
        self.eq_classes_path.append(cursor_eq_classes)
        # At this point, eq_classes_path contains two items:
        #   - the eq_class at index 0 computed only from the dimensions;
        #   - the eq_class at index 1 computed from the initial point
        #     (which splits the equivalence classes computed from the
        #     dimensions).

    def get_useful_points(self):
        """Returns one point from each equivalence class, not more.

        For example, if dimensions is [3, 3, 3], then useful points are
        [[0, 0, 0], [0, 0, 1], [0, 1, 1], [1, 1, 1]]
        which are, respectively:
        [a corner, an edge, a center, the core (middle of the cube)]
        All other points are equivalent to one point in this minimal set.
        """
        assert len(self.eq_classes_path) == 1, \
               'When computing useful points, there is only 1 eq_classes, ' +\
               'computed from the dimensions'
        # start with point 0
        for point in self.get_useful_points_rec(0, [0] * len(self.dimensions)):
            yield point

    def get_useful_points_rec(self, index, minimums):
        if index == len(self.dimensions):
            yield []
        else:
            eq_class = self.eq_classes[index]
            # We want to remove symmetry-or-rotation-equivalent-points.
            # The easiest way to achive this goal is to:
            #   - consider only one half of dimension length (+1);
            #   - always use sorted coordinates inside equivalence classes.
            #
            # For a dimension of length 3, we consider only the values 0 and 1.
            # For a dimension of length 4, we consider 0 and 1 too.
            # For a dimension of length 5, we consider 0, 1 and 2.
            #
            # Only use sorted coordinates avoid to check [0, 1] and [1, 0],
            # because they are equivalent. When building a vector, we keep a
            # "minimum" value (rather a "minimums" array, one minimum for each
            # equivalence class) to guarantee this condition.
            minimum = minimums[eq_class]
            # h is "head", t is "tail"
            for h in xrange(minimum, (self.dimensions[index] + 1) / 2):
                # change the minimum for deeper recursive calls
                minimums[eq_class] = h
                for t in self.get_useful_points_rec(index + 1, minimums):
                    yield [h] + t
            # restore the previous value
            minimums[eq_class] = minimum 

    def move(self, vector):
        """Compute the new equivalent classes after a move by the vector."""
        assert self.eq_classes[vector.position] == vector.position,\
               'A move must always concern the first vector of an ' +\
               'equivalence class'

        # create a copy of eq_classes only if it changes, else use the same
        # instance
        has_changes = False
        position = vector.position
        new_eq_classes = self.eq_classes
        new_eq_class = None
        # the axis is now alone in its equivalence class (the vector has moved
        # the cursor on this axis, but not on the others), we have to update
        # the others (if any)
        for i in xrange(position + 1, len(self.eq_classes)):
            if self.eq_classes[i] == position:
                if not has_changes:
                    has_changes = True
                    new_eq_classes = self.eq_classes[:]
                if new_eq_class is None:
                    new_eq_class = i
                new_eq_classes[i] = new_eq_class
        self.eq_classes = new_eq_classes
        self.eq_classes_path.append(new_eq_classes)

    def back(self):
        """Cancel the last move."""
        self.eq_classes_path.pop()
        self.eq_classes = self.eq_classes_path[-1]

    def must_explore(self, i):
        """Returns True if the vector position is the first in its equivalence
        class (the others will be ignored, because equivalents)"""
        return self.eq_classes[i] == i

    @staticmethod
    def __eq_cmp(p1, p2, dim):
        """Comparator which tests if two point are "equivalent" on an axis.

        Keyword arguments:
        p1 -- projection of the point 1 on the axis
        p2 -- projection of the point 2 on the axis
        dim -- length of dimension associated to the axis

        p1 and p2 are equivalent if and only if:
          -    v1 == v2 (they are at the same place)
          - or v1 + v2 + 1 = dim (they are symmetrically opposite)
        """
        return p1 == p2 or p1 + p2 + 1 == dim

class SnakeCubeSolver:

    """Solver."""

    def __init__(self, dimensions, structure):
        """Create a new solver.

        Keyword arguments:
        dimensions -- the dimensions of the target volume
                      (for example [3, 3, 3])
        structure -- the snake structure
        """
        self.dimensions = dimensions
        self.structure = structure
        self.volume_helper = VolumeHelper(dimensions)
        self.symmetry_helper = SymmetryHelper(dimensions)

    def solve(self):
        """Solve the snake.

        This function returns an iterator: the full list of solutions is not
        created."""

        # the structure length must exactly fill the target volume
        structure_length = sum(self.structure)
        needed_length = reduce(lambda x, y: x * y, self.dimensions) - 1
        if structure_length != needed_length:
            print 'Structure has not the right length (' +\
                  str(structure_length) + ' instead of ' +\
                  str(needed_length) + ')'
        else:
            for init_point in self.symmetry_helper.get_useful_points():
                # for each useful initial point (in a minimal set where no
                # points are equivalent to another)
                self.volume_helper.set_cursor(init_point)
                self.symmetry_helper.set_cursor(init_point)
                # recursively solve and yield the solutions
                for solution in self.__solve_rec(init_point[:], 0):
                    yield solution

    def __solve_rec(self, init_cursor, step):
        """Recursively solve.

        Keyword arguments:
        init_cursor -- starting point, only used to put it in found solutions
        step -- recursivity depth, index of current vector in snake structure
        """
        if step == len(self.structure):
            # a new solution is found, copy the path and yield the solution
            yield init_cursor, self.volume_helper.path[:]
        else:
            if len(self.volume_helper.path) == 0:
                # empty path, use an inexistant position (-1),
                # i != previous_position will always be True
                previous_position = -1
            else:
                previous_position = self.volume_helper.path[-1].position
            # get the vector norm for the current step
            norm = self.structure[step]
            # iterate over the next possible vectors, i.e. all vectors which
            # are orthogonal to the previous one

            for possible_vector in ( Vector(i, v)
                                     for v in [norm, -norm]
                                     for i in xrange(len(self.dimensions))
                                     if i != previous_position and
                                        self.symmetry_helper.must_explore(i)):
                if self.volume_helper.can_move(possible_vector):
                    # if it is possible to move the cursor by the vector, then
                    # move it
                    self.volume_helper.move(possible_vector)
                    # and recursively solve
                    self.symmetry_helper.move(possible_vector)
                    for solution in self.__solve_rec(init_cursor, step + 1):
                        yield solution
                    # cancel the move to put back the state of the helper
                    self.volume_helper.back()
                    self.symmetry_helper.back()

def main():
    solver = SnakeCubeSolver(VOLUME_DIMENSIONS, SNAKE_STRUCTURE)
    # print all solutions
    for solution in solver.solve():
        print solution

    # print the first solutions
#    max_solutions = 5
#    solutions = solver.solve()
#    for i in xrange(max_solutions):
#        try:
#            print solutions.next()
#        except StopIteration:
#            break
    exit(0)

if __name__ == "__main__":
    main()</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2011/09/resoudre-le-cube-serpent-en-python/feed/</wfw:commentRss>
		<slash:comments>19</slash:comments>
		</item>
		<item>
		<title>Extraire les recherches Google des logs Apache</title>
		<link>http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache/</link>
		<comments>http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache/#comments</comments>
		<pubDate>Fri, 24 Jun 2011 13:10:36 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Astuces]]></category>
		<category><![CDATA[Insolite]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[puf]]></category>
		<category><![CDATA[auto-hébergement]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[serveur]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/?p=2685</guid>
		<description><![CDATA[Aujourd&#8217;hui, c&#8217;est un billet de distraction pour geeks. Lister les recherches Si vous utilisez Apache, voici une commande qui liste dans l&#8217;ordre alphabétique les recherches Google ayant permis aux internautes d&#8217;arriver sur vos sites&#160;: php -r "echo urldecode(\"`zgrep 'http://www\.google\.\w*/' /var/log/apache2/*&#124;grep -o '[?&#038;]q=[^&#038;"]*'&#124;cut -c4-`\");"&#124;sort&#124;uniq -c EDIT 25/06/2011&#160;: cette commande semble échouer lorsque la liste des recherches [...]]]></description>
			<content:encoded><![CDATA[<p>Aujourd&#8217;hui, c&#8217;est un billet de <em>distraction pour geeks</em>.</p>
<h3>Lister les recherches</h3>
<p>Si vous utilisez <em>Apache</em>, voici une commande qui liste dans l&#8217;ordre alphabétique les recherches <em>Google</em> ayant permis aux internautes d&#8217;arriver sur vos sites&nbsp;:</p>
<pre>php -r "echo urldecode(\"`zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&#038;]q=[^&#038;"]*'|cut -c4-`\");"|sort|uniq -c</pre>
<p><em><strong>EDIT 25/06/2011&nbsp;:</strong> cette commande <a href="http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache/#comment-67664">semble échouer</a> lorsque la liste des recherches est trop longue, celle donnée à la fin du billet est donc à préférer.</em><br />
(pour les autres moteurs de recherche, il faudrait s&#8217;inspirer de ce qu&#8217;ont fait les développeurs de <a href="http://piwik.org/faq/general/#faq_39">Piwik</a>)</p>
<p>Voici à quoi ressemble le résultat de la commande&nbsp;:</p>
<pre>      1 ubtunu tiny tiny rss
      5 ubuntu
      1 ubuntu 10.04 change startup screen
      1 ubuntu 10.04 configurer compte messagerie hotmail dans couriel
      3 ubuntu 10.04 cryptage
      1 ubuntu 10.04 écran grub invisible au démarrage
      2 ubuntu 10.04 ecran noir nvidia
      1 ubuntu 10.04 et video nvidia
      1 ubuntu 10.04 grub nvidia
      1 ubuntu 10.04 grub-pc couleur
      4 ubuntu 10.04 installation partition home chiffrée</pre>
<p>Le texte correspond aux recherches, le numéro devant indique le nombre de fois où elles ont été effectuées.</p>
<h3>Analyse</h3>
<h4>Billets les plus recherchés</h4>
<p>Sans conteste, les deux billets qui amènent le plus d&#8217;internautes par <em>Google</em> concernent <a href="http://blog.rom1v.com/2010/07/pluzz-fr-france-televisions-lance-son-service-de-tv-de-rattrapage-non-lisible/">pluzz</a> et <a href="http://blog.rom1v.com/2010/01/installer-une-application-apk-sur-android-a-partir-dun-pc/">apk</a>. Et parfois ça ne doit pas les aider beaucoup&nbsp;: certains recherchent par exemple <em>&laquo;&nbsp;pluzz plus belle la vie&nbsp;&raquo;</em> dans <em>Google</em> à partir d&#8217;<em>Internet Explorer</em>, je ne suis pas sûr que mon script shell pour <em>pluzz</em> réponde à leurs attentes.</p>
<h4>Recherches insolites</h4>
<p>Dans la liste, il y a forcément des recherches drôles ou étranges. En voici quelques unes que j&#8217;ai trouvées dans mes logs&nbsp;:</p>
<ul>
<li><em>&laquo;&nbsp;clitoris.apk&nbsp;&raquo;</em> : il y a vraiment une application pour tout&nbsp;!</li>
<li><em>&laquo;&nbsp;comment afficher 350 sous linux&nbsp;&raquo;</em> : c&#8217;est si différent que sur les autres systèmes d&#8217;exploitation&nbsp;?</li>
<li><em>&laquo;&nbsp;comment on invente une machine pour voler à usage individuelle&nbsp;&raquo;</em>&nbsp;: dérober ou s&#8217;envoler&nbsp;?</li>
<li><em>&laquo;&nbsp;du-ble-plein-les-poches es ce une arnaque&nbsp;&raquo;</em>&nbsp;: sans doute…</li>
<li><em>&laquo;&nbsp;est-il possible de prelever de l&#8217;argent sans que ça se voit sur le compte&nbsp;&raquo;</em>&nbsp;: je veux rester discret…</li>
<li><em>&laquo;&nbsp;l&#8217;ecran d&#8217;un ordinateur portable est de 14,1 pouce avec 1024*768 pixel quelle est la taille de l&#8217;ecran N*H en cm ?&nbsp;&raquo;</em>&nbsp;: le moteur de recherche, j&#8217;en suis sûr, va comprendre ma question, faire le calcul, et me répondre…</li>
<li><em>&laquo;&nbsp;logiciel adopy&nbsp;&raquo;</em>&nbsp;: comme ça se prononce&nbsp;!</li>
<li><em>&laquo;&nbsp;pour quel raison on doit interdire le zoo&nbsp;&raquo;</em>&nbsp;: les animaux c&#8217;est dangereux&nbsp;!
</li>
</ul>
<p>Il y a certains sites qui s&#8217;amusent à référencer ce genre de recherches, par exemple <a href="http://devenirunninjagratuitement.tumblr.com/">Comment devenir un ninja gratuitement&nbsp;?</a></p>
<p>N&#8217;hésitez pas à poster les vôtres…</p>
<h3>Challenge</h3>
<p>J&#8217;ai essayé d&#8217;écrire la commande la plus courte possible. Je n&#8217;ai pas réussi à faire moins de <strong>129 caractères</strong> sans perdre d&#8217;information ou prendre plus de risque (par exemple on pourrait remplacer <code>apache2</code> par <code>a*2</code>, mais c&#8217;est plus risqué).</p>
<p>Par contre, cette commande ne fonctionne pas correctement si l&#8217;on rajoute <code>|less</code> (on ne peut pas se déplacer avec haut et bas), je ne sais pas trop pourquoi ni comment le résoudre (si certains ont une idée).<br />
Une autre commande, sans <code>php</code> (en 141 caractères), ne pose pas ce problème&nbsp;:</p>
<pre>zgrep 'http://www\.google\.\w*/' /var/log/apache2/*|grep -o '[?&#038;]q=[^&#038;"]*'|cut -c4-|echo -e $(sed 's/$/\\n/;s/+/ /g;s/%/\\x/g')|sort|uniq -c</pre>
<p>Si vous avez des astuces pour faire mieux que 129 (ou 141), ne vous gênez pas <img src='http://blog.rom1v.com/wp-includes/images/smilies/icon_wink.gif' alt=';-)' class='wp-smiley' /> </p>
<h3>Scripts</h3>
<p><a href="http://www.bortzmeyer.org/je-parle-a-mon-moteur-de-recherche.html">D&#8217;autres</a> ont fait <a href="http://www.bortzmeyer.org/files/SearchEngineQueries.py">des scripts plus complets</a>, qui permettent de récupérer des informations supplémentaires, par exemple la page sur laquelle l&#8217;internaute est arrivé en effectuant cette recherche…</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2011/06/extraire-les-recherches-google-des-logs-apache/feed/</wfw:commentRss>
		<slash:comments>20</slash:comments>
		</item>
		<item>
		<title>pspxconv : script d&#8217;encodage de vidéos pour PSP</title>
		<link>http://blog.rom1v.com/2008/10/pspxconv-script-dencodage-de-videos-pour-psp/</link>
		<comments>http://blog.rom1v.com/2008/10/pspxconv-script-dencodage-de-videos-pour-psp/#comments</comments>
		<pubDate>Wed, 29 Oct 2008 17:37:00 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Outils]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[puf]]></category>
		<category><![CDATA[gnu/linux]]></category>
		<category><![CDATA[psp]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[ubuntu]]></category>
		<category><![CDATA[video]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/2008/10/pspxconv%c2%a0-script-dencodage-de-videos-pour-psp/</guid>
		<description><![CDATA[J&#8217;ai récemment acheté une PSP, et je voulais pouvoir encoder mes vidéos facilement, avec les réglages que je voulais. J&#8217;ai donc écrit un script. Il s&#8217;utilise comme ceci : pspxconv fichier.avi fichier.mp4 500 si l&#8217;on veut convertir un .avi en .mp4 lisible par la PSP avec un bitrate vidéo de 500Kbps (pour l&#8217;instant le bitrate [...]]]></description>
			<content:encoded><![CDATA[<p>J&#8217;ai récemment acheté une PSP, et je voulais pouvoir encoder mes vidéos facilement, avec les réglages que je voulais. J&#8217;ai donc écrit un script.</p>
<p><a href="http://blog.rom1v.com/wp-content/uploads/2008/10/psp.png"><img src="http://blog.rom1v.com/wp-content/uploads/2008/10/psp-300x180.png" alt="psp" title="psp" width="300" height="180" class="aligncenter size-medium wp-image-227" /></a></p>
<p>Il s&#8217;utilise comme ceci :</p>
<pre>pspxconv fichier.avi fichier.mp4 500</pre>
<p>si l&#8217;on veut convertir un <code>.avi</code> en <code>.mp4</code> lisible par la PSP avec un bitrate vidéo de 500Kbps (pour l&#8217;instant le bitrate audio est fixé à 96).</p>
<p>Il est également possible de définir une qualité constante plutôt qu&#8217;un bitrate moyen (préférable pour <em>-hurry</em> et <em>-afap</em>) :</p>
<pre>pspxconv fichier.avi fichier.mp4 q20 -hurry</pre>
<p>Plusieurs <em>presets</em> de qualité sont disponibles :</p>
<ul>
<li><code>-afap</code> : as fast as possible (le plus rapidement possible) ; 1 seule pass en <strong>mpeg4</strong>, en 320×240, j&#8217;obtiens 190fps ;</li>
<li><code>-hurry</code> : 1 seule passe, en <strong>x264</strong>, en 480×272, meilleure qualité, j&#8217;ai environ 95fps ;</li>
<li><code>-std</code> (standard) : 2 pass, qualité normale (réglage à privilégier si on n&#8217;a pas de contraintes particulières), environ 55fps ;</li>
<li><code>-hq</code> : 2 pass, qualité un peu supérieure, environ 38 fps ;</li>
<li><code>-vhq</code> : extra haute qualité (inutile), environ 23 fps.</li>
</ul>
<p>On peut aussi rajouter des options de mencoder :<br />
<code>pspxconv fichier.avi fichier.mp4 500 -std -ss 10 -endpos 100</code><br />
n&#8217;encodera que de 10s à 110s.</p>
<pre>pspxconv fichier.avi fichier.mp4 500 -std -audiofile fichier.mp3</pre>
<p>encodera la vidéo <code>.avi</code> en utilisant la bande son <code>.mp3</code>.</p>
<p>Ce qu&#8217;il reste à améliorer (votre aide est la bienvenue) :</p>
<ul>
<li>possibilité d&#8217;utiliser plusieurs processeurs à 100% en <code>-afap</code> (même si on met plusieurs threads, 1 seul est à 100%)</li>
<li>possibilité de choisir à partir d&#8217;un <code>.mkv</code> la piste audio et la piste de sous-titres à utiliser et incruster.</li>
</ul>
<p>Voici le script :</p>
<pre>#!/bin/sh
# pspxconv : video encoding script for PSP
#
# 28th october 2008 - Romain Vimont (®om)
#
# v0.4 (25th june 2009)
#
# Converts input video and audio to mp4 { x264|mpeg4 + faac }, accepted by PSP.
#
# Syntax:
#   pspxconv input_file output_mp4 video_bitrate [quality_preset]
#     [mencoder_options [...]]
#
# video_bitrate can be an integer, or can define a quantifier if it starts
#   with q : q19.5 for example.
#
# quality_preset must be one of :
#   -afap : as fast as possible (poor quality mpeg4, 1 pass, fastest)
#   -hurry : (default) quite fast (poor quality, x264, 1 pass, fast)
#   -std :  standard quality (good quality, normal)
#   -hq  : high quality (very good quality, slow)
#   -vhq : very high quality (best quality, very slow)
#
# mencoder_options are appended to command line of mencoder call.
# For example :
#   -ss 10 -endpos 56 : trim the video from 10s to 66s.
#
# (of course, there are no options -afap, -hurry, -std, -hq nor -vhq)
#

# Syntax error detected.
# Exits the program with return code 1.
syntax_error() {
    printf '%s%s\n' "Syntaxe : $0 input_file output_mp4 video_bitrate " \
      '[quality_preset] [mencoder_options [...]]' >&#038;2
    exit 1
}

# Indicates whether the argument represents an integer.
#
# $1: value to test
# return: the integer if the argument represents an integer, an empty string if
#   it doesn't
is_integer() {
    local value="$1"
    printf %s "$1" | grep -o "^[[:digit:]]\+$"
}

# Returns the quantification value if the bitrate represents such a value.
#
# $1: value to test
# return: the value if the argument represents a quantifier, an empty string if
#   it doesn't
get_q_value() {
    local value="$1"
    printf %s "$1" | grep -o "^q[[:digit:]]\+\(\.[[:digit:]]\+\)\?$" | cut -c2-
}

# Gets a unique id, based on the clock (seconds + nanoseconds).
#
# return: unique id
uid() {
    date +'%s%N'
}

# Indicates whether the encoder must use x264 codec.
#
# $1: preset
# return: 'yes' if it must use it, nothing otherwise
use_x264() {
    local preset="$1"
    if [ "$preset" != '-afap' ]
    then
        printf 'yes'
    fi
}

# Returns the x264opts command line arguments for the selected preset.
#
# $1: preset
# return: command line arguments
x264_preset() {
    local preset="$1"
    local common='global_header:threads=auto'
#vbv_maxrate=1536:vbv_bufsize=2000:level_idc=30
    local common_q="$common:bframes=1:b_adapt:b_pyramid:weight_b:trellis=2"
    local q_value=$(get_q_value "$q")
    if [ "$q_value" ]
    then
        printf "crf=$q_value:"
    else
        printf "bitrate=$q:"
    fi
    case "$preset" in
    '-hurry') printf "$common:me=dia:subq=1" ;;
    '-std')  printf "$common_q" ;;
    '-hq')   printf "$common_q:me=umh:subq=6" ;;
    '-vhq')  printf "$common_q:me=esa:subq=7" ;;
    esac
}

# Returns the lavcopts command line arguments for the selected preset.
#
# return: command line arguments
mpeg4_preset() {
    local q_value=$(get_q_value "$q" | sed 's/\..*$//')
    if [ "$q_value" ]
    then
        printf "vqscale=$q_value:"
    else
        printf "vbitrate=$q:"
    fi
    printf 'aglobal=1:vglobal=1'
}

# Indicates whether the preset needs 2 passes.
#
# $1: preset
# return: '2' if the selected preset have 2 passes, nothing otherwise
two_passes_preset() {
    local preset="$1"
    if [ "$preset" != '-afap' -a "$preset" != '-hurry' ]
    then
        printf 2
    fi
}

# Indicates the scale selected for the preset.
#
# $1: preset
# return: width:height scaling
scale_preset() {
    local preset="$1"
    case "$preset" in
    '-afap')  printf 'dsize=320:240:0:16,scale=0:0' ;;
    *)        printf 'dsize=480:272:0:16,scale=0:0'
    esac
}

# error when less than 3 arguments
[ $# -ge 3 ] || syntax_error

# reads the arguments
in="$1"; shift
out="$1"; shift
q="$1"; shift

# audio bitrate
ab=96

# input file must exist
if [ ! -f "$in" ]
then
    printf '%s\n' "Input file doesn't exist : $in" >&#038;2
    exit 2
fi

# bitrate value must be integer or quantifier
if [ ! "$(is_integer "$q")" -a ! "$(get_q_value "$q")" ]
then
    printf '%s\n' "Bitrate value must be integer : $bitrate" >&#038;2
    exit 3
fi

# gets a unique filename, in order to avoid collisions when encoding two video
# at the same time
uid=$(uid)
tmp_log=$(printf '%s' "/tmp/$uid.log")

# chooses the quality preset
quality='-hurry'
if [ "$1" = '-afap' -o "$1" = '-hurry' -o "$1" = '-std' -o "$1" = '-hq' \
  -o "$1" = '-vhq' ]
then
    quality="$1"
    shift
fi

vf=$(scale_preset "$quality")

if [ $(use_x264 "$quality") ]
then
    # gets the x264 options
    opts=$(x264_preset $quality)
    if [ $(two_passes_preset "$quality") ]
    then
        # encodes the first pass
        mencoder "$in" -o /dev/null -of lavf -lavfopts format=mp4 -passlogfile \
        "$tmp_log" -ovc x264 -x264encopts "$opts:pass=1" \
        -vf "$vf" -nosound $@ &#038;&#038;

        # encodes the second pass
        mencoder "$in" -o "$out" -of lavf -lavfopts format=mp4 -passlogfile \
        "$tmp_log" -ovc x264 -x264encopts "$opts:pass=2" \
        -vf "$vf" -oac faac \
        -faacopts br=$ab:raw=yes:object=2 $@ &#038;&#038;

        exit 0
    else
        # encodes the unique pass
        mencoder "$in" -o "$out" -of lavf -lavfopts format=mp4 -passlogfile \
        "$tmp_log" -ovc x264 -x264encopts "$opts" \
        -vf "$vf" -oac faac \
        -faacopts br=$ab:raw=yes:object=2 $@ &#038;&#038;

        exit 0
    fi
else
    # encodes the unique pass
    mencoder "$in" -o "$out" -of lavf -lavfopts format=mp4 -passlogfile \
    "$tmp_log" -ovc lavc -lavcopts $(mpeg4_preset) -vf "$vf" -oac faac \
    -faacopts br=$ab:raw=yes:object=2 $@ &#038;&#038;

    exit 0
fi</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2008/10/pspxconv-script-dencodage-de-videos-pour-psp/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>sed : changer de séparateur</title>
		<link>http://blog.rom1v.com/2008/09/sed-changer-de-separateur/</link>
		<comments>http://blog.rom1v.com/2008/09/sed-changer-de-separateur/#comments</comments>
		<pubDate>Tue, 30 Sep 2008 15:03:00 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Astuces]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[puf]]></category>
		<category><![CDATA[gnu/linux]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[ubuntu]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/2008/09/sed%c2%a0-changer-de-separateur/</guid>
		<description><![CDATA[Si vous effectuez quelques traitements simples en ligne de commande, vous connaissez forcément l&#8217;outil sed, et plus particulièrement la commande : sed 's/ancien/récent/' qui permet de remplacer ancien par récent : sed 's/ancien/récent/' &#60;&#60;&#60; 'ce système est ancien, voire très ancien' ce système est récent, voire très ancien Pour remplacer toutes les occurrences, on rajoute un [...]]]></description>
			<content:encoded><![CDATA[<p>Si vous effectuez quelques traitements simples en ligne de commande, vous connaissez forcément l&#8217;outil <code>sed</code>, et plus particulièrement la commande :</p>
<pre>sed 's/ancien/récent/'</pre>
<p>qui permet de remplacer <code>ancien</code> par <code>récent</code> :</p>
<pre>sed 's/ancien/récent/' &lt;&lt;&lt; 'ce système est ancien, voire très ancien'
ce système est récent, voire très ancien</pre>
<p>Pour remplacer toutes les occurrences, on rajoute un <code>g</code> :</p>
<pre>sed 's/ancien/récent/g' &lt;&lt;&lt; 'ce système est ancien, voire très ancien'
ce système est récent, voire très récent</pre>
<p>Cependant, la syntaxe est assez lourde lorsqu&#8217;on veut remplacer des <code>/</code>, par exemple pour remplacer <code>/home/rom/sh/</code> par <code>/usr/bin/</code> :</p>
<pre>sed 's//home/rom/sh///usr/bin//' &lt;&lt;&lt; /home/rom/sh/myscript
/usr/bin/myscript</pre>
<p>Heureusement, il est possible de changer le séparateur, et très facilement : c&#8217;est simplement le caractère après le <code>s</code>, et on met ce que l&#8217;on veut :</p>
<pre>sed 's:/home/rom/sh:/usr/bin:' &lt;&lt;&lt; /home/rom/sh/myscript
/usr/bin/myscript</pre>
<pre>sed 's|/home/rom/sh|/usr/bin|' &lt;&lt;&lt; /home/rom/sh/myscript
/usr/bin/myscript</pre>
<p><em>(il suffit de backslasher le caractère qui sert de séparateur dans les tokens)</em></p>
<p>Allez, pour s&#8217;amuser :</p>
<pre>sed sachagena &lt;&lt;&lt; 'des choux'</pre>
<pre>sed subucu &lt;&lt;&lt; 'trop bon'</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2008/09/sed-changer-de-separateur/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>screex264 : réencodez vos captures d&#8217;écran vidéos (screencasts) sous Ubuntu</title>
		<link>http://blog.rom1v.com/2008/09/screex264-reencodez-vos-captures-decran-videos-screencasts-sous-ubuntu/</link>
		<comments>http://blog.rom1v.com/2008/09/screex264-reencodez-vos-captures-decran-videos-screencasts-sous-ubuntu/#comments</comments>
		<pubDate>Fri, 12 Sep 2008 09:11:00 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Outils]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[puf]]></category>
		<category><![CDATA[gnu/linux]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[ubuntu]]></category>
		<category><![CDATA[video]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/2008/09/screex264%c2%a0-reencodez-vos-captures-decran-videos-screencasts-sous-ubuntu/</guid>
		<description><![CDATA[Vous connaissez sans doute l&#8217;outil gtk-recordmydesktop, qui permet de faire une capture vidéo (un screencast) de votre écran. Pour obtenir une bonne qualité, dans les options vidéos, il faut vérifier que &#171;&#160;compression nulle&#160;&#187; est bien sur l&#8217;option &#171;&#160;Activé&#160;&#187; (malheureusement, la compression à la volée utilisée provoque quand même une légère perte de qualité). Mais une [...]]]></description>
			<content:encoded><![CDATA[<p>Vous connaissez sans doute l&#8217;outil <a href="apt://gtk-recordmydesktop">gtk-recordmydesktop</a>, qui permet de faire une capture vidéo (un screencast) de votre écran.</p>
<p>Pour obtenir une bonne qualité, dans les options vidéos, il faut vérifier que &laquo;&nbsp;compression nulle&nbsp;&raquo; est bien sur l&#8217;option &laquo;&nbsp;Activé&nbsp;&raquo; (malheureusement, la compression à la volée utilisée provoque quand même une légère perte de qualité).</p>
<p>Mais une telle vidéo prend un peu de place. Je vous propose donc de la réencoder, dans le format <a href="http://fr.wikipedia.org/wiki/X264">x264</a> (issu du projet VideoLAN, ayant donné naissance à VLC), multiplexé dans le conteneur <a href="http://fr.wikipedia.org/wiki/Matroska">MKV</a>.</p>
<p>Le fichier ainsi généré sera lisible par exemple avec <a href="apt://vlc">VLC</a>.</p>
<p>Disons-le tout de suite, le <strong>x264</strong> est actuellement LE meilleur codec de compression vidéo, proposant un rapport qualité/taille impressionnant. Le <strong>mkv</strong> permet, par exemple, de contenir une piste vidéo, plusieurs pistes audio dans différentes langues, des sous-titres, des chapitres, des pièces jointes… Ici, ce qui nous intéresse, c&#8217;est qu&#8217;il est libre, et qu&#8217;à contenu égal, il prend moins de place que les autres conteneurs (l&#8217;overhead est quasi-nul).</p>
<p>Voici comment utiliser le script.</p>
<p>Pour encoder la vidéo <code>mavideo.ogg</code> en <code>mavideo.mkv</code>, avec un débit de 400Kbps :</p>
<pre>screex264 mavideo.ogg mavideo.mkv 400</pre>
<p>Pour encoder la vidéo <code>mavideo.ogg</code> en <code>mavideo.mkv</code>, avec un débit de 400Kbps avec une meilleure qualité (plus lent) :</p>
<pre>screex264 mavideo.ogg mavideo.mkv 400 -hq</pre>
<p>Pour encoder la vidéo <code>mavideo.ogg</code> en <code>mavideo.mkv</code>, avec un débit de 400Kbps avec une qualité maximale (très lent) :</p>
<pre>screex264 mavideo.ogg mavideo.mkv 400 -vhq</pre>
<p>Il est également possible d&#8217;utiliser n&#8217;importe quels paramètres de mencoder.<br />
Ainsi, pour encoder la vidéo <code>mavideo.ogg</code> en <code>mavideo.mkv</code>, avec un débit de 400Kbps, à partir de la 10e seconde et pour une durée de 20 secondes :</p>
<pre>screex264 mavideo.ogg mavideo.mkv 400 -ss 10 -endpos 20</pre>
<p>Ce script peut également permettre à réencoder les petites vidéos enregistrées grâce à un appareil photo, et diviser leur taille d&#8217;un facteur 15.</p>
<p>Passons donc aux choses sérieuses. Tout d&#8217;abord, ce script nécessite <a href="apt://x264,mencoder,mkvtoolnix">x264, mencoder et mkvtoolnix</a> pour fonctionner.</p>
<pre>#!/bin/sh
# screex264 : screencast encoding script
#
# 11th september 2008 - Romain Vimont (®om)
#
# v0.2 (26th june 2009)
#
# Converts input video (ignoring audio) to x264 video, muxed in mkv.
#
# Syntax:
#   screex264 input_file output_mkv bitrate [quality_preset]
#     [mencoder_options [...]]
#
# quality_preset must be one of :
#   -std : (default) standard quality (good quality, normal)
#   -hq  : high quality (very good quality, slow)
#   -vhq : very high quality (best quality, very slow)
#
# mencoder_options are appended to command line of mencoder call.
# For example :
#   -ss 10 -endpos 56 : trim the video from 10s to 66s.
#
# (of course, there are no options -std, -hq nor -vhq)
#

# Syntax error detected.
# Exits the program with return code 1.
syntax_error() {
   printf '%s%sn' "Syntaxe : $0 input_file output_mkv bitrate "
     '[quality_preset] [mencoder_options [...]]' &gt;&amp;2
   exit 1
}

# Indicates whether the argument represents an integer.
# Returns the integer if the argument represents an integer, an empty string if
#   it doesn't.
#
# $1: value to test
is_integer() {
   local value="$1"
   printf %s "$1" | grep -o "^[[:digit:]]+$"
}

# Gets a unique id, based on the clock (seconds + nanoseconds).
# Returns a unique id.
uid() {
   date +'%s%N'
}

# Returns the command line arguments for the selected preset.
#
# $1: preset
# return: command line arguments
x264_preset() {
   local preset="$1"
   case "$preset" in
   '-std')
       printf '%s%s%s' "bitrate=$bitrate:frameref=8:mixed_refs:"
         "bframes=3:b_adapt:b_pyramid:weight_b:partitions=all:8x8dct:"
         "me=hex:subq=5:trellis=2:threads=auto" ;;
   '-hq')
       printf '%s%s%s' "bitrate=$bitrate:frameref=16:mixed_refs:"
         "bframes=3:b_adapt:b_pyramid:weight_b:partitions=all:8x8dct:"
         "me=umh:subq=6:trellis=2:threads=auto" ;;
   '-vhq')
       printf '%s%s%s' "bitrate=$bitrate:frameref=16:mixed_refs:"
         "bframes=3:b_adapt:b_pyramid:weight_b:partitions=all:8x8dct:"
         "me=esa:subq=7:trellis=2:threads=auto" ;;
   *)
       printf '%s%sn' 'Quality preset must be one of {-std,-hq,-vhq} : '
         "$preset" &gt;&amp;2
       exit 4
   esac
}

# error when less than 3 arguments
[ $# -ge 3 ] || syntax_error

# reads the arguments
in=$1; shift
out=$1; shift
bitrate=$1; shift

# input file must exist
if [ ! -f "$in" ]
then
   printf '%sn' "Input file doesn't exist : $in" &gt;&amp;2
   exit 2
fi

# bitrate value must be integer
if [ ! $(is_integer "$bitrate") ]
then
   printf '%sn' "Bitrate value must be integer : $bitrate" &gt;&amp;2
   exit 3
fi

# choose the quality preset
quality='-std'
if [ "$1" = '-std' -o "$1" = '-hq' -o "$1" = '-vhq' ]
then
   quality="$1"
   shift
fi

# gets the x264 options
opts=$(x264_preset $quality)

# gets a unique filename, in order to avoid collisions when encoding two video
# at the same time
uid=$(uid)
tmp_x264=$(printf '%s' "/tmp/$uid.avi")
tmp_log=$(printf '%s' "/tmp/$uid.log")

# encodes the first pass
mencoder "$in" -o /dev/null -passlogfile $tmp_log -ovc x264 -x264encopts
 "$opts:pass=1" -nosound $@ &amp;&amp;

# encodes the second pass
mencoder "$in" -o $tmp_x264 -passlogfile $tmp_log -ovc x264 -x264encopts
 "$opts:pass=2" -nosound $@ &amp;&amp;

# muxes the result in a mkv
mkvmerge -o "$out" -d 0 -A -S "$tmp_x264" --track-order 0:0</pre>
<p>Pour l&#8217;installer, copiez-le dans un fichier <code>/usr/bin/screex264</code> que vous rendez exécutable :</p>
<pre>gksudo gedit /usr/bin/screex264</pre>
<p>Faites un copier-coller du script, sauvez-le. Puis :</p>
<pre>sudo chmod 755 /usr/bin/screex264</pre>
<p>Vous pouvez maintenant faire chauffer le processeur !</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2008/09/screex264-reencodez-vos-captures-decran-videos-screencasts-sous-ubuntu/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>imagup : uploader une image en 2 clics</title>
		<link>http://blog.rom1v.com/2008/08/imagup-uploader-une-image-en-2-clics/</link>
		<comments>http://blog.rom1v.com/2008/08/imagup-uploader-une-image-en-2-clics/#comments</comments>
		<pubDate>Fri, 29 Aug 2008 13:27:00 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Outils]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[puf]]></category>
		<category><![CDATA[gnu/linux]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[ubuntu]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/2008/08/imagup%c2%a0-uploader-une-image-en-2-clics/</guid>
		<description><![CDATA[J&#8217;ai écrit un petit script pour uploader en ligne de commande une image sur imagup, et récupérer le lien (pratique pour poster sur les forums). Voici comment l&#8217;utiliser : imagup monimage.jpg Les extensions jpg, jpeg, png et]]></description>
			<content:encoded><![CDATA[<p>J&#8217;ai écrit un petit script pour uploader en ligne de commande une image sur <a href="http://www.imagup.com/">imagup</a>, et récupérer le lien (pratique pour poster sur les forums).</p>
<p>Voici comment l&#8217;utiliser :</p>
<pre>imagup monimage.jpg</pre>
<p>Les extensions <strong>jpg</strong>, <strong>jpeg</strong>, <strong>png</strong> et <strong">gif</strong> sont autorisées.</p>
<p>Voici un exemple de résultat :</p>
<pre>$ imagup rom-avatar.png
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                               Dload  Upload   Total   Spent    Left  Speed
100 28706    0 14129  100 14577   3638   3754  0:00:03  0:00:03 --:--:--  5059
rom-avatar.png : http://uploads.imagup.com/05/1220023287_rom-avatar.png</pre>
<p>Il est possible d&#8217;uploader plusieurs images en une seule ligne :</p>
<pre>imagup image1.png image2.jpg</pre>
<p>Avec l&#8217;option <code>-open</code>, une fois l&#8217;image uploadée, elle est ouverte dans le navigateur par défaut.</p>
<p><a href="apt://curl">curl</a> doit être installé.</p>
<p>Voici le script :</p>
<pre>#!/bin/sh
# IMAGUP script
#
# 29th august 2008 - Romain Vimont (®om)
#
# Uploads images to www.imagup.com, and returns the http:// link.
#
# Syntax:
#   imagup [-open] fichier1 [fichier2 [...]]
#
# If -open is specified, the link is opened in the default associated program
# (the default browser).
#

# Syntax error detected
# Exits the program with return code 1.
syntax_error() {
    printf '%sn' "Syntaxe : $0 [-open] fichier1 [fichier2 [...]]" &gt;&amp;2
    exit 1
}

# Bad extension detected. Prints messages on stderr.
#
# $1: bad extension
bad_extension() {
    local extension="$1"
    printf '%sn' "Extension not supported: $extension" &gt;&amp;2
    printf '%sn' "Must be one of {png, jpg, jpeg, gif}." &gt;&amp;2
}

# Returns the canonical name of the file, from its (full) path.
#
# $1: (full) path of the file
# return: canonical name of the file
#
# example: get_filename 'a/b.c/d.e.f.jpg' returns 'd.e.f.jpg'
get_filename() {
    local path="$1"
    printf %s "$path" | grep -o '[^/]+$'
}

# Returns the radical of a filename (its name without the extension), from its
# (full) path.
#
# $1: (full) path of the file
# return: radical of the file
#
# example: get_radical 'a/b.c/d.e.f.jpg' returns 'd.e.f'
get_radical() {
    local path="$1"
    local filename="$(get_filename "$path")"
    printf %s "$filename" | sed 's/.[^.]*$//'
}

# Returns the extension of a file, from its (full) path.
#
# $1: (full) path of the file
# return: extension of the file
#
# example: get_extension 'a/b.c/d.e.f.jpg' returns 'jpg'
get_extension() {
    local path="$1"
    local filename="$(get_filename "$path")"
    printf %s "$filename" | grep -o '[^.]+$'
}

# Converts a String to lower case.
#
# $1: input text
# return: lower cased text
#
# example: to_lower_case 'AbCdE' returns 'abcde'
to_lower_case() {
    local text="$1"
    printf %s "$text" | tr -s [A-Z] [a-z]
}

# error when no arguments
[ $# -ge 1 ] || syntax_error

if [ "$1" = '-open' ]
then
    # -open is enabled
    open=true
    shift
    # should remain other arguments
    [ $# -ge 1 ] || syntax_error
fi

# for each argument (a file to upload)
for path
do
    extension=$(get_extension "$path")
    ext=$(to_lower_case "$extension")
    # extention must be one of {png, jpg, jpeg, gif}
    if [ "$ext" = png -o "$ext" = jpg -o "$ext" = jpeg -o "$ext" = gif ]
    then
        filename=$(get_filename "$path")
        radical=$(get_radical "$path")
        # uploads and gets the url back
        url=$(curl www.imagup.com -F
          "fichier=@$path;filename=$radical.$ext;type=image/$ext" |
        grep image-upload |
        grep -o "http://[[:alpha:]]+.imagup.com/[^"]+.(png|jpg|jpeg|gif)")
        if [ "$url" ]
        then
            # if it worked, prints the url
            printf '%sn' "$path : $url"
            # if -open is selected, open-it in the default application
            [ $open ] &amp;&amp; xdg-open "$url"
        else
            # it didn't work
            printf '%sn' "Problem while uploading $path" &gt;&amp;2
        fi
    else
        # file extension is bad
        bad_extension "$extension"
    fi
done</pre>
<p>Pour l&#8217;installer, copiez-le dans un fichier <code>/usr/bin/imagup</code> que vous rendez exécutable :</p>
<pre>gksudo gedit /usr/bin/imagup</pre>
<p>Faites un copier-coller du script, sauvez-le. Puis :</p>
<pre>sudo chmod 755 /usr/bin/imagup</pre>
<p>Une fois installé, vous pouvez également l&#8217;utiliser comme <a href="http://doc.ubuntu-fr.org/nautilus_scripts">script nautilus</a>. Mettez le script suivant dans <code>~/.gnome2/nautilus-script/imagup-wrapper</code> et rendez-le exécutable :</p>
<pre>#!/bin/sh
# nautilus IMAGUP script wrapper
#
# 29th august 2008 - Romain Vimont (®om)
#
# Needs "imagup" core script to be installed.
#

# Use only n as field separator
IFS='
'

# Calls imagup with all args
imagup -open $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS</pre>
<p>Ensuite, dans <strong>nautilus</strong>, il est possible d&#8217;envoyer l&#8217;image en cliquant sur Script → imagup-wrapper. Les images ainsi envoyées s&#8217;ouvriront dans le navigateur par défaut.</p>
<p>Cependant, en écrivant ce script, je me suis aperçu de trois problèmes dans <strong>nautilus</strong> : <a href="http://bugzilla.gnome.org/show_bug.cgi?id=549823">un problème de proxy</a>, <a href="http://bugzilla.gnome.org/show_bug.cgi?id=549816">un problème de vue liste</a> et <a href="http://bugzilla.gnome.org/show_bug.cgi?id=549910">un problème avec le lancement de scripts à partir du bureau</a>.</p>
<p>Les deux derniers problèmes sont contournés avec le script <strong>imagup-wrapper</strong> (le dernier script).</p>
<p>Si, au lieu d&#8217;appeler une fois <strong>imagup</strong> avec tous les arguments, on voulait l&#8217;appeler <em>n</em> fois avec un seul argument (utile lorsque le programme appeler ne boucle pas sur les arguments), on aurait pu utiliser :</p>
<pre>printf %s "$NAUTILUS_SCRIPT_SELECTED_FILE_PATHS" |
while read -r arg
do
    imagup -open "$arg"
done</pre>
<p>ou encore :</p>
<pre>IFS='
'
for arg in $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS
do
    imagup -open "$arg"
done</pre>
<p>J&#8217;en ai profité pour écrire une section <a href="http://doc.ubuntu-fr.org/nautilus_scripts#les_pieges_a_eviter">les pièges à éviter</a> sur la doc ubuntu-fr de <strong>nautilus-scripts</strong>.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2008/08/imagup-uploader-une-image-en-2-clics/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>Recompresser ses photos en masse de manière incrémentale</title>
		<link>http://blog.rom1v.com/2008/08/recompresser-ses-photos-en-masse-de-maniere-incrementale/</link>
		<comments>http://blog.rom1v.com/2008/08/recompresser-ses-photos-en-masse-de-maniere-incrementale/#comments</comments>
		<pubDate>Wed, 27 Aug 2008 18:45:00 +0000</pubDate>
		<dc:creator>®om</dc:creator>
				<category><![CDATA[Astuces]]></category>
		<category><![CDATA[planet-libre]]></category>
		<category><![CDATA[puf]]></category>
		<category><![CDATA[gnu/linux]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[ubuntu]]></category>

		<guid isPermaLink="false">http://blog.rom1v.com/2008/08/recompresser-ses-photos-en-masse-de-maniere-incrementale/</guid>
		<description><![CDATA[Mon appareil photo possède 3 réglages de &#171;&#160;qualité&#160;&#187; (niveau de compression JPEG) : fin normal économique Mais lorsque l&#8217;on choisit un réglage, les photos font à peu près toutes la même taille, qu&#8217;elles soient simples ou complexes. Par exemple, en réglage fin, leur taille est quasiment toujours comprise entre 2,8Mio et 2,9Mio. Or, une photo [...]]]></description>
			<content:encoded><![CDATA[<p>Mon appareil photo possède 3 réglages de &laquo;&nbsp;qualité&nbsp;&raquo; (niveau de compression JPEG) :</p>
<ul>
<li>fin</li>
<li>normal</li>
<li>économique</li>
</ul>
<p>Mais lorsque l&#8217;on choisit un réglage, les photos font à peu près toutes la même taille, qu&#8217;elles soient simples ou complexes. Par exemple, en réglage <em>fin</em>, leur taille est quasiment toujours comprise entre 2,8Mio et 2,9Mio.</p>
<p>Or, une photo uniforme devrait prendre beaucoup moins de place qu&#8217;une photo très complexe, avec beaucoup de contours.</p>
<p>D&#8217;ailleurs, on s&#8217;en rend compte lorsqu&#8217;on recompresse ces photos sur un ordinateur :<br />
<a href="http://blog.rom1v.com/wp-content/uploads/2008/08/photos-tree.png"><img src="http://blog.rom1v.com/wp-content/uploads/2008/08/photos-tree-184x300.png" alt="photos-tree" title="photos-tree" width="184" height="300" class="aligncenter size-medium wp-image-148" /></a></p>
<p>Sur cette capture d&#8217;écran, les photos contenues à la racine (les 5 dernières) sont les photos originales, prises par l&#8217;appareil photo.<br />
Les photos contenues dans les répertoires <em>convertXX</em> sont les photos converties avec :</p>
<pre>convert image.jpg -quality XX convertXX/image.jpg</pre>
<p>(<a href="apt://imagemagick">imagemagick</a> doit être installé)</p>
<p>On se rend compte que les photos ont une taille beaucoup plus variable, ce qui est une bonne chose. Sur l&#8217;ensemble de mes albums, j&#8217;ai des photos à 500Kio et d&#8217;autres à 1,8Mio : <strong>c&#8217;est la qualité finale de la photo qui est prise en compte</strong>, et non la taille à atteindre.</p>
<p>J&#8217;ai donc décidé de prendre les photos en qualité maximale sur l&#8217;appareil photo, et de les recompresser à l&#8217;importation.</p>
<p>J&#8217;utilise <a href="apt://digikam">digiKam</a> pour gérer mes photos, qui possède une fonctionnalité pour recompresser les photos (utilisant exacement le même algorithme que <strong>imagemagick</strong>).</p>
<p>Le problème, c&#8217;est que je veux éviter, par inattention, de recompresser plusieurs fois les mêmes photos (perte de qualité inutile). Par exemple, lorsqu&#8217;on importe les photos d&#8217;une carte mémoire, qu&#8217;on les recompresse, puis qu&#8217;on importe les photos d&#8217;une seconde carte mémoire, difficile de différencier les photos déjà recompressées des autres (pour les photos se trouvant dans le même dossier).</p>
<p>J&#8217;ai donc écrit un petit script, qui garde dans un fichier la liste des photos déjà recompressées, et qui compresse toutes celles qui ne sont pas présentes dans le fichier.</p>
<p>Ainsi, après une importation de photos, j&#8217;exécute le script, seules les nouvelles seront recompressés.</p>
<pre>#!/bin/sh
#
# Mogrifie tous les fichiers jpeg non encore mogrifiés (recompressés en jpeg)
#

# le fichier "mogrified" doit exister, si ce n'est pas le cas, faire
# touch mogrified" avant de lancer le script
if [ ! -f mogrified ]
then
    echo 'mogrified file not found.'
    exit 1
fi

# liste tous les fichiers .jpg dans les répertoires décrivant une année
# (2005, 2006...)
find -iname '*.jpg' | grep ^./20 | sort &gt; filelist &amp;&amp;

# supprime de cette liste tous les fichiers déjà mogrifiés
comm -23 filelist mogrified |
while read photo
do
    # mogrifie ces fichiers
    echo "$photo"
    mogrify -quality 90 "$photo"
done

# les fichiers mogrifiés sont maintenant les fichiers de la liste complète
mv filelist mogrified</pre>
<p>L&#8217;outil <strong>mogrify</strong> fait la même chose que <strong>convert</strong>, sauf qu&#8217;il modifie le fichier sur place (et écrase donc la source).</p>
<p><strong>Cela permet d&#8217;avoir un très bon rapport qualité/taille, sans provoquer des pertes visibles sur les photos très complexes, ni utiliser de l&#8217;espace inutilement sur les photos simples.</strong></p>
]]></content:encoded>
			<wfw:commentRss>http://blog.rom1v.com/2008/08/recompresser-ses-photos-en-masse-de-maniere-incrementale/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>

