lundi 23 avril 2012

Impression de planches d'étiquette avec Latex (publipostage)

Je m'occupe d'une association avec laquelle nous faisons de temps en temps du mailing "classique" (par la poste!) en générant des planches d'étiquettes autocollantes à partir d'un fichier de contacts maintenu sous la forme d'un fichier "tableur".
Pendant longtemps, je me suis acharné, (voire épuisé!) avec les "fonctionnalités" de publipostage des suites bureautiques classiques.
Cette fonctionalité, pourtant à priori basique, est en effet difficile à manier, du moins sur les versions que j'ai pu essayer (Office 2003 et OOo 3.3). On passe des heures à créer un modèle, à cliquer, à en enregistrer les versions successives, à identifier le format parmi les formats prédéfinis, à cliquer, à ouvrir la base de données (qu'il faut aller rechercher dans l'arborescence à chaque fois...), à fusionner, à recommencer parce qu'on a un message disant "impossible de trouver la source de données" ou quelque chose du genre, à recliquer, à re-naviguer jusqu'au dossier, à cliquer encore, à re-générer, à essayer autre chose, à re-naviquer, à re, à re, etc.
L'enfer! Pour quelque chose qui au final, peut fonctionner, mais surtout a un comportement erratique: un coup, c'est bon, un coup, c'est pas bon....

Bref. Insupportable.

A coté de ça, on trouve aussi des logiciels, libres ou propriétaires, qui vous proposent de faire peu ou prou la même chose. Mais bon, encore un bidule à installer, qui ne va pas forcémment être exactement adapté à ce que vous voulez...

Je refléchissais depuis quelque temps sur la possibilité d'une solution automatisée 'en 1 clic', utilisant des solutions éprouvées, libres et multiplateforme, mais faute de temps, j'avais laissé ça un peu de coté. Etant utilisateur quotididien de Latex, j'ai presque par hasard recherché s'il n'y avait pas un package qui m'aiderait dans cette tâche. Et là, Bingo!
Premier hit: le package "labels", qui fait... tout!
Pour la faire courte, et pour les familiers de Latex, il suffit du petit source suivant. Pour les autres, c'est peut-être une excellente occasion de se mettre à Latex ?

\documentclass{article}
\usepackage{labels}

\begin{document}
\begin{labels}
\input names.dat
\end{labels}
\end{document}

Et le tour est joué: après compilation, vous disposez d'un pdf avec les étiquettes, à condition que vous fournissiez dans le dossier courant le fichier names.dat contenant les noms et adresses au bon format (voir ci-dessous). Evidemment, en général, on devra personnaliser un peu, ajouter babel pour l'alphabet occidental, le nombre de lignes et de colonnes, etc. On peut évidemment régler les marges et autres espacements avec les commandes adéquates, la doc est très complète là dessus, et l'archive de téléchargement contient en bonus une multitude d'exemples.

# my_labels.tex

\documentclass[a4paper,12pt]{article}
\usepackage[french]{babel}
\usepackage[utf8]{inputenc}% ou iso8859-15 en général sur windows
\usepackage{labels}

\LabelCols=2
\LabelRows=7

\begin{document}
\bfseries
\begin{labels}
\input names.dat
\end{labels}
\end{document}

Pour le format des données, ce package n'attend pas de format particulier: il se contente d'imprimer sur les étiquettes le texte fourni, ligne par ligne, avec comme séparateur d'étiquette un simple saut de ligne. Comme par exemple (tiré de la doc):

Professor R. Bercov, Chair
Department of Mathematics
University of Alberta
Edmonton, Alberta
Canada T6G 2G1

Chair of the Search Committee
Department of Mathematics
and Statistics
University of Regina
Regina, Saskatchewan
Canada S4S 0A2

...

Il faudra donc commencer par exporter les données au format csv, puis procéder à un petit traitement pour générer ce fichier, et ensuite lancer Latex dessus. Sur plateforme Linux standard l'ensemble des outils nécessaires est normalement disponible, pour Windows, c'est faisable, mais un peu plus délicat.

D'abord, il faut expurger préalablement le fichier .csv des guillemets qui peuvent avoir été générés par le tableur (Il semble qu'OOo génère systématiquement ces guillemets autour des cellulles contenant du texte). On peut faire ça à la main via un "rechercher/remplacer" dans son éditeur favori, mais c'est plus élégant et surtout plus fiable de faire ça avec un outil dédié: sed

Depuis le shell, la commande
sed s/XXXX/YYYY/ input_file > output_file
va remplacer dans le fichier d'entrée input_file la première occurence de 'XXXX' par 'YYYY'. La sortie de programme se fait par défaut sur la "sortie standard" (la console), et il faut donc la rediriger vers le fichier souhaité (output_file) via le caractère de redirection '>'.

Pour remplacer toutes les occurences, on ajoute un 'g':
sed s/XXXX/YYYY/g input_file > output_file
Pour éliminer les occurences d'une chaine, il suffit de laisser YYYY vide.

En général, la première ligne du fichier contient le nom des colonnes, et il faut donc la supprimer. sed permet ceci avec la syntaxe:
sed 1d input_file > output_file
Pour regrouper les 2 commandes en une ligne, il faut ajouter l'option -e indiquant que la chaine qui suit est une commande:
sed -e 1d -e s/XXXX/YYYY/g input_file > output_file

Au final la commande est donc:
sed -e 1d -e s/\"//g ooo_export.csv > adresses.csv
(il faut "backslasher" le guillemet)

Un exemple de script bash est donné ci-dessous, qui enchaîne les 3 passes:
- preprocessing du fichier csv,
- génération du fichier d'adresses,
- appel de pdflatex pour la génération du pdf final.
Il faudra aussi adapter l'ordre des champs à ce que vous avez dans votre fichier d'origine.

#! /bin/bash

outfile=names.dat
sed -e 1d -e s/\"//g ooo_export.csv > adresses.csv

echo "" > $outfile

# var. spéciale: séparateur de champ
IFS=';'

while read LINE; do
 echo "$LINE";
 a=( $LINE )
 LNAME=${a[0]}
 FNAME=${a[1]}
 ADR=${a[2]}
 CODE=${a[3]}
 CITY=${a[4]}

 echo "$FNAME $LNAME"  >> $outfile
 echo "$ADR"  >> $outfile
 echo "$CODE $CITY"  >> $outfile
 echo "" >> $outfile

done < adresses.csv

pdflatex -interaction=batchmode my_labels.tex

Attention:
On a parfois dans les fichiers d'adresses des champs du style "Gérard & Jacqueline". Latex interprétant le '&' comme un caractère spécial, mieux vaut le supprimer de votre fichier source et remplacer par "Gérard et Jacqueline". Autre solution : "Gérard \& Jacqueline".

La suite ? Réaliser un export en ligne de commande du fichier d'adresses originel... Mais OOo ne semble pas disposer de cette fonctionnalité là, il faudrait passer par d'autres outils tiers.

Edit 10/2012: en fait, l'export en .ods  (Open Document Spreadsheet) depuis le fichier Excel de départ permet de disposer d'un fichier xml du contenu, dont on peut extraire le contenu. OpenDocument Fellowship propose des outils pour manipuler les fichiers Open Document, mais qui semblent à ce jour ne concerner que les fichiers odt (de type "texte", donc, et pas les feuilles de calcul ods).

L'autre approche consisterait à écrire un parser adéquat, qui récupère  les champs et les copie dans le fichier décrit ci-dessus.  pyxml semble l'outil le plus adapté, une piste à suivre...

Edit 02/2015: LibreOffice 4.2 (et OpenOffice ?) propose désormais un outil en ligne de commande qui permet d'exporter un fichier .ods en .csv, avec la syntaxe suivante:
libreoffice --headless --convert-to csv *.ods

lundi 2 avril 2012

GNU Make and the foreach() function

Make is a nice tool for building stuff. Software, of course, but not only, I use it also for building pdf files from Latex sources. However, its usage is not obvious at all, and many pitfalls lie in front of the newcomer. And the manual is not a great help when it comes to learning new features. Joyfully, there are bunches of tutorials out there.

However, some advanced tricks are rarely covered. Here is one trick that can be useful in some situations. Beware, it assumes you already know the basics about Make (GNU flavor), in case not, please go read some tutorial and come back in a while...

Lets say you have a folder containing some source files of type ".in", say foo.in and boo.in. These files need to be processed with some tool (call it "some_tool" for now) to build the output files. This (awesome) thing has a rather classical syntax:
some_tool in_file out_file [option]

Say you need to build these with two different configurations to produce two output files for each input files, say one of type "X" and one of type "Y". Here, from the two input files, you need to build:
foo_X.out
foo_Y.out
boo_X.out
boo_Y.out

How can Make help you so you only build the ones that are needed ? For the example, say that each build process takes several hours, so you really don't want to build them if it's not needed.

First create a variable holding all the input files:
infiles := $(wildcard *.in) 

Then, add a generic rule telling how to build a ".out" file:
%.out: %.in 
    @some_tool $< $@ 
Yes, remember these special variables: $< is the first dependency, and $@ is the target (%.out here), so that will generate the correct command-line. Oh, and for the different output flavors (say, by adding some option), just dupe the generic rule:
%_X.out: %.in
 @some_tool $< $@ X
%_Y.out: %.in
 @some_tool $< $@ Y
Finally, you will try to build the main (all) rule. Its dependencies are all the input files (thats easy, its $(infiles) ) and the output files, so that each of them gets produced only in case it is not present. Ah. Here comes some trouble. How do you create a variable holding all the expected targets ? Well, first, lets get the files names without extension. Easy:
filenames :=$(basename $(infiles))

Okay, and now, how do I automatically generate a string holding all the output files names ? This is a job for the foreach() function. Its description in the manual is (IMHO) mostly unreadable, so lets see this through a simpler example:
what = $(foreach a, ham cheese salad, john likes $(a) )
all:
    @echo "what=$(what)"
produces:
what= john likes ham  john likes cheese  john likes salad
Usually, this is used using some variables, and it would be written:
types= ham cheese salad
what = $(foreach a, $(types), john likes $(a) )

Mmmh, I'm sure you're starting to see the point. The 'types' will be our different output flavors, say, something like:
types= X Y
outfile := $(foreach a, $(types), boo_$(a).out )
     @echo "outfile=$(outfile)"
that will produce:
outfile= boo_X.out boo_Y.out
But this has to be done for each input files, and the following fails:
types= X Y
outfiles := $(foreach a, $(types), $(filenames)_$(a).out )
It gets expanded to:
boo foo_X.out boo foo_Y.out

So the solution is to use nested foreach() loops, just as you would do with some regular procedural programming language.
types:=X Y
outfiles := $(foreach a,$(filenames), \
     $(foreach b,$(types), \
         $(a)_$(b).out ) ) 
Then, the main rule can just get written as follows:
all: $(infiles) $(outfiles)
    @echo "That's all, folks !"

For further reading, check this series of articles on advanced topics on Make.