Presentation d'un rapport concernant l'algorithme de compression Lempel-Ziv. Le programme est livre avec les source ADA.
Ceci n'est qu'un apercu du contenu du fichier PDF a telecharger ici (a venir) Il manque simplement les figures ...
" Compression sans perte : Ziv-Lempel "
Introduction
C'est en 1977 que Abraham Lempel et Jacob Ziv développèrent l'algorithme que nous allons étudier ci
dessous. Cette compression est particulièrement adaptée aux données ayant un haut degré de
répétition.
Table des matières
Introduction 2
Table des matières 3
Table des figures 3
1. Documentation 4
1.1 Documentation d'utilisation 4
1.2 Documentation de conception 4
1.2.1 Compresser 4
1.2.2 Décompression 7
2 Architecture des programmes 9
2.1 Compression 9
2.2 Décompression 10
3. Jeux de test 11
3.1 Test du programme de compression 11
3.1.1 Test du programme compresser avec une séquence de caractères quelconque 11
3.1.2 Test du programme compresser avec une séquence de caractères commençant par une
lettre qui n'est pas la première de la séquence dan l'ordre alphabétique. 11
3.1.3 Test du programme compresser avec une séquence de caractères qui se termine par une
séquence déjà présente dans le dictionnaire 12
3.2 Test de programme de décompression 12
4. Listing des programmes 13
4.1 Compression 13
4.2 Décompression 20
Conclusion 22
Perspectives 22
Table des figures
Figure 1 : Structure du premier dictionnaire 5
Figure 2 : Structure finale du dictionnaire de compression 6
Figure 3 : Structure finale détaillée du dictionnaire de compression 7
Figure 4 : Structure du dictionnaire de décompression 8
1. Documentation
1.1 Documentation d'utilisation
La fonction du logiciel est de compresser et de décompresser des fichiers de données. Pour
compresser un fichier, on exécute compresser avec comme argument le nom du fichier à compresser.
On obtient le fichier compressé avec comme extension .cmp.
$ compresser nom_du_fichier
Résultat dans nom_du_fichier.cmp
Pour décompresser, on exécute décompresser avec comme paramètre le nom du fichier compressé.
On obtient un fichier avec une extension .dcp.
$ decompresser nom_du_fichier.cmp
Résultat dans nom_du_fichier.cmp.dcp
Lorsqu'on compresse ou décompresse un fichier vide, il n'y a pas d'erreur, on obtient un fichier vide.
Si l'on passe comme argument au programme compresser ou décompresser aucun nom de fichier, on
obtient alors une erreur du type :
raised CONSTRAINT_ERROR : a-comlin.adb:57 explicit raise
Si l'on passe comme argument au programme compresser ou décompresser un nom de fichier qui
n'existe pas, on obtient alors une erreur du type :
raised ADA.IO_EXCEPTIONS.NAME_ERROR : s-fileio.adb:889
Si l'on passe comme argument au programme décompresser un fichier autre qu'un fichier de couple,
on obtient alors une erreur du type :
raised ADA.IO_EXCEPTIONS.NAME_ERROR : s-fileio.adb:889
Le programme compresser peut prendre n'importe quel type de fichier en entrée.
1.2 Documentation de conception
1.2.1 Compresser
Pour compresser un fichier, on exécute l'algorithme suivant :
Tant que le fichier n'est pas finit
Lecture d'un caractère
Concaténation du caractère avec une chaîne
Recherche de la chaîne dans le dictionnaire
Si la chaîne n'existe pas dans le dictionnaire
Ajout de la chaîne
Ecriture dans le fichier du couple caractère et numéro de
séquence ou avait été trouvée la chaîne précédente
Fin si
Fin tant que
Pour le programme compresser, nous avons besoin d'avoir un dictionnaire, d'ajouter des mots et de
chercher des mots dans ce même dictionnaire.
Dans un premier temps, pour les algorithmes compresser et décompresser nous avons utilisé le
même dictionnaire sous une forme de liste chaînée. Le dictionnaire était gérer de façon complètement
indépendante dans un paquetage " Dictionary ". Il contenait des chaînes de caractères non
contraintes ainsi qu'un index et un pointeur vers l'élément suivant.
En définitive la structure du premier dictionnaire se résumait à :
Figure 1 : Structure du premier dictionnaire
Ce type de stockage s'est avéré être très lent. Cependant, il nous a permit de valider le
fonctionnement des algorithmes. Nous avons ensuite mis en place un système plus rapide.
Pour améliorer ses performances, nous avons pensé à plusieurs solutions :
- Utilisation d'un arbre de tableau de taille constante de 255 caractères adressés non
pas par des pointeurs mais par des lettres(character) de manière à pouvoir s'y
déplacer rapidement.
Le problème d'un tel tableau réside dans l'occupation mémoire. En effet si la longueur
d'un mot est trop grande, la mémoire ne pourra pas accuser la taille du tableau
- Utilisation d'un arbre généralisé dans lequel un mot serai représenté par un parcours
de la racine aux feuilles. Cependant ce type de structure imposerait de faire une
recherche au maximum de 255 caractères par lettre du mot. Ce qui en terme de coût
représente une quantité non négligeable si la taille du mot est importante.
- Finalement nous avons décidé d'utiliser une méthode originale pour la compression.
Nous utilisons un classement par taille, et par ordre alphabétique des chaînes grâce à
une structure bidimensionnelle.
Pour ce faire, le dictionnaire utilise un classement des séquences selon deux critères à savoir la taille
et l'ordre alphanumérique. Ce type de dictionnaire permet une recherche relativement rapide par
rapport à la solution une. De plus, l'utilisation de mémoire reste limitée.
On obtient donc la structure suivante :
Figure 2 : Structure finale du dictionnaire de compression
Les listes de mots de même taille (liste chaînée verticale) sont accessibles par la liste chaînée
verticale. Chaque élément constituant la liste verticale pointe vers une liste chaînée de mots classés
dans l'ordre alphanumérique.
Ce procédé accroît notablement la vitesse. En effet, plus la longueur du mot est grande, plus la
probabilité que la quantité de mot se trouvant à un étage est réduite (on nommera étage une liste de
mot de même taille). Et donc plus la recherche du mot dans la liste chaînée Elem est rapide.
De plus, le classement par ordre alphabétique permet de réduire le parcours dans la liste chaînée. En
moyenne, on réduit le parcours de la liste par deux lorsque la chaîne est triée. Cependant cela
augmente les difficultés lors de l'insertion d'un élément.
Voyons dès à présent plus en détail la structure bidimensionnelle. On aura donc deux principaux
types :
- Les éléments constituant la liste chaînée de taille (dimension 1 : verticale) :
ElemTaille
- Les éléments constituant la liste chaînée de mot de même taille (dimension 2 :
Horizontale) : Elem
On peut visualiser sur ce schéma les interconnexions entre ces deux éléments. On remarque à
gauche du schéma ci-dessous un élément de type ElemTaille (3 cases verticales). A droite et
au centre il y a les éléments de type Elem contenant les chaînes en elles-mêmes.
Figure 3 : Structure finale détaillée du dictionnaire de compression
1.2.2 Décompression
Pour le programme décompresser, plusieurs solutions se sont présentées à nous. Pour optimiser la
décompression nous avons déterminé une technique permettant de rechercher un mot à partir de son
index. Pour ce faire plusieurs solutions ont été envisagées :
- La première solution envisagée était de décomposer le numéro " index " en base
deux. En utilisant une structure particulière nous aurions pu déterminer le mot à
chercher en un maximum de 32 sauts (Sachant que Index est un integer et qu'il
est codé sur 32 bits).
- La seconde solution envisagée a été de calculer le nombre de couple total présent
dans le fichier compressé. De cette manière nous pouvons ensuite déclarer un
tableau de chaîne non contrainte indexé par le numéro index. Cette méthode accroît
particulièrement la vitesse de décompression. C'est la raison pour laquelle nous
l'avons choisit.
Figure 4 : Structure du dictionnaire de décompression
Finalement, pour la décompression :
- On effectue un premier parcourt du fichier à décompresser, on compte le nombre de couple
présent dans le fichier.
- On déclare ensuite, un tableau dont la taille est égale aux nombres de couples. Ce tableau
sert de dictionnaire. Le tableau est constitué de chaînes non contraintes indexées par les
numéros de séquences.
- On parcourt une deuxième fois le fichier à décompresser en remplissant au fur et a mesure
le dictionnaire et on écrit au fur et à mesure du parcours le résultat dans le fichier
décompresser.
2 Architecture des programmes
2.1 Compression
La compression utilise le package Dictionary. Ce package contient deux types : un type Elem et
un type ElemTaille tous deux décris par le schéma étudié précédemment que nous avons remis ci
dessous.
Il contient aussi deux procédures et une fonction.
procedure Dictionary_Init( L : in out Listetaille );
procedure Dictionary_Add_Word( Addchaine: in Unbounded_String;
L: in Listetaille);
function Dictionary_Get_Word_Position( SearchChaine: in unbounded_string;
L:in listeTaille) return integer;
La procédure Dictionary_Init permet d'initialiser le dictionnaire au tout début du lancement du
programme. Cette procédure construit l'élément fictif permettant la gestion globale du dictionnaire.
Elle prend pour paramètre un élément de type ElemTaille, elle modifie cet élément et le renvoi une
fois initialisé. En d'autres termes, nous pourrions initialiser grâce à cette procédure plusieurs
dictionnaires en parallèles.
La procédure Dictionary_Add_Word permet d'ajouter un mot au dictionnaire. Cette fonction prend
en paramètre un dictionnaire (de type ElemTaille), et une chaîne non contrainte.
La fonction Dictionary_get_Word_Position permet de savoir si un élément est dans la liste et si
tel est le cas elle renvoi l'index du mot recherché.
Une fois le dictionnaire créé, le programme de compression n'est pas très compliqué a coder. Il a
l'avantage d'être complètement indépendant de la gestion du dictionnaire. On prendra simplement
soin de déclarer une variable de gestion du dictionnaire concordant avec le paquetage de dictionnaire
utilisé.
2.2 Décompression
La décompression s'est avérée plus facile que prévu. Son architecture en est donc simplifiée. Nous
avons découpé le problème en trois grandes sous parties. Nous avons tout d'abord crée une fonction
permettant de calculer le nombre de couple dans un fichier de couple. Ensuite nous avons créé deux
procédures permettant de gérer le dictionnaire de chaîne non contrainte qui n'est en réalité qu'un
tableau de chaînes indexées par des indexes de couples.
Enfin nous avons créé l'algorithme de décompression qui ne s'est pas avérer d'une grande difficulté
une fois les fonctions de dictionnaires réalisées.
function Max return Integer
procedure Dictionary_Add_Word(Addchaine: in Unbounded_String ;
Tab: in out T)
procedure Dictionary_Add_Word(Searchaine: out Unbounded_String
;index: in Integer
Tab: in out T)
La fonction Max retourne le nombre de couple, sous la forme d'un integer.
La procédure Dictionary_Get_Word permet de récupérer une chaîne à partir de son index.
La procédure Dictionary_Add_Word permet d'ajouter un mot au dictionnaire.
Nous utilisons une structure particulière permettant de gérer à la fois un tableau de chaînes non
contraintes ainsi que l'index maximum atteint dans ce même tableau.
Cette structure est initialisée après avoir calculé le nombre de couple contenu dans le fichier
compressé.
Enfin, le programme décompresser en lui-même va utiliser les fonctions de dictionnaires et la fonction
de calcul de couple. Son fonctionnement consiste à lire tous les couples d'un fichier de couple pour
ensuite créer à la fois le dictionnaire, et le fichier décompresser.
3. Jeux de test
3.1 Test du programme de compression
Pour faciliter la compréhension des résultats de la compression, on compressera dans ces tests, des
fichiers uniquement de type texte. Le programme fonctionne de la même manière avec n'importe quel
type de caractère.
3.1.1 Test du programme compresser avec une séquence de caractère quelconque
On choisit de compresser la séquence suivante :
Abbbabac
L'analyse du fichier nous donne les couples suivants :
0 a
0 b
2 b
1 b
1 c
C'est bien le résultat attendu
3.1.2 Test du programme compresser avec une séquence de caractère
commençant par une lettre qui n'est pas la première de la séquence dans l'ordre
alphabétique.
En effet, afin de classer les listes de mots, le programme compresser doit effectuer des instructions
particulières, lorsqu'il faut insérer un élément avant le premier élément d'une liste de mots. Pour faire
ce test, on passe comme argument au programme une séquence dont la première lettre n'est pas
première dans l'ordre alphanumérique. De cette façon le programme devra insérer un élément avant
l'élément représentant la première lettre de la séquence.
On passe comme arguments au programme la séquence suivante :
vaavazac
Dans la liste de mot de taille 1, " a " devra être placé avant " v ". Si cette opération ne se fait pas la
compression sera erronée, car la fonction Dictionnary_Get_Word_Position renverra des
résultats erronés, car cette fonction se sert du classement alphanumérique pour rechercher des
chaînes. Quand on exécute l'analyse du fichier compressé, on obtient :
0 v
0 a
2 v
2 z
2 c
On obtient bien le résultat escompté.
3.1.3 Test du programme compresser avec une séquence de caractère qui se
termine par une séquence déjà présente dans le dictionnaire
En effet, dans l'algorithme de compression, si on trouve une chaîne de caractère dans le dictionnaire,
on ne l'ajoute pas, et on lit le caractère suivant. Mais si la séquence trouvée est la dernière séquence,
on doit l'ajouter. Nous avons détecté ce problème de la dernière séquence lorsque nous avons voulut
compresser une image BMP, en effet après la décompression, il était impossible d'ouvrir l'image. Pour
effectuer ce test, on compresse donc une séquence de caractère qui se termine par une séquence
déjà présente dans le dictionnaire.
On passe comme arguments au programme la séquence suivante :
abacac
Quand on exécute l'analyse du fichier compressé, on obtient :
0 a
0 b
1 c
1 c
C'est bien le résultat qu'on voulait avoir. Le dernier couple est " ac ", il a été ajouté au fichier malgré
qu'il soit déjà présent dans le dictionnaire. On remarque que c'est le seul cas ou deux couples
peuvent être égaux (il y a deux fois le couple " 1 c ").
3.2 Test de programme de décompression
Le programme de décompression ne comporte pas de cas particulier. En effet le programme
comporte un seul if. On entre dans cette condition pour toutes les chaînes qui comporte qu'un seul
caractère. Pour tester le programme en entier il suffit de décompresser un fichier ou il y a des couples
dont la sous séquence est 0, et d'autres couples ou la sous séquence est différente de 0. On peut par
exemple décompresser la séquence de couple suivante (utilisé dans les tests de compresser).
0 a
0 b
2 b
1 b
1 c
On obtient par décompression la séquence suivante :
vaavazac
C'est bien la séquence que l'on souhaitait décompresser.
Pour compléter ces tests, nous avons compressé et décompressé de plus gros fichier de type bmp ou
doc. On s 'est ainsi aperçu que la décompression est très rapide, et que la compression est nettement
plus lente. Par exemple, il faut 10 secondes pour compresser un fichier de 600 Ko. La solution que
nous avons utilisée n'est donc sûrement pas la plus rapide, une structure en forme d'arbre généralisé
s'avérerait plus rapide, mais notre solution nous a semblé intéressante à développer.
4. Listing des programmes
4.1 Compression
---------------------------------------------------------------------------
----
-- Compresser : corps de procedure
--
-- Auteur : Romain Le Guen
-- Alexandre Fustier
-- Affiliation : INPG-Telecom, Etudiants
--
-- Historique :
-- 11/02/2004
--
---------------------------------------------------------------------------
--| Programme de compression de fichier
--| Usage :
--| compresser nom_de_fichier
--| Effet :
--| compresse le fichier nom_de_fichier dans le fichier
nom_de_fichier.cmp
with Ada.Command_Line; use Ada.Command_Line;
----------------------------------------------------
-- Package de gestion des chaines non contraintes --
----------------------------------------------------
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
-----------------------------------------------------------------
-- Package de gestion des fichiers de couples et de caracteres --
-----------------------------------------------------------------
with Cars_IO; use Cars_IO;
with Couples_IO; use Couples_IO;
----------------------------------------
-- Package de gestion du dictionnaire --
----------------------------------------
with Dictionary; use Dictionary;
--------------------------
-- Procedure Principale --
--------------------------
procedure Compresser is
index :integer:=0;
octet :character;
chaine :unbounded_string;
Maliste:ListeTaille;
NbTour:integer:=0;
begin
--------------------------------------------------------
-- Ouverture des fichiers de caracteres et de couples --
--------------------------------------------------------
Ouvrir_Fichier_Cars (Argument (1));
Creer_Fichier_Couples (Argument (1) & ".cmp");
------------------------------------
-- Initialisation du dictionnaire --
------------------------------------
Dictionary_Init(Maliste);
-------------------------------------------------
-- Boucle Principale de traitement des données --
-- On poursuit le traitement jusqu'a la fin du --
-- fichier de caractere --
-------------------------------------------------
--------------------------------------------------
-- Initialisation de la chaine de concatenation --
--------------------------------------------------
chaine := to_unbounded_string("");
while not Fin_De_Fichier_Cars loop
-- Lit un octet dans le fichier a compresser
Lire(octet);
-- Ajoute l'octet lu a la fin de la chaine à etudier en cours
Append(chaine,octet);
-- Regarde si la chaine existe dans le dictionnaire
-- Si elle existe la fonction retourne l'index de la chaine
-- Sinon elle retourne 0
Index:=Dictionary_Get_Word_Position(Chaine,Maliste);
-- 0 signifie que le chaine n'existe pas dans le dictionnaire
if Index = 0 then
-- Ajoute le mot dans le dictionnaire
Dictionary_Add_Word(chaine,MaListe);
-- Retire le dernier caractere de la chaine word
-- Cherche la postion de cette nouvelle chaine
NbTour:=Dictionary_Get_Word_Position(
head(chaine,ada.Strings.Unbounded.Length(chaine)-1) ,MaListe);
-- Ecrit le couple (position, octet)
Ecrire(Nbtour,Octet);
-- reinitialisationde la chaine
chaine := to_unbounded_string("");
end if;
end loop;
----------------------------------
-- Gestion d'un cas aux limites --
----------------------------------
-- Si la derniere sequence lu est deja presente dans le dictionnaire
-- => elle n'a pas été ajouté
if Index > 0 then
-- Donc on l'ajoute
Ecrire(Dictionary_Get_Word_Position(
head(chaine,ada.Strings.Unbounded.Length(chaine)-1) ,MaListe),octet);
end if;
--------------------------------------------------------
-- Fermeture des fichiers de couples et de caracteres --
--------------------------------------------------------
Fermer_Fichier_Cars;
Fermer_Fichier_Couples;
end Compresser;
---------------------------------------------------------------------------
-- Dictionary : corps de paquetage
--
-- Auteur : Alexandre Fustier
-- Romain Le Guen
-- Affiliation : INPG-Telecom, étudiant
--
-- Historique :
-- 11/02/2004
--
---------------------------------------------------------------------------
-- Package de gestion des string non contraintes
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Text_Io; use Ada.Text_Io;
package body Dictionary is
----------------------------
-- Initialisation du dico --
----------------------------
procedure Dictionary_Init (
L : in out Listetaille ) is
begin
-- on crée le permier élément, cet élément est un fictif
L := new Elemtaille;
L.Next:=null;
-- On se sert du champ taille du premier élément pour indexer les
numéro de séquence
L.Taille:=0;
L.Chaine:=null;
end Dictionary_Init;
--------------------------------------------------
-- Procedure d'ajout d'un mot au dictionnaire --
-- qui prend pour paramètre la chaine a ajouter --
-- et le dictionnaire --
--------------------------------------------------
procedure Dictionary_Add_Word (
Addchaine : in Unbounded_String;
L : in Listetaille ) is
Pointeur : Listetaille := L;
-- PointeurH et Savepointeur pointent sur une liste de mot de meme
taille
PointeurH : liste;
Savepointeur : Liste;
begin
-- On parcourt la liste des taille tant qu'on est pas au bout ou tant
-- qu'on ne trouve pas un champ taille égal a celui de la chaine a
ajouter
while Pointeur.Next /= null
and then Pointeur.Next.Taille Addchaine then
-- On met a jour l'index des numero de sequence
L.Taille:=L.Taille+1;
--On insere un élément en premiere position
Savepointeur:=Pointeur.next.chaine;
Pointeur.next.chaine:=new
Elem'(L.Taille,Addchaine,SavePointeur);
-- On sort apres avoir insere l'element
return;
end if;
-- On parcourt la liste afin de savoir ou il faut placer l'élément
pour qu'il soit classer
while Pointeurh.next /=null and then pointeurh.next.word )of Unbounded_String;
-------------------------------------------------
-- Calcul de la taille du tableau a instancier --
-------------------------------------------------
function Max return Integer is
Max:Integer:=0;
begin
Ouvrir_Fichier_Couples (Argument (1));
Creer_Fichier_Cars (Argument (1) & ".dcp");
while not Fin_De_Fichier_Couples loop
Lire(N,C);
Max:=Max+1;
end loop;
Fermer_Fichier_Couples;
Fermer_Fichier_Cars;
return Max;
end Max;
-----------------------------------------------------------
-- Structure gérant le tableau de chaine non conatrainte --
-- ainsi que le numéro de l'élément courant ---------------
-----------------------------------------------------------
type T is record
Tableau:Table(1..Max);
index:Integer:=1;
end record;
---------------------------------------------------------
---------------------------------------------------------
-- Gestion d'un dictionaire a partir du type précédent --
---------------------------------------------------------
---------------------------------------------------------
-----------------------------------------
-- Ajout d'un mot dans le dictionnaire --
-----------------------------------------
procedure Dictionary_Add_Word (Addchaine: in Unbounded_String ;
Tab: in out T) is
begin
Tab.Tableau(Tab.Index):=Addchaine;
Tab.index:=Tab.index+1;
end Dictionary_Add_Word;
-----------------------------------------------------
-- Recherche d'un mot a partir d'un numero d'index --
-----------------------------------------------------
procedure Dictionary_Get_Word(N : in Integer;
Tab: in T;
S : out unbounded_string) is
begin
S:=Tab.tableau(N);
end Dictionary_Get_Word;
----------------------------------------------------
-- Declaration du tableau contenant les séquences --
----------------------------------------------------
tab:T;
begin
--------------------------------------------------------
-- Ouverture des fichiers de couples et de caracteres --
--------------------------------------------------------
Ouvrir_Fichier_Couples (Argument (1));
Creer_Fichier_Cars (Argument (1) & ".dcp");
-----------------------------------------------------------------
-- Execute le traitement des couples jusqu'a la fin du fichier --
-----------------------------------------------------------------
while not Fin_De_Fichier_Couples loop
----------------------------------------------------------
-- Reinitialisation de la chaine de stockage temporaire --
----------------------------------------------------------
S:=To_Unbounded_String("");
-- Lit le couple courant
Lire(N,C);
-- Si la séquence est composé d'un seul caractere
if N = 0 then
-- On peut l'ecrire directement dans le fichier decompressé
Ecrire(C);
-- On copie le caractere a la fin de la chaine
Append(S,C);
-- On ajoute le caractere dans le dictionnaire
Dictionary_Add_Word(S,Tab);
-- Si la séquence est composé de plusieurs caracteres
else
-- On recherche la chaine portant le numero "N"
-- La chaine est retourné par référence dans S
Dictionary_Get_Word(N,Tab,S);
-- Ajoute a la chaine le caractere courant
Append(S,C);
-- Ajoute la sequence "Chaine + caractere" au dictionnaire
Dictionary_Add_Word(S,Tab);
-- Enfin on ecrit la sequence decodé dans le fichier
-- Et ceci caractere par caractere
for I in 1..Ada.Strings.Unbounded.Length(S) loop
Ecrire(Element(S,I));
end loop;
end if;
end loop;
-- Enfin on ferme les 2 fichiers.
Fermer_Fichier_Couples;
Fermer_Fichier_Cars;
end Decompresser;
Conclusion
Nous avons donc réussit à développer un algorithme permettant d'effectuer la compression LZ, celui
ci s'avère relativement efficace pour des textes ayant de longues séquences répétitives. La méthode
de gestion du dictionnaire employée est relativement originale, cependant elle s'avère coûteuse en
temps de calcul. La méthode de décompression employée est rapide et simple à mettre en ouvre.
Nous aurions peut etre pu améliorer l'algorithme de compression en utilisant un arbre généralisé ou
bien même en ajoutant des pointeurs de parcours multi niveaux dans notre structure.
Perspectives
Il s'avérerait intéressant d'étudier des méthodes de compression plus efficace telle que le Zip ou bien
l'algorithme Huffman. Nous ne manquerons donc pas de poursuivre nos études dans ce domaine.
|