Comment écrire un OTP externe

Introduction

Les OTP (Omega Translation Process, ainsi que les OCP, Omega Compiled Process, qui contiennent la même chose sous une autre forme) permettent de modifier les flux de caractères que manipule LaTeX, à la manière d'un pipeline et non pas à l'aide d'expansion de macros comme d'habitude. Par exemple, pour transformer le codage d'entrée (qui peut varier) en le codage utilisé de manière inter par Omega (UCS2). Par exemple pour transformer le codage utilisé par Omega en celui utilisé par la fonte. Par exemple, pour choisir des ligatures dans des langues dans lesquelles elles sont beaucoup plus complexes que nos langues européennes. Par exemple, pour rajouter diverses informations (prononciation des caractères complexes en japonais (furigana), séparation des mots pour faciliter la césure en thaï, mise en relief des mots qui semblent étranges à un correcteur orthographique).

Un fichier Omega (ou plutôt Lambda) ressemble à

\ocp\TexUTF=inutf8
\InputTranslation currentfile \TexUTF

\documentclass[12pt]{article}

\usepackage[T1]{fontenc}
\DeclareFontFamily{T1}{cyber}{}
\DeclareFontShape{T1}{cyber}{m}{n}{<-> omcyberb}{}
\def\cyber{\fontfamily{cyber}\selectfont}

\begin{document}
  ...
\end{document}

Il se compile en tapant

lambda toto.tex

Il se visualise en tapant (xdvi ne marchera pas, car ce n'est pas un vrai fichier dvi) :

oxdvi toto.dvi

ou après conversion en PostScript (de même, dvips ne marchera pas, par contre le fichier PostScript obtenu est un vrai fichier PostScript, tout à fait portable).

odvips -o toto.ps toto.dvi
gv toto.ps

OTP d'entrée

Les deux premières lignes du fichier précédent définissent un OTP, que l'on appellera \TexUTF et qui se trouve dans un fichier inutf8.ocp

\ocp\TexUTF=inutf8

Et on lui dit que c'est l'OPT d'entrée pour le fichier courrant, i.e., cet OTP va permettre de convertir le fichier en UCS2 (le codage Unicode utilisé par Omega de manière interne).

\InputTranslation currentfile \TexUTF

OTP interne

Les OTP internes sont définis dans des fichiers *.otp, utilisant une syntaxe particulière, peu lisible et peu puissante (pour passer d'un codage à un autre, c'est très bien, mais pour des traitements plus complexes, non). Voici un exemple de (morceau de) fichier *.otp

% File ingb.otp
% Conversion to Unicode from Chinese Big 5 (HKU)
% Copyright (c) 1995 John Plaice and Yannis Haralambous
% This file is part of the Omega project.
%
% This file was derived from data in the tcs program
% (ftp://plan9.att.com/plan9/unixsrc/tcs.shar.Z, 16 November 1994)
%
  
input:  1;
output: 2;
  
tables:
  
in_gb[@"225b] = {
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd, @"fffd,
@"fffd, @"fffd, @"fffd, @"3000, @"3001, @"3002, @"30fb,
@"203e, @"02c7, @"00a8, @"3003, @"3005, @"2015, @"30fc,
@"2225, @"2026, @"2018, @"2019, @"201c, @"201d, @"3014,
@"3015, @"3008, @"3009, @"300a, @"300b, @"300c, @"300d,
@"300e, @"300f, @"3016, @"3017, @"3010, @"3011, @"00b1,
...
@"7e3b, @"9e82, @"9e87, @"9e88, @"9e8b, @"9e92, @"93d6,
@"9e9d, @"9e9f, @"9edb, @"9edc, @"9edd, @"9ee0, @"9edf,
@"9ee2, @"9ee9, @"9ee7, @"9ee5, @"9eea, @"9eef, @"9f22,
@"9f2c, @"9f2f, @"9f39, @"9f37, @"9f3d, @"9f3e, @"9f44 };
  
expressions:
  
@"00-@"a0              => \1;
(@"a1-@"ff)(@"a1-@"ff) => #(in_gb[(\1-@"a0)*@"64 + (\2-@"a0)]);
. .                    => @"FFFD;

Omega ne lit pas directement les fichiers *.otp : il en lit une version compilée, *.ocp, obtenue à l'aide de la commande otp2ocp.

Nous n'écrirons pas de fichier *.otp dans la suite de ces notes.

OTP externe

Un OTP externe se déclare ainsi

\externalocp\myocp=ocp.pl {}

Ici, il s'agit d'un fichier ocp.pl (d'après l'extension, il s'agit d'un script en Perl, mais n'importe quel fichier exécutable fera l'affaire). Il faut que ce fichier soit exécutable. Ce programme prend du texte sur son entrée standard, le modifie, le renvoie sur sa sortie standard.

Listes d'OTP

Il arrive qu'on veuille utiliser plusieurs OTPs en même temps, en les chaînant les uns aux autres. Pour cette raison, on ne demande généralement pas qu'un OTP soit actif, mais qu'une liste d'OTP soit active. On peut par exemple définir une liste d'OTP comme suit (on peut mettre n'importe quoi comme numéros, les OTP seront appelés dans l'odre des numéros croissant, quel que soit l'ordre dans lequel ils ont été mis dans la liste).

\ocplist\myocplist
  \addbeforeocplist 1 \myfirstocp
  \addbeforeocplist 2 \mysecondocp
\nullocplist

On l'utilise ainsi.

% Some text that won't be touched by these OTPs
...
\pushocplist\myocplist
% Some text that will be modified by these OTPs
...

Le squelette de nos fichiers LaTeX sera donc

\ocp\TexUTF=inutf8
\InputTranslation currentfile \TexUTF

\externalocp\myocp=ocp5.pl {}
\ocplist\myocplist
  \addbeforeocplist 1 \myocp
\nullocplist

\documentclass[12pt]{article}

\DeclareFontFamily{U}{cyber}{}
\DeclareFontShape{U}{cyber}{m}{n}{<-> omkochimincho}{}
\def\encodingdefault{U}
\def\familydefault{cyber}

\begin{document}
\pushocplist\myocplist
...
\end{document}

Codages utilisés par les OTPs

Je constate que l'OTP qui convertit l'UTF8 en UCS2 est invoqué en dernier, après les OTPs que l'on définiera nous-mêmes : on manipulera donc des flux de caractères dans le même codage que le codage d'entrée, UTF8.

Si le codage d'entrée n'était pas UTF8, il faudrait d'abord convertir ce codage d'entrée en UCS2, puis en UTF8, puis lancer notre ou nos OTPs, puis enfin convertir l'UTF8 en UCS2. On a donc bien besoin de listes d'OTPs.

OTP externe qui ne fait rien

Voici notre premier exemple : un OTP externe qui ne fait rien. À des fins de débugguage, il affiche ce qu'il reçoit en entrée et ce qu'il renvoie.

#! /share/nfs/users1/umr-tge/zoonek/gnu/Linux/bin/perl -w
use strict;

while(my $line = <>){
  print STDERR "\n";
  print STDERR "IN:  $line\n";
  # Do something useful
  print STDERR "OUT: $line\n";
  print $line;
}

OTP externe qui rajoute un caractère entre chaque octet

C'est juste pour voir : comme une chaine de caractères Unicode (UTF8, ici) n'est pas une suite d'octets, on va immanquablement couper des caractères en deux et ça va planter.

$line =~ s/(.)/X$1/g;

Comme prévu, ça plante.

Missing character: There is no ^^^^0558 in font ombitstreamcyberbitroman!
  
! Text line contains an invalid character.
OCP stack 1.0 entry 0:...6616X^^^^01d8^^^^762bX^^X
                                                  ^^^^3601X^^^^01d8^^^^3601X...
  
? 
Missing character: There is no ^^^^0658 in font ombitstreamcyberbitroman!

Perl et UTF8

À l'heure actuelle (Perl 5.6 ou 5.7), la seule façon raisonnable d'utiliser des chaines de caractères UTF8 en Perl, c'est au travers du module Unicode::String. (Il y a un peu de support d'Unicode dans 5.6, mais il est marqué comme « EXPERIMENTAL, subject to change » et effectivement, il change dans 5.7. Dans 5.7, je me retrouve quand même avec des caractères corrompus.)

OTP externe qui rajoute un caractère entre chaque caractère

C'est le même exemple que précédemment, mais cette fois-ci, ça marche.

#! /share/nfs/users1/umr-tge/zoonek/gnu/Linux/bin/perl -w
use strict;
use Unicode::String qw(utf8);
  
while(my $line = <>){
  print STDERR "\n";
  print STDERR "IN:  $line\n";
  $line = utf8(" ".$line);
  my $new = utf8("");
  for(my $i=0; $i < $line->length; $i++){
    $new .= " " . $line->substr($i,1);
  }
  $new .= " ";
  $new = $new->utf8;
  print STDERR "OUT: $new\n";
  print $new;
}

OTP externe qui implémente les règles de typographie françaises

Il s'agit de rajouter (éventuellement) un espace (insécable) avant « ; » et « . », un espace fin (insécable) avant « ! » ou « ? ».

#! /share/nfs/users1/umr-tge/zoonek/gnu/Linux/bin/perl -w
use strict;
use Unicode::String qw(utf8);
  
while(my $line = <>){
  print STDERR "\n";
  print STDERR "IN:  $line\n";
  $line = utf8(" ".$line);
  my $new = utf8("");
  for(my $i=0; $i < $line->length; $i++){
    my $current = $line->substr($i,1);
    if( $current eq "," or
	$current eq "." ) {
      $new .= $current . " ";
    } elsif( $current eq "(" or
	     $current eq "[" ){
      $new .= " " . $current;
    } elsif( $current eq ":" or
	     $current eq ";" ){
      $new .= "\\unbreakablespace" . $current . " ";
    } elsif( $current eq "!" or
	     $current eq "?" ){
      $new .= "\\unbreakablethinspace" . $current . " ";
    } else {
      $new .= $current;
    }
  }
  $new .= " ";
  $new = $new->utf8;
  print STDERR "OUT: $new\n";
  print $new;
}

Les macros \unbreakablethinspace et \unbreakablespace sont définies comme suit.

% Stolen from frenchb.ldf
\def\unbreakablethinspace{%
  \ifhmode
    \ifdim\lastskip>\z@
      \unskip\penalty\@M\thinspace
    \else
      \penalty\@M\thinspace
    \fi
  \fi
}
\def\unbreakablespace{%
  \ifhmode
    \ifdim\lastskip>\z@
      \unskip\penalty\@M\space
    \else
      \penalty\@M\space
    \fi
  \fi}

OTP externe qui implémente les règles de typographie japonaises

La règle est la suivante : on a le droit d'aller à la ligne n'importe quand, sauf avant les petits kana, le signe d'allongement des kana, une parenthèse ou un guillemet fermant, et après une parenthèse ou un guillement ouvrant.

#! /share/nfs/users1/umr-tge/zoonek/gnu/Linux/bin/perl -w
use strict;
use constant TRUE  => (0==0);
use constant FALSE => (0==1);
use Unicode::String qw(utf8);
  
sub allow_break_before {
  my $char = shift;
  if( $char eq "ー" or
      $char eq "ぁ" or
      $char eq "ぃ" or
      $char eq "ぅ" or
      $char eq "ぇ" or
      $char eq "ぉ" or
      $char eq "ゎ" or
      $char eq "ゃ" or
      $char eq "ゅ" or
      $char eq "ょ" or
      $char eq "っ" or
      $char eq "。" or
      $char eq "、" or
      $char eq ")" or
      $char eq "」" ) {
    return FALSE;
  } else {
    return TRUE;
  }
}
  
sub allow_break_after {
  my $char = shift;
  if( $char eq "(" or
      $char eq "「" ){
    return FALSE;
  } else {
    return TRUE;
  }
}
  
while(my $line = <>){
  print STDERR "\n";
  print STDERR "IN:  $line\n";
  $line = utf8(" ".$line);
  my $new = utf8("");
  my $previous = "";
  for(my $i=0; $i < $line->length; $i++){
    my $current = $line->substr($i,1);
    if(allow_break_before($current) and
       allow_break_after($previous)){
      $new .= $previous . "\\breakableCJKkern ";
    } else {
      $new .= $previous . "\\unbreakableCJKkern ";
    }
    if( $current eq " " ){
      $previous = "";
    } else {
      $previous = $current;
    }
  }
  $new .= $previous;
  $new = $new->utf8;
  print STDERR "OUT: $new\n";
  print $new;
}

(Exercice : j'ai oublié les katakana, les rajouter)

Les espaces sont définis ainsi.

\def\unbreakableCJKkern{%
  \nobreak
  \hskip 0sp plus 2sp minus 2sp
  \nobreak
}
\def\breakableCJKkern{\hskip 0sp plus 2pt minus 2sp}

Variante

Une variante de ces règles autorise les caractères 。、)」 à déborder.

#! /share/nfs/users1/umr-tge/zoonek/gnu/Linux/bin/perl -w
use strict;
use constant TRUE  => (0==0);
use constant FALSE => (0==1);
use Unicode::String qw(utf8);
  
sub overlap {
  my $char = shift;
  if( $char eq "。" or
      $char eq "、" or
      $char eq ")" or
      $char eq "」" ) {
    return TRUE;
  } else {
    return FALSE;
  }
}
  
sub allow_break_before {
  my $char = shift;
  if( $char eq "ー" or
      $char eq "ぁ" or
      $char eq "ぃ" or
      $char eq "ぅ" or
      $char eq "ぇ" or
      $char eq "ぉ" or
      $char eq "ゎ" or
      $char eq "ゃ" or
      $char eq "ゅ" or
      $char eq "ょ" or
      $char eq "っ" or
      $char eq "。" or
      $char eq "、" or
      $char eq ")" or
      $char eq "」" ) {
    return FALSE;
  } else {
    return TRUE;
  }
}
  
sub allow_break_after {
  my $char = shift;
  if( $char eq "(" or
      $char eq "「" ){
    return FALSE;
  } else {
    return TRUE;
  }
}
  
while(my $line = <>){
  print STDERR "\n";
  print STDERR "IN:  $line\n";
  $line = utf8(" ".$line);
  my $new = utf8("");
  my $previous = "";
  for(my $i=0; $i < $line->length; $i++){
    my $current = $line->substr($i,1);
    if(overlap($current)){
      $new .= $previous . "\\unbreakableCJKkern" .
	"\\discretionary{\\rlap{" .
	  $current . "}}{}{" . $current . "}";
    } elsif(allow_break_before($current) and
	    allow_break_after($previous)){
      $new .= $previous . "\\breakableCJKkern ";
    } else {
      $new .= $previous . "\\unbreakableCJKkern ";
    }
    if( $current eq " " or overlap($current) ){
      $previous = "";
    } else {
      $previous = $current;
    }
  }
  $new .= $previous;
  $new = $new->utf8;
  print STDERR "OUT: $new\n";
  print $new;
}

Pour voir les effets de cet OTP, il peut être nécessaire de réduire les espaces maximaux entre les caractères

\def\unbreakableCJKkern{%
  \nobreak\hskip 0sp plus 2sp minus 2sp\nobreak}
\def\breakableCJKkern{\hskip 0sp plus 1pt minus 2sp}

et de lui dire que ce n'est pas si mauvais que cela de couper des mots.

\hyphenpenalty=10
\exhyphenpenalty=10

Voici un exemple (la partie gauche du texte a été coupée, donc ça ne veut plus dire grand-chose). On remarquera que, contrairement à la situation précédente, les caractères sont bien alignés.

*

Exercices (si vous avez quelques notions de japonais)

Exercice 1

Écrire une macro qui ajoute des furiganas au dessus de certains mots. Les furiganas sont déjà présents dans le texte, sous la forme (par exemple)

これは\furigana{車}{くるま}です。

Exercice 2

Écrire un OTP qui transforme les furigana tapés dans le texte sous la forme (il faudra que le programme repère tout seul le début du mot)

これは車《くるま》です。

en furigana de la forme

これは\furigana{車}{くるま}です。

Les furigana sont tapés de la sorte dans les textes de http://www.aozora.gr.jp/

Exercice 3

Écrire un OPT qui rajoute automatiquement des furiganas sur un texte qui n'en a pas, en utilisant (par exemple) le logiciel chasen http://chasen.aist-nara.ac.jp/chasen/distribution.html.en pour les calculer. (Je crois qu'il préfère que le texte soit en EUC-JP : on pourra utiliser le module Text::Iconv pour la conversion.)

Exercice 4

Dans les texte japonais, les furiganas ne sont indiqués au dessus d'un caractère que si cette prononciation de ce caractère n'a pas encore été utilisée dans la page courrante. Y a-t-il un moyen de faire cela en LaTeX ? [Je n'ai pas réfléchi très longtemps, mais je ne vois pas comment faire.]

Notes

Pour compiler quelques exemples, j'ai été amené à changer les valeurs suivantes dans le fichier texmf.cnf.

buf_size = 500000 % was 50_000

% character buffers for ocp filters.
ocp_buf_size = 1000000  % was 20_000

Puis à recompiler le format de Lambda.

lambda --ini lambda.ini

Vincent Zoonekynd
<zoonek@math.jussieu.fr>
latest modification on Thu May 2 16:50:09 CEST 2002